Skip to content

fix(ui): reduce badge width overestimation in fallback text measurement#2199

Merged
ghostdevv merged 6 commits intonpmx-dev:mainfrom
MathurAditya724:patch-2
Mar 23, 2026
Merged

fix(ui): reduce badge width overestimation in fallback text measurement#2199
ghostdevv merged 6 commits intonpmx-dev:mainfrom
MathurAditya724:patch-2

Conversation

@MathurAditya724
Copy link
Copy Markdown
Contributor

@MathurAditya724 MathurAditya724 commented Mar 22, 2026

🔗 Linked issue

Closes #2187

🧭 Context

Badge widths looked too wide in production for long values (for example https://main.npmx.dev/api/registry/badge/engines/vitest), while local dev looked tighter.

The root cause is that when exact canvas measurement is unavailable, the fallback used a fixed per-character width, which overestimates strings containing many narrow characters (spaces, separators, punctuation).

📚 Description

This updates the badge text-width fallback logic to be less aggressive and more realistic, replace the fixed-width fallback with a small character-class estimator

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 22, 2026

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

Project Deployment Actions Updated (UTC)
docs.npmx.dev Ready Ready Preview, Comment Mar 23, 2026 11:36pm
npmx.dev Ready Ready Preview, Comment Mar 23, 2026 11:36pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Mar 23, 2026 11:36pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 00e24754-1ea2-4b06-abaf-3f1bf36abb6c

📥 Commits

Reviewing files that changed from the base of the PR and between 333d12b and f452ac1.

📒 Files selected for processing (1)
  • server/api/registry/badge/[type]/[...pkg].get.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/api/registry/badge/[type]/[...pkg].get.ts

📝 Walkthrough

Walkthrough

Replaces constant-based fallback width multipliers with a character-driven heuristic estimateTextWidth(text, fallbackFont) that classifies characters (narrow/medium/digit/uppercase/other) and applies font-specific coefficients for 'default' and 'shieldsio'. measureDefaultTextWidth now accepts an optional fallbackExtraPadding and, when canvas measurement is unavailable, falls back to estimateTextWidth(..., 'default') + badge horizontal padding + extra padding, clamped to MIN_BADGE_TEXT_WIDTH. measureShieldsTextLength now uses estimateTextWidth(..., 'shieldsio'). The value segment rendering uses FALLBACK_VALUE_EXTRA_PADDING_X = 4 for fallback padding.

Possibly related PRs

  • npmx-dev/npmx.dev PR 908: Introduced per-strategy character-width constants and an optional charWidth parameter in the same badge measurement file.
  • npmx-dev/npmx.dev PR 1486: Reworked badge text-width measurement in the same endpoint, replacing fixed per-character width logic with a different measurement approach.
  • npmx-dev/npmx.dev PR 1584: Adjusted badge text-measurement and fallback validation in the same file, tightening measured-width checks and fallback behavior.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset, explaining the root cause of badge width overestimation and the solution implemented via character-class estimation.
Linked Issues check ✅ Passed The PR directly addresses issue #2187 by replacing fixed-width fallback with character-class estimator to reduce badge width overestimation, matching the objective to prevent fallback logic from overestimating text width.
Out of Scope Changes check ✅ Passed All changes are scoped to the badge text-width fallback logic in a single file, directly addressing the identified root cause of badge width overestimation without introducing unrelated modifications.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
server/api/registry/badge/[type]/[...pkg].get.ts (1)

75-103: Externalise width coefficients and add a brief calibration note.

The embedded literals (3, 5, 6, 7, 5.5) make future tuning brittle. Moving them into named constants (per font profile) will make this safer to adjust.

♻️ Proposed refactor
+const FALLBACK_WIDTHS = {
+  default: {
+    narrow: 3,
+    medium: 5,
+    digit: 6,
+    uppercase: 7,
+    other: 6,
+  },
+  shieldsio: {
+    narrow: 3,
+    medium: 5,
+    digit: 6,
+    uppercase: 7,
+    other: 5.5,
+  },
+} as const
+
 function estimateTextWidth(text: string, fallbackFont: 'default' | 'shieldsio'): number {
+  // Heuristic coefficients tuned to keep fallback rendering close to canvas metrics.
+  const widths = FALLBACK_WIDTHS[fallbackFont]
   let totalWidth = 0
 
   for (const character of text) {
     if (NARROW_CHARS.has(character)) {
-      totalWidth += 3
+      totalWidth += widths.narrow
       continue
     }
 
     if (MEDIUM_CHARS.has(character)) {
-      totalWidth += 5
+      totalWidth += widths.medium
       continue
     }
 
     if (/\d/.test(character)) {
-      totalWidth += 6
+      totalWidth += widths.digit
       continue
     }
 
     if (/[A-Z]/.test(character)) {
-      totalWidth += 7
+      totalWidth += widths.uppercase
       continue
     }
 
-    totalWidth += fallbackFont === 'shieldsio' ? 5.5 : 6
+    totalWidth += widths.other
   }
 
   return Math.max(1, Math.round(totalWidth))
 }

As per coding guidelines "Add comments only to explain complex logic or non-obvious implementations".


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b026675c-7f1b-4eb9-ade0-52af6d8595bb

📥 Commits

Reviewing files that changed from the base of the PR and between 7f2fc1a and 9c66f01.

📒 Files selected for processing (1)
  • server/api/registry/badge/[type]/[...pkg].get.ts

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
server/api/registry/badge/[type]/[...pkg].get.ts (1)

90-98: Consider pre-compiling regex patterns outside the loop.

The regex literals /\d/ and /[A-Z]/ are recreated on each character iteration. For short badge text this has negligible impact, but pre-compiling them as module-level constants would be marginally more efficient and consistent with the existing NARROW_CHARS and MEDIUM_CHARS sets.

♻️ Optional: pre-compile regex patterns
 const MEDIUM_CHARS = new Set([
   // ... existing entries
 ])
+const DIGIT_PATTERN = /\d/
+const UPPERCASE_PATTERN = /[A-Z]/
 
 function estimateTextWidth(text: string, fallbackFont: 'default' | 'shieldsio'): number {
   // ...
-    if (/\d/.test(character)) {
+    if (DIGIT_PATTERN.test(character)) {
       totalWidth += 6
       continue
     }
 
-    if (/[A-Z]/.test(character)) {
+    if (UPPERCASE_PATTERN.test(character)) {
       totalWidth += 7
       continue
     }
   // ...
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3407ba18-2f99-4d5b-9aa3-a9316d542e95

📥 Commits

Reviewing files that changed from the base of the PR and between 9c66f01 and a66454e.

📒 Files selected for processing (1)
  • server/api/registry/badge/[type]/[...pkg].get.ts

@MathurAditya724
Copy link
Copy Markdown
Contributor Author

Check this out - https://npmxdev-git-fork-mathuraditya724-patch-2-npmx.vercel.app/api/registry/badge/engines/vitest

Copy link
Copy Markdown
Contributor

@trueberryless trueberryless left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution and the conveniently provided link to a working example!
The changes look good to me and allow for flexible adjustments 🙌

In the future, we might wanna rethink the generation and maybe get inspired by shields.io, but for this bug fix, that would be too much...

Copy link
Copy Markdown
Contributor

@ghostdevv ghostdevv left a comment

Choose a reason for hiding this comment

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

I don't love the workaround but it seems to work so let's ship it and figure it out later!

@ghostdevv ghostdevv enabled auto-merge March 23, 2026 23:34
@ghostdevv ghostdevv added this pull request to the merge queue Mar 23, 2026
Merged via the queue into npmx-dev:main with commit 1fd2d53 Mar 23, 2026
20 checks passed
@github-actions github-actions bot mentioned this pull request Mar 23, 2026
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.

Custom badges width too wide

3 participants