Skip to content

Conversation

@vinnymac
Copy link
Contributor

@vinnymac vinnymac commented Feb 8, 2026

Built on the backs of @graphieros's vue-ui-xy and @NullVoxPopuli's majors to add a npmx themed version distributions feature.

image

Also added hide old here because that's something I personally wanted for this. Idk if anyone else was working on a related feature here, so if this crosses paths with some ongoing work, just let me know.

@vercel
Copy link

vercel bot commented Feb 8, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 10, 2026 0:34am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 10, 2026 0:34am
npmx-lunaria Ignored Ignored Feb 10, 2026 0:34am

Request Review

@github-actions
Copy link

github-actions bot commented Feb 8, 2026

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

File Note
lunaria/files/en-GB.json Localization changed, will be marked as complete. 🔄️
lunaria/files/en-US.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ 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.

@NullVoxPopuli
Copy link
Contributor

Oooo fancy !

@codecov
Copy link

codecov bot commented Feb 9, 2026

@vinnymac
Copy link
Contributor Author

vinnymac commented Feb 9, 2026

Need to change the title to feat(ui) oops

@danielroe danielroe changed the title feat(frontend): community version distribution feat(ui): community version distribution Feb 9, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

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 version download distribution feature: new VersionDistribution Vue component and a useVersionDistribution composable; server API route /api/registry/downloads/:slug/versions and server utils to group/filter per-version downloads; shared types and re-export added; Versions.vue now opens a modal and defers mounting the chart until the dialog transition finishes; ChartModal accepts a title prop and emits a transitioned event; WeeklyDownloadStats passes the modal title; Tooltip App/Base accept teleportTo and offset props; localisation keys added; VersionDistribution is skipped in a11y component coverage and tests updated.

Possibly related PRs

Suggested labels

idea

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is clearly related to the changeset, explaining the feature being added: a version distribution UI built on vue-ui-xy with major/minor grouping, date ranges, and version filtering options.

✏️ 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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (4)
server/api/registry/downloads/[...slug].get.ts (2)

83-89: totalDownloads is computed after filtering, which may not match expected semantics.

The totalDownloads on line 89 is calculated from data.downloads (all versions), but groups on line 83 has already been created from groupVersionDownloads. If the intention is for totalDownloads to represent the sum of the returned groups only (after threshold filtering on line 86), the current calculation is inconsistent.

Consider whether:

  1. totalDownloads should reflect the pre-filter total (current behaviour) — this is likely correct for percentage context.
  2. Or if it should match the sum of filtered groups.

If the current behaviour is intentional, a brief comment would clarify this.


116-117: Consider defensive access when reading versionData.versions.

The cast as { versions: string[] } assumes the external API always returns this shape. If the response structure changes or is malformed, accessing versionData.versions could throw or assign undefined.

🛡️ Suggested defensive access
-          const versionData = (await fastMetaResponse.json()) as { versions: string[] }
-          apiResponse.recentVersions = versionData.versions
+          const versionData = (await fastMetaResponse.json()) as { versions?: string[] }
+          if (Array.isArray(versionData.versions)) {
+            apiResponse.recentVersions = versionData.versions
+          }
app/components/Package/Versions.vue (1)

336-344: Remove inline focus-visible utility class from button.

The focus-visible:outline-accent/70 class on line 338 should be removed. Per project conventions, focus-visible styling for buttons is applied globally via main.css, and individual buttons should not use inline focus-visible utility classes.

♻️ Suggested fix
       <ButtonBase
         variant="secondary"
-        class="text-fg-subtle hover:text-fg transition-colors duration-200 inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1 focus-visible:outline-accent/70 rounded"
+        class="text-fg-subtle hover:text-fg transition-colors duration-200 inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1 rounded"
         :title="$t('package.downloads.community_distribution')"
         classicon="i-carbon:load-balancer-network"
         `@click`="openDistributionModal"

Based on learnings: "focus-visible styling for buttons and selects is applied globally via main.css... individual buttons or selects in Vue components should not rely on inline focus-visible utility classes".

server/utils/version-downloads.ts (1)

120-129: Comment is misleading — the grouping logic is identical for all versions.

The comment at lines 123-124 suggests special handling for 0.x versions, but the actual implementation groups all versions by major.minor uniformly. The comment should be updated to reflect that this function always groups by minor version, which happens to align with semver conventions where 0.x minor bumps are breaking changes.

