Skip to content

Commit 9342f6c

Browse files
authored
fix: refine compare grid header layout (#2072)
1 parent 59b67e2 commit 9342f6c

File tree

5 files changed

+77
-62
lines changed

5 files changed

+77
-62
lines changed

app/components/Compare/ComparisonGrid.vue

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const props = defineProps<{
1717
1818
/** Total column count including the optional no-dep column */
1919
const totalColumns = computed(() => props.columns.length + (props.showNoDependency ? 1 : 0))
20+
const visibleColumns = computed(() => Math.min(totalColumns.value, 4))
2021
2122
/** Compute plain-text tooltip for a replacement column */
2223
function getReplacementTooltip(col: ComparisonGridColumn): string {
@@ -30,32 +31,43 @@ function getReplacementTooltip(col: ComparisonGridColumn): string {
3031
<div class="overflow-x-auto">
3132
<div
3233
class="comparison-grid"
33-
:class="[totalColumns === 4 ? 'min-w-[800px]' : 'min-w-[600px]', `columns-${totalColumns}`]"
34-
:style="{ '--columns': totalColumns }"
34+
:style="{
35+
'--package-count': totalColumns,
36+
'--visible-columns': visibleColumns,
37+
}"
3538
>
3639
<!-- Header row -->
3740
<div class="comparison-header">
38-
<div class="comparison-label" />
41+
<div class="comparison-label relative bg-bg" />
3942

4043
<!-- Package columns -->
41-
<div v-for="col in columns" :key="col.name" class="comparison-cell comparison-cell-header">
42-
<span class="inline-flex items-center gap-1.5 truncate">
44+
<div
45+
v-for="col in columns"
46+
:key="col.name"
47+
class="comparison-cell comparison-cell-header min-w-0"
48+
>
49+
<div class="flex items-start justify-center gap-1.5 min-w-0">
4350
<LinkBase
4451
:to="packageRoute(col.name, col.version)"
45-
class="text-sm truncate"
46-
block
52+
class="flex min-w-0 flex-col items-center text-center text-sm"
4753
:title="col.version ? `${col.name}@${col.version}` : col.name"
4854
>
49-
{{ col.name }}<template v-if="col.version">@{{ col.version }}</template>
55+
<span class="min-w-0 break-words line-clamp-1">
56+
{{ col.name }}
57+
</span>
58+
<span v-if="col.version" class="text-fg-muted line-clamp-1">
59+
@{{ col.version }}
60+
</span>
5061
</LinkBase>
62+
5163
<TooltipApp v-if="col.replacement" :text="getReplacementTooltip(col)" position="bottom">
5264
<span
53-
class="i-lucide:lightbulb w-3.5 h-3.5 text-amber-500 shrink-0 cursor-help"
65+
class="i-lucide:lightbulb mt-0.5 h-3.5 w-3.5 shrink-0 cursor-help text-amber-500"
5466
role="img"
5567
:aria-label="$t('package.replacement.title')"
5668
/>
5769
</TooltipApp>
58-
</span>
70+
</div>
5971
</div>
6072

6173
<!-- "No dep" column (always last) -->
@@ -100,29 +112,30 @@ function getReplacementTooltip(col: ComparisonGridColumn): string {
100112

101113
<style scoped>
102114
.comparison-grid {
115+
--label-column-width: 140px;
116+
--package-column-width: calc((100% - var(--label-column-width)) / var(--visible-columns));
103117
display: grid;
104118
gap: 0;
105-
}
106-
107-
.comparison-grid.columns-2 {
108-
grid-template-columns: minmax(120px, 180px) repeat(2, 1fr);
109-
}
110-
111-
.comparison-grid.columns-3 {
112-
grid-template-columns: minmax(120px, 160px) repeat(3, 1fr);
113-
}
114-
115-
.comparison-grid.columns-4 {
116-
grid-template-columns: minmax(100px, 140px) repeat(4, 1fr);
119+
grid-template-columns:
120+
var(--label-column-width)
121+
repeat(var(--package-count), minmax(var(--package-column-width), var(--package-column-width)));
117122
}
118123
119124
.comparison-header {
120125
display: contents;
121126
}
122127
123128
.comparison-header > .comparison-label {
124-
padding: 0.75rem 1rem;
125-
border-bottom: 1px solid var(--color-border);
129+
z-index: 3;
130+
}
131+
132+
.comparison-label {
133+
position: sticky;
134+
left: 0;
135+
z-index: 2;
136+
inline-size: var(--label-column-width);
137+
min-inline-size: var(--label-column-width);
138+
isolation: isolate;
126139
}
127140
128141
.comparison-header > .comparison-cell-header {

app/components/Compare/FacetRow.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ function isCellLoading(index: number): boolean {
8888
<template>
8989
<div class="contents">
9090
<!-- Label cell -->
91-
<div class="comparison-label flex items-center gap-1.5 px-4 py-3 border-b border-border">
91+
<div
92+
class="comparison-label relative bg-bg flex items-center gap-1.5 px-4 py-3 border-b border-border"
93+
>
9294
<span class="text-xs text-fg-muted uppercase tracking-wider">{{ label }}</span>
9395
<TooltipApp v-if="description" :text="description" position="top">
9496
<span class="i-lucide:info w-3 h-3 text-fg-subtle cursor-help" aria-hidden="true" />
@@ -151,3 +153,13 @@ function isCellLoading(index: number): boolean {
151153
</div>
152154
</div>
153155
</template>
156+
157+
<style lang="css" scoped>
158+
.comparison-label {
159+
position: sticky;
160+
left: 0;
161+
z-index: 2;
162+
inline-size: var(--label-column-width);
163+
min-inline-size: var(--label-column-width);
164+
}
165+
</style>

app/pages/compare.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const { locale } = useI18n()
1111
const router = useRouter()
1212
const canGoBack = useCanGoBack()
1313
const { copied, copy } = useClipboard({ copiedDuring: 2000 })
14+
const maxPackages = 4
1415
1516
// Sync packages with URL query param (stable ref - doesn't change on other query changes)
1617
const packagesParam = useRouteQuery<string>('packages', '', { mode: 'replace' })
@@ -23,7 +24,7 @@ const packages = computed({
2324
.split(',')
2425
.map(p => p.trim())
2526
.filter(p => p.length > 0)
26-
.slice(0, 4)
27+
.slice(0, maxPackages)
2728
},
2829
set(value) {
2930
packagesParam.value = value.length > 0 ? value.join(',') : ''
@@ -61,12 +62,12 @@ const gridColumns = computed(() =>
6162
6263
// Whether we can add the no-dep column (not already added and have room)
6364
const canAddNoDep = computed(
64-
() => packages.value.length < 4 && !packages.value.includes(NO_DEPENDENCY_ID),
65+
() => packages.value.length < maxPackages && !packages.value.includes(NO_DEPENDENCY_ID),
6566
)
6667
6768
// Add "no dependency" column to comparison
6869
function addNoDep() {
69-
if (packages.value.length >= 4) return
70+
if (packages.value.length >= maxPackages) return
7071
if (packages.value.includes(NO_DEPENDENCY_ID)) return
7172
packages.value = [...packages.value, NO_DEPENDENCY_ID]
7273
}
@@ -191,7 +192,7 @@ useSeoMeta({
191192
<h2 id="packages-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
192193
{{ $t('compare.packages.section_packages') }}
193194
</h2>
194-
<ComparePackageSelector v-model="packages" :max="4" />
195+
<ComparePackageSelector v-model="packages" :max="maxPackages" />
195196

196197
<!-- "No dep" replacement suggestions (native, simple) -->
197198
<div v-if="noDepSuggestions.length > 0" class="mt-3 space-y-2">

test/e2e/interactions.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ test.describe('Compare Page', () => {
3838
await expect(page).toHaveURL(/packages=vue,nuxt,__no_dependency__/)
3939

4040
// Verify column order in the grid: vue, nuxt, then no-dep
41-
const headerLinks = grid.locator('.comparison-cell-header a.truncate')
41+
const headerLinks = grid.locator('.comparison-cell-header a[title]')
4242
await expect(headerLinks).toHaveCount(2)
4343
await expect(headerLinks.nth(0)).toContainText('vue')
4444
await expect(headerLinks.nth(1)).toContainText('nuxt')

test/nuxt/components/compare/ComparisonGrid.spec.ts

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ describe('ComparisonGrid', () => {
1717
columns: cols('lodash@4.17.21', 'underscore@1.13.6'),
1818
},
1919
})
20-
expect(component.text()).toContain('lodash@4.17.21')
21-
expect(component.text()).toContain('underscore@1.13.6')
20+
expect(component.text()).toContain('lodash')
21+
expect(component.text()).toContain('@4.17.21')
22+
expect(component.text()).toContain('underscore')
23+
expect(component.text()).toContain('@1.13.6')
2224
})
2325

2426
it('renders correct number of header cells', async () => {
@@ -43,74 +45,61 @@ describe('ComparisonGrid', () => {
4345
expect(component.find('.comparison-cell-nodep').exists()).toBe(true)
4446
})
4547

46-
it('truncates long header text with title attribute', async () => {
48+
it('renders package name and version on separate clamped lines with a full title attribute', async () => {
4749
const longName = 'very-long-package-name@1.0.0-beta.1'
4850
const component = await mountSuspended(ComparisonGrid, {
4951
props: {
5052
columns: cols(longName, 'short'),
5153
},
5254
})
53-
const links = component.findAll('a.truncate')
54-
const longLink = links.find(a => a.text() === longName)
55-
expect(longLink?.attributes('title')).toBe(longName)
55+
56+
const link = component.find(`a[title="${longName}"]`)
57+
expect(link.exists()).toBe(true)
58+
expect(link.attributes('title')).toBe(longName)
59+
expect(link.findAll('.line-clamp-1')).toHaveLength(2)
5660
})
5761
})
5862

5963
describe('column layout', () => {
60-
it('applies columns-2 class for 2 columns', async () => {
64+
it('sets --package-count to the number of package columns', async () => {
6165
const component = await mountSuspended(ComparisonGrid, {
6266
props: {
6367
columns: cols('a', 'b'),
6468
},
6569
})
66-
expect(component.find('.columns-2').exists()).toBe(true)
70+
const grid = component.find('.comparison-grid')
71+
expect(grid.attributes('style')).toContain('--package-count: 2')
6772
})
6873

69-
it('applies columns-3 class for 2 packages + no-dep', async () => {
74+
it('includes the no-dependency column in --package-count', async () => {
7075
const component = await mountSuspended(ComparisonGrid, {
7176
props: {
7277
columns: cols('a', 'b'),
7378
showNoDependency: true,
7479
},
7580
})
76-
expect(component.find('.columns-3').exists()).toBe(true)
77-
})
78-
79-
it('applies columns-4 class for 4 columns', async () => {
80-
const component = await mountSuspended(ComparisonGrid, {
81-
props: {
82-
columns: cols('a', 'b', 'c', 'd'),
83-
},
84-
})
85-
expect(component.find('.columns-4').exists()).toBe(true)
81+
const grid = component.find('.comparison-grid')
82+
expect(grid.attributes('style')).toContain('--package-count: 3')
8683
})
8784

88-
it('sets min-width for 4 columns to 800px', async () => {
85+
it('supports four package columns with the generic grid layout', async () => {
8986
const component = await mountSuspended(ComparisonGrid, {
9087
props: {
9188
columns: cols('a', 'b', 'c', 'd'),
9289
},
9390
})
94-
expect(component.find('.min-w-\\[800px\\]').exists()).toBe(true)
95-
})
96-
97-
it('sets min-width for 2-3 columns to 600px', async () => {
98-
const component = await mountSuspended(ComparisonGrid, {
99-
props: {
100-
columns: cols('a', 'b'),
101-
},
102-
})
103-
expect(component.find('.min-w-\\[600px\\]').exists()).toBe(true)
91+
const grid = component.find('.comparison-grid')
92+
expect(grid.attributes('style')).toContain('--package-count: 4')
10493
})
10594

106-
it('sets --columns CSS variable', async () => {
95+
it('sets --package-count CSS variable', async () => {
10796
const component = await mountSuspended(ComparisonGrid, {
10897
props: {
10998
columns: cols('a', 'b', 'c'),
11099
},
111100
})
112101
const grid = component.find('.comparison-grid')
113-
expect(grid.attributes('style')).toContain('--columns: 3')
102+
expect(grid.attributes('style')).toContain('--package-count: 3')
114103
})
115104
})
116105

0 commit comments

Comments
 (0)