@@ -105,43 +105,66 @@ const openPopoverAppId = ref<string | null>(null)
105105const popoverRef = ref <HTMLElement | null >(null )
106106
107107// Fixed pixel coordinates, computed from the icon's rect at open-time. The
108- // popover is teleported to <body> and positioned this way (rather than
109- // nested + position:absolute inside the dock) because the dock's icon strip
110- // scrolls (overflow-x/y: auto) when there are many pinned icons — a
111- // popover positioned relative to a scrollable ancestor gets clipped by it.
108+ // popover (and tooltip, below) are teleported to <body> and positioned this
109+ // way (rather than nested + position:absolute inside the dock) because the
110+ // dock's icon strip scrolls (overflow-x/y: auto) when there are many pinned
111+ // icons — a popover positioned relative to a scrollable ancestor gets
112+ // clipped by it.
112113const popoverStyle = ref <Record <string , string >>({})
113114const POPOVER_GAP = 10
114115
115- function computePopoverStyle(appId : string ) {
116+ // Anchors a flyout (popover or tooltip) to the icon's edge that faces away
117+ // from the dock, offset by `gap`.
118+ function anchoredStyleFor(appId : string , gap : number ): Record <string , string > | null {
116119 const iconEl = iconEls .get (appId )
117- if (! iconEl ) return
120+ if (! iconEl ) return null
118121 const rect = iconEl .getBoundingClientRect ()
119122
120123 switch (props .position ) {
121124 case ' left' :
122- popoverStyle . value = {
123- left: ` ${rect .right + POPOVER_GAP }px ` ,
125+ return {
126+ left: ` ${rect .right + gap }px ` ,
124127 top: ` ${rect .top + rect .height / 2 }px ` ,
125128 transform: ' translateY(-50%)' ,
126129 }
127- break
128130 case ' right' :
129- popoverStyle . value = {
130- left: ` ${rect .left - POPOVER_GAP }px ` ,
131+ return {
132+ left: ` ${rect .left - gap }px ` ,
131133 top: ` ${rect .top + rect .height / 2 }px ` ,
132134 transform: ' translate(-100%, -50%)' ,
133135 }
134- break
135136 default :
136- popoverStyle . value = {
137+ return {
137138 left: ` ${rect .left + rect .width / 2 }px ` ,
138- top: ` ${rect .top - POPOVER_GAP }px ` ,
139+ top: ` ${rect .top - gap }px ` ,
139140 transform: ' translate(-50%, -100%)' ,
140141 }
141142 }
142143}
143144
145+ function computePopoverStyle(appId : string ) {
146+ const style = anchoredStyleFor (appId , POPOVER_GAP )
147+ if (style ) popoverStyle .value = style
148+ }
149+
150+ const hoveredAppId = ref <string | null >(null )
151+ const tooltipStyle = ref <Record <string , string >>({})
152+ const TOOLTIP_GAP = 8
153+
154+ function showTooltip(appId : string ) {
155+ if (dragState || openPopoverAppId .value ) return
156+ const style = anchoredStyleFor (appId , TOOLTIP_GAP )
157+ if (! style ) return
158+ tooltipStyle .value = style
159+ hoveredAppId .value = appId
160+ }
161+
162+ function hideTooltip() {
163+ hoveredAppId .value = null
164+ }
165+
144166function togglePopover(appId : string ) {
167+ hideTooltip ()
145168 if (openPopoverAppId .value === appId ) {
146169 openPopoverAppId .value = null
147170 return
@@ -201,6 +224,7 @@ let dragState: DragState | null = null
201224
202225function onIconPointerDown(appId : string , e : PointerEvent ) {
203226 if (e .button !== 0 ) return
227+ hideTooltip ()
204228 dragState = { appId , startX: e .clientX , startY: e .clientY , moved: false }
205229 window .addEventListener (' pointermove' , onIconPointerMove )
206230 window .addEventListener (' pointerup' , onIconPointerUp )
@@ -302,15 +326,25 @@ onUnmounted(() => window.removeEventListener('click', onWindowClick, true))
302326 class =" dock-icon"
303327 :class =" { 'dock-icon-focused': isAppFocused(app.id), 'dock-icon-disabled': offline }"
304328 :style =" { color: app.iconColor, background: app.iconBg }"
305- :title =" offline ? `${app.label} (offline)` : app.label"
329+ :aria-label =" offline ? `${app.label} (offline)` : app.label"
306330 @pointerdown =" onIconPointerDown(app.id, $event)"
307331 @contextmenu.prevent =" handleIconContextMenu(app.id)"
332+ @mouseenter =" showTooltip(app.id)"
333+ @mouseleave =" hideTooltip"
308334 >
309335 <i class =" bi" :class =" app.icon" ></i >
310- <span v-if =" dotCount(app.id) > 0" class =" dock-dots" >
311- <span v-for =" n in dotCount(app.id)" :key =" n" class =" dock-dot" ></span >
312- </span >
313336 </button >
337+ <span v-if =" dotCount(app.id) > 0" class =" dock-dots" >
338+ <span v-for =" n in dotCount(app.id)" :key =" n" class =" dock-dot" ></span >
339+ </span >
340+
341+ <Teleport to="body">
342+ <div
343+ v-if =" hoveredAppId === app.id"
344+ class =" dock-tooltip"
345+ :style =" tooltipStyle"
346+ >{{ offline ? `${app.label} (offline)` : app.label }}</div >
347+ </Teleport >
314348
315349 <Teleport to="body">
316350 <div
@@ -341,7 +375,7 @@ onUnmounted(() => window.removeEventListener('click', onWindowClick, true))
341375 <span class =" dock-popover-icon dock-popover-icon-new" >
342376 <i class =" bi bi-plus-lg" ></i >
343377 </span >
344- <span class =" dock-popover-label" >New window </span >
378+ <span class =" dock-popover-label" >New Window </span >
345379 </button >
346380 <button
347381 v-if =" (instancesByApp.get(app.id)?.length ?? 0) > 0"
@@ -352,7 +386,7 @@ onUnmounted(() => window.removeEventListener('click', onWindowClick, true))
352386 <i class =" bi bi-power" ></i >
353387 </span >
354388 <span class =" dock-popover-label" >
355- {{ instancesByApp.get(app.id)!.length === 1 ? 'Quit window ' : `Quit ${instancesByApp.get(app.id)!.length} windows ` }}
389+ {{ instancesByApp.get(app.id)!.length === 1 ? 'Quit' : `Quit ${instancesByApp.get(app.id)!.length} Windows ` }}
356390 </span >
357391 </button >
358392 </div >
@@ -501,35 +535,52 @@ onUnmounted(() => window.removeEventListener('click', onWindowClick, true))
501535 position : absolute ;
502536 display : flex ;
503537 gap : 3px ;
538+ pointer-events : none ;
504539}
505540
506- /* Indicator sits on the icon's edge that touches the screen border */
541+ /* Indicator sits just outside the icon square, on the edge that touches the
542+ screen border — not on top of the glyph, which fills the square. */
507543.dock-bottom .dock-dots {
508- bottom : 3 px ;
544+ bottom : -5 px ;
509545 left : 50% ;
510546 transform : translateX (-50% );
511547 flex-direction : row ;
512548}
513549
514550.dock-left .dock-dots {
515- left : 3 px ;
551+ left : -5 px ;
516552 top : 50% ;
517553 transform : translateY (-50% );
518554 flex-direction : column ;
519555}
520556
521557.dock-right .dock-dots {
522- right : 3 px ;
558+ right : -5 px ;
523559 top : 50% ;
524560 transform : translateY (-50% );
525561 flex-direction : column ;
526562}
527563
528564.dock-dot {
529- width : 5px ;
530- height : 5px ;
565+ width : 6. 5px ;
566+ height : 6. 5px ;
531567 border-radius : 50% ;
532- background : #475569 ;
568+ background : #f97316 ;
569+ }
570+
571+ /* App-name tooltip — same teleport-and-anchor approach as .dock-popover below. */
572+ .dock-tooltip {
573+ position : fixed ;
574+ padding : 0.3rem 0.6rem ;
575+ background : rgba (20 , 20 , 22 , 0.92 );
576+ color : #fff ;
577+ font-size : 0.72rem ;
578+ font-weight : 500 ;
579+ white-space : nowrap ;
580+ border-radius : 6px ;
581+ box-shadow : 0 4px 12px rgba (0 , 0 , 0 , 0.25 );
582+ pointer-events : none ;
583+ z-index : 2100 ;
533584}
534585
535586/* Instance switcher popover — teleported to <body> and positioned via an
0 commit comments