diff --git a/resources/js/packages/ui/src/FullCalendar/CalendarDayColumn.vue b/resources/js/packages/ui/src/FullCalendar/CalendarDayColumn.vue
index 27491ea2..7c78c00f 100644
--- a/resources/js/packages/ui/src/FullCalendar/CalendarDayColumn.vue
+++ b/resources/js/packages/ui/src/FullCalendar/CalendarDayColumn.vue
@@ -4,7 +4,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '..';
import type { DayEvent, ActivityBox } from './calendarTypes';
import type { WindowActivityInPeriod } from './activityTypes';
-defineProps<{
+const props = defineProps<{
dayStr: string;
totalGridHeight: number;
hasActivityStatus: boolean;
@@ -34,6 +34,8 @@ defineProps<{
getActivityBoxActivities: (box: ActivityBox) => WindowActivityInPeriod[];
getActivityPercentage: (count: number, total: number) => string;
getActivityText: (activity: WindowActivityInPeriod) => string;
+ getTopActivity: (box: ActivityBox) => WindowActivityInPeriod | null;
+ isDayView: boolean;
// Selection
showSelection: boolean;
@@ -46,6 +48,16 @@ defineProps<{
selectionEndHeight: number;
}>();
+function isUncoveredByEvents(abox: ActivityBox): boolean {
+ return !props.dayEvents.some((de) => {
+ const eTop = de.top;
+ const eBottom = de.top + de.height;
+ const aTop = abox.top;
+ const aBottom = abox.top + abox.height;
+ return eTop < aBottom && eBottom > aTop;
+ });
+}
+
const emit = defineEmits<{
(e: 'event-pointerdown', event: PointerEvent, dayEvent: DayEvent): void;
(e: 'event-keydown-enter', dayEvent: DayEvent): void;
@@ -55,6 +67,7 @@ const emit = defineEmits<{
dayEvent: DayEvent,
edge: 'start' | 'end'
): void;
+ (e: 'activity-pointerdown', event: PointerEvent): void;
}>();
@@ -63,12 +76,16 @@ const emit = defineEmits<{
class="fc-timegrid-col relative border-r border-border bg-transparent pointer-events-none"
:class="{
'has-activity-status': hasActivityStatus,
+ 'activity-expanded': hasActivityStatus && isDayView,
}"
:data-date="dayStr"
:style="{ height: totalGridHeight + 'px' }">
+ :class="{
+ 'fc-events-inset': hasActivityStatus && !isDayView,
+ 'fc-events-inset-expanded': hasActivityStatus && isDayView,
+ }">
-
+
+ :class="[
+ abox.isIdle ? 'idle' : 'active',
+ {
+ 'activity-status-box-expanded': isDayView,
+ 'activity-status-box-uncovered':
+ !isDayView &&
+ !abox.isIdle &&
+ getTopActivity(abox) &&
+ isUncoveredByEvents(abox),
+ },
+ ]"
+ :style="{ top: abox.top + 'px', height: abox.height + 'px' }"
+ @pointerdown="emit('activity-pointerdown', $event)">
+
+
![]()
+
+ {{ getTopActivity(abox)!.appName.charAt(0).toUpperCase() }}
+
+
+ {{ getTopActivity(abox)!.label || getTopActivity(abox)!.appName }}
+
+
+
-
+
{{ getActivityBoxLabel(abox) }}
@@ -269,13 +318,99 @@ const emit = defineEmits<{
background-color: rgba(156, 163, 175, 0.5);
}
.activity-status-box.active::before {
- background-color: rgba(34, 197, 94, 0.3);
+ background-color: rgba(14, 165, 233, 0.3);
}
.activity-status-box.active:hover::before {
- background-color: rgba(34, 197, 94, 1);
+ background-color: rgba(14, 165, 233, 1);
+}
+
+/* Uncovered activity boxes in week view — fill column width */
+.activity-status-box-uncovered {
+ width: calc(100% - 4px);
+ border-radius: 3px;
+ overflow: hidden;
+}
+.activity-status-box-uncovered::before {
+ left: 0;
+ right: 0;
+ width: auto;
+}
+.activity-status-box-uncovered.active::before {
+ background-color: rgba(14, 165, 233, 0.12);
+}
+.activity-status-box-uncovered.active:hover::before {
+ background-color: rgba(14, 165, 233, 0.25);
+}
+
+/* Expanded activity boxes for day view */
+.activity-status-box-expanded {
+ width: 200px;
+ border-radius: 3px;
+ overflow: hidden;
+}
+.activity-status-box-expanded::before {
+ left: 0;
+ right: 0;
+ width: auto;
+}
+.activity-status-box-expanded.idle::before {
+ background-color: rgba(156, 163, 175, 0.08);
+}
+.activity-status-box-expanded.idle:hover::before {
+ background-color: rgba(156, 163, 175, 0.2);
+}
+.activity-status-box-expanded.active::before {
+ background-color: rgba(14, 165, 233, 0.12);
+}
+.activity-status-box-expanded.active:hover::before {
+ background-color: rgba(14, 165, 233, 0.25);
+}
+
+.activity-status-content {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 4px;
+ height: 100%;
+ overflow: hidden;
+}
+
+.activity-status-icon {
+ width: 14px;
+ height: 14px;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.activity-status-icon-fallback {
+ width: 14px;
+ height: 14px;
+ border-radius: 2px;
+ background-color: rgba(14, 165, 233, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 8px;
+ flex-shrink: 0;
+ color: rgba(14, 165, 233, 0.8);
+}
+
+.activity-status-label {
+ font-size: 10px;
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ opacity: 0.8;
}
.fc-events-inset {
left: 8px;
}
+
+.fc-events-inset-expanded {
+ left: 204px;
+}
diff --git a/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue b/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue
index ef8256ea..d470bd1e 100644
--- a/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue
+++ b/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue
@@ -163,6 +163,7 @@ const {
getActivityBoxActivities,
getActivityPercentage,
getActivityText,
+ getTopActivity,
} = useActivityBoxes({
activityPeriods: () => props.activityPeriods,
viewDays,
@@ -280,6 +281,22 @@ watch(showEditTimeEntryModal, (value) => {
}
});
+/**
+ * Guards slot pointer-down so that clicks which dismiss an open Reka UI
+ * layer (context menu, popover, dialog) don't simultaneously start a
+ * new time-entry selection on the calendar grid.
+ *
+ * Because Reka's DismissableLayer registers its document-level
+ * `pointerdown` listener *without* capture, it fires AFTER the
+ * calendar grid's own handler. That means when this guard runs,
+ * `contextMenuOpen` (and modal refs) still reflect the *open* state.
+ */
+function guardedSlotPointerDown(e: PointerEvent) {
+ if (contextMenuOpen.value) return;
+ if (showCreateTimeEntryModal.value || showEditTimeEntryModal.value) return;
+ onSlotPointerDown(e);
+}
+
const scrollToCurrentTime = () => {
nextTick(() => {
if (!scrollerRef.value) return;
@@ -490,7 +507,7 @@ function getEventDurationSeconds(dayEvent: DayEvent, dayStr: string): number {