@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,281 @@
1
+ # Treegrid Component Contract
2
+
3
+ ## Purpose
4
+
5
+ `Treegrid` provides a headless APG-aligned model for hierarchical tabular data, combining the multi-column structure of a `Grid` with the expansion/collapse behavior of a `Treeview`.
6
+
7
+ ## Component Files
8
+
9
+ - `src/treegrid/index.ts` - model and public `createTreegrid` API
10
+ - `src/treegrid/treegrid.test.ts` - unit behavior tests
11
+
12
+ ## Exported Types
13
+
14
+ ```ts
15
+ type TreegridSelectionMode = 'single' | 'multiple'
16
+ type TreegridCellRole = 'gridcell' | 'rowheader' | 'columnheader'
17
+
18
+ interface TreegridRow {
19
+ id: string
20
+ index?: number
21
+ disabled?: boolean
22
+ children?: readonly TreegridRow[]
23
+ }
24
+
25
+ interface TreegridColumn {
26
+ id: string
27
+ index?: number
28
+ disabled?: boolean
29
+ cellRole?: TreegridCellRole
30
+ }
31
+
32
+ interface TreegridCellId {
33
+ rowId: string
34
+ colId: string
35
+ }
36
+
37
+ interface TreegridKeyboardEventLike {
38
+ key: string
39
+ shiftKey?: boolean
40
+ ctrlKey?: boolean
41
+ metaKey?: boolean
42
+ altKey?: boolean
43
+ }
44
+
45
+ interface TreegridState {
46
+ activeCellId: Atom<TreegridCellId | null>
47
+ expandedRowIds: Atom<Set<string>>
48
+ selectedRowIds: Atom<Set<string>>
49
+ rowCount: Computed<number>
50
+ columnCount: Computed<number>
51
+ }
52
+
53
+ interface TreegridActions {
54
+ moveUp(): void
55
+ moveDown(): void
56
+ moveLeft(): void
57
+ moveRight(): void
58
+ moveRowStart(): void
59
+ moveRowEnd(): void
60
+ expandRow(rowId: string): void
61
+ collapseRow(rowId: string): void
62
+ toggleRowExpanded(rowId: string): void
63
+ selectRow(rowId: string): void
64
+ toggleRowSelection(rowId: string): void
65
+ handleKeyDown(event: TreegridKeyboardEventLike): void
66
+ }
67
+
68
+ interface TreegridContracts {
69
+ getTreegridProps(): TreegridProps
70
+ getRowProps(rowId: string): TreegridRowProps
71
+ getCellProps(rowId: string, colId: string): TreegridCellProps
72
+ }
73
+
74
+ interface TreegridModel {
75
+ readonly state: TreegridState
76
+ readonly actions: TreegridActions
77
+ readonly contracts: TreegridContracts
78
+ }
79
+ ```
80
+
81
+ ## Options (`CreateTreegridOptions`)
82
+
83
+ | Option | Type | Default | Description |
84
+ | ----------------------- | --------------------------- | ------------ | ---------------------------------------------------------- |
85
+ | `rows` | `readonly TreegridRow[]` | required | Hierarchical row definitions (children nested recursively) |
86
+ | `columns` | `readonly TreegridColumn[]` | required | Column definitions with optional role and disabled state |
87
+ | `disabledCells` | `readonly TreegridCellId[]` | `[]` | Individual cells disabled beyond row/column level |
88
+ | `idBase` | `string` | `'treegrid'` | Prefix for all generated DOM ids and atom names |
89
+ | `ariaLabel` | `string` | `undefined` | `aria-label` for the root treegrid element |
90
+ | `ariaLabelledBy` | `string` | `undefined` | `aria-labelledby` for the root treegrid element |
91
+ | `selectionMode` | `TreegridSelectionMode` | `'single'` | Single or multi-row selection |
92
+ | `initialExpandedRowIds` | `readonly string[]` | `[]` | Row ids that begin expanded; invalid/leaf ids are filtered |
93
+ | `initialActiveCellId` | `TreegridCellId \| null` | `null` | Initially active cell; normalized to first enabled cell |
94
+ | `initialSelectedRowIds` | `readonly string[]` | `[]` | Initially selected rows; trimmed to 1 for single mode |
95
+
96
+ ## Public API
97
+
98
+ ### `createTreegrid(options: CreateTreegridOptions): TreegridModel`
99
+
100
+ Returns a `TreegridModel` with three namespaces:
101
+
102
+ ### `state` (signal-backed)
103
+
104
+ | Signal | Type | Description |
105
+ | ---------------- | ------------------------------ | ------------------------------------------------- |
106
+ | `activeCellId` | `Atom<TreegridCellId \| null>` | Currently focused cell; `null` when grid is empty |
107
+ | `expandedRowIds` | `Atom<Set<string>>` | Set of expanded branch row identifiers |
108
+ | `selectedRowIds` | `Atom<Set<string>>` | Set of selected row identifiers |
109
+ | `rowCount` | `Computed<number>` | Total number of rows (all, not just visible) |
110
+ | `columnCount` | `Computed<number>` | Total number of columns |
111
+
112
+ ### `actions`
113
+
114
+ | Action | Description |
115
+ | ------------------------------------------------- | ----------------------------------------------------------------------- |
116
+ | `moveUp()` | Move active cell to the same column in the previous visible enabled row |
117
+ | `moveDown()` | Move active cell to the same column in the next visible enabled row |
118
+ | `moveLeft()` | APG ArrowLeft behavior (see Keyboard Contract) |
119
+ | `moveRight()` | APG ArrowRight behavior (see Keyboard Contract) |
120
+ | `moveRowStart()` | Move active cell to the first enabled cell in the current row |
121
+ | `moveRowEnd()` | Move active cell to the last enabled cell in the current row |
122
+ | `expandRow(rowId: string)` | Expand a branch row; no-op on leaf rows or already-expanded rows |
123
+ | `collapseRow(rowId: string)` | Collapse a branch row; migrates focus if active cell is a descendant |
124
+ | `toggleRowExpanded(rowId: string)` | Toggle expanded state of a branch row |
125
+ | `selectRow(rowId: string)` | Replace selection with the given row (single and multiple modes) |
126
+ | `toggleRowSelection(rowId: string)` | In multiple mode: add/remove from selection; in single mode: set |
127
+ | `handleKeyDown(event: TreegridKeyboardEventLike)` | Dispatch keyboard events to the appropriate action |
128
+
129
+ Note: `moveGridStart` (Ctrl+Home) and `moveGridEnd` (Ctrl+End) are internal actions invoked via `handleKeyDown` and are not exposed on the `actions` interface.
130
+
131
+ ### `contracts`
132
+
133
+ | Contract | Description |
134
+ | --------------------------------------------------------------- | ----------------------------------------------------- |
135
+ | `getTreegridProps(): TreegridProps` | Props to spread on the root `[role=treegrid]` element |
136
+ | `getRowProps(rowId: string): TreegridRowProps` | Props to spread on each `[role=row]` element |
137
+ | `getCellProps(rowId: string, colId: string): TreegridCellProps` | Props to spread on each cell element |
138
+
139
+ ## Contract Prop Shapes
140
+
141
+ ### `TreegridProps` (root element)
142
+
143
+ ```ts
144
+ interface TreegridProps {
145
+ id: string // `${idBase}-root`
146
+ role: 'treegrid'
147
+ tabindex: '-1'
148
+ 'aria-label'?: string
149
+ 'aria-labelledby'?: string
150
+ 'aria-multiselectable': 'true' | 'false'
151
+ 'aria-rowcount': number // total row count (all rows, not just visible)
152
+ 'aria-colcount': number // total column count
153
+ }
154
+ ```
155
+
156
+ ### `TreegridRowProps` (row element)
157
+
158
+ ```ts
159
+ interface TreegridRowProps {
160
+ id: string // `${idBase}-row-${rowId}`
161
+ role: 'row'
162
+ 'aria-level': number // starts at 1 for root rows
163
+ 'aria-posinset': number // 1-based position within sibling set
164
+ 'aria-setsize': number // total siblings count
165
+ 'aria-rowindex': number // row.index if provided, else 1-based declaration order
166
+ 'aria-expanded'?: 'true' | 'false' // only present on branch rows
167
+ 'aria-selected': 'true' | 'false'
168
+ 'aria-disabled'?: 'true' // only present when row is disabled
169
+ }
170
+ ```
171
+
172
+ ### `TreegridCellProps` (cell element)
173
+
174
+ ```ts
175
+ interface TreegridCellProps {
176
+ id: string // `${idBase}-cell-${rowId}-${colId}`
177
+ role: TreegridCellRole // from column.cellRole, defaults to 'gridcell'
178
+ tabindex: '0' | '-1' // '0' only for the active non-disabled cell
179
+ 'aria-colindex': number // column.index if provided, else 1-based declaration order
180
+ 'aria-selected': 'true' | 'false'
181
+ 'aria-disabled'?: 'true' // only present when cell is disabled
182
+ 'data-active': 'true' | 'false'
183
+ onFocus: () => void // calls setActiveCell to sync focus with state
184
+ }
185
+ ```
186
+
187
+ ## APG and A11y Contract
188
+
189
+ - root role: `treegrid`
190
+ - row role: `row`
191
+ - cell role: `gridcell`, `rowheader`, or `columnheader` (per column definition)
192
+ - required attributes:
193
+ - root: `aria-label` or `aria-labelledby`, `aria-multiselectable`, `aria-rowcount`, `aria-colcount`
194
+ - row: `aria-level`, `aria-posinset`, `aria-setsize`, `aria-rowindex`, `aria-expanded` (for branch rows), `aria-selected`
195
+ - cell: `aria-selected`, `aria-colindex`, `tabindex`, `data-active`
196
+
197
+ ## Keyboard Contract
198
+
199
+ - `ArrowUp` / `ArrowDown`: move focus to the cell in the previous/next visible enabled row (same column)
200
+ - `ArrowLeft`:
201
+ - if the focused row is a branch row and is expanded: collapse it
202
+ - if the focused row has a parent: move focus to the same column of the parent row (fallback to first enabled cell in parent if that column is disabled)
203
+ - otherwise (root leaf row): move focus to the previous enabled cell in the same row
204
+ - `ArrowRight`:
205
+ - if the focused row is a branch row and is collapsed: expand it (focus stays)
206
+ - if the focused row is a branch row and is expanded: move focus to the same column of the first child row (fallback to first enabled cell in child)
207
+ - otherwise (leaf row or no children): move focus to the next enabled cell in the same row
208
+ - `Home`: move focus to the first enabled cell in the current row
209
+ - `End`: move focus to the last enabled cell in the current row
210
+ - `Ctrl+Home` / `Meta+Home`: move focus to the first enabled cell in the first visible enabled row
211
+ - `Ctrl+End` / `Meta+End`: move focus to the last enabled cell in the last visible enabled row
212
+
213
+ ## Invariants
214
+
215
+ - `activeCellId` must always point to a visible, enabled cell (not inside a collapsed branch, not disabled)
216
+ - collapsing a branch row that contains the `activeCellId` (or any descendant) migrates focus to the collapsed row (same column if not disabled, else first enabled cell in that row)
217
+ - `aria-level` starts at 1 for root rows
218
+ - `initialSelectedRowIds` is clamped to the first entry in `single` mode
219
+ - `expandRow` and `collapseRow` are no-ops on leaf rows
220
+ - disabled rows are excluded from all navigation traversal and from selection
221
+
222
+ ## Minimum Test Matrix
223
+
224
+ - 2D navigation across hierarchical rows (`ArrowUp`, `ArrowDown`)
225
+ - `ArrowRight` expand then move to first child in same column
226
+ - `ArrowLeft` parent transition and collapse behavior
227
+ - focus migration when a parent row is collapsed while a descendant is active
228
+ - structural ARIA metadata (`aria-level`, `aria-posinset`, `aria-setsize`, `aria-rowindex`, `aria-expanded`, `aria-rowcount`, `aria-colcount`) correctness
229
+ - skipping disabled rows and disabled cells during navigation
230
+ - `Home` / `End` within row; `Ctrl+Home` / `Ctrl+End` across grid
231
+
232
+ ## Adapter Expectations
233
+
234
+ UIKit bindings (e.g., `cv-treegrid` web component) MUST interact with the headless model as follows:
235
+
236
+ ### Signals UIKit reads
237
+
238
+ | Signal | When to read |
239
+ | ------------------------ | ------------------------------------------------------------------------------------------------------ |
240
+ | `state.activeCellId()` | To derive which cell has `tabindex="0"` and `data-active="true"` (already embedded in `getCellProps`) |
241
+ | `state.expandedRowIds()` | To compute which child rows are visible / to drive `aria-expanded` (already embedded in `getRowProps`) |
242
+ | `state.selectedRowIds()` | To drive selection styling beyond ARIA (already embedded in `getRowProps` / `getCellProps`) |
243
+ | `state.rowCount()` | If UIKit renders a virtual list and needs the total count |
244
+ | `state.columnCount()` | If UIKit renders a virtual list and needs the total count |
245
+
246
+ ### Actions UIKit calls
247
+
248
+ | Action | Trigger |
249
+ | ----------------------------------- | --------------------------------------------------------- |
250
+ | `actions.handleKeyDown(event)` | `keydown` event on any focusable cell or the root element |
251
+ | `actions.toggleRowExpanded(rowId)` | Click on expand/collapse toggle affordance |
252
+ | `actions.toggleRowSelection(rowId)` | Pointer click on a row (accumulate for multi-select) |
253
+ | `actions.selectRow(rowId)` | Programmatic selection replacement (replaces current set) |
254
+ | `actions.expandRow(rowId)` | Programmatic expand |
255
+ | `actions.collapseRow(rowId)` | Programmatic collapse |
256
+
257
+ ### Contracts UIKit spreads
258
+
259
+ | Contract | Spread target |
260
+ | -------------------------------------- | ---------------------------------------------------------------- |
261
+ | `contracts.getTreegridProps()` | Root `<div role="treegrid">` or `<table>` wrapper |
262
+ | `contracts.getRowProps(rowId)` | Each `<tr role="row">` or `<div role="row">` |
263
+ | `contracts.getCellProps(rowId, colId)` | Each `<td role="gridcell/rowheader">` or `<div role="gridcell">` |
264
+
265
+ Note: `getCellProps` includes an `onFocus` handler that must be wired to the cell's `focus` event so that mouse-driven focus is reflected back into headless state.
266
+
267
+ ## ADR-001 Compliance
268
+
269
+ - **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
270
+ - **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
271
+ - **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
272
+ - **Verification**: Mandatory adapter integration tests and standalone package test execution.
273
+
274
+ ## Out of Scope (Current)
275
+
276
+ - async loading of child rows
277
+ - column-specific sorting
278
+ - drag and drop reordering
279
+ - multiple cell selection (only row selection is prioritized)
280
+ - level-sync from parent `cv-treegrid` (UIKit concern, not headless)
281
+ - multi-select click accumulation vs programmatic replace distinction (UIKit concern; headless exposes both `toggleRowSelection` and `selectRow`)
@@ -0,0 +1,91 @@
1
+ # Treeview Component Contract
2
+
3
+ ## Purpose
4
+
5
+ `Treeview` provides a headless APG-aligned model for hierarchical navigation,
6
+ expansion state, focus management, and selection behavior.
7
+
8
+ ## Component Files
9
+
10
+ - `src/treeview/index.ts` - model and public `createTreeview` API
11
+ - `src/treeview/treeview.test.ts` - unit behavior tests
12
+
13
+ ## Public API
14
+
15
+ - `createTreeview(options)`
16
+ - `state` (signal-backed): `activeId()`, `selectedIds()`, `expandedIds()`
17
+ - `actions`:
18
+ - focus: `setActive(id)` — programmatically set focus to a specific enabled visible node (or null to clear)
19
+ - navigation: `moveNext`, `movePrev`, `moveFirst`, `moveLast` — advance focus through the visible enabled node list; in single-select mode each call also moves selection to the newly focused node
20
+ - expansion: `expand`, `collapse`, `toggleExpanded`, `expandActive`, `collapseActive`
21
+ - selection: `select`, `toggleSelected`, `clearSelected`
22
+ - keyboard: `handleKeyDown` — dispatches navigation, expansion, and selection intents from keyboard events; ArrowUp/Down/Home/End trigger navigation (and selection-follows-focus in single-select mode); ArrowRight/Left trigger expansion/collapse or parent navigation; Space triggers `toggleSelected`; Enter triggers `select`; Ctrl/Cmd+A triggers select-all in multiple mode
23
+ - `contracts`:
24
+ - `getTreeProps()`
25
+ - `getItemProps(id)`
26
+ - `getVisibleNodeIds()`
27
+
28
+ ## APG and A11y Contract
29
+
30
+ - tree role: `tree`
31
+ - node role: `treeitem`
32
+ - node metadata:
33
+ - `aria-level`
34
+ - `aria-posinset`
35
+ - `aria-setsize`
36
+ - branch metadata:
37
+ - `aria-expanded`
38
+ - selection metadata:
39
+ - `aria-selected`
40
+ - disabled metadata:
41
+ - `aria-disabled`
42
+
43
+ ## Keyboard Contract
44
+
45
+ - `ArrowUp` / `ArrowDown`: visible-node traversal; in single-select mode also moves selection to the newly focused enabled node
46
+ - `Home` / `End`: first/last visible node; in single-select mode also moves selection to the newly focused enabled node
47
+ - `ArrowRight`:
48
+ - expands collapsed branch
49
+ - moves to first child when already expanded
50
+ - `ArrowLeft`:
51
+ - collapses expanded branch
52
+ - moves to parent when already collapsed
53
+ - `Enter`: select active node (both modes)
54
+ - `Space`: toggle selection on active node (both modes; in multiple mode, focus and selection are independent)
55
+ - `Ctrl/Cmd + A` in multiple mode: select all enabled nodes
56
+
57
+ ## Selection-Follows-Focus (Single-Select Mode)
58
+
59
+ In single-select mode (`selectionMode: 'single'`), selection follows focus for navigation actions. When `moveNext`, `movePrev`, `moveFirst`, or `moveLast` moves focus to a new enabled visible node, `selectedIds` is simultaneously updated to contain only that node's id. This is the APG-recommended roving-tabindex pattern for single-select trees.
60
+
61
+ In multiple-select mode (`selectionMode: 'multiple'`), focus and selection remain independent. Navigation actions only update `activeId`; selection changes require explicit `Space`, `Enter`, or `Ctrl/Cmd+A` interactions.
62
+
63
+ ## Selection and Collapse Invariants
64
+
65
+ - `activeId` is always `null` or an enabled visible node id
66
+ - in single-select mode, after any navigation action (`moveNext`, `movePrev`, `moveFirst`, `moveLast`) that moves focus to a new node, `selectedIds` equals `[newActiveId]`
67
+ - collapsing a branch while focus is inside descendants moves focus to the collapsed parent
68
+ - selected descendants remain selected after collapse unless explicitly changed
69
+ - disabled nodes cannot become selected through actions
70
+
71
+ ## Minimum Test Matrix
72
+
73
+ - deterministic visible traversal from expansion state
74
+ - Arrow traversal with expand/collapse transitions
75
+ - structural ARIA metadata coverage
76
+ - multi-select behavior including select-all shortcut
77
+ - focus and selection invariants under collapse
78
+
79
+ ## ADR-001 Compliance
80
+
81
+ - **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
82
+ - **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
83
+ - **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
84
+ - **Verification**: Mandatory adapter integration tests and standalone package test execution.
85
+
86
+ ## Out of Scope (Current)
87
+
88
+ - async/lazy node loading
89
+ - drag-and-drop reordering
90
+ - checkbox/radio treeitem variants
91
+ - typeahead across visible nodes