|
1 | 1 | <script setup lang="ts"> |
2 | 2 | import type { ModuleReplacement } from 'module-replacements' |
3 | 3 |
|
4 | | -const props = defineProps<{ |
5 | | - /** Number of columns (2-4) */ |
6 | | - columns: number |
7 | | - /** Column headers (package names or version numbers) */ |
8 | | - 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)[] |
13 | | -}>() |
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 | | - } |
| 4 | +export interface ComparisonGridColumn { |
| 5 | + /** Display text (e.g. "lodash@4.17.21") */ |
| 6 | + header: string |
| 7 | + /** Module replacement data for this package (if available) */ |
| 8 | + replacement?: ModuleReplacement | null |
69 | 9 | } |
70 | 10 |
|
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 | | -} |
| 11 | +const props = defineProps<{ |
| 12 | + /** Column definitions for each package being compared */ |
| 13 | + columns: ComparisonGridColumn[] |
| 14 | + /** Whether to show the "no dependency" baseline as the last column */ |
| 15 | + showNoDependency?: boolean |
| 16 | +}>() |
84 | 17 |
|
85 | | -function hideTooltip() { |
86 | | - hideTimeout.value = setTimeout(() => { |
87 | | - activeTooltip.value = null |
88 | | - }, 150) |
89 | | -} |
| 18 | +/** Total column count including the optional no-dep column */ |
| 19 | +const totalColumns = computed(() => props.columns.length + (props.showNoDependency ? 1 : 0)) |
90 | 20 |
|
91 | | -function onTooltipEnter() { |
92 | | - cancelHide() |
93 | | -} |
| 21 | +/** Compute plain-text tooltip for a replacement column */ |
| 22 | +function getReplacementTooltip(col: ComparisonGridColumn): string { |
| 23 | + if (!col.replacement) return '' |
94 | 24 |
|
95 | | -function onTooltipLeave() { |
96 | | - activeTooltip.value = null |
| 25 | + return [$t('package.replacement.title'), $t('package.replacement.learn_more_above')].join(' ') |
97 | 26 | } |
98 | 27 | </script> |
99 | 28 |
|
100 | 29 | <template> |
101 | 30 | <div class="overflow-x-auto"> |
102 | 31 | <div |
103 | 32 | class="comparison-grid" |
104 | | - :class="[columns === 4 ? 'min-w-[800px]' : 'min-w-[600px]', `columns-${columns}`]" |
105 | | - :style="{ '--columns': columns }" |
| 33 | + :class="[totalColumns === 4 ? 'min-w-[800px]' : 'min-w-[600px]', `columns-${totalColumns}`]" |
| 34 | + :style="{ '--columns': totalColumns }" |
106 | 35 | > |
107 | 36 | <!-- Header row --> |
108 | 37 | <div class="comparison-header"> |
109 | 38 | <div class="comparison-label" /> |
| 39 | + |
| 40 | + <!-- Package columns --> |
110 | 41 | <div |
111 | | - v-for="(header, index) in headers" |
112 | | - :key="index" |
| 42 | + v-for="col in columns" |
| 43 | + :key="col.header" |
113 | 44 | class="comparison-cell comparison-cell-header" |
114 | | - :class="{ 'comparison-cell-special': specialColumns?.[index] }" |
115 | 45 | > |
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> |
| 46 | + <span class="inline-flex items-center gap-1.5 truncate"> |
| 47 | + <NuxtLink |
| 48 | + :to="`/package/${col.header}`" |
| 49 | + class="link-subtle font-mono text-sm font-medium text-fg truncate" |
| 50 | + :title="col.header" |
| 51 | + > |
| 52 | + {{ col.header }} |
| 53 | + </NuxtLink> |
| 54 | + <TooltipApp v-if="col.replacement" :text="getReplacementTooltip(col)" position="bottom"> |
| 55 | + <span |
| 56 | + class="i-carbon:idea w-3.5 h-3.5 text-amber-500 shrink-0 cursor-help" |
| 57 | + role="img" |
| 58 | + :aria-label="$t('package.replacement.title')" |
| 59 | + /> |
| 60 | + </TooltipApp> |
| 61 | + </span> |
| 62 | + </div> |
149 | 63 |
|
150 | | - <!-- Regular package header (no replacement) --> |
151 | | - <NuxtLink |
152 | | - v-else |
153 | | - :to="`/package/${header}`" |
154 | | - class="link-subtle font-mono text-sm font-medium text-fg truncate" |
155 | | - :title="header" |
| 64 | + <!-- "No dep" column (always last) --> |
| 65 | + <div |
| 66 | + v-if="showNoDependency" |
| 67 | + class="comparison-cell comparison-cell-header comparison-cell-nodep" |
| 68 | + > |
| 69 | + <span |
| 70 | + class="inline-flex items-center gap-1.5 text-sm font-medium text-accent italic truncate" |
156 | 71 | > |
157 | | - {{ header }} |
158 | | - </NuxtLink> |
| 72 | + {{ $t('compare.no_dependency.label') }} |
| 73 | + <TooltipApp interactive position="bottom"> |
| 74 | + <span |
| 75 | + class="i-carbon:idea w-3.5 h-3.5 text-amber-500 shrink-0 cursor-help" |
| 76 | + role="img" |
| 77 | + :aria-label="$t('package.replacement.title')" |
| 78 | + /> |
| 79 | + <template #content> |
| 80 | + <p class="text-sm font-medium text-fg mb-1"> |
| 81 | + {{ $t('compare.no_dependency.tooltip_title') }} |
| 82 | + </p> |
| 83 | + <p class="text-xs text-fg-muted"> |
| 84 | + <i18n-t keypath="compare.no_dependency.tooltip_description" tag="span"> |
| 85 | + <template #link> |
| 86 | + <a |
| 87 | + href="https://e18e.dev/docs/replacements/" |
| 88 | + target="_blank" |
| 89 | + rel="noopener noreferrer" |
| 90 | + class="text-accent hover:underline" |
| 91 | + >{{ $t('compare.no_dependency.e18e_community') }}</a |
| 92 | + > |
| 93 | + </template> |
| 94 | + </i18n-t> |
| 95 | + </p> |
| 96 | + </template> |
| 97 | + </TooltipApp> |
| 98 | + </span> |
159 | 99 | </div> |
160 | 100 | </div> |
161 | 101 |
|
162 | 102 | <!-- Facet rows --> |
163 | 103 | <slot /> |
164 | 104 | </div> |
165 | | - |
166 | | - <!-- Tooltips (teleported to body to avoid overflow clipping) --> |
167 | | - <Teleport to="body"> |
168 | | - <Transition |
169 | | - enter-active-class="transition-opacity duration-150" |
170 | | - enter-from-class="opacity-0" |
171 | | - leave-active-class="transition-opacity duration-100" |
172 | | - leave-to-class="opacity-0" |
173 | | - > |
174 | | - <!-- "No dep" tooltip --> |
175 | | - <div |
176 | | - v-if="activeTooltip?.type === 'nodep'" |
177 | | - role="tooltip" |
178 | | - class="fixed z-[100] w-72 p-3 bg-bg-elevated border border-border rounded-lg shadow-lg text-start -translate-x-1/2" |
179 | | - :style="{ |
180 | | - top: `${tooltipPosition.top}px`, |
181 | | - left: `${tooltipPosition.left}px`, |
182 | | - }" |
183 | | - @mouseenter="onTooltipEnter" |
184 | | - @mouseleave="onTooltipLeave" |
185 | | - > |
186 | | - <div class="flex gap-3"> |
187 | | - <img |
188 | | - src="/43081j.png" |
189 | | - alt="James Garbutt" |
190 | | - class="w-12 h-12 rounded-lg object-cover flex-shrink-0" |
191 | | - /> |
192 | | - <div class="min-w-0"> |
193 | | - <p class="text-xs text-accent italic mb-0.5"> |
194 | | - {{ $t('compare.no_dependency.james_says') }} |
195 | | - </p> |
196 | | - <p class="text-sm font-medium text-fg mb-1"> |
197 | | - {{ $t('package.replacement.title') }} |
198 | | - </p> |
199 | | - <p class="text-xs text-fg-muted"> |
200 | | - <i18n-t keypath="compare.no_dependency.tooltip_description" tag="span"> |
201 | | - <template #link> |
202 | | - <a |
203 | | - href="https://e18e.dev/docs/replacements/" |
204 | | - target="_blank" |
205 | | - rel="noopener noreferrer" |
206 | | - class="text-accent hover:underline" |
207 | | - >{{ $t('compare.no_dependency.e18e_community') }}</a |
208 | | - > |
209 | | - </template> |
210 | | - </i18n-t> |
211 | | - </p> |
212 | | - </div> |
213 | | - </div> |
214 | | - <!-- Tooltip arrow --> |
215 | | - <div |
216 | | - 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" |
217 | | - /> |
218 | | - </div> |
219 | | - |
220 | | - <!-- Replacement tooltip --> |
221 | | - <div |
222 | | - v-else-if="activeReplacementMessage" |
223 | | - role="tooltip" |
224 | | - 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" |
225 | | - :style="{ |
226 | | - top: `${tooltipPosition.top}px`, |
227 | | - left: `${tooltipPosition.left}px`, |
228 | | - }" |
229 | | - @mouseenter="onTooltipEnter" |
230 | | - @mouseleave="onTooltipLeave" |
231 | | - > |
232 | | - <div class="flex items-start gap-2"> |
233 | | - <span |
234 | | - class="i-carbon:idea w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" |
235 | | - aria-hidden="true" |
236 | | - /> |
237 | | - <div class="min-w-0"> |
238 | | - <p class="text-sm font-medium text-fg mb-1"> |
239 | | - {{ $t('package.replacement.title') }} |
240 | | - </p> |
241 | | - <p class="text-xs text-fg-muted"> |
242 | | - <i18n-t :keypath="activeReplacementMessage[0]" tag="span"> |
243 | | - <template #replacement> |
244 | | - {{ activeReplacementMessage[1].replacement ?? '' }} |
245 | | - </template> |
246 | | - <template #nodeVersion> |
247 | | - {{ activeReplacementMessage[1].nodeVersion ?? '' }} |
248 | | - </template> |
249 | | - <template #community> |
250 | | - {{ $t('package.replacement.community') }} |
251 | | - </template> |
252 | | - </i18n-t> |
253 | | - </p> |
254 | | - </div> |
255 | | - </div> |
256 | | - <!-- Tooltip arrow --> |
257 | | - <div |
258 | | - 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" |
259 | | - /> |
260 | | - </div> |
261 | | - </Transition> |
262 | | - </Teleport> |
263 | 105 | </div> |
264 | 106 | </template> |
265 | 107 |
|
@@ -298,7 +140,7 @@ function onTooltipLeave() { |
298 | 140 | } |
299 | 141 |
|
300 | 142 | /* "No dep" column styling */ |
301 | | -.comparison-header > .comparison-cell-header.comparison-cell-special { |
| 143 | +.comparison-header > .comparison-cell-header.comparison-cell-nodep { |
302 | 144 | background: linear-gradient( |
303 | 145 | 135deg, |
304 | 146 | var(--color-bg-subtle) 0%, |
|
0 commit comments