11<script setup lang="ts">
2- defineProps <{
2+ import type { ModuleReplacement } from ' module-replacements'
3+
4+ const props = defineProps <{
35 /** Number of columns (2-4) */
46 columns: number
57 /** Column headers (package names or version numbers) */
68 headers: string []
9+ /** Which columns are the "no dependency" column */
10+ specialColumns? : boolean []
11+ /** Replacement data for each column (if available) */
12+ replacements? : (ModuleReplacement | null )[]
713}>()
14+
15+ // Tooltip state
16+ const activeTooltip = shallowRef <{
17+ type: ' nodep' | ' replacement'
18+ index: number
19+ } | null >(null )
20+
21+ // Computed message for active replacement tooltip
22+ const activeReplacementMessage = computed <
23+ [string , { replacement?: string ; nodeVersion ?: string }] | null
24+ > (() => {
25+ if (activeTooltip .value ?.type !== ' replacement' ) return null
26+ const replacement = props .replacements ?.[activeTooltip .value .index ]
27+ if (! replacement ) return null
28+
29+ switch (replacement .type ) {
30+ case ' native' :
31+ return [
32+ ' package.replacement.native' ,
33+ {
34+ replacement: replacement .replacement ,
35+ nodeVersion: replacement .nodeVersion ,
36+ },
37+ ]
38+ case ' simple' :
39+ return [
40+ ' package.replacement.simple' ,
41+ {
42+ replacement: replacement .replacement ,
43+ },
44+ ]
45+ case ' documented' :
46+ return [' package.replacement.documented' , {}]
47+ default :
48+ return null
49+ }
50+ })
51+ const tooltipTrigger = shallowRef <HTMLElement | null >(null )
52+ const tooltipPosition = shallowRef ({ top: 0 , left: 0 })
53+ const hideTimeout = shallowRef <ReturnType <typeof setTimeout > | null >(null )
54+
55+ function updateTooltipPosition() {
56+ if (! tooltipTrigger .value ) return
57+ const rect = tooltipTrigger .value .getBoundingClientRect ()
58+ tooltipPosition .value = {
59+ top: rect .bottom + 8 ,
60+ left: rect .left + rect .width / 2 ,
61+ }
62+ }
63+
64+ function cancelHide() {
65+ if (hideTimeout .value ) {
66+ clearTimeout (hideTimeout .value )
67+ hideTimeout .value = null
68+ }
69+ }
70+
71+ function showNoDep(event : MouseEvent | FocusEvent ) {
72+ cancelHide ()
73+ tooltipTrigger .value = event .currentTarget as HTMLElement
74+ updateTooltipPosition ()
75+ activeTooltip .value = { type: ' nodep' , index: - 1 }
76+ }
77+
78+ function showReplacement(event : MouseEvent | FocusEvent , index : number ) {
79+ cancelHide ()
80+ tooltipTrigger .value = event .currentTarget as HTMLElement
81+ updateTooltipPosition ()
82+ activeTooltip .value = { type: ' replacement' , index }
83+ }
84+
85+ function hideTooltip() {
86+ hideTimeout .value = setTimeout (() => {
87+ activeTooltip .value = null
88+ }, 150 )
89+ }
90+
91+ function onTooltipEnter() {
92+ cancelHide ()
93+ }
94+
95+ function onTooltipLeave() {
96+ activeTooltip .value = null
97+ }
898 </script >
999
10100<template >
@@ -21,8 +111,44 @@ defineProps<{
21111 v-for =" (header, index) in headers"
22112 :key =" index"
23113 class =" comparison-cell comparison-cell-header"
114+ :class =" { 'comparison-cell-special': specialColumns?.[index] }"
24115 >
25- <span class =" font-mono text-sm font-medium text-fg truncate" :title =" header" >
116+ <!-- "No dep" header with tooltip (James easter egg) -->
117+ <button
118+ v-if =" specialColumns?.[index]"
119+ type =" button"
120+ class =" inline-flex items-center gap-1.5 cursor-help bg-transparent border-0 p-0"
121+ :aria-label =" $t('compare.no_dependency.label')"
122+ @mouseenter =" showNoDep"
123+ @mouseleave =" hideTooltip"
124+ @focus =" showNoDep"
125+ @blur =" hideTooltip"
126+ >
127+ <span class =" text-sm font-medium text-accent italic truncate" >
128+ {{ header }}
129+ </span >
130+ <span class =" i-carbon:help w-3 h-3 text-accent/60" aria-hidden =" true" />
131+ </button >
132+
133+ <!-- Package header with replacement info -->
134+ <button
135+ v-else-if =" replacements?.[index]"
136+ type =" button"
137+ class =" inline-flex items-center gap-1.5 cursor-help bg-transparent border-0 p-0"
138+ :aria-label =" $t('package.replacement.title')"
139+ @mouseenter =" showReplacement($event, index)"
140+ @mouseleave =" hideTooltip"
141+ @focus =" showReplacement($event, index)"
142+ @blur =" hideTooltip"
143+ >
144+ <span class =" font-mono text-sm font-medium text-fg truncate" :title =" header" >
145+ {{ header }}
146+ </span >
147+ <span class =" i-carbon:idea w-3.5 h-3.5 text-amber-500" aria-hidden =" true" />
148+ </button >
149+
150+ <!-- Regular package header (no replacement) -->
151+ <span v-else class =" font-mono text-sm font-medium text-fg truncate" :title =" header" >
26152 {{ header }}
27153 </span >
28154 </div >
@@ -31,6 +157,104 @@ defineProps<{
31157 <!-- Facet rows -->
32158 <slot />
33159 </div >
160+
161+ <!-- Tooltips (teleported to body to avoid overflow clipping) -->
162+ <Teleport to =" body" >
163+ <Transition
164+ enter-active-class =" transition-opacity duration-150"
165+ enter-from-class =" opacity-0"
166+ leave-active-class =" transition-opacity duration-100"
167+ leave-to-class =" opacity-0"
168+ >
169+ <!-- "No dep" tooltip -->
170+ <div
171+ v-if =" activeTooltip?.type === 'nodep'"
172+ role =" tooltip"
173+ class =" fixed z-[100] w-72 p-3 bg-bg-elevated border border-border rounded-lg shadow-lg text-start -translate-x-1/2"
174+ :style =" {
175+ top: `${tooltipPosition.top}px`,
176+ left: `${tooltipPosition.left}px`,
177+ }"
178+ @mouseenter =" onTooltipEnter"
179+ @mouseleave =" onTooltipLeave"
180+ >
181+ <div class =" flex gap-3" >
182+ <img
183+ src =" /43081j.png"
184+ alt =" James Garbutt"
185+ class =" w-12 h-12 rounded-lg object-cover flex-shrink-0"
186+ />
187+ <div class =" min-w-0" >
188+ <p class =" text-xs text-accent italic mb-0.5" >
189+ {{ $t('compare.no_dependency.james_says') }}
190+ </p >
191+ <p class =" text-sm font-medium text-fg mb-1" >
192+ {{ $t('package.replacement.title') }}
193+ </p >
194+ <p class =" text-xs text-fg-muted" >
195+ <i18n-t keypath =" compare.no_dependency.tooltip_description" tag =" span" >
196+ <template #link >
197+ <a
198+ href =" https://e18e.dev/docs/replacements/"
199+ target =" _blank"
200+ rel =" noopener noreferrer"
201+ class =" text-accent hover:underline"
202+ >{{ $t('compare.no_dependency.e18e_community') }}</a
203+ >
204+ </template >
205+ </i18n-t >
206+ </p >
207+ </div >
208+ </div >
209+ <!-- Tooltip arrow -->
210+ <div
211+ class =" absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-bg-elevated border-t border-l border-border rotate-45"
212+ />
213+ </div >
214+
215+ <!-- Replacement tooltip -->
216+ <div
217+ v-else-if =" activeReplacementMessage"
218+ role =" tooltip"
219+ class =" fixed z-[100] w-80 p-3 bg-bg-elevated border border-amber-600/30 rounded-lg shadow-lg text-start -translate-x-1/2"
220+ :style =" {
221+ top: `${tooltipPosition.top}px`,
222+ left: `${tooltipPosition.left}px`,
223+ }"
224+ @mouseenter =" onTooltipEnter"
225+ @mouseleave =" onTooltipLeave"
226+ >
227+ <div class =" flex items-start gap-2" >
228+ <span
229+ class =" i-carbon:idea w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5"
230+ aria-hidden =" true"
231+ />
232+ <div class =" min-w-0" >
233+ <p class =" text-sm font-medium text-fg mb-1" >
234+ {{ $t('package.replacement.title') }}
235+ </p >
236+ <p class =" text-xs text-fg-muted" >
237+ <i18n-t :keypath =" activeReplacementMessage[0]" tag =" span" >
238+ <template #replacement >
239+ {{ activeReplacementMessage[1].replacement ?? '' }}
240+ </template >
241+ <template #nodeVersion >
242+ {{ activeReplacementMessage[1].nodeVersion ?? '' }}
243+ </template >
244+ <template #community >
245+ {{ $t('package.replacement.community') }}
246+ </template >
247+ </i18n-t >
248+ </p >
249+ </div >
250+ </div >
251+ <!-- Tooltip arrow -->
252+ <div
253+ class =" absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-bg-elevated border-t border-l border-amber-600/30 rotate-45"
254+ />
255+ </div >
256+ </Transition >
257+ </Teleport >
34258 </div >
35259</template >
36260
@@ -68,6 +292,16 @@ defineProps<{
68292 text-align : center ;
69293}
70294
295+ /* "No dep" column styling */
296+ .comparison-header > .comparison-cell-header.comparison-cell-special {
297+ background : linear-gradient (
298+ 135deg ,
299+ var (--color-bg-subtle ) 0% ,
300+ color-mix(in srgb , var (--color-accent ) 8% , var (--color-bg-subtle )) 100%
301+ );
302+ border-bottom-color : color-mix(in srgb , var (--color-accent ) 30% , var (--color-border ));
303+ }
304+
71305/* First header cell rounded top-start */
72306.comparison-header > .comparison-cell-header :first-of-type {
73307 border-start-start-radius : 0.5rem ;
0 commit comments