@chromvoid/headless-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/a11y-contracts/index.d.ts +23 -0
- package/dist/a11y-contracts/index.js +1 -0
- package/dist/accordion/index.d.ts +78 -0
- package/dist/accordion/index.js +264 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +1 -0
- package/dist/alert/index.d.ts +33 -0
- package/dist/alert/index.js +54 -0
- package/dist/alert-dialog/index.d.ts +69 -0
- package/dist/alert-dialog/index.js +94 -0
- package/dist/badge/index.d.ts +48 -0
- package/dist/badge/index.js +89 -0
- package/dist/breadcrumb/index.d.ts +55 -0
- package/dist/breadcrumb/index.js +77 -0
- package/dist/button/index.d.ts +46 -0
- package/dist/button/index.js +86 -0
- package/dist/callout/index.d.ts +41 -0
- package/dist/callout/index.js +63 -0
- package/dist/card/index.d.ts +54 -0
- package/dist/card/index.js +103 -0
- package/dist/carousel/index.d.ts +98 -0
- package/dist/carousel/index.js +243 -0
- package/dist/checkbox/index.d.ts +50 -0
- package/dist/checkbox/index.js +87 -0
- package/dist/combobox/index.d.ts +114 -0
- package/dist/combobox/index.js +431 -0
- package/dist/command-palette/index.d.ts +73 -0
- package/dist/command-palette/index.js +147 -0
- package/dist/context-menu/index.d.ts +111 -0
- package/dist/context-menu/index.js +372 -0
- package/dist/copy-button/index.d.ts +62 -0
- package/dist/copy-button/index.js +183 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +2 -0
- package/dist/core/selection.d.ts +5 -0
- package/dist/core/selection.js +39 -0
- package/dist/core/value-range.d.ts +49 -0
- package/dist/core/value-range.js +134 -0
- package/dist/date-picker/index.d.ts +210 -0
- package/dist/date-picker/index.js +895 -0
- package/dist/dialog/index.d.ts +95 -0
- package/dist/dialog/index.js +153 -0
- package/dist/disclosure/index.d.ts +52 -0
- package/dist/disclosure/index.js +159 -0
- package/dist/drawer/index.d.ts +30 -0
- package/dist/drawer/index.js +39 -0
- package/dist/feed/index.d.ts +77 -0
- package/dist/feed/index.js +260 -0
- package/dist/grid/index.d.ts +103 -0
- package/dist/grid/index.js +415 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/input/index.d.ts +86 -0
- package/dist/input/index.js +156 -0
- package/dist/interactions/composite-navigation.d.ts +69 -0
- package/dist/interactions/composite-navigation.js +169 -0
- package/dist/interactions/index.d.ts +15 -0
- package/dist/interactions/index.js +4 -0
- package/dist/interactions/keyboard-intents.d.ts +16 -0
- package/dist/interactions/keyboard-intents.js +33 -0
- package/dist/interactions/overlay-focus.d.ts +40 -0
- package/dist/interactions/overlay-focus.js +93 -0
- package/dist/interactions/typeahead.d.ts +20 -0
- package/dist/interactions/typeahead.js +41 -0
- package/dist/landmarks/index.d.ts +39 -0
- package/dist/landmarks/index.js +58 -0
- package/dist/link/index.d.ts +34 -0
- package/dist/link/index.js +39 -0
- package/dist/listbox/index.d.ts +92 -0
- package/dist/listbox/index.js +337 -0
- package/dist/menu/index.d.ts +132 -0
- package/dist/menu/index.js +541 -0
- package/dist/menu-button/index.d.ts +71 -0
- package/dist/menu-button/index.js +121 -0
- package/dist/meter/index.d.ts +45 -0
- package/dist/meter/index.js +106 -0
- package/dist/number/index.d.ts +113 -0
- package/dist/number/index.js +252 -0
- package/dist/popover/index.d.ts +70 -0
- package/dist/popover/index.js +126 -0
- package/dist/progress/index.d.ts +49 -0
- package/dist/progress/index.js +79 -0
- package/dist/radio-group/index.d.ts +61 -0
- package/dist/radio-group/index.js +150 -0
- package/dist/select/index.d.ts +92 -0
- package/dist/select/index.js +239 -0
- package/dist/sidebar/index.d.ts +74 -0
- package/dist/sidebar/index.js +186 -0
- package/dist/slider/index.d.ts +61 -0
- package/dist/slider/index.js +150 -0
- package/dist/slider-multi-thumb/index.d.ts +70 -0
- package/dist/slider-multi-thumb/index.js +222 -0
- package/dist/spinbutton/index.d.ts +75 -0
- package/dist/spinbutton/index.js +214 -0
- package/dist/spinner/index.d.ts +1 -0
- package/dist/spinner/index.js +1 -0
- package/dist/spinner/spinner.d.ts +23 -0
- package/dist/spinner/spinner.js +25 -0
- package/dist/switch/index.d.ts +40 -0
- package/dist/switch/index.js +61 -0
- package/dist/table/index.d.ts +117 -0
- package/dist/table/index.js +377 -0
- package/dist/tabs/index.d.ts +63 -0
- package/dist/tabs/index.js +174 -0
- package/dist/textarea/index.d.ts +68 -0
- package/dist/textarea/index.js +137 -0
- package/dist/toast/index.d.ts +67 -0
- package/dist/toast/index.js +145 -0
- package/dist/toolbar/index.d.ts +59 -0
- package/dist/toolbar/index.js +139 -0
- package/dist/tooltip/index.d.ts +52 -0
- package/dist/tooltip/index.js +169 -0
- package/dist/treegrid/index.d.ts +101 -0
- package/dist/treegrid/index.js +463 -0
- package/dist/treeview/index.d.ts +68 -0
- package/dist/treeview/index.js +370 -0
- package/dist/window-splitter/index.d.ts +65 -0
- package/dist/window-splitter/index.js +204 -0
- package/package.json +92 -0
- package/specs/ADR-001-headless-architecture.md +461 -0
- package/specs/ADR-002-repo-release-model.md +108 -0
- package/specs/ADR-003-public-api-versioning.md +136 -0
- package/specs/ADR-004-focus-selection-policy.md +117 -0
- package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
- package/specs/ISSUE-BACKLOG.md +681 -0
- package/specs/RELEASE-CANDIDATE.md +30 -0
- package/specs/components/accordion.md +130 -0
- package/specs/components/alert-dialog.md +72 -0
- package/specs/components/alert.md +65 -0
- package/specs/components/badge.md +220 -0
- package/specs/components/breadcrumb.md +74 -0
- package/specs/components/button.md +115 -0
- package/specs/components/callout.md +195 -0
- package/specs/components/card.md +280 -0
- package/specs/components/carousel.md +140 -0
- package/specs/components/checkbox.md +172 -0
- package/specs/components/combobox.md +423 -0
- package/specs/components/command-palette.md +92 -0
- package/specs/components/context-menu.md +556 -0
- package/specs/components/copy-button.md +293 -0
- package/specs/components/date-picker.md +400 -0
- package/specs/components/dialog.md +298 -0
- package/specs/components/disclosure.md +257 -0
- package/specs/components/drawer.md +353 -0
- package/specs/components/feed.md +265 -0
- package/specs/components/grid.md +186 -0
- package/specs/components/input.md +254 -0
- package/specs/components/landmarks.md +136 -0
- package/specs/components/link.md +134 -0
- package/specs/components/listbox.md +351 -0
- package/specs/components/menu-button.md +76 -0
- package/specs/components/menu.md +623 -0
- package/specs/components/meter.md +149 -0
- package/specs/components/number.md +393 -0
- package/specs/components/popover.md +252 -0
- package/specs/components/progress.md +188 -0
- package/specs/components/radio-group.md +151 -0
- package/specs/components/select.md +144 -0
- package/specs/components/sidebar.md +321 -0
- package/specs/components/slider-multi-thumb.md +78 -0
- package/specs/components/slider.md +84 -0
- package/specs/components/spinbutton.md +140 -0
- package/specs/components/spinner.md +132 -0
- package/specs/components/switch.md +175 -0
- package/specs/components/table.md +403 -0
- package/specs/components/tabs.md +265 -0
- package/specs/components/textarea.md +185 -0
- package/specs/components/toast.md +198 -0
- package/specs/components/toolbar.md +278 -0
- package/specs/components/tooltip.md +252 -0
- package/specs/components/treegrid.md +281 -0
- package/specs/components/treeview.md +91 -0
- package/specs/components/window-splitter.md +297 -0
- package/specs/ops/git-shard-sync.md +107 -0
- package/specs/ops/release-checklist.md +76 -0
- package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
- package/specs/release/api-freeze-candidate.md +54 -0
- package/specs/release/changelog-automation.md +76 -0
- package/specs/release/changelog.generated.md +53 -0
- package/specs/release/changelog.patch.generated.md +46 -0
- package/specs/release/consumer-integration.md +53 -0
- package/specs/release/migration-notes-pre-v1.md +40 -0
- package/specs/release/mvp-changelog.md +57 -0
- package/specs/release/release-notes-template.md +61 -0
- package/specs/release/release-rehearsal.md +113 -0
- package/specs/release/semver-deprecation-dry-run.md +89 -0
- package/specs/release/shard-release-drill-report.md +50 -0
- package/specs/release/shard-release-follow-ups.md +31 -0
- package/specs/signals.md +208 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Headless Release Candidate Evidence
|
|
2
|
+
|
|
3
|
+
## APG coverage summary
|
|
4
|
+
|
|
5
|
+
- Component specs in `specs/components/*.md`: 30
|
|
6
|
+
- Implemented component modules in `src/<component>/index.ts`: 30
|
|
7
|
+
- Component tests in `src/<component>/*.test.ts`: 30
|
|
8
|
+
- Shared APG harness: `src/testing/apg-contract-harness.ts`
|
|
9
|
+
- Cross-component APG regression suite: `src/testing/apg-contracts.regression.test.ts`
|
|
10
|
+
|
|
11
|
+
Coverage focus for the newest tranche (HLS-131..HLS-134):
|
|
12
|
+
|
|
13
|
+
- `treegrid`: role/aria hierarchy metadata and keyboard transitions
|
|
14
|
+
- `feed`: role/aria stream metadata and paging keyboard contract
|
|
15
|
+
- `carousel`: role/aria-live semantics, control linkage, and rotation pause rules
|
|
16
|
+
- `window-splitter`: separator aria-value contract and orientation-aware keyboard resizing
|
|
17
|
+
|
|
18
|
+
## Release-ready checklist
|
|
19
|
+
|
|
20
|
+
- [x] `src/index.ts` exports include all implemented components
|
|
21
|
+
- [x] `README.md` implemented components list matches package surface
|
|
22
|
+
- [x] LSP diagnostics clean for changed source and test files
|
|
23
|
+
- [x] Package lint gates pass (`lint:types`, `lint:oxlint`, `lint:format`, `lint:boundaries`)
|
|
24
|
+
- [x] Package test gate passes (`vitest`)
|
|
25
|
+
- [x] APG contract helper and regression suite added
|
|
26
|
+
|
|
27
|
+
## Verification commands
|
|
28
|
+
|
|
29
|
+
- `npm run lint`
|
|
30
|
+
- `npm run test`
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Accordion Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Accordion` is a headless APG-aligned contract for a set of vertically stacked sections that can be expanded or collapsed to reveal or hide content. It manages expansion state, keyboard navigation between headers, and accessibility attributes.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/accordion/index.ts` - model and public `createAccordion` API
|
|
10
|
+
- `src/accordion/accordion.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
- `createAccordion(options)`
|
|
15
|
+
- `options`:
|
|
16
|
+
- `sections`: `AccordionSection[]` — `{id: string, disabled?: boolean}`
|
|
17
|
+
- `idBase?`: string
|
|
18
|
+
- `allowMultiple?`: boolean (default: `false`)
|
|
19
|
+
- `allowZeroExpanded?`: boolean (default: `true`)
|
|
20
|
+
- `initialExpandedIds?`: string[]
|
|
21
|
+
- `ariaLabel?`: string
|
|
22
|
+
- `headingLevel?`: number (default: `3`, clamped 1–6)
|
|
23
|
+
- `state` (signal-backed):
|
|
24
|
+
- `expandedIds()` — set of currently expanded section IDs
|
|
25
|
+
- `focusedId()` — ID of the section header currently holding focus
|
|
26
|
+
- `value()` — first expanded section ID, or `null` (computed)
|
|
27
|
+
- `expandedValues()` — array of expanded section IDs (computed)
|
|
28
|
+
- `sections()` — current sections list (reactive)
|
|
29
|
+
- `allowMultiple()` — current allow-multiple setting (reactive)
|
|
30
|
+
- `allowZeroExpanded()` — current allow-zero-expanded setting (reactive)
|
|
31
|
+
- `headingLevel()` — current heading level 1–6 (reactive)
|
|
32
|
+
- `ariaLabel()` — current aria-label (reactive)
|
|
33
|
+
- `actions`:
|
|
34
|
+
- `toggle(id)` — expands or collapses a section
|
|
35
|
+
- `expand(id)` — expands a section
|
|
36
|
+
- `collapse(id)` — collapses a section
|
|
37
|
+
- `setFocused(id)` — sets roving focus to a section
|
|
38
|
+
- `moveNext()` — moves focus to the next header
|
|
39
|
+
- `movePrev()` — moves focus to the previous header
|
|
40
|
+
- `moveFirst()` — moves focus to the first header
|
|
41
|
+
- `moveLast()` — moves focus to the last header
|
|
42
|
+
- `handleKeyDown(event)` — processes keyboard navigation and activation
|
|
43
|
+
- `setSections(sections)` — replaces sections list; enforces expanded invariants
|
|
44
|
+
- `setAllowMultiple(value)` — updates allow-multiple; clamps expanded to first if switching to single
|
|
45
|
+
- `setAllowZeroExpanded(value)` — updates allow-zero-expanded; expands first if none expanded
|
|
46
|
+
- `setHeadingLevel(level)` — updates heading level (clamped 1–6)
|
|
47
|
+
- `setAriaLabel(label)` — updates aria-label
|
|
48
|
+
- `setExpandedIds(ids)` — programmatically sets expanded sections; respects constraints
|
|
49
|
+
- `contracts`:
|
|
50
|
+
- `getRootProps()`
|
|
51
|
+
- `getHeaderProps(id)`
|
|
52
|
+
- `getTriggerProps(id)`
|
|
53
|
+
- `getPanelProps(id)`
|
|
54
|
+
|
|
55
|
+
## APG and A11y Contract
|
|
56
|
+
|
|
57
|
+
- root role: none (usually a `div` or `dl`)
|
|
58
|
+
- header role: none (usually a heading level `h1-h6`)
|
|
59
|
+
- trigger role: `button`
|
|
60
|
+
- panel role: `region`
|
|
61
|
+
- required attributes:
|
|
62
|
+
- trigger: `aria-expanded`, `aria-controls`, `id`, `aria-disabled`
|
|
63
|
+
- panel: `aria-labelledby`, `id`, `role="region"`
|
|
64
|
+
- focus management:
|
|
65
|
+
- triggers are in the page tab sequence
|
|
66
|
+
- focus moves between triggers using arrow keys (optional but recommended for large accordions)
|
|
67
|
+
|
|
68
|
+
## Behavior Contract
|
|
69
|
+
|
|
70
|
+
- **Single/Multiple Expansion**:
|
|
71
|
+
- if `allowMultiple` is `false`, expanding one section collapses others
|
|
72
|
+
- if `allowMultiple` is `true`, multiple sections can be expanded simultaneously
|
|
73
|
+
- **Collapsible**:
|
|
74
|
+
- if `allowZeroExpanded` is `false`, at least one section must remain expanded; clicking an expanded trigger does nothing if it's the only one expanded
|
|
75
|
+
- **Keyboard Navigation**:
|
|
76
|
+
- `Enter` or `Space`: toggles the expanded state of the panel associated with the focused trigger
|
|
77
|
+
- `ArrowDown`: moves focus to the next trigger; wraps if configured
|
|
78
|
+
- `ArrowUp`: moves focus to the previous trigger; wraps if configured
|
|
79
|
+
- `Home`: moves focus to the first trigger
|
|
80
|
+
- `End`: moves focus to the last trigger
|
|
81
|
+
|
|
82
|
+
## Reactive Config Invariants
|
|
83
|
+
|
|
84
|
+
When config atoms change at runtime, the model enforces invariants:
|
|
85
|
+
|
|
86
|
+
- `setAllowMultiple(false)` — if multiple sections are expanded, clamps to the first expanded section
|
|
87
|
+
- `setAllowZeroExpanded(false)` — if no sections are expanded, expands the first enabled section
|
|
88
|
+
- `setSections(...)` — removes expanded IDs that no longer exist in the new sections list; enforces `allowMultiple` and `allowZeroExpanded` constraints after pruning
|
|
89
|
+
- `setExpandedIds(...)` — filters out unknown section IDs; clamps to first if `allowMultiple` is false; expands first enabled if `allowZeroExpanded` is false and result is empty
|
|
90
|
+
|
|
91
|
+
## Invariants
|
|
92
|
+
|
|
93
|
+
- a disabled section cannot be expanded or collapsed via user interaction
|
|
94
|
+
- `expandedIds` must always respect the `allowMultiple` and `allowZeroExpanded` constraints
|
|
95
|
+
- `aria-controls` on the trigger must match the `id` of the panel
|
|
96
|
+
- `aria-labelledby` on the panel must match the `id` of the trigger
|
|
97
|
+
|
|
98
|
+
## Minimum Test Matrix
|
|
99
|
+
|
|
100
|
+
- initialize with specific sections expanded
|
|
101
|
+
- toggle expansion on `Enter`/`Space`
|
|
102
|
+
- respect `allowMultiple: false` (auto-collapse others)
|
|
103
|
+
- respect `allowZeroExpanded: false` (prevent collapsing last expanded)
|
|
104
|
+
- navigate between triggers using `ArrowDown`/`ArrowUp`/`Home`/`End`
|
|
105
|
+
- skip disabled sections during keyboard navigation
|
|
106
|
+
- verify `aria-expanded` and `aria-controls` linkage
|
|
107
|
+
- verify `aria-labelledby` linkage
|
|
108
|
+
- computed `value()` reflects first expanded ID
|
|
109
|
+
- computed `expandedValues()` reflects all expanded IDs
|
|
110
|
+
- `setSections` removes stale expanded IDs
|
|
111
|
+
- `setSections` respects `allowZeroExpanded` after pruning
|
|
112
|
+
- `setAllowMultiple(false)` clamps to first expanded
|
|
113
|
+
- `setAllowZeroExpanded(false)` expands first when none expanded
|
|
114
|
+
- `setHeadingLevel` clamps to 1–6
|
|
115
|
+
- `setAriaLabel` updates `getRootProps()` aria-label
|
|
116
|
+
- `setExpandedIds` respects `allowMultiple` and `allowZeroExpanded`
|
|
117
|
+
- `setExpandedIds` ignores unknown section IDs
|
|
118
|
+
|
|
119
|
+
## ADR-001 Compliance
|
|
120
|
+
|
|
121
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
122
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
123
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
124
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
125
|
+
|
|
126
|
+
## Out of Scope (Current)
|
|
127
|
+
|
|
128
|
+
- nested accordions (recursive state management)
|
|
129
|
+
- animation/transition state (handled by visual layer)
|
|
130
|
+
- drag-and-drop reordering
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Alert Dialog Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`AlertDialog` is a specialized modal dialog for critical confirmations or alerts that require immediate user attention. It differs from a standard dialog by its role and focus behavior, specifically prioritizing the least destructive action.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/alert-dialog/index.ts` - model and public `createAlertDialog` API
|
|
10
|
+
- `src/alert-dialog/alert-dialog.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
- `createAlertDialog(options)`
|
|
15
|
+
- `state` (signal-backed):
|
|
16
|
+
- `isOpen()`
|
|
17
|
+
- `actions`:
|
|
18
|
+
- `open`, `close`
|
|
19
|
+
- `handleKeyDown`
|
|
20
|
+
- `contracts`:
|
|
21
|
+
- `getDialogProps()`
|
|
22
|
+
- `getOverlayProps()`
|
|
23
|
+
- `getTitleProps()`
|
|
24
|
+
- `getDescriptionProps()`
|
|
25
|
+
- `getCancelButtonProps()`
|
|
26
|
+
- `getActionButtonProps()`
|
|
27
|
+
|
|
28
|
+
## APG and A11y Contract
|
|
29
|
+
|
|
30
|
+
- content role: `alertdialog`
|
|
31
|
+
- required attributes:
|
|
32
|
+
- content: `aria-modal="true"`, `aria-labelledby`, `aria-describedby`
|
|
33
|
+
- focus management:
|
|
34
|
+
- initial focus MUST be on the least destructive element (e.g., "Cancel" button)
|
|
35
|
+
- focus trap within the dialog while open
|
|
36
|
+
- focus restore to the trigger on close
|
|
37
|
+
|
|
38
|
+
## Behavior Contract
|
|
39
|
+
|
|
40
|
+
- `Escape` key closes the dialog (if appropriate for the context)
|
|
41
|
+
- focus management: specifically prioritizes the "Cancel" or "No" action to prevent accidental destructive actions
|
|
42
|
+
- scroll lock on the body when open
|
|
43
|
+
- focus trap: `Tab` and `Shift+Tab` cycle through focusable elements inside the dialog
|
|
44
|
+
|
|
45
|
+
## Invariants
|
|
46
|
+
|
|
47
|
+
- `isOpen` is a boolean
|
|
48
|
+
- `aria-describedby` is mandatory for `alertdialog` to ensure the alert message is announced
|
|
49
|
+
- focus is always trapped while open
|
|
50
|
+
- initial focus is placed on the cancel/least-destructive action by default
|
|
51
|
+
|
|
52
|
+
## Minimum Test Matrix
|
|
53
|
+
|
|
54
|
+
- open/close state transitions
|
|
55
|
+
- initial focus on the "Cancel" button by default
|
|
56
|
+
- focus trap behavior (Tab/Shift+Tab wrapping)
|
|
57
|
+
- focus restore on close
|
|
58
|
+
- `Escape` key dismissal
|
|
59
|
+
- mandatory `aria-describedby` presence check
|
|
60
|
+
|
|
61
|
+
## ADR-001 Compliance
|
|
62
|
+
|
|
63
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
64
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
65
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
66
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
67
|
+
|
|
68
|
+
## Out of Scope (Current)
|
|
69
|
+
|
|
70
|
+
- non-modal alerts (use `alert` component)
|
|
71
|
+
- complex multi-step confirmation flows
|
|
72
|
+
- custom focus trap logic (uses shared dialog primitive)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Alert Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Alert` is a headless contract for passive live region announcements. It is used to communicate important and usually time-sensitive information without interrupting the user's flow.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/alert/index.ts` - model and public `createAlert` API
|
|
10
|
+
- `src/alert/alert.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
- `createAlert(options)`
|
|
15
|
+
- `state` (signal-backed):
|
|
16
|
+
- `isVisible()`
|
|
17
|
+
- `message()`
|
|
18
|
+
- `actions`:
|
|
19
|
+
- `show(message)`, `hide`
|
|
20
|
+
- `contracts`:
|
|
21
|
+
- `getAlertProps()`
|
|
22
|
+
|
|
23
|
+
## APG and A11y Contract
|
|
24
|
+
|
|
25
|
+
- role: `alert`
|
|
26
|
+
- required attributes:
|
|
27
|
+
- `aria-live="assertive"`
|
|
28
|
+
- `aria-atomic="true"`
|
|
29
|
+
- behavior:
|
|
30
|
+
- assistive technologies should announce the content immediately when it changes
|
|
31
|
+
- does not take focus
|
|
32
|
+
|
|
33
|
+
## Behavior Contract
|
|
34
|
+
|
|
35
|
+
- `show`: updates `message` and sets `isVisible` to `true`
|
|
36
|
+
- `hide`: sets `isVisible` to `false`
|
|
37
|
+
- passive: does not manage focus or keyboard interactions
|
|
38
|
+
- automatic dismissal: can be configured via `duration` option
|
|
39
|
+
|
|
40
|
+
## Invariants
|
|
41
|
+
|
|
42
|
+
- `isVisible` is a boolean
|
|
43
|
+
- `role="alert"` is always present on the root element
|
|
44
|
+
- `aria-live` is set to `assertive` by default to ensure immediate announcement
|
|
45
|
+
|
|
46
|
+
## Minimum Test Matrix
|
|
47
|
+
|
|
48
|
+
- visibility state transitions
|
|
49
|
+
- message update reactivity
|
|
50
|
+
- correct ARIA attributes (`role`, `aria-live`, `aria-atomic`)
|
|
51
|
+
- verification that it does not interfere with focus
|
|
52
|
+
- auto-dismiss timer verification (if enabled)
|
|
53
|
+
|
|
54
|
+
## ADR-001 Compliance
|
|
55
|
+
|
|
56
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
57
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
58
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
59
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
60
|
+
|
|
61
|
+
## Out of Scope (Current)
|
|
62
|
+
|
|
63
|
+
- interactive alerts (use `alert-dialog`)
|
|
64
|
+
- multiple concurrent alerts (handled by consumer or higher-level toast manager)
|
|
65
|
+
- rich content within alerts
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Badge Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Badge` is a headless contract for a non-interactive status indicator that displays short labels, counts, or colored dots. It provides ARIA semantics for live-region announcements when badge content changes dynamically, and decorative hiding when the badge is purely visual.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/badge/index.ts` - model and public `createBadge` API
|
|
10
|
+
- `src/badge/badge.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Options (`CreateBadgeOptions`)
|
|
13
|
+
|
|
14
|
+
| Option | Type | Default | Description |
|
|
15
|
+
| -------------- | --------------------- | ----------- | ------------------------------------------------------------------------------ |
|
|
16
|
+
| `variant` | `BadgeVariant` | `'neutral'` | Visual variant: `'primary' \| 'success' \| 'neutral' \| 'warning' \| 'danger'` |
|
|
17
|
+
| `size` | `BadgeSize` | `'medium'` | Display size: `'small' \| 'medium' \| 'large'` |
|
|
18
|
+
| `dot` | `boolean` | `false` | Dot mode: hides textual content, shows a colored circle indicator |
|
|
19
|
+
| `pulse` | `boolean` | `false` | Whether the badge should animate to draw attention |
|
|
20
|
+
| `pill` | `boolean` | `false` | Whether to apply a pill (fully rounded) shape modifier |
|
|
21
|
+
| `isDynamic` | `boolean` | `false` | Whether content changes at runtime (enables live-region semantics) |
|
|
22
|
+
| `isDecorative` | `boolean` | `false` | Whether the badge is purely decorative (hides from assistive technology) |
|
|
23
|
+
| `ariaLabel` | `string \| undefined` | `undefined` | Accessible label override; useful in dot mode where visible text is absent |
|
|
24
|
+
|
|
25
|
+
## Type Definitions
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
type BadgeVariant = 'primary' | 'success' | 'neutral' | 'warning' | 'danger'
|
|
29
|
+
type BadgeSize = 'small' | 'medium' | 'large'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Public API
|
|
33
|
+
|
|
34
|
+
### `createBadge(options?: CreateBadgeOptions): BadgeModel`
|
|
35
|
+
|
|
36
|
+
### State (signal-backed)
|
|
37
|
+
|
|
38
|
+
| Signal | Type | Description |
|
|
39
|
+
| ---------------- | -------------------- | -------------------------------------------------------- |
|
|
40
|
+
| `variant()` | `Atom<BadgeVariant>` | Current visual variant |
|
|
41
|
+
| `size()` | `Atom<BadgeSize>` | Current display size |
|
|
42
|
+
| `dot()` | `Atom<boolean>` | Whether dot mode is active |
|
|
43
|
+
| `pulse()` | `Atom<boolean>` | Whether pulse animation is active |
|
|
44
|
+
| `pill()` | `Atom<boolean>` | Whether pill shape modifier is active |
|
|
45
|
+
| `isDynamic()` | `Atom<boolean>` | Whether the badge is a live region |
|
|
46
|
+
| `isDecorative()` | `Atom<boolean>` | Whether the badge is decorative-only |
|
|
47
|
+
| `isEmpty()` | `Computed<boolean>` | Derived: `true` when `dot` is `true` (content is hidden) |
|
|
48
|
+
|
|
49
|
+
### Actions
|
|
50
|
+
|
|
51
|
+
| Action | Signature | Description |
|
|
52
|
+
| --------------- | ------------------------------- | ----------------------------- |
|
|
53
|
+
| `setVariant` | `(value: BadgeVariant) => void` | Updates the visual variant |
|
|
54
|
+
| `setSize` | `(value: BadgeSize) => void` | Updates the display size |
|
|
55
|
+
| `setDot` | `(value: boolean) => void` | Toggles dot mode |
|
|
56
|
+
| `setPulse` | `(value: boolean) => void` | Toggles pulse animation |
|
|
57
|
+
| `setPill` | `(value: boolean) => void` | Toggles pill shape modifier |
|
|
58
|
+
| `setDynamic` | `(value: boolean) => void` | Toggles live-region semantics |
|
|
59
|
+
| `setDecorative` | `(value: boolean) => void` | Toggles decorative mode |
|
|
60
|
+
|
|
61
|
+
### Contracts
|
|
62
|
+
|
|
63
|
+
| Contract | Return type | Description |
|
|
64
|
+
| ----------------- | ------------ | -------------------------------------------------------- |
|
|
65
|
+
| `getBadgeProps()` | `BadgeProps` | Ready-to-spread ARIA attribute map for the badge element |
|
|
66
|
+
|
|
67
|
+
#### `BadgeProps` Shape
|
|
68
|
+
|
|
69
|
+
The returned object depends on the current state:
|
|
70
|
+
|
|
71
|
+
**When `isDecorative` is `true`:**
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
{
|
|
75
|
+
role: 'presentation'
|
|
76
|
+
'aria-hidden': 'true'
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**When `isDynamic` is `true` (and not decorative):**
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
{
|
|
84
|
+
role: 'status'
|
|
85
|
+
'aria-live': 'polite'
|
|
86
|
+
'aria-atomic': 'true'
|
|
87
|
+
'aria-label'?: string // from options.ariaLabel (recommended for dot mode)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Default (static, non-decorative):**
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
{
|
|
95
|
+
'aria-label'?: string // from options.ariaLabel (recommended for dot mode)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## APG and A11y Contract
|
|
100
|
+
|
|
101
|
+
- **Dynamic badge** (content changes at runtime):
|
|
102
|
+
- `role="status"` — implicit live region with polite politeness
|
|
103
|
+
- `aria-live="polite"` — explicit for broader assistive technology support
|
|
104
|
+
- `aria-atomic="true"` — announce the entire badge content on change
|
|
105
|
+
- **Decorative badge** (purely visual, no semantic meaning):
|
|
106
|
+
- `role="presentation"` — removes semantic meaning
|
|
107
|
+
- `aria-hidden="true"` — hidden from assistive technology
|
|
108
|
+
- **Static badge** (content does not change after render):
|
|
109
|
+
- No role or live-region attributes needed; content is read inline
|
|
110
|
+
- **Dot mode**:
|
|
111
|
+
- When `dot` is `true`, visible text content is hidden; `aria-label` should be provided for accessible meaning
|
|
112
|
+
- **Non-interactive**: no keyboard interaction, no `tabindex`, no focus management
|
|
113
|
+
|
|
114
|
+
## Keyboard Contract
|
|
115
|
+
|
|
116
|
+
Badge is not keyboard-interactive. No keyboard handling is needed.
|
|
117
|
+
|
|
118
|
+
## Behavior Contract
|
|
119
|
+
|
|
120
|
+
- All state properties are set programmatically; there are no user-driven interactions.
|
|
121
|
+
- `isEmpty` is derived from `dot`: when dot mode is active, the badge has no visible text content.
|
|
122
|
+
- `variant` accepts only the five defined values; invalid values should be ignored or defaulted to `'neutral'`.
|
|
123
|
+
- `size` accepts only the three defined values; invalid values should be ignored or defaulted to `'medium'`.
|
|
124
|
+
- `isDecorative` takes precedence over `isDynamic`: a decorative badge never becomes a live region regardless of `isDynamic` state.
|
|
125
|
+
|
|
126
|
+
## Transitions Table
|
|
127
|
+
|
|
128
|
+
| Trigger | Precondition | State Change | Contract Effect |
|
|
129
|
+
| -------------------------- | ------------- | ------------------------------- | ------------------------------------------------------------------------------------- |
|
|
130
|
+
| `actions.setVariant(v)` | valid variant | `variant` = v | no ARIA change |
|
|
131
|
+
| `actions.setSize(v)` | valid size | `size` = v | no ARIA change |
|
|
132
|
+
| `actions.setDot(v)` | any | `dot` = v; `isEmpty` recomputes | no ARIA change; UIKit hides/shows content |
|
|
133
|
+
| `actions.setPulse(v)` | any | `pulse` = v | no ARIA change |
|
|
134
|
+
| `actions.setPill(v)` | any | `pill` = v | no ARIA change |
|
|
135
|
+
| `actions.setDynamic(v)` | any | `isDynamic` = v | `getBadgeProps()` adds/removes `role="status"`, `aria-live`, `aria-atomic` |
|
|
136
|
+
| `actions.setDecorative(v)` | any | `isDecorative` = v | `getBadgeProps()` switches to `role="presentation"` + `aria-hidden="true"` or reverts |
|
|
137
|
+
|
|
138
|
+
## Invariants
|
|
139
|
+
|
|
140
|
+
- `isDecorative` takes precedence: when `true`, `getBadgeProps()` always returns `{ role: 'presentation', 'aria-hidden': 'true' }` regardless of `isDynamic`.
|
|
141
|
+
- `isEmpty` must be `true` if and only if `dot` is `true`.
|
|
142
|
+
- `variant` must always be one of `'primary' | 'success' | 'neutral' | 'warning' | 'danger'`.
|
|
143
|
+
- `size` must always be one of `'small' | 'medium' | 'large'`.
|
|
144
|
+
- Badge must never produce `tabindex`, keyboard event handlers, or focus-related attributes.
|
|
145
|
+
- When `isDynamic` is `true` and `isDecorative` is `false`, `role` must be `'status'` and `aria-live` must be `'polite'`.
|
|
146
|
+
|
|
147
|
+
## Adapter Expectations
|
|
148
|
+
|
|
149
|
+
This section defines what UIKit (`cv-badge`) binds to from the headless model.
|
|
150
|
+
|
|
151
|
+
### Signals read by adapter
|
|
152
|
+
|
|
153
|
+
| Signal | UIKit usage |
|
|
154
|
+
| ----------------- | ---------------------------------------------------------------------- |
|
|
155
|
+
| `state.variant()` | Maps to `variant` host attribute and CSS class for color theming |
|
|
156
|
+
| `state.size()` | Maps to `size` host attribute and CSS class for dimension styling |
|
|
157
|
+
| `state.dot()` | Sets `dot` attribute on host; conditionally hides default slot content |
|
|
158
|
+
| `state.pulse()` | Sets `pulse` attribute on host; toggles CSS pulse animation |
|
|
159
|
+
| `state.pill()` | Sets `pill` attribute on host; applies fully rounded border radius |
|
|
160
|
+
| `state.isEmpty()` | Used to conditionally suppress content rendering in dot mode |
|
|
161
|
+
|
|
162
|
+
### Actions called by adapter
|
|
163
|
+
|
|
164
|
+
| Action | UIKit trigger |
|
|
165
|
+
| -------------------------- | ---------------------------------------------------------------- |
|
|
166
|
+
| `actions.setVariant(v)` | When `variant` attribute/property changes on the host element |
|
|
167
|
+
| `actions.setSize(v)` | When `size` attribute/property changes on the host element |
|
|
168
|
+
| `actions.setDot(v)` | When `dot` attribute/property changes on the host element |
|
|
169
|
+
| `actions.setPulse(v)` | When `pulse` attribute/property changes on the host element |
|
|
170
|
+
| `actions.setPill(v)` | When `pill` attribute/property changes on the host element |
|
|
171
|
+
| `actions.setDynamic(v)` | When `dynamic` attribute/property changes on the host element |
|
|
172
|
+
| `actions.setDecorative(v)` | When `decorative` attribute/property changes on the host element |
|
|
173
|
+
|
|
174
|
+
### Contracts spread by adapter
|
|
175
|
+
|
|
176
|
+
| Contract | Target element | Notes |
|
|
177
|
+
| ----------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
178
|
+
| `getBadgeProps()` | Root badge element (`part="base"`) | Spread as attributes; provides `role`, `aria-live`, `aria-atomic`, `aria-hidden`, `aria-label` as applicable |
|
|
179
|
+
|
|
180
|
+
### Options passed through from UIKit attributes
|
|
181
|
+
|
|
182
|
+
| UIKit attribute | Headless option | Notes |
|
|
183
|
+
| --------------- | --------------- | -------------------------------------------------- |
|
|
184
|
+
| `variant` | `variant` | String enum, defaults to `'neutral'` |
|
|
185
|
+
| `size` | `size` | String enum, defaults to `'medium'` |
|
|
186
|
+
| `dot` | `dot` | Boolean attribute |
|
|
187
|
+
| `pulse` | `pulse` | Boolean attribute |
|
|
188
|
+
| `pill` | `pill` | Boolean attribute |
|
|
189
|
+
| `dynamic` | `isDynamic` | Boolean attribute; enables live-region semantics |
|
|
190
|
+
| `decorative` | `isDecorative` | Boolean attribute; hides from assistive technology |
|
|
191
|
+
| `aria-label` | `ariaLabel` | Labeling; recommended when `dot` is `true` |
|
|
192
|
+
|
|
193
|
+
## Minimum Test Matrix
|
|
194
|
+
|
|
195
|
+
- default state: `variant='neutral'`, `size='medium'`, all booleans `false`
|
|
196
|
+
- `getBadgeProps()` returns no role/live-region attrs for static non-decorative badge
|
|
197
|
+
- `getBadgeProps()` returns `role="status"`, `aria-live="polite"`, `aria-atomic="true"` when `isDynamic` is `true`
|
|
198
|
+
- `getBadgeProps()` returns `role="presentation"`, `aria-hidden="true"` when `isDecorative` is `true`
|
|
199
|
+
- `isDecorative` takes precedence over `isDynamic` in contract output
|
|
200
|
+
- `isEmpty` is `true` when `dot` is `true`, `false` otherwise
|
|
201
|
+
- `setVariant` updates variant signal; invalid values rejected
|
|
202
|
+
- `setSize` updates size signal; invalid values rejected
|
|
203
|
+
- `setDot(true)` makes `isEmpty` compute to `true`
|
|
204
|
+
- `aria-label` is included in props when provided
|
|
205
|
+
- badge never produces `tabindex` or keyboard handler attributes
|
|
206
|
+
|
|
207
|
+
## ADR-001 Compliance
|
|
208
|
+
|
|
209
|
+
- **Runtime Policy**: Reatom v1000 only; no `@statx/*` in headless core.
|
|
210
|
+
- **Layering**: `core -> interactions -> a11y-contracts -> adapters`; adapters remain thin mappings.
|
|
211
|
+
- **Independence**: No imports from `@project/*`, `apps/*`, or other out-of-package modules.
|
|
212
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
213
|
+
|
|
214
|
+
## Out of Scope (Current)
|
|
215
|
+
|
|
216
|
+
- notification count management or capping (consumer responsibility)
|
|
217
|
+
- positioning relative to parent element (layout/CSS concern)
|
|
218
|
+
- removable/dismissible badges (would require interactive contract)
|
|
219
|
+
- animation orchestration for pulse (handled by visual layer)
|
|
220
|
+
- badge groups or stacking
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Breadcrumb Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Breadcrumb` is a headless APG-aligned contract for a navigation landmark that helps users understand their current location within a hierarchical structure. It ensures the correct navigation role and identifies the current page within the sequence.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/breadcrumb/index.ts` - model and public `createBreadcrumb` API
|
|
10
|
+
- `src/breadcrumb/breadcrumb.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
- `createBreadcrumb(options)`
|
|
15
|
+
- `options`:
|
|
16
|
+
- `items`: array of breadcrumb items `{ id, label, href, isCurrent? }`
|
|
17
|
+
- `state` (signal-backed):
|
|
18
|
+
- `items()` - list of breadcrumb items with `id`, `label`, `href`, and `isCurrent`
|
|
19
|
+
- `actions`: none (primarily a structural/navigational component)
|
|
20
|
+
- `contracts`:
|
|
21
|
+
- `getRootProps()` - returns props for the `<nav>` element
|
|
22
|
+
- `getListProps()` - returns props for the list element (`<ol>` or `<ul>`)
|
|
23
|
+
- `getItemProps(id)` - returns props for the list item element (`<li>`)
|
|
24
|
+
- `getLinkProps(id)` - returns props for the link element (`<a>`)
|
|
25
|
+
- `getSeparatorProps(id)` - returns props for the separator element (usually `aria-hidden="true"`)
|
|
26
|
+
|
|
27
|
+
## APG and A11y Contract
|
|
28
|
+
|
|
29
|
+
- root role: `nav`
|
|
30
|
+
- list role: none (usually `ol` or `ul`)
|
|
31
|
+
- item role: none (usually `li`)
|
|
32
|
+
- link role: `link`
|
|
33
|
+
- required attributes:
|
|
34
|
+
- root: `aria-label="Breadcrumb"` (or localized equivalent)
|
|
35
|
+
- current link: `aria-current="page"`
|
|
36
|
+
- focus management:
|
|
37
|
+
- links are in the page tab sequence
|
|
38
|
+
- the current page link may or may not be focusable depending on implementation, but must have `aria-current="page"`
|
|
39
|
+
|
|
40
|
+
## Behavior Contract
|
|
41
|
+
|
|
42
|
+
- **Structural Integrity**:
|
|
43
|
+
- the component provides the necessary ARIA attributes to identify the navigation landmark and the current page
|
|
44
|
+
- **Current Page**:
|
|
45
|
+
- exactly one item (usually the last one) should have `isCurrent: true`
|
|
46
|
+
- the `getLinkProps` for the current item must include `aria-current="page"`
|
|
47
|
+
|
|
48
|
+
## Invariants
|
|
49
|
+
|
|
50
|
+
- the root element must be a `<nav>` or have `role="navigation"`
|
|
51
|
+
- `aria-label` (or `aria-labelledby`) is required on the root to distinguish it from other navigation landmarks
|
|
52
|
+
- only the current page link should have `aria-current="page"`
|
|
53
|
+
|
|
54
|
+
## Minimum Test Matrix
|
|
55
|
+
|
|
56
|
+
- render a list of breadcrumb items
|
|
57
|
+
- verify `aria-label="Breadcrumb"` on the root
|
|
58
|
+
- verify `aria-current="page"` on the current item link
|
|
59
|
+
- verify other items do not have `aria-current`
|
|
60
|
+
- verify correct `href` mapping for links
|
|
61
|
+
- verify separators are `aria-hidden="true"`
|
|
62
|
+
|
|
63
|
+
## ADR-001 Compliance
|
|
64
|
+
|
|
65
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
66
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
67
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
68
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
69
|
+
|
|
70
|
+
## Out of Scope (Current)
|
|
71
|
+
|
|
72
|
+
- collapsible breadcrumbs (overflow management)
|
|
73
|
+
- dropdown menus within breadcrumbs
|
|
74
|
+
- dynamic path updates (handled by routing integration)
|