11import { createPlugin , PluginDef } from '@fullcalendar/core' ;
2+ import { computePosition , flip , shift , offset } from '@floating-ui/dom' ;
23
34export interface ActivityPeriod {
45 start : string ;
@@ -17,6 +18,52 @@ declare module '@fullcalendar/core' {
1718 }
1819}
1920
21+ /**
22+ * Creates and manages a tooltip element for activity status boxes
23+ */
24+ function createTooltip ( ) : HTMLElement {
25+ const tooltip = document . createElement ( 'div' ) ;
26+ tooltip . className =
27+ 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground' ;
28+ tooltip . style . position = 'fixed' ;
29+ tooltip . style . pointerEvents = 'none' ;
30+ tooltip . style . opacity = '0' ;
31+ tooltip . style . whiteSpace = 'nowrap' ;
32+ tooltip . style . transform = 'scale(0.95)' ;
33+ tooltip . style . transition = 'opacity 150ms, transform 150ms' ;
34+ document . body . appendChild ( tooltip ) ;
35+ return tooltip ;
36+ }
37+
38+ /**
39+ * Shows tooltip for an activity status box
40+ */
41+ function showTooltip ( box : HTMLElement , tooltip : HTMLElement , text : string ) {
42+ tooltip . textContent = text ;
43+ tooltip . style . opacity = '1' ;
44+ tooltip . style . transform = 'scale(1)' ;
45+
46+ const updatePosition = ( ) => {
47+ computePosition ( box , tooltip , {
48+ placement : 'right' ,
49+ middleware : [ offset ( 8 ) , flip ( ) , shift ( { padding : 5 } ) ] ,
50+ } ) . then ( ( { x, y } ) => {
51+ tooltip . style . left = `${ x } px` ;
52+ tooltip . style . top = `${ y } px` ;
53+ } ) ;
54+ } ;
55+
56+ updatePosition ( ) ;
57+ }
58+
59+ /**
60+ * Hides the tooltip
61+ */
62+ function hideTooltip ( tooltip : HTMLElement ) {
63+ tooltip . style . opacity = '0' ;
64+ tooltip . style . transform = 'scale(0.95)' ;
65+ }
66+
2067/**
2168 * Renders activity status boxes in the calendar time grid
2269 */
@@ -30,6 +77,10 @@ export function renderActivityStatusBoxes(
3077 const existingBoxes = calendarEl . querySelectorAll ( '.activity-status-box' ) ;
3178 existingBoxes . forEach ( ( box ) => box . remove ( ) ) ;
3279
80+ // Clean up existing tooltips
81+ const existingTooltips = document . querySelectorAll ( '.activity-status-tooltip' ) ;
82+ existingTooltips . forEach ( ( tooltip ) => tooltip . remove ( ) ) ;
83+
3384 // Remove has-activity-status class from all lanes
3485 const allLanes = calendarEl . querySelectorAll ( '.fc-timegrid-col' ) ;
3586 allLanes . forEach ( ( lane ) => lane . classList . remove ( 'has-activity-status' ) ) ;
@@ -53,6 +104,9 @@ export function renderActivityStatusBoxes(
53104 activityPeriods . length
54105 ) ;
55106
107+ // Create a single tooltip instance to be reused
108+ const tooltip = createTooltip ( ) ;
109+
56110 // Get the calendar's current view to determine dates
57111 const dateHeaders = calendarEl . querySelectorAll ( '.fc-col-header-cell' ) ;
58112
@@ -115,7 +169,31 @@ export function renderActivityStatusBoxes(
115169 box . style . left = '4px' ;
116170 box . style . right = '4px' ;
117171 box . style . zIndex = '10' ;
118- box . style . borderRadius = '4px' ;
172+ box . style . cursor = 'default' ;
173+
174+ // Calculate duration in minutes
175+ const actualStart = periodStart > laneDateStart ? periodStart : laneDateStart ;
176+ const actualEnd = periodEnd < laneDateEnd ? periodEnd : laneDateEnd ;
177+ const durationMs = actualEnd . getTime ( ) - actualStart . getTime ( ) ;
178+ const durationMinutes = Math . round ( durationMs / 60000 ) ;
179+
180+ // Format duration
181+ const hours = Math . floor ( durationMinutes / 60 ) ;
182+ const minutes = durationMinutes % 60 ;
183+ const durationText = hours > 0 ? `${ hours } h ${ minutes } m` : `${ minutes } m` ;
184+
185+ // Add tooltip text based on status
186+ const status = period . isIdle ? 'Idling' : 'Active' ;
187+ const tooltipText = `${ status } (${ durationText } )` ;
188+
189+ // Add hover event listeners for tooltip
190+ box . addEventListener ( 'mouseenter' , ( ) => {
191+ showTooltip ( box , tooltip , tooltipText ) ;
192+ } ) ;
193+
194+ box . addEventListener ( 'mouseleave' , ( ) => {
195+ hideTooltip ( tooltip ) ;
196+ } ) ;
119197
120198 // Position relative to the lane
121199 const laneFrame = lane . querySelector ( '.fc-timegrid-col-frame' ) ;
0 commit comments