Skip to content

feat: copy compare table as markdown - #1533

Merged
danielroe merged 13 commits into
npmx-dev:mainfrom
mikouaji:feat/copy-compare-table-as-md-string
Feb 24, 2026
Merged

feat: copy compare table as markdown#1533
danielroe merged 13 commits into
npmx-dev:mainfrom
mikouaji:feat/copy-compare-table-as-md-string

Conversation

@mikouaji

@mikouajimikouaji commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Implements #1515

  1. Added ability to copy the comparison table as markdown. Had to parse css grid into a table.
  2. Added new util function to parse any html element to markdown.
  3. Extracted the copy button as a separate component.
  4. Moved some imports, added translations and packages.

Packages used for html to markdown util are dependencies that already were used in the project.

How it work:

Screencast_20260216_192439.mp4

@vercel

vercelBot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmx.devReadyReadyPreview, CommentFeb 23, 2026 3:22pm
2 Skipped Deployments
ProjectDeploymentActionsUpdated (UTC)
docs.npmx.devIgnoredIgnoredPreviewFeb 23, 2026 3:22pm
npmx-lunariaIgnoredIgnoredFeb 23, 2026 3:22pm

Request Review

@github-actions

github-actionsBot commented Feb 16, 2026

Copy link
Copy Markdown

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

FileNote
lunaria/files/en-GB.jsonLocalization changed, will be marked as complete. 🔄️
lunaria/files/en-US.jsonSource changed, localizations will be marked as outdated.
lunaria/files/pl-PL.jsonLocalization changed, will be marked as complete. 🔄️
Warnings reference
IconDescription
🔄️The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@codecov

codecovBot commented Feb 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.33333% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing linesPatch %Lines
app/components/CopyToClipboardButton.vue84.61%2 Missing ⚠️
app/pages/package/[[org]]/[name].vue0.00%0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitaiBot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Vue component CopyToClipboardButton (script setup, TypeScript) and replaces several ad-hoc floating copy buttons with it across the site. Adds exportComparisonDataAsMarkdown() to build a Markdown table from the comparison grid and copy it to the clipboard, and wraps the comparison header with CopyToClipboardButton. Introduces i18n keys/schema entries for copy_as_markdown and updates lunaria locale files. Adds accessibility tests for CopyToClipboardButton and removes the previous floating copy-button styles/markup on the package page.

Possibly related PRs

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check nameStatusExplanation
Description check✅ PassedThe pull request description clearly relates to the changeset, describing the implementation of issue #1515 for copying comparison tables as Markdown, extracting components, and adding translations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
server/utils/docs/text.ts (1)

121-124: Avoid the non‑null assertion on array indexing.

The guideline calls for explicit checks when indexing arrays; this avoids ! and keeps the code strictly type‑safe.

