Repository navigation
feat(ui): M4 core component kit (accessible, RTL-aware, axe-tested) + local dev navigation - #13
Merged
Merged
Conversation
- axe-core ^4.14.0 as a dev dependency (owner-approved), called directly; no wrapper package. resources/js/test/axe.ts returns readable violations (NFR-A11Y-04). colour-contrast is off because jsdom does not compute styles; token contrast has its own tests. region is off because components are tested outside page landmarks. - resources/js/test/setup.ts: activates English before each test, cleans the DOM after, and stubs HTMLDialogElement.showModal/close, which jsdom lacks (open state, focus into the dialog, Escape -> cancel -> close).
- FormField provides its control's id, aria-describedby (error first, then hint), aria-invalid and required through provide/inject, so pages write <FormField label hint error><TextInput v-model /></FormField> (NFR-A11Y-02). Required fields announce "(required)". - TextInput (email, URL and phone values always LTR so "aisyah@" stays intact on Arabic pages), SelectInput (native select, disabled placeholder, logical chevron), CheckboxInput (native, own label, linked hint and error). - ui.json catalogues for en/ms/ar with the kit's own strings. Tests: labelling, describedby order, invalid/required states, v-model, LTR types, and axe for each control.
- AppButton: primary/secondary/ghost/danger, sm/md/lg (md and lg meet the 44px target), type="button" by default, loading state with aria-busy, a hidden spinner and an announced "Loading". - TextLink: Inertia link, or an external link opening in a new tab with rel="noopener noreferrer" and an announced "(opens in a new tab)"; can look like a button; aria-current for the current page. - StatusBadge with a generic tone (neutral/success/warning/danger/info), so domain vocabularies map onto it rather than living in it. - SurfaceCard (a labelled region when titled), LoadingSkeleton (aria-hidden, pulse only without reduced motion), EmptyState. - MoneyPrice: formats server Money without arithmetic (F3) in an LTR <bdi>, with screen-reader labels for sale and original prices. Tests cover behaviour, keyboard reachability and axe for each.
- ModalDialog on native <dialog> with showModal(): inert background,
focus trap and Escape from the browser; v-model:open; labelled by its
title and described by its description; scroll lock; focus returns to
the trigger; optional non-dismissible mode.
- Toasts: a queue (push/dismiss) and ToastRegion, a polite live region
where errors are role="alert". Success and info dismiss after 6s,
paused on hover or focus; errors stay until dismissed (WCAG 2.2.1).
Server responses use Inertia 3 flash data
({ toast: { tone, key, params } }), typed via flashDataType and
translated in the browser.
- PageNav: a labelled nav of links, current page marked and not a link,
previous/next arrows flipped in RTL, numbers via formatNumber, optional
label for pages with more than one; pageWindow() decides which pages
show (all when they fit, otherwise first/last/current±1 with gaps).
Tests cover keyboard (Escape, focus return), timers, flash translation,
RTL labels, the page window and axe.
Storefront (storefront/**, pages/Storefront/**) and vendor (vendor/**, pages/Vendor/**) code may import shared code but not each other, and shared code imports neither. ESLint is not installed (owner decision), so like F4 this runs as a Vitest check: it resolves @/ and relative imports, static and dynamic, and includes a self-check against known samples.
/dev/components shows every kit component in its main states, with an en / ms / ar switch that runs activateLocale(), so <html lang dir> flips as on the storefront. Sample copy is per locale; the components' own labels come from the real catalogues. Registered from routes/local.php, so it exists only when APP_ENV=local. Vitest renders the gallery in all three locales (every section present, translated labels, RTL for ar) and runs axe on it. Pest (TokenGalleryTest renamed LocalGalleriesTest) proves both galleries 404 in production, staging, demo and testing and render in local.
- code-quality.md: kit location, components, form wiring, dialogs, toasts (client and Inertia flash), money display and test approach. - dependency-rules.md: F1 is enforced by Vitest until ESLint is approved, with the surface path mapping.
The app had no links between its pages, so nothing was clickable when running it locally. app.blade.php now includes partials/dev-bar only when APP_ENV=local: a slim, labelled <nav> (English, LTR) linking to Home in en/ms/ar and to the token and component galleries. It is plain Blade outside the Vue app (full page loads), so it adds no JavaScript and no shared props. Pest proves it renders on storefront and gallery pages in local and is absent in production, staging, demo and testing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M4 core component kit: 14 accessible, RTL-aware Vue components in
resources/js/shared/ui/.axe-core.Summary
Eight commits on
main(7b1fdea):test(ui): axe accessibility checks and shared Vitest setupfeat(ui): form field and controlsfeat(ui): buttons, links and display componentsfeat(ui): dialog, toasts and paginationtest(rules): surface boundaries (dependency rule F1)feat(dev): local-only component gallerydocs(engineering): component kit and F1 enforcementfeat(dev): local-only development navigation barEach commit passes Vitest on its own.
Related Issues
N/A
Requirement
M4 (core component kit), per the Phase 3 milestone plan:
aria-describedby, automated axe);Type of Change
What Changed
Components (
resources/js/shared/ui/)AppButtontype="button"by default; loading = disabled +aria-busy+ hidden spinner + announced "Loading"TextLinkexternal(new tab,rel="noopener noreferrer", announced "(opens in a new tab)"); can look like a button;aria-currentFormField+TextInput/SelectInputaria-describedby(error first, then hint),aria-invalidandrequiredvia provide/inject (NFR-A11Y-02); email / URL / phone inputs are always LTR; native<select>with a logical chevronCheckboxInputStatusBadgetone(neutral / success / warning / danger / info); callers map domain states onto itSurfaceCardLoadingSkeletonaria-hidden; pulses only without reduced motionEmptyStateMoneyPriceMoneywithout arithmetic (F3) in an LTR<bdi>; sale and original prices labelled for screen readersModalDialog<dialog>+showModal()(inert background, focus trap, Escape);v-model:open; labelled and described; scroll lock; focus returns to the trigger; optional non-dismissible modeToastRegion+toasts.tsrole="alert"; success and info dismiss after 6s, paused on hover or focus; errors stay until dismissed (WCAG 2.2.1); server toasts through Inertia 3 flash data{ toast: { tone, key, params } }, typed viaflashDataTypeand translated in the browserPageNav<nav>of links; current page marked and not a link; arrows flip in RTL; numbers viaformatNumber; optionallabelfor pages with more than one;pageWindow()shows all pages when they fit, otherwise first / last / current±1 with gapsui.jsonfor en, ms and ar, covered by the M3 coverage test.Link.Accessibility testing (NFR-A11Y-04)
axe-core^4.14.0 (dev only, owner-approved, released 2026-10-05) is called directly fromresources/js/test/axe.ts, with no wrapper package.color-contrastis off because jsdom has no layout; token contrast has its own tests (M2).regionis off because components are tested outside page landmarks.resources/js/test/setup.tsstubsHTMLDialogElement.showModal/close, which jsdom lacks.Dependency rule F1
resources/js/rules/surface-boundaries.test.ts:storefront/**,pages/Storefront/**) and vendor code (vendor/**,pages/Vendor/**) may import shared code but not each other, and shared code imports neither;@/and relative imports, static and dynamic, with a self-check.ESLint is not installed (owner decision), so F1 runs in Vitest like F4. Both migrate to ESLint if it is approved (documented in
dependency-rules.md).Local-only pages
/dev/components(routes/local.php): every component in its main states, with an en / ms / ar switch that runsactivateLocale()so<html lang dir>flips as on the storefront.resources/views/partials/dev-bar.blade.php):app.blade.phponly whenAPP_ENV=local;Docs
code-quality.mdgains a "Component kit" section.dependency-rules.mdrecords F1 enforcement and the surface path mapping.Design / Architecture
<dialog>,<select>, checkbox and button give keyboard, screen-reader and mobile behaviour without custom focus management or a headless-UI dependency.Testing
Test Evidence
Local, Node 24.22.2 / npm 11.19.1, PHP 8.5, with
APP_CONFIG_CACHEpointed at a path that doesn't exist:npm cinpm run buildcomposer checkcomposer.jsonvalid · 0 advisories · Pint passed · PHPStan 0 errors · Pest 45 passed (206 assertions)npm run typechecknpm testnpm audit --omit=devaxeViolations(). Removing the dialog close button'saria-labelfails withbutton-name(reverted)focusinand resume onfocusout; checkbox is native and toggled by its labelDev/Components.test.tsmounts it in each locale: all six sections present, translated labels (ترقيم الصفحات,Penomboran halaman),dir="rtl"for ar,RM 2,418.50; axe passes on the whole gallerysurface-boundaries.test.tsself-check catches storefront↔vendor and shared→storefront imports (static, dynamic,@/, relative)/dev/tokensand/dev/components404 in production, staging, demo and testing, 200 in local; the dev bar renders on/,/arand/dev/componentsin local and is absent on/in the other four:modal, Escape closes, focus back on the trigger, scroll restored. RTL: layout mirrors, PageNav arrows computedscale: -1 1, no horizontal overflowdist65.78 +app2.62 + one locale's catalogues +Home. CSS 7.51 kB gzipBugs found and fixed while building:
PageNavs on one page with the same landmark name, solabelwas added.aisyah@as@aisyahin an Arabic form, so email, URL and phone inputs became LTR. Both fixes have tests.Security / Data Isolation
rel="noopener noreferrer";APP_ENV=local;axe-coreis dev-only (MPL-2.0, never bundled);npm audit --omit=devis still 0. The fullnpm auditshows only the two dev-only advisories already recorded insecurity.md.Database / Migration
Screenshots / Recording
Run locally (
APP_ENV=local) and use the dev bar at the top: Home (en / ms / ar), Design tokens, Components. The component gallery has its own en / ms / ar switch. Screenshots can be added on request.Documentation
docs/engineering/code-quality.md,docs/architecture/dependency-rules.md)Reviewer Notes
ModalDialog(open/close watchers, focus return);toasts.tstimer pause and resume;FormFieldcontext;pageWindow();<dialog>: the test stub models the behaviour the components rely on, and real-browser behaviour was checked manually; it belongs in the Phase 9 browser tests.msandarstrings inui.jsonneed native-speaker review, like M3's.hreflangis rendered server-side only, so it goes stale after in-app navigation;origin-*,object-*/bg-*positions, arbitrary properties).Node 24 / Vue 3a required check.Definition of Done
composer checkpasses locally (validate, audit, Pint, PHPStan, Pest)