📝 Suggested comment update
   // Group by major.minor
   const groups = new Map<string, ProcessedVersion[]>()
   for (const version of processed) {
-    // For 0.x versions, treat each minor as significant (0.9.x, 0.10.x are different)
-    // For 1.x+, group by major.minor normally
+    // Group all versions by major.minor (e.g., 1.2.x, 1.3.x)
+    // This aligns with semver conventions where 0.x minor bumps indicate breaking changes
     const groupKey = `${version.major}.${version.minor}`

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/components/Package/VersionDistribution.vue (1)

66-175: Split chartConfig into smaller helpers to keep functions manageable.

chartConfig is well over the guideline’s 50‑line threshold. Consider extracting sub-builders (e.g., grid/tooltip/zoom config) to keep the computed function focused and easier to test.

As per coding guidelines: Keep functions focused and manageable (generally under 50 lines).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (1)
app/components/Package/VersionDistribution.vue (1)

66-175: Consider splitting chartConfig into smaller computed blocks.

This computed exceeds the “generally under 50 lines” guidance. Extract grid/tooltip/zoom config builders (or separate computeds) to keep the logic manageable.

As per coding guidelines, "Keep functions focused and manageable (generally under 50 lines)".

Comment on lines +308 to +310
:deep(.vue-ui-xy) svg rect {
transition: none !important;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@graphieros I tried to disable the vue-ui-xy animations that I saw in the modal, as they looked a bit odd in this context (bars on the graph flying in from bottom left into position).

I couldn't figure out what the reasonable way to disable this was, so this is what I came up with for now. If there was something obvious I was missing, please let me know 🙏🏼

Copy link
Contributor

Choose a reason for hiding this comment

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

What you did is fine!
It's part of the things to improve on the library side.

@vinnymac
Copy link
Contributor Author

vinnymac commented Feb 9, 2026

@coderabbitai resume

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

✅ Actions performed

Reviews resumed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
server/api/registry/downloads/[...slug].get.ts (1)

37-138: Consider extracting helper functions to keep the handler under ~50 lines.

The handler is now ~100 lines, making it harder to scan and test. Splitting fetch/parsing, filtering, and enrichment into helpers would keep it focused and easier to maintain.

As per coding guidelines, "Keep functions focused and manageable (generally under 50 lines)".

Double encoding broke the requests here
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/api/registry/downloads/[...slug].get.ts (1)

44-45: Consider extracting helper functions to keep the handler focused.

The handler is well over ~50 lines; splitting query parsing, npm fetch, and recent-versions enrichment into helpers would improve readability and testability.

As per coding guidelines, "Keep functions focused and manageable (generally under 50 lines)".

Comment on lines 69 to 73
const query = getQuery(event)
const parsed = v.parse(QuerySchema, query)
const mode = parsed.mode
const filterThreshold = parsed.filterThreshold ?? 1
const filterOldVersionsBool = parsed.filterOldVersions === 'true'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the full file to understand the try/catch structure
cat -n server/api/registry/downloads/\[...\]slug\].get.ts | head -100

Repository: npmx-dev/npmx.dev

Length of output: 142


🏁 Script executed:

# Also search for where the try/catch block is located
grep -n "try\|catch" server/api/registry/downloads/\[...\]slug\].get.ts

Repository: npmx-dev/npmx.dev

Length of output: 141


🏁 Script executed:

# Check valibot library usage and whether it throws on validation failure
rg "v\.parse|import.*valibot" server/api/registry/downloads/ -A 2 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 817


🏁 Script executed:

# Look for error handling patterns in the codebase - how other handlers deal with validation
rg "v\.parse" server/ -l --type ts --type tsx | head -10

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check if createError is used elsewhere and what status codes it supports
rg "createError.*400|statusCode.*400" server/ --type ts --type tsx | head -5

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Find the exact file
fd -t f "slug\]\.get\.ts" server/api/registry/downloads/

Repository: npmx-dev/npmx.dev

Length of output: 107


🏁 Script executed:

# Read using the path from the fd output, using find + cat
find server/api/registry/downloads -name "*slug*.get.ts" -exec cat -n {} \;

Repository: npmx-dev/npmx.dev

Length of output: 6523


🏁 Script executed:

# Check valibot documentation or examples in the codebase for error handling
rg "v\.parse" --type ts -B 5 -A 5 | head -50

Repository: npmx-dev/npmx.dev

Length of output: 3328


🏁 Script executed:

# Find and examine the handleApiError implementation
fd -t f "error" server/utils --type ts | xargs grep -l "handleApiError"

Repository: npmx-dev/npmx.dev

Length of output: 232


🏁 Script executed:

# Search for handleApiError implementation
rg "export.*handleApiError|function handleApiError" server/ -A 15

Repository: npmx-dev/npmx.dev

Length of output: 1161


🏁 Script executed:

# Check test file to understand how handleApiError handles valibot errors
cat -n test/unit/server/utils/error-handler.spec.ts | grep -A 20 "throws a 404"

Repository: npmx-dev/npmx.dev

Length of output: 939


🏁 Script executed:

# Get the full handleApiError implementation to see complete ValiError handling
cat -n server/utils/error-handler.ts

Repository: npmx-dev/npmx.dev

Length of output: 1414


🏁 Script executed:

# Check if there's a global error handler in the framework that catches unhandled errors
rg "unhandledRejection|error.*handler|middleware" server/middleware --type ts -l

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check other API routes to see if they wrap v.parse in try/catch when it's the first validation
rg -B 5 -A 5 "v\.parse" server/api --type ts | head -80

Repository: npmx-dev/npmx.dev

Length of output: 5219


🏁 Script executed:

# Check if there's a global error handler or middleware that catches unhandled errors
rg "defineEventHandler|defineEventHandler.*error" server/middleware --type ts -A 5 | head -40

Repository: npmx-dev/npmx.dev

Length of output: 501


🏁 Script executed:

# Search for global error handling in Nitro config or middleware
fd -t f "nitro\|error\|middleware" -E node_modules | xargs grep -l "unhandled\|catch.*error" 2>/dev/null | head -5

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Verify the pattern: check if the downloads route is unique in having parse outside try/catch
rg "v\.parse.*QuerySchema|v\.parse.*query" server/api -B 3 -A 3

Repository: npmx-dev/npmx.dev

Length of output: 2565


Move query validation inside the try/catch block to ensure consistent error handling.

The v.parse() call at line 69 throws a ValiError on invalid input but occurs before the try/catch block that starts at line 74. This means validation errors will surface as unhandled 500s rather than being caught and converted to a 4xx response by handleApiError. All other routes in the codebase place validation inside try/catch. Wrap the parse call in the existing try/catch to align with established error handling patterns.

@graphieros
Copy link
Contributor

graphieros commented Feb 9, 2026

Looks great !
@vinnymac would you mind to check out what's done on DownloadAnalytics.vue regarding:

  • svg slot
  • custom legend placement
  • watermark for SVG & PNG print
  • dedicated slots for the context menu to customize icons
  • xAxis labels could probably have a bigger font size
  • I wonder if the version toggles could be positioned before the labels
  • you should not have to leave more bottom padding for rotated labels, the chart auto sizes for that (or tell me if you had issues with this ?)
  • I think in mobile you can stretch the aspect ratio vertically
  • checkout the css overrides in DownloadAnalytics.vue for the placement of the reset button of the minimap

},
},
},
timeTag: {
Copy link
Contributor

@graphieros graphieros Feb 9, 2026

Choose a reason for hiding this comment

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

time tag can be removed (false by default)

xAxisLabels: {
show: xAxisLabels.value.length <= 25,
values: xAxisLabels.value,
fontSize: isMobile.value ? 14 : 12,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the font size could be bigger

padding: {
top: 24,
right: 24,
bottom: xAxisLabels.value.length > 10 ? 100 : 72, // More space for rotated labels
Copy link
Contributor

Choose a reason for hiding this comment

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

When labels rotate the chart resizes to avoid overflow automatically, so you don't need that much bottom padding

},
},
},
userOptions: {
Copy link
Contributor

Choose a reason for hiding this comment

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

userOptions is correctly declared on L.95, this can be removed

class="chart-container w-full h-[400px] sm:h-[400px]"
:class="{ 'h-[500px]': isMobile }"
>
<VueUiXy :dataset="xyDataset" :config="chartConfig" class="[direction:ltr]" />
Copy link
Contributor

Choose a reason for hiding this comment

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

To be added:

  • svg scoped slot (see implementation in prod of DownloadAnalytics.vue, to inject a custom legend for SVG print, and npmx watermark)
  • legend scoped slot for a single series
  • all the scoped slots for the contextual menu:
    • menuIcon
    • optionCsv
    • optionImg
    • optionSvg
    • all the annotator-action-xxx scoped slots

:class="{ 'h-[500px]': isMobile }"
>
<VueUiXy :dataset="xyDataset" :config="chartConfig" class="[direction:ltr]" />
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use the fallback slot provided by ClientOnly (see DownloadAnalytics.vue)

@graphieros graphieros marked this pull request as draft February 9, 2026 18:29
Sign up for free to 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