♻️ Suggested fix
- for (let i = 0; i < codeBlockData.length; i++) {- const { lang, code } = codeBlockData[i]!+ for (let i = 0; i < codeBlockData.length; i++) {+ const entry = codeBlockData[i]+ if (!entry) continue+ const { lang, code } = entry
const highlighted = await highlightCodeBlock(code, lang)
result = result.replace(`__CODE_BLOCK_${i}__`, highlighted)
}

As per coding guidelines, "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".

app/pages/compare.vue (1)

95-100: Consider adding a type guard for safer array access.

Accessing children[0] at line 97 could return undefined if the grid has no children. While line 100 handles the falsy case, TypeScript may not narrow the type correctly. The current logic works but could be made more explicit.

Proposed defensive check
 function gridToMarkdown(gridEl: HTMLElement): string {
const children = Array.from(gridEl.children)
- const headerRow = children[0]+ const headerRow = children[0] as Element | undefined
const dataRows = children.slice(1)
if (!headerRow || dataRows.length === 0) return ''
app/components/CopyToClipboardButton.vue (1)

51-83: Consider adding graceful degradation for older browsers without allow-discrete support.

The allow-discrete transitions are well-supported across modern browsers (Chrome 117+, Safari 17.4+, Firefox 129+), but this is a feature projected to reach "Baseline widely available" by February 2027. For enhanced compatibility with older browser versions, consider either removing allow-discrete from production builds or implementing a progressive enhancement strategy. Without it, transitions become discrete jumps rather than smooth animations—the component remains functional, but the visual refinement is lost.

Comment threadapp/pages/compare.vue Outdated
Comment threadi18n/locales/pl-PL.json Outdated

@danielroedanielroe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although these were already dependencies, I don't think any of them were used in the client.

I think there must be a better way to render the table to markdown without converting from html - as we have the source for that html, the most complex piece here would only be padding to ensure the columns are the same width

@mikouaji

Copy link
Copy Markdown
ContributorAuthor

although these were already dependencies, I don't think any of them were used in the client.

I think there must be a better way to render the table to markdown without converting from html - as we have the source for that html, the most complex piece here would only be padding to ensure the columns are the same width

I see no issue with changing this and can update the code to just generate markdown from the client side data without doing the entire html(grid -> table) -> markdown conversion. It will be simpler.

My idea was here to create a function we could re-use in the future while allowing this functionality as the first one using it.

@danielroe

Copy link
Copy Markdown
Member

thank you ❤️

My idea was here to create a function we could re-use in the future while allowing this functionality as the first one using it.

let's wait until we see if we need it in other cases - we can refer back to this PR for implementation if so. 🙏

@mikouaji

Copy link
Copy Markdown
ContributorAuthor

100%, will update once I have a moment :)

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
lunaria/files/en-US.json (1)

915-915: Optional: consider aligning the label with the existing copy_as_markdown pattern.

The sibling key package.readme.copy_as_markdown (line 249) uses the value "Copy README as Markdown", which tells the user about the output format. The new compare.packages.copy_as_markdown value "Copy table" omits any mention of Markdown, so users pasting into a plain-text editor may be surprised by the raw Markdown syntax.

"Copy table as Markdown" (or simply "Copy as Markdown") would be consistent with the existing pattern and make the output format explicit. This should be addressed in the base i18n/locales/en.json as well, since lunaria/files/en-US.json mirrors it.

@mikouaji

Copy link
Copy Markdown
ContributorAuthor

@danielroe updated

Since you mentioned padding, do you think the link in the package headers should be removed? I padded only to names.

| | [vue@3.5.28](https://npmx.dev/package/vue/v/3.5.28) | [solid-js@1.9.11](https://npmx.dev/package/solid-js/v/1.9.11) | [svelte@5.53.2](https://npmx.dev/package/svelte/v/5.53.2) | [nuxt@4.3.1](https://npmx.dev/package/nuxt/v/4.3.1) |
| --------------- | --------------- | ------------- | ------------ | ----------------------------- |
| Package Size | 2.5 MB | 1.1 MB | 2.8 MB | 716.2 kB |
| Install Size | 16.7 MB | 3.6 MB | 4.9 MB | 123.6 MB |
| Direct Deps | 5 | 3 | 16 | 57 |
| Total Deps | 22 | 3 | 19 | 500 |
| Downloads/wk | 8.5M | 1.5M | 2.7M | 1.2M |
| Likes | 68 | 15 | 163 | 86 |
| Published | Feb 9, 2026 | Jan 23, 2026 | Feb 21, 2026 | Feb 7, 2026 |
| Deprecated? | No | No | No | No |
| Engines | Any | Any | Node.js >=18 | Node.js ^20.19.0 ǀǀ >=22.12.0 |
| Types | Included | Included | Included | Included |
| Module Format | ESM + CJS | ESM + CJS | ESM + CJS | ESM + CJS |
| License | MIT | MIT | MIT | MIT |
| Vulnerabilities | None | None | None | None |
vue@3.5.28solid-js@1.9.11svelte@5.53.2nuxt@4.3.1
Package Size2.5 MB1.1 MB2.8 MB716.2 kB
Install Size16.7 MB3.6 MB4.9 MB123.6 MB
Direct Deps531657
Total Deps22319500
Downloads/wk8.5M1.5M2.7M1.2M
Likes681516386
PublishedFeb 9, 2026Jan 23, 2026Feb 21, 2026Feb 7, 2026
Deprecated?NoNoNoNo
EnginesAnyAnyNode.js >=18Node.js ^20.19.0 ǀǀ >=22.12.0
TypesIncludedIncludedIncludedIncluded
Module FormatESM + CJSESM + CJSESM + CJSESM + CJS
LicenseMITMITMITMIT
VulnerabilitiesNoneNoneNoneNone

@mikouaji

Copy link
Copy Markdown
ContributorAuthor

@gameroman hi, since you proposed this, what's your stance on URLs in headers package names?

@gameroman

Copy link
Copy Markdown
Member

@gameroman hi, since you proposed this, what's your stance on URLs in headers package names?

I think without them

@mikouaji

Copy link
Copy Markdown
ContributorAuthor

@gameroman updated :)

| | nuxt@4.3.1 | angular@1.8.3 | vue@3.5.28 |
| --------------- | ----------------------------- | ------------- | ----------- |
| Package Size | 716.2 kB | 2.1 MB | 2.5 MB |
| Install Size | 124 MB | 2.1 MB | 16.7 MB |
| Direct Deps | 57 | 0 | 5 |
| Total Deps | 500 | 0 | 22 |
| Downloads/wk | 1.2M | 466.6K | 8.5M |
| Likes | 86 | 0 | 68 |
| Published | Feb 7, 2026 | Apr 8, 2022 | Feb 9, 2026 |
| Deprecated? | No | Deprecated | No |
| Engines | Node.js ^20.19.0 ǀǀ >=22.12.0 | Any | Any |
| Types | Included | @types | Included |
| Module Format | ESM + CJS | CJS | ESM + CJS |
| License | MIT | MIT | MIT |
| Vulnerabilities | 1 (0C/1H) | 9 (0C/1H) | None |
nuxt@4.3.1angular@1.8.3vue@3.5.28
Package Size716.2 kB2.1 MB2.5 MB
Install Size124 MB2.1 MB16.7 MB
Direct Deps5705
Total Deps500022
Downloads/wk1.2M466.6K8.5M
Likes86068
PublishedFeb 7, 2026Apr 8, 2022Feb 9, 2026
Deprecated?NoDeprecatedNo
EnginesNode.js ^20.19.0 ǀǀ >=22.12.0AnyAny
TypesIncluded@typesIncluded
Module FormatESM + CJSCJSESM + CJS
LicenseMITMITMIT
Vulnerabilities1 (0C/1H)9 (0C/1H)None

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/pages/compare.vue (1)

93-112: index parameter in the inner forEach shadows the outer facet index.

Both the outer selectedFacets.value.forEach((facet, index) and the inner mdData?.[index + 1]?.forEach((item, index) use index. While the outer index is evaluated correctly before the inner loop begins (in mdData?.[index + 1]), the shadowing makes the code harder to reason about and could silently break if the inner reference is ever moved inside the callback.

✏️ Suggested rename
- mdData?.[index + 1]?.forEach((item, index) => {- if (item.length > (maxLengths?.[index] || 0)) {- maxLengths[index] = item.length+ mdData?.[index + 1]?.forEach((item, colIndex) => {+ if (item.length > (maxLengths?.[colIndex] || 0)) {+ maxLengths[colIndex] = item.length
}
})

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 712e473 and aa755fe.

📒 Files selected for processing (1)
  • app/pages/compare.vue

Comment threadapp/pages/compare.vue Outdated
Comment threadapp/pages/compare.vue Outdated
Comment threadapp/pages/compare.vue
Comment threadapp/pages/compare.vue Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
app/pages/compare.vue (2)

244-263: ⚠️ Potential issue | 🟠 Major

<h2 id="comparison-heading"> hidden on mobile breaks the section's aria-labelledby label.

When data is present, the heading is inside the CopyToClipboardButton which carries hidden md:inline-flex. Below the md breakpoint the element (and its id) is removed from the accessibility tree, leaving <section aria-labelledby="comparison-heading"> with no accessible name on mobile. The v-else fallback heading is only rendered when there is no data, so there is no mobile fallback when data is loaded.


248-249: ⚠️ Potential issue | 🟡 Minor

inline-block is never applied — dead utility class.

hidden sets display: none at the default breakpoint, overriding inline-block before it can take effect. At md+, md:inline-flex takes over. inline-block is unreachable.

✏️ Proposed fix
- class="mb-4 inline-block hidden md:inline-flex"+ class="mb-4 hidden md:inline-flex"
🧹 Nitpick comments (1)
app/pages/compare.vue (1)

107-111: index in the inner forEach shadows the outer forEach's index.

The inner callback reuses index as the cell index, which shadows the outer index (the facet index). It works correctly here because the outer index is only needed for mdData?.[index + 1]before the inner callback body, but it is a maintenance hazard.

♻️ Proposed rename
- mdData?.[index + 1]?.forEach((item, index) => {- if (item.length > (maxLengths?.[index] || 0)) {- maxLengths[index] = item.length+ mdData?.[index + 1]?.forEach((item, cellIndex) => {+ if (item.length > (maxLengths?.[cellIndex] || 0)) {+ maxLengths[cellIndex] = item.length
}
})

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa755fe and 4832795.

📒 Files selected for processing (1)
  • app/pages/compare.vue

Comment threadapp/pages/compare.vue
@gameroman

Copy link
Copy Markdown
Member

Looks very good so far

@mikouaji

mikouaji commented Feb 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@danielroe It's finished, can you have a look?

@danielroe
danielroe added this pull request to the merge queueFeb 24, 2026
Merged via the queue into npmx-dev:main with commit 4aab312Feb 24, 2026
17 checks passed
@mikouaji
mikouaji deleted the feat/copy-compare-table-as-md-string branch February 24, 2026 15:46
alex-key pushed a commit to alex-key/npmx.dev that referenced this pull request Feb 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@mikouaji@danielroe@gameroman