@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.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/a11y-contracts/index.d.ts +23 -0
  4. package/dist/a11y-contracts/index.js +1 -0
  5. package/dist/accordion/index.d.ts +78 -0
  6. package/dist/accordion/index.js +264 -0
  7. package/dist/adapters/index.d.ts +9 -0
  8. package/dist/adapters/index.js +1 -0
  9. package/dist/alert/index.d.ts +33 -0
  10. package/dist/alert/index.js +54 -0
  11. package/dist/alert-dialog/index.d.ts +69 -0
  12. package/dist/alert-dialog/index.js +94 -0
  13. package/dist/badge/index.d.ts +48 -0
  14. package/dist/badge/index.js +89 -0
  15. package/dist/breadcrumb/index.d.ts +55 -0
  16. package/dist/breadcrumb/index.js +77 -0
  17. package/dist/button/index.d.ts +46 -0
  18. package/dist/button/index.js +86 -0
  19. package/dist/callout/index.d.ts +41 -0
  20. package/dist/callout/index.js +63 -0
  21. package/dist/card/index.d.ts +54 -0
  22. package/dist/card/index.js +103 -0
  23. package/dist/carousel/index.d.ts +98 -0
  24. package/dist/carousel/index.js +243 -0
  25. package/dist/checkbox/index.d.ts +50 -0
  26. package/dist/checkbox/index.js +87 -0
  27. package/dist/combobox/index.d.ts +114 -0
  28. package/dist/combobox/index.js +431 -0
  29. package/dist/command-palette/index.d.ts +73 -0
  30. package/dist/command-palette/index.js +147 -0
  31. package/dist/context-menu/index.d.ts +111 -0
  32. package/dist/context-menu/index.js +372 -0
  33. package/dist/copy-button/index.d.ts +62 -0
  34. package/dist/copy-button/index.js +183 -0
  35. package/dist/core/index.d.ts +20 -0
  36. package/dist/core/index.js +2 -0
  37. package/dist/core/selection.d.ts +5 -0
  38. package/dist/core/selection.js +39 -0
  39. package/dist/core/value-range.d.ts +49 -0
  40. package/dist/core/value-range.js +134 -0
  41. package/dist/date-picker/index.d.ts +210 -0
  42. package/dist/date-picker/index.js +895 -0
  43. package/dist/dialog/index.d.ts +95 -0
  44. package/dist/dialog/index.js +153 -0
  45. package/dist/disclosure/index.d.ts +52 -0
  46. package/dist/disclosure/index.js +159 -0
  47. package/dist/drawer/index.d.ts +30 -0
  48. package/dist/drawer/index.js +39 -0
  49. package/dist/feed/index.d.ts +77 -0
  50. package/dist/feed/index.js +260 -0
  51. package/dist/grid/index.d.ts +103 -0
  52. package/dist/grid/index.js +415 -0
  53. package/dist/index.d.ts +51 -0
  54. package/dist/index.js +51 -0
  55. package/dist/input/index.d.ts +86 -0
  56. package/dist/input/index.js +156 -0
  57. package/dist/interactions/composite-navigation.d.ts +69 -0
  58. package/dist/interactions/composite-navigation.js +169 -0
  59. package/dist/interactions/index.d.ts +15 -0
  60. package/dist/interactions/index.js +4 -0
  61. package/dist/interactions/keyboard-intents.d.ts +16 -0
  62. package/dist/interactions/keyboard-intents.js +33 -0
  63. package/dist/interactions/overlay-focus.d.ts +40 -0
  64. package/dist/interactions/overlay-focus.js +93 -0
  65. package/dist/interactions/typeahead.d.ts +20 -0
  66. package/dist/interactions/typeahead.js +41 -0
  67. package/dist/landmarks/index.d.ts +39 -0
  68. package/dist/landmarks/index.js +58 -0
  69. package/dist/link/index.d.ts +34 -0
  70. package/dist/link/index.js +39 -0
  71. package/dist/listbox/index.d.ts +92 -0
  72. package/dist/listbox/index.js +337 -0
  73. package/dist/menu/index.d.ts +132 -0
  74. package/dist/menu/index.js +541 -0
  75. package/dist/menu-button/index.d.ts +71 -0
  76. package/dist/menu-button/index.js +121 -0
  77. package/dist/meter/index.d.ts +45 -0
  78. package/dist/meter/index.js +106 -0
  79. package/dist/number/index.d.ts +113 -0
  80. package/dist/number/index.js +252 -0
  81. package/dist/popover/index.d.ts +70 -0
  82. package/dist/popover/index.js +126 -0
  83. package/dist/progress/index.d.ts +49 -0
  84. package/dist/progress/index.js +79 -0
  85. package/dist/radio-group/index.d.ts +61 -0
  86. package/dist/radio-group/index.js +150 -0
  87. package/dist/select/index.d.ts +92 -0
  88. package/dist/select/index.js +239 -0
  89. package/dist/sidebar/index.d.ts +74 -0
  90. package/dist/sidebar/index.js +186 -0
  91. package/dist/slider/index.d.ts +61 -0
  92. package/dist/slider/index.js +150 -0
  93. package/dist/slider-multi-thumb/index.d.ts +70 -0
  94. package/dist/slider-multi-thumb/index.js +222 -0
  95. package/dist/spinbutton/index.d.ts +75 -0
  96. package/dist/spinbutton/index.js +214 -0
  97. package/dist/spinner/index.d.ts +1 -0
  98. package/dist/spinner/index.js +1 -0
  99. package/dist/spinner/spinner.d.ts +23 -0
  100. package/dist/spinner/spinner.js +25 -0
  101. package/dist/switch/index.d.ts +40 -0
  102. package/dist/switch/index.js +61 -0
  103. package/dist/table/index.d.ts +117 -0
  104. package/dist/table/index.js +377 -0
  105. package/dist/tabs/index.d.ts +63 -0
  106. package/dist/tabs/index.js +174 -0
  107. package/dist/textarea/index.d.ts +68 -0
  108. package/dist/textarea/index.js +137 -0
  109. package/dist/toast/index.d.ts +67 -0
  110. package/dist/toast/index.js +145 -0
  111. package/dist/toolbar/index.d.ts +59 -0
  112. package/dist/toolbar/index.js +139 -0
  113. package/dist/tooltip/index.d.ts +52 -0
  114. package/dist/tooltip/index.js +169 -0
  115. package/dist/treegrid/index.d.ts +101 -0
  116. package/dist/treegrid/index.js +463 -0
  117. package/dist/treeview/index.d.ts +68 -0
  118. package/dist/treeview/index.js +370 -0
  119. package/dist/window-splitter/index.d.ts +65 -0
  120. package/dist/window-splitter/index.js +204 -0
  121. package/package.json +92 -0
  122. package/specs/ADR-001-headless-architecture.md +461 -0
  123. package/specs/ADR-002-repo-release-model.md +108 -0
  124. package/specs/ADR-003-public-api-versioning.md +136 -0
  125. package/specs/ADR-004-focus-selection-policy.md +117 -0
  126. package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
  127. package/specs/ISSUE-BACKLOG.md +681 -0
  128. package/specs/RELEASE-CANDIDATE.md +30 -0
  129. package/specs/components/accordion.md +130 -0
  130. package/specs/components/alert-dialog.md +72 -0
  131. package/specs/components/alert.md +65 -0
  132. package/specs/components/badge.md +220 -0
  133. package/specs/components/breadcrumb.md +74 -0
  134. package/specs/components/button.md +115 -0
  135. package/specs/components/callout.md +195 -0
  136. package/specs/components/card.md +280 -0
  137. package/specs/components/carousel.md +140 -0
  138. package/specs/components/checkbox.md +172 -0
  139. package/specs/components/combobox.md +423 -0
  140. package/specs/components/command-palette.md +92 -0
  141. package/specs/components/context-menu.md +556 -0
  142. package/specs/components/copy-button.md +293 -0
  143. package/specs/components/date-picker.md +400 -0
  144. package/specs/components/dialog.md +298 -0
  145. package/specs/components/disclosure.md +257 -0
  146. package/specs/components/drawer.md +353 -0
  147. package/specs/components/feed.md +265 -0
  148. package/specs/components/grid.md +186 -0
  149. package/specs/components/input.md +254 -0
  150. package/specs/components/landmarks.md +136 -0
  151. package/specs/components/link.md +134 -0
  152. package/specs/components/listbox.md +351 -0
  153. package/specs/components/menu-button.md +76 -0
  154. package/specs/components/menu.md +623 -0
  155. package/specs/components/meter.md +149 -0
  156. package/specs/components/number.md +393 -0
  157. package/specs/components/popover.md +252 -0
  158. package/specs/components/progress.md +188 -0
  159. package/specs/components/radio-group.md +151 -0
  160. package/specs/components/select.md +144 -0
  161. package/specs/components/sidebar.md +321 -0
  162. package/specs/components/slider-multi-thumb.md +78 -0
  163. package/specs/components/slider.md +84 -0
  164. package/specs/components/spinbutton.md +140 -0
  165. package/specs/components/spinner.md +132 -0
  166. package/specs/components/switch.md +175 -0
  167. package/specs/components/table.md +403 -0
  168. package/specs/components/tabs.md +265 -0
  169. package/specs/components/textarea.md +185 -0
  170. package/specs/components/toast.md +198 -0
  171. package/specs/components/toolbar.md +278 -0
  172. package/specs/components/tooltip.md +252 -0
  173. package/specs/components/treegrid.md +281 -0
  174. package/specs/components/treeview.md +91 -0
  175. package/specs/components/window-splitter.md +297 -0
  176. package/specs/ops/git-shard-sync.md +107 -0
  177. package/specs/ops/release-checklist.md +76 -0
  178. package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
  179. package/specs/release/api-freeze-candidate.md +54 -0
  180. package/specs/release/changelog-automation.md +76 -0
  181. package/specs/release/changelog.generated.md +53 -0
  182. package/specs/release/changelog.patch.generated.md +46 -0
  183. package/specs/release/consumer-integration.md +53 -0
  184. package/specs/release/migration-notes-pre-v1.md +40 -0
  185. package/specs/release/mvp-changelog.md +57 -0
  186. package/specs/release/release-notes-template.md +61 -0
  187. package/specs/release/release-rehearsal.md +113 -0
  188. package/specs/release/semver-deprecation-dry-run.md +89 -0
  189. package/specs/release/shard-release-drill-report.md +50 -0
  190. package/specs/release/shard-release-follow-ups.md +31 -0
  191. 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)