Skip to content

feat(a11y): add reusable Alert component#1955

Open
ShroXd wants to merge 7 commits intonpmx-dev:mainfrom
ShroXd:create-alert-component
Open

feat(a11y): add reusable Alert component#1955
ShroXd wants to merge 7 commits intonpmx-dev:mainfrom
ShroXd:create-alert-component

Conversation

@ShroXd
Copy link
Contributor

@ShroXd ShroXd commented Mar 6, 2026

🔗 Linked issue

Resolves #1872

🧭 Context

As the issue mentioned, there are two aspects that need to be improved. First is the low contrast of the warning alert, second is there are many different alert stylings in the repo, it's worth creating a common one.

📚 Description

I checked the existing design pattern in the npmx and built a common Alert component. It supports warning and error variants for now, and is currently only used in the claim package name modal. Once maintainers and the community think it's good enough, I'll create another PR to replace the remaining alert UI across the website.

Modal with warning message

Scenario Screenshot - before Screenshot - after
Light mode Screenshot From 2026-03-06 12-27-36 Screenshot From 2026-03-06 13-31-58
Dark mode Screenshot From 2026-03-06 12-30-02 Screenshot From 2026-03-06 13-32-40

Modal with validation warning and error

I didn't find an easy way to trigger these scenarios in the UI or browser, so I hardcoded some messages in the code to verify the appearance. Since this PR only includes UI changes, I think this approach is acceptable. Please point it out if I missed something.

// ClaimPackageModal.vue

const checkResult = shallowRef({
  name: 'some--invalid!!package',
  available: false,
  valid: false,
  validationErrors: [
    'name can only contain URL-friendly characters',
    'name cannot contain special characters ("~\'!()*")',
  ],
  validationWarnings: [
    'name should not contain uppercase letters',
    'name length must be less than or equal to 214 characters',
  ],
})
const checkAvailability = () => {}
const status = shallowRef('success')
const checkError = shallowRef(null)
Scenario Screenshot - before Screenshot - after
Light mode Screenshot From 2026-03-06 13-38-46 Screenshot From 2026-03-06 13-37-46
Dark mode Screenshot From 2026-03-06 13-39-51 Screenshot From 2026-03-06 13-37-10

@vercel
Copy link

vercel bot commented Mar 6, 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 9, 2026 1:07pm
npmx.dev Ready Ready Preview, Comment Mar 9, 2026 1:07pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Mar 9, 2026 1:07pm

Request Review

@codecov
Copy link

codecov bot commented Mar 6, 2026

Codecov Report

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

Files with missing lines Patch % Lines
app/components/Package/ClaimPackageModal.vue 25.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ShroXd ShroXd changed the title Create alert component feat(a11y): add reusable Alert component Mar 6, 2026
@ShroXd ShroXd marked this pull request as ready for review March 6, 2026 06:29
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 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: 23db4625-52da-45ff-8457-2aab13c346c8

📥 Commits

Reviewing files that changed from the base of the PR and between d89b38b and ae315f0.

📒 Files selected for processing (1)
  • test/nuxt/a11y.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/nuxt/a11y.spec.ts

📝 Walkthrough

Walkthrough

Adds a new Vue 3 Single-File Component at app/components/Alert.vue with typed props: required variant ('warning' | 'error') and optional title. Replaces multiple inline validation, warning and error markup in app/components/Package/ClaimPackageModal.vue with the new Alert component while preserving existing control flow and content. Adds Axe accessibility tests in test/nuxt/a11y.spec.ts to cover Alert variants with and without a title and registers Alert variants in the test component pool.

Suggested reviewers

  • danielroe
🚥 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 new Alert component and its application in the ClaimPackageModal with visual before/after comparisons.
Linked Issues check ✅ Passed The PR successfully addresses #1872 by creating a reusable Alert component with proper colour contrast for warning and error variants, improving readability in both light and dark themes.
Out of Scope Changes check ✅ Passed All changes are within scope: the new Alert component, its integration in ClaimPackageModal, and accessibility testing all directly address the linked issue objectives.

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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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
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)
test/nuxt/a11y.spec.ts (1)

3515-3542: Please cover theme-specific contrast here as well.

