Skip to content

Commit e7ac427

Browse files
committed
feat: allow copying comparison grid as markdown, translations
1 parent a661cb0 commit e7ac427

7 files changed

Lines changed: 122 additions & 5 deletions

File tree

app/pages/compare.vue

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ definePageMeta({
88
99
const router = useRouter()
1010
const canGoBack = useCanGoBack()
11+
const { copied, copy } = useClipboard({ copiedDuring: 2000 })
12+
const gridRef = useTemplateRef<HTMLDivElement>('gridRef')
1113
1214
// Sync packages with URL query param (stable ref - doesn't change on other query changes)
1315
const packagesParam = useRouteQuery<string>('packages', '', { mode: 'replace' })
@@ -79,6 +81,57 @@ const gridHeaders = computed(() =>
7981
gridColumns.value.map(col => (col.version ? `${col.name}@${col.version}` : col.name)),
8082
)
8183
84+
function copyComparisonGridAsMd() {
85+
const grid = gridRef.value?.querySelector('.comparison-grid')
86+
if (!grid) return
87+
const md = gridToMarkdown(grid as HTMLElement)
88+
copy(md)
89+
}
90+
91+
/*
92+
* Convert the comparison grid DOM to a Markdown table.
93+
* We build a proper HTML <table> from the grid structure and delegate to `htmlToMarkdown` for the actual conversion.
94+
*/
95+
function gridToMarkdown(gridEl: HTMLElement): string {
96+
const children = Array.from(gridEl.children)
97+
const headerRow = children[0]
98+
const dataRows = children.slice(1)
99+
100+
if (!headerRow || dataRows.length === 0) return ''
101+
102+
const headerCells = Array.from(headerRow.children).slice(1)
103+
if (headerCells.length === 0) return ''
104+
105+
const ths = headerCells.map(cell => {
106+
const link = cell.querySelector('a')
107+
if (link) {
108+
const href = link.getAttribute('href') || ''
109+
const absoluteHref = /^https?:\/\/|^\/\//.test(href) ? href : `${NPMX_SITE}${href}`
110+
return `<th><a href="${escapeHtml(absoluteHref)}">${escapeHtml(link.textContent?.trim() || '')}</a></th>`
111+
}
112+
return `<th>${escapeHtml(cell.textContent?.trim() || '')}</th>`
113+
})
114+
115+
const trs = dataRows.map(row => {
116+
const rowChildren = Array.from(row.children)
117+
const label = rowChildren[0]?.textContent?.trim() || ''
118+
const valueCells = rowChildren.slice(1)
119+
const tds = [label, ...valueCells.map(cell => cell.textContent?.trim() || '-')]
120+
.map(v => `<td>${escapeHtml(v)}</td>`)
121+
.join('')
122+
return `<tr>${tds}</tr>`
123+
})
124+
125+
const tableHtml = [
126+
'<table>',
127+
`<thead><tr><th>Metric</th>${ths.join('')}</tr></thead>`,
128+
`<tbody>${trs.join('')}</tbody>`,
129+
'</table>',
130+
].join('')
131+
132+
return htmlToMarkdown(tableHtml, { tablePipeAlign: false })
133+
}
134+
82135
useSeoMeta({
83136
title: () =>
84137
packages.value.length > 0
@@ -193,9 +246,30 @@ useSeoMeta({
193246

194247
<!-- Comparison grid -->
195248
<section v-if="canCompare" class="mt-10" aria-labelledby="comparison-heading">
196-
<h2 id="comparison-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-4">
197-
{{ $t('compare.packages.section_comparison') }}
198-
</h2>
249+
<div class="relative group mb-4 inline-block">
250+
<h2 id="comparison-heading" class="text-xs text-fg-subtle uppercase tracking-wider">
251+
{{ $t('compare.packages.section_comparison') }}
252+
</h2>
253+
254+
<button
255+
v-if="packagesData && packagesData.some(p => p !== null)"
256+
type="button"
257+
class="absolute z-20 inset-is-0 top-full hidden md:inline-flex items-center gap-1 px-2 py-1 rounded border text-xs font-mono whitespace-nowrap transition-all duration-150 opacity-0 -translate-y-1 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto focus-visible:opacity-100 focus-visible:translate-y-0 focus-visible:pointer-events-auto"
258+
:class="[
259+
$style.copyButton,
260+
copied ? 'text-accent bg-accent/10' : 'text-fg-muted bg-bg border-border',
261+
]"
262+
:aria-label="copied ? $t('common.copied') : $t('compare.packages.copy_as_markdown')"
263+
@click="copyComparisonGridAsMd"
264+
>
265+
<span
266+
:class="copied ? 'i-lucide:check' : 'i-lucide:copy'"
267+
class="w-3.5 h-3.5"
268+
aria-hidden="true"
269+
/>
270+
{{ copied ? $t('common.copied') : $t('compare.packages.copy_as_markdown') }}
271+
</button>
272+
</div>
199273

200274
<div
201275
v-if="
@@ -209,7 +283,7 @@ useSeoMeta({
209283

210284
<div v-else-if="packagesData && packagesData.some(p => p !== null)">
211285
<!-- Desktop: Grid layout -->
212-
<div class="hidden md:block overflow-x-auto">
286+
<div ref="gridRef" class="hidden md:block overflow-x-auto">
213287
<CompareComparisonGrid :columns="gridColumns" :show-no-dependency="showNoDependency">
214288
<CompareFacetRow
215289
v-for="facet in selectedFacets"
@@ -241,7 +315,7 @@ useSeoMeta({
241315
</div>
242316

243317
<h2
244-
id="comparison-heading"
318+
id="trends-comparison-heading"
245319
class="text-xs text-fg-subtle uppercase tracking-wider mb-4 mt-10"
246320
>
247321
{{ $t('compare.facets.trends.title') }}
@@ -277,3 +351,38 @@ useSeoMeta({
277351
</div>
278352
</main>
279353
</template>
354+
355+
<style module>
356+
.copyButton {
357+
clip: rect(0 0 0 0);
358+
clip-path: inset(50%);
359+
height: 1px;
360+
overflow: hidden;
361+
width: 1px;
362+
transition:
363+
opacity 0.25s 0.1s,
364+
translate 0.15s 0.1s,
365+
clip 0.01s 0.34s allow-discrete,
366+
clip-path 0.01s 0.34s allow-discrete,
367+
height 0.01s 0.34s allow-discrete,
368+
width 0.01s 0.34s allow-discrete;
369+
}
370+
371+
:global(.group):hover .copyButton,
372+
.copyButton:focus-visible {
373+
clip: auto;
374+
clip-path: none;
375+
height: auto;
376+
overflow: visible;
377+
width: auto;
378+
transition:
379+
opacity 0.15s,
380+
translate 0.15s;
381+
}
382+
383+
@media (hover: none) {
384+
.copyButton {
385+
display: none;
386+
}
387+
}
388+
</style>

i18n/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,7 @@
910910
"section_packages": "Packages",
911911
"section_facets": "Facets",
912912
"section_comparison": "Comparison",
913+
"copy_as_markdown": "Copy table",
913914
"loading": "Loading package data...",
914915
"error": "Failed to load package data. Please try again.",
915916
"empty_title": "Select packages to compare",

i18n/locales/pl-PL.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,7 @@
910910
"section_packages": "Pakiety",
911911
"section_facets": "Aspekty",
912912
"section_comparison": "Porównanie",
913+
"copy_as_markdown": "Kopiuj tabele",
913914
"loading": "Ładowanie danych pakietów...",
914915
"error": "Nie udało się wczytać danych pakietów. Spróbuj ponownie.",
915916
"empty_title": "Wybierz pakiety do porównania",

i18n/schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2734,6 +2734,9 @@
27342734
"section_comparison": {
27352735
"type": "string"
27362736
},
2737+
"copy_as_markdown": {
2738+
"type": "string"
2739+
},
27372740
"loading": {
27382741
"type": "string"
27392742
},

lunaria/files/en-GB.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,7 @@
909909
"section_packages": "Packages",
910910
"section_facets": "Facets",
911911
"section_comparison": "Comparison",
912+
"copy_as_markdown": "Copy table",
912913
"loading": "Loading package data...",
913914
"error": "Failed to load package data. Please try again.",
914915
"empty_title": "Select packages to compare",

lunaria/files/en-US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,7 @@
909909
"section_packages": "Packages",
910910
"section_facets": "Facets",
911911
"section_comparison": "Comparison",
912+
"copy_as_markdown": "Copy table",
912913
"loading": "Loading package data...",
913914
"error": "Failed to load package data. Please try again.",
914915
"empty_title": "Select packages to compare",

lunaria/files/pl-PL.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,7 @@
909909
"section_packages": "Pakiety",
910910
"section_facets": "Aspekty",
911911
"section_comparison": "Porównanie",
912+
"copy_as_markdown": "Kopiuj tabele",
912913
"loading": "Ładowanie danych pakietów...",
913914
"error": "Nie udało się wczytać danych pakietów. Spróbuj ponownie.",
914915
"empty_title": "Wybierz pakiety do porównania",

0 commit comments

Comments
 (0)