These cases only exercise the default theme, but the component exists to fix alert contrast across light/dark and background-theme combinations. Adding Alert to the pooled theme matrix will catch the exact regression this PR is meant to prevent.

Example addition to the pooled theme suite
   const components = [
+    {
+      name: 'AlertWarning',
+      mount: () =>
+        mountSuspended(Alert, {
+          props: { variant: 'warning', title: 'Warning title' },
+          slots: { default: 'Warning body' },
+        }),
+    },
+    {
+      name: 'AlertError',
+      mount: () =>
+        mountSuspended(Alert, {
+          props: { variant: 'error', title: 'Error title' },
+          slots: { default: 'Error body' },
+        }),
+    },
     { name: 'AppHeader', mount: () => mountSuspended(AppHeader) },
     { name: 'AppFooter', mount: () => mountSuspended(AppFooter) },

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d9d44df0-631a-4bde-8b8c-83b1988f2928

📥 Commits

Reviewing files that changed from the base of the PR and between 58da597 and ed98046.

📒 Files selected for processing (3)
  • app/components/Alert.vue
  • app/components/Package/ClaimPackageModal.vue
  • test/nuxt/a11y.spec.ts

>
{{ mergedError }}
</div>
<Alert v-if="mergedError" variant="error">{{ mergedError }}</Alert>
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 6, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This publish-error alert never renders.

mergedError is computed as null whenever checkResult exists, so once the availability check has succeeded this branch cannot show failures from handleClaim(). If publishing fails, the modal stays open without surfacing the error. Please separate availability-check errors from publish errors, or let publishError win before the checkResult !== null guard.

One way to untangle the two error paths
-const mergedError = computed(() => {
-  return checkResult.value !== null
-    ? null
-    : (publishError.value ??
-        (checkError.value instanceof Error
-          ? checkError.value.message
-          : $t('claim.modal.failed_to_check')))
-})
+const availabilityError = computed(() => {
+  if (checkResult.value !== null) return null
+  return checkError.value instanceof Error
+    ? checkError.value.message
+    : $t('claim.modal.failed_to_check')
+})
+
+const mergedError = computed(() => {
+  return publishError.value ?? availabilityError.value
+})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is existing logic, since this PR only focuses on the UI part, ignore it for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ShroXd, understood — that logic predates this PR and is outside its scope. I'll leave it for a future fix. The UI refactoring with the new Alert component looks clean and the before/after screenshots show a clear improvement in contrast for both light and dark themes.


🧠 Learnings used
Learnt from: alexdln
Repo: npmx-dev/npmx.dev PR: 838
File: app/pages/package/[...package].vue:445-449
Timestamp: 2026-02-03T13:59:33.392Z
Learning: The copy button pattern in app/pages/package/[...package].vue may be made into a reusable component or pattern in the future, but currently it's acceptable to keep it inline with the CSS-only approach for smooth animations.

Learnt from: jellydeck
Repo: npmx-dev/npmx.dev PR: 904
File: app/components/Package/AccessControls.vue:253-253
Timestamp: 2026-02-04T05:34:20.527Z
Learning: In the npmx.dev project, ensure that focus-visible styling for button and select elements is implemented globally in app/assets/main.css with the rule: button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }. Do not apply per-element inline utility classes like focus-visible:outline-accent/70 on these elements in Vue templates or components (e.g., AccessControls.vue). Rely on the global rule for consistency and maintainability; only use inline focus-visible utilities when styling non-button/select elements or in exceptional cases outside the global scope.

Learnt from: jellydeck
Repo: npmx-dev/npmx.dev PR: 904
File: app/components/Package/Versions.vue:332-332
Timestamp: 2026-02-04T05:34:54.335Z
Learning: In the npmx.dev project, focus-visible styling for buttons and selects is applied globally via main.css with: button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }. Therefore, individual buttons or selects in Vue components should not rely on inline focus-visible utility classes like focus-visible:outline-accent/70. Ensure components do not add per-element focus-visible utilities; rely on the global rule. If a component seems to require a different focus state, adjust only through global CSS or maintain accessibility with native focus styles, rather than inserting inline utility classes.

Learnt from: userquin
Repo: npmx-dev/npmx.dev PR: 1170
File: app/components/Package/MetricsBadges.vue:66-66
Timestamp: 2026-02-08T15:02:02.232Z
Learning: In Vue components that use UnoCSS with the preset-icons collection, prefer colon-syntax for icons (e.g., i-carbon:checkmark) over the dash-separated form (i-carbon-checkmark). This aids UnoCSS in resolving the correct collection directly, which improves performance for long icon names. Apply this pattern to all Vue components (e.g., app/components/**/*.vue) where UnoCSS icons are used; ensure UnoCSS is configured with the preset-icons collection.

Learnt from: userquin
Repo: npmx-dev/npmx.dev PR: 1335
File: app/components/Compare/FacetSelector.vue:72-78
Timestamp: 2026-02-10T15:47:33.467Z
Learning: In the npmx.dev project, ButtonBase (used via app/components/ButtonBase.vue or similar) provides default classes: border border-border. When styling ButtonBase instances in Vue components (e.g., app/components/Compare/FacetSelector.vue and other files under app/components), avoid duplicating the border class to prevent the HTML validator error no-dup-class and CI failures. If styling overrides are needed, ensure only unique classes are applied (remove redundant border classes or adjust via props) so the default border remains intact without duplication.

Learnt from: abbeyperini
Repo: npmx-dev/npmx.dev PR: 1049
File: app/components/Settings/Toggle.client.vue:22-29
Timestamp: 2026-02-11T00:01:33.121Z
Learning: In Vue 3.4 and later, you can use same-name shorthand for attribute bindings: use :attributeName instead of :attributeName="attributeName" when binding to a variable with the same name in scope. For example, :id is equivalent to :id="id" when an id variable exists. Apply this shorthand in .vue components (notably in Settings/Toggle.client.vue) to simplify templates. Ensure the bound variable exists and that you are using a Vue version that supports this shorthand.

Learnt from: alexdln
Repo: npmx-dev/npmx.dev PR: 1845
File: app/components/InstantSearch.vue:6-11
Timestamp: 2026-03-03T09:42:52.533Z
Learning: Maintain the established prehydration pattern across the project: use JSON.parse(localStorage.getItem('npmx-settings') || '{}') without per-call try-catch blocks. Do not introduce try-catch error handling for this pattern unless a coordinated, project-wide refactor of all onPrehydrate readers is planned and executed.

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.

🧹 Nitpick comments (1)
test/nuxt/a11y.spec.ts (1)

3515-3542: Assert the live-region role in these new Alert tests.

These checks only prove the markup is axe-clean. They will not catch a regression where warning stops rendering role="status" or error stops rendering role="alert", which is the key a11y contract of this component.

Suggested test tightening
   describe('Alert', () => {
     it('should have no accessibility violations for warning variant', async () => {
       const component = await mountSuspended(Alert, {
         props: { variant: 'warning', title: 'Warning title' },
         slots: { default: 'This is a warning message.' },
       })
+      expect(component.attributes('role')).toBe('status')
       const results = await runAxe(component)
       expect(results.violations).toEqual([])
     })

     it('should have no accessibility violations for error variant', async () => {
       const component = await mountSuspended(Alert, {
         props: { variant: 'error', title: 'Error title' },
         slots: { default: 'This is an error message.' },
       })
+      expect(component.attributes('role')).toBe('alert')
       const results = await runAxe(component)
       expect(results.violations).toEqual([])
     })

     it('should have no accessibility violations without title', async () => {
       const component = await mountSuspended(Alert, {
         props: { variant: 'warning' },
         slots: { default: 'This is a warning message.' },
       })
+      expect(component.attributes('role')).toBe('status')
       const results = await runAxe(component)
       expect(results.violations).toEqual([])
     })
   })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b16334e-ece5-40b1-8fc8-dbf83bb2eadf

📥 Commits

Reviewing files that changed from the base of the PR and between 0b81644 and d89b38b.

📒 Files selected for processing (3)
  • app/components/Alert.vue
  • app/components/Package/ClaimPackageModal.vue
  • test/nuxt/a11y.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/components/Alert.vue

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.

Claim package name modal does not have enough contrast

1 participant