@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,297 @@
1
+ # Window Splitter Component Contract
2
+
3
+ ## Purpose
4
+
5
+ `Window Splitter` (or Splitter) provides a headless APG-aligned model for a moveable separator between two panes, enabling users to resize them via keyboard or pointer interactions.
6
+
7
+ ## Component Files
8
+
9
+ - `src/window-splitter/index.ts` - model and public `createWindowSplitter` API
10
+ - `src/window-splitter/window-splitter.test.ts` - unit behavior tests
11
+
12
+ ---
13
+
14
+ ## Orientation Semantics (ARIA-aligned)
15
+
16
+ The `orientation` option describes the physical orientation of the **separator bar itself**, not the layout direction. This matches ARIA conventions.
17
+
18
+ | `orientation` value | Separator bar direction | Layout split | Active arrow keys |
19
+ | ------------------- | ------------------------------- | ------------------ | ----------------------------------------------- |
20
+ | `'vertical'` | Vertical bar (standing upright) | Left / right panes | `ArrowLeft` (decrease), `ArrowRight` (increase) |
21
+ | `'horizontal'` | Horizontal bar (lying flat) | Top / bottom panes | `ArrowUp` (decrease), `ArrowDown` (increase) |
22
+
23
+ **Important correction from earlier convention:** Prior versions of this spec had the mapping reversed (horizontal → left/right keys). The ARIA-aligned convention used here treats `orientation` as describing the separator element itself, not the axis of movement. A vertical separator divides content left and right; a horizontal separator divides content top and bottom.
24
+
25
+ The `aria-orientation` attribute exposed on the separator element reflects this same value directly.
26
+
27
+ ---
28
+
29
+ ## Public API
30
+
31
+ ### `CreateWindowSplitterOptions`
32
+
33
+ | Option | Type | Default | Description |
34
+ | ------------------ | ---------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `idBase` | `string` | `'window-splitter'` | Base string used to derive stable element IDs and atom names. |
36
+ | `min` | `number` | `0` | Minimum allowed position value. |
37
+ | `max` | `number` | `100` | Maximum allowed position value. |
38
+ | `position` | `number` | mid-point of `[min, max]` | Initial position. |
39
+ | `step` | `number` | `1` | Amount moved per arrow-key press. |
40
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Orientation of the separator bar (see Orientation Semantics section). |
41
+ | `isFixed` | `boolean` | `false` | When `true`, the splitter is in fixed mode: arrow keys are disabled and `Enter` toggles between min and max. |
42
+ | `ariaLabel` | `string` | — | Value for `aria-label` on the separator element. |
43
+ | `ariaLabelledBy` | `string` | — | Value for `aria-labelledby` on the separator element. |
44
+ | `primaryPaneId` | `string` | `'{idBase}-pane-primary'` | Explicit ID for the primary pane element. |
45
+ | `secondaryPaneId` | `string` | `'{idBase}-pane-secondary'` | Explicit ID for the secondary pane element. |
46
+ | `formatValueText` | `(value: number) => string` | — | Custom formatter for `aria-valuetext`. |
47
+ | `snap` | `string` | — | Optional space-separated list of snap positions. Each token is either a bare number (value in `[min, max]` units) or a percentage string ending in `%` (resolved as `min + pct/100 * (max - min)`). Example: `"25% 50% 75%"` or `"10 50 90"`. |
48
+ | `snapThreshold` | `number` | `12` | Maximum distance (in the same units as position) between the candidate position and a snap point for snapping to activate. |
49
+ | `onPositionChange` | `(value: number) => void` | — | Callback fired after position changes. Receives the post-snap, clamped value. Only called when the value actually changes. |
50
+
51
+ ### State Signals
52
+
53
+ | Signal | Type | Description |
54
+ | ------------------- | ---------------------------------- | ------------------------------------------------- |
55
+ | `state.position` | `Atom<number>` | Current position, always clamped to `[min, max]`. |
56
+ | `state.min` | `Atom<number>` | Current minimum bound. |
57
+ | `state.max` | `Atom<number>` | Current maximum bound. |
58
+ | `state.orientation` | `Atom<'horizontal' \| 'vertical'>` | Current orientation of the separator bar. |
59
+ | `state.isDragging` | `Atom<boolean>` | Whether a pointer drag is in progress. |
60
+
61
+ ### Actions
62
+
63
+ | Action | Signature | Description |
64
+ | --------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
65
+ | `setPosition` | `(value: number) => void` | Set position to `value`. Applies snap logic then range clamping (see Snap Behavior). Fires `onPositionChange` if the value changed. |
66
+ | `moveStep` | `(direction: -1 \| 1) => void` | Move position by one `step` in the given direction. Fires `onPositionChange` if the value changed. |
67
+ | `moveToMin` | `() => void` | Move position to `min`. Fires `onPositionChange` if the value changed. |
68
+ | `moveToMax` | `() => void` | Move position to `max`. Fires `onPositionChange` if the value changed. |
69
+ | `startDragging` | `() => void` | Set `isDragging` to `true`. |
70
+ | `stopDragging` | `() => void` | Set `isDragging` to `false`. |
71
+ | `handleKeyDown` | `(event: Pick<KeyboardEvent, 'key'>) => void` | Dispatch keyboard intent to the appropriate action. |
72
+
73
+ ### Contracts
74
+
75
+ | Method | Returns | Description |
76
+ | ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------- |
77
+ | `getSplitterProps()` | `WindowSplitterProps` | ARIA and event props for the separator element. Must be called inside a reactive scope (reads signals). |
78
+ | `getPrimaryPaneProps()` | `WindowSplitterPaneProps` | Data attributes for the primary pane element. |
79
+ | `getSecondaryPaneProps()` | `WindowSplitterPaneProps` | Data attributes for the secondary pane element. |
80
+
81
+ ### TypeScript Shapes
82
+
83
+ ```ts
84
+ export interface WindowSplitterProps {
85
+ id: string
86
+ role: 'separator'
87
+ tabindex: '0'
88
+ 'aria-valuenow': string
89
+ 'aria-valuemin': string
90
+ 'aria-valuemax': string
91
+ 'aria-valuetext'?: string
92
+ 'aria-orientation': WindowSplitterOrientation
93
+ 'aria-controls': string
94
+ 'aria-label'?: string
95
+ 'aria-labelledby'?: string
96
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void
97
+ }
98
+
99
+ export interface WindowSplitterPaneProps {
100
+ id: string
101
+ 'data-pane': 'primary' | 'secondary'
102
+ 'data-orientation': WindowSplitterOrientation
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## APG and A11y Contract
109
+
110
+ - role: `separator`
111
+ - Required attributes on the separator element:
112
+ - `aria-valuenow`: current position (string)
113
+ - `aria-valuemin`: minimum position (string)
114
+ - `aria-valuemax`: maximum position (string)
115
+ - `aria-orientation`: `'horizontal'` or `'vertical'` (see Orientation Semantics)
116
+ - `aria-controls`: space-separated IDs of the pane elements being resized
117
+ - `tabindex`: `'0'` — the splitter must be focusable
118
+ - Optional attributes:
119
+ - `aria-label` or `aria-labelledby` — at least one should be provided for accessible labelling
120
+ - `aria-valuetext` — human-readable position label via `formatValueText`
121
+
122
+ ---
123
+
124
+ ## Keyboard Contract
125
+
126
+ | Key | Orientation condition | Effect |
127
+ | ------------ | ------------------------------ | ------------------------------------------- |
128
+ | `ArrowLeft` | `orientation === 'vertical'` | Decrease position by one `step` |
129
+ | `ArrowRight` | `orientation === 'vertical'` | Increase position by one `step` |
130
+ | `ArrowUp` | `orientation === 'horizontal'` | Decrease position by one `step` |
131
+ | `ArrowDown` | `orientation === 'horizontal'` | Increase position by one `step` |
132
+ | `Home` | any | Move position to `min` |
133
+ | `End` | any | Move position to `max` |
134
+ | `Enter` | any (`isFixed === true`) | Toggle between min and max (see Fixed Mode) |
135
+
136
+ Keys for the **inactive orientation** (e.g., `ArrowLeft`/`ArrowRight` when `orientation === 'horizontal'`) are no-ops and must not change state.
137
+
138
+ When `isFixed === true`, all arrow keys are disabled. Only `Enter`, `Home`, and `End` remain active.
139
+
140
+ ---
141
+
142
+ ## Fixed Mode
143
+
144
+ When `isFixed: true` is passed in options:
145
+
146
+ - Arrow keys (`ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown`) are **disabled** — pressing them does nothing.
147
+ - `Home` and `End` still work as normal.
148
+ - `Enter` **toggles** position between `min` and `max`:
149
+ - If `position <= midpoint` (where `midpoint = min + (max - min) / 2`), set position to `max`.
150
+ - Otherwise, set position to `min`.
151
+ - `isFixed` defaults to `false`. When `false`, `Enter` has no effect.
152
+
153
+ This mode supports use cases like a collapsible sidebar where the splitter can only be "open" or "closed".
154
+
155
+ ---
156
+
157
+ ## Snap Behavior
158
+
159
+ When the `snap` option is provided, `setPosition` (and any action that ultimately calls it) applies snap logic before finalizing the value.
160
+
161
+ ### Algorithm
162
+
163
+ 1. **Parse** the `snap` string into an array of numeric positions:
164
+ - Tokens ending in `%` are resolved as: `min + (pct / 100) * (max - min)`
165
+ - Bare numeric tokens are used as-is (as values in `[min, max]` space)
166
+ - Invalid tokens are ignored
167
+ 2. **Clamp** the incoming position to `[min, max]`.
168
+ 3. **Find** the nearest snap point from the parsed array.
169
+ 4. **Snap**: if `|clampedPosition - nearestSnapPoint| <= snapThreshold`, use `nearestSnapPoint` as the final position.
170
+ 5. Otherwise, use the clamped position unchanged.
171
+ 6. **Fire** `onPositionChange` with the final post-snap value (only if it differs from the previous position).
172
+
173
+ ### Defaults
174
+
175
+ - `snap`: not set (snapping disabled)
176
+ - `snapThreshold`: `12`
177
+
178
+ ### Examples
179
+
180
+ ```
181
+ min=0, max=100, snap="25% 50% 75%", snapThreshold=12
182
+ setPosition(28) → nearest snap = 25, distance = 3 ≤ 12 → final = 25
183
+ setPosition(40) → nearest snap = 50, distance = 10 ≤ 12 → final = 50
184
+ setPosition(62) → nearest snap = 75, distance = 13 > 12 → final = 62 (no snap)
185
+
186
+ min=0, max=200, snap="50 100 150", snapThreshold=12
187
+ setPosition(55) → nearest snap = 50, distance = 5 ≤ 12 → final = 50
188
+ setPosition(70) → nearest snap = 50, distance = 20 > 12 and to 100 distance = 30 > 12 → final = 70
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Behavior Contract
194
+
195
+ - `Window Splitter` manages the numerical state of the split. The actual resizing of DOM elements is handled by the adapter (e.g., via CSS variables or direct style updates).
196
+ - `orientation` determines which arrow keys are active (see Orientation Semantics and Keyboard Contract).
197
+ - `step` size for keyboard navigation is configurable via `CreateWindowSplitterOptions`.
198
+ - `onPositionChange` is only fired when the position value **actually changes** (comparing pre- and post-action values). No spurious calls on no-ops.
199
+
200
+ ---
201
+
202
+ ## Invariants
203
+
204
+ - `position` must always be clamped between `min` and `max`.
205
+ - `aria-valuenow` must be updated in real-time during dragging or keyboard interaction.
206
+ - The splitter must be focusable (tabindex `'0'`) to allow keyboard users to resize panes.
207
+ - Arrow keys for the inactive orientation must have no effect on state.
208
+ - `onPositionChange` must never fire when the position did not change.
209
+
210
+ ---
211
+
212
+ ## Adapter Expectations
213
+
214
+ The UIKit adapter (e.g., a Solid.js or Angular binding) is expected to:
215
+
216
+ ### Signals read by the adapter
217
+
218
+ | Signal | Usage |
219
+ | ------------------- | ---------------------------------------------------- |
220
+ | `state.position` | Drive CSS variable or style for pane size |
221
+ | `state.isDragging` | Apply a drag-active CSS class or `user-select: none` |
222
+ | `state.orientation` | Conditionally apply layout CSS |
223
+
224
+ ### Actions called by the adapter
225
+
226
+ | Action | When |
227
+ | ---------------------- | ---------------------------------------------------------------------- |
228
+ | `startDragging()` | On `pointerdown` on the separator element |
229
+ | `setPosition(value)` | On `pointermove` while dragging (adapter computes pixel-to-unit value) |
230
+ | `stopDragging()` | On `pointerup` |
231
+ | `handleKeyDown(event)` | On `keydown` on the separator element |
232
+
233
+ ### Contracts spread by the adapter
234
+
235
+ The adapter calls `getSplitterProps()`, `getPrimaryPaneProps()`, and `getSecondaryPaneProps()` inside a reactive computation and spreads the returned objects directly onto the corresponding DOM elements.
236
+
237
+ ### Pointer event drag contract
238
+
239
+ ```
240
+ pointerdown on separator
241
+ → actions.startDragging()
242
+ → element.setPointerCapture(event.pointerId)
243
+
244
+ pointermove (while captured)
245
+ → actions.setPosition(computedValue) // adapter converts pointer offset to numeric position
246
+
247
+ pointerup / pointercancel
248
+ → actions.stopDragging()
249
+ → element.releasePointerCapture(event.pointerId) // optional; browser releases automatically on pointerup
250
+ ```
251
+
252
+ ### Snap and `snapThreshold` pass-through
253
+
254
+ The UIKit adapter reads `snap` and `snapThreshold` from element attributes (or component props) and passes them directly to `createWindowSplitter`. The adapter does not implement snap logic itself — it is fully handled inside the headless model.
255
+
256
+ ---
257
+
258
+ ## Minimum Test Matrix
259
+
260
+ | Test case | What is verified |
261
+ | ------------------------------------------------- | -------------------------------------------------------------------------------- |
262
+ | Arrow keys move position (vertical orientation) | `ArrowLeft` decreases, `ArrowRight` increases; `ArrowUp`/`ArrowDown` are no-ops |
263
+ | Arrow keys move position (horizontal orientation) | `ArrowUp` decreases, `ArrowDown` increases; `ArrowLeft`/`ArrowRight` are no-ops |
264
+ | Inactive-orientation keys are no-ops | Neither signal changes nor `onPositionChange` fires |
265
+ | `Home` moves to `min` | Position becomes `min` regardless of current position |
266
+ | `End` moves to `max` | Position becomes `max` regardless of current position |
267
+ | Clamping at boundaries | `setPosition` beyond `max` clamps to `max`; below `min` clamps to `min` |
268
+ | `aria-valuenow` synchronization | Reflects current position after every update |
269
+ | Drag lifecycle | `startDragging` → `isDragging === true`; `stopDragging` → `isDragging === false` |
270
+ | `aria-controls` linkage | `getSplitterProps()['aria-controls']` contains both pane IDs |
271
+ | `onPositionChange` only fires on actual change | Calling `setPosition(currentValue)` does not invoke the callback |
272
+ | Fixed mode — Enter toggles to max | When `position <= midpoint`, Enter sets position to `max` |
273
+ | Fixed mode — Enter toggles to min | When `position > midpoint`, Enter sets position to `min` |
274
+ | Fixed mode — arrow keys disabled | Arrow keys do nothing when `isFixed === true` |
275
+ | Snap within threshold | `setPosition` value within `snapThreshold` of a snap point → snaps to that point |
276
+ | Snap beyond threshold | `setPosition` value beyond `snapThreshold` of all snap points → no snap |
277
+ | Snap percentage resolution | `"50%"` with `min=0, max=200` resolves to `100` |
278
+ | Snap no-op when `snap` not set | Normal `setPosition` behavior without snap string |
279
+ | `onPositionChange` receives post-snap value | Callback value equals the snapped position, not the raw input |
280
+
281
+ ---
282
+
283
+ ## ADR-001 Compliance
284
+
285
+ - **Runtime Policy**: Reatom v1000 only; no `@statx/*` in headless core.
286
+ - **Layering**: core → interactions → a11y-contracts → adapters; adapters remain thin mappings.
287
+ - **Independence**: No imports from `@project/*`, `apps/*`, or other out-of-package modules.
288
+ - **Verification**: Mandatory adapter integration tests and standalone package test execution.
289
+
290
+ ---
291
+
292
+ ## Out of Scope (Current)
293
+
294
+ - Multiple splitters in a single container (nested splitters)
295
+ - "Collapsible" panes (where the pane can be hidden completely)
296
+ - Persistent state (saving position to localStorage)
297
+ - Touch-specific gesture optimization (should be handled in the adapter)
@@ -0,0 +1,107 @@
1
+ # Git-Shard Sync Operations Guide
2
+
3
+ ## Purpose
4
+
5
+ This document defines the operational sync flow between:
6
+
7
+ - monorepo mirror: `packages/headless`
8
+ - canonical public repository: git-shard
9
+
10
+ It implements `HLS-001` and supports ADR-001/ADR-002.
11
+
12
+ ## Source of Truth
13
+
14
+ - git-shard is the canonical source for tags, release history, and package publication.
15
+ - monorepo is a development mirror used for local integration and fast iteration.
16
+
17
+ ## Sync Directions
18
+
19
+ ## 1) Outbound Sync (mirror -> shard)
20
+
21
+ Use this when changes were developed inside monorepo and must be promoted to canonical shard.
22
+
23
+ ### Preconditions
24
+
25
+ 1. changes are limited to `packages/headless/**`
26
+ 2. local checks pass:
27
+ - `npm run lint`
28
+ - `npm run test`
29
+ 3. boundary check is green
30
+
31
+ ### Procedure
32
+
33
+ 1. create a dedicated sync branch in git-shard:
34
+ - naming: `sync/mono-YYYYMMDD-<topic>`
35
+ 2. copy/sync files from mirror into shard working tree
36
+ 3. run shard-local checks:
37
+ - lint
38
+ - test
39
+ 4. open PR in shard with title prefix: `sync(mirror): ...`
40
+ 5. merge only after CI is green
41
+
42
+ ### Required Evidence in PR
43
+
44
+ - list of synced files
45
+ - lint/test outputs
46
+ - statement confirming no monorepo-internal imports
47
+
48
+ ## 2) Inbound Sync (shard -> mirror)
49
+
50
+ Use this after shard releases or direct shard-first development.
51
+
52
+ ### Preconditions
53
+
54
+ 1. identify source tag/commit in shard
55
+ 2. confirm shard CI is green for that state
56
+
57
+ ### Procedure
58
+
59
+ 1. create monorepo branch:
60
+ - naming: `sync/shard-YYYYMMDD-<tag-or-topic>`
61
+ 2. copy/sync shard files into `packages/headless`
62
+ 3. run mirror checks:
63
+ - `npm run lint`
64
+ - `npm run test`
65
+ 4. open monorepo PR with title prefix: `sync(shard): ...`
66
+ 5. merge after CI is green
67
+
68
+ ### Required Evidence in PR
69
+
70
+ - shard commit/tag reference
71
+ - changed file list
72
+ - local validation outputs
73
+
74
+ ## Conflict Resolution Rules
75
+
76
+ When mirror and shard diverge:
77
+
78
+ 1. prefer shard behavior for released contracts
79
+ 2. prefer newer docs if they are contract-compatible
80
+ 3. if behavior differs and contract impact is unclear:
81
+ - stop merge
82
+ - create reconciliation issue
83
+ - resolve before sync merge
84
+
85
+ ## Branch and Tag Handling
86
+
87
+ - release tags (`vX.Y.Z`) are created only in shard
88
+ - mirror must not create package release tags for `headless`
89
+ - mirror sync PRs must reference source shard tag when applicable
90
+
91
+ ## Emergency Rules
92
+
93
+ If a critical bugfix is required:
94
+
95
+ 1. patch in shard first
96
+ 2. release from shard
97
+ 3. back-sync to mirror immediately
98
+
99
+ ## Operational Checklist (Quick)
100
+
101
+ Before any sync merge:
102
+
103
+ - [ ] scope limited to `packages/headless/**`
104
+ - [ ] lint/test checks are green
105
+ - [ ] no forbidden imports
106
+ - [ ] source and destination references documented
107
+ - [ ] PR title uses `sync(mirror)` or `sync(shard)` prefix
@@ -0,0 +1,76 @@
1
+ # Headless Release Checklist (Git-Shard)
2
+
3
+ ## Purpose
4
+
5
+ This checklist is mandatory for shard releases and implements `HLS-002`.
6
+
7
+ It enforces ADR-002 (release ownership) and ADR-003 (versioning/deprecation policy).
8
+
9
+ ## Inputs
10
+
11
+ - target release version
12
+ - release branch/PR
13
+ - changelog draft
14
+ - list of included issues/PRs
15
+
16
+ ## 1) Scope and Source Verification
17
+
18
+ - [ ] release branch is in git-shard (not monorepo mirror)
19
+ - [ ] release scope is limited to package-owned files
20
+ - [ ] all sync references are documented (if changes originated in mirror)
21
+
22
+ ## 2) Quality Gates
23
+
24
+ - [ ] `npm run lint` passes in shard
25
+ - [ ] `npm run test` passes in shard
26
+ - [ ] boundary checks pass (no forbidden imports)
27
+ - [ ] no unresolved TODO/FIXME in contract-critical files
28
+
29
+ ## 3) SemVer Classification (ADR-003)
30
+
31
+ - [ ] classify release as `patch`, `minor`, or `major`
32
+ - [ ] release PR body includes `SemVer: patch|minor|major`
33
+ - [ ] classification includes runtime API impact
34
+ - [ ] classification includes type-level API impact
35
+ - [ ] classification includes behavior-contract impact (keyboard/focus/a11y)
36
+
37
+ Decision log:
38
+
39
+ - Selected version type:
40
+ - Rationale:
41
+
42
+ ## 4) Deprecation Review (ADR-003)
43
+
44
+ - [ ] all newly deprecated APIs are marked with `@deprecated`
45
+ - [ ] replacement paths are documented
46
+ - [ ] deprecation timeline is explicit
47
+ - [ ] no removal happens before required deprecation cycle
48
+ - [ ] for `major` releases, PR body includes `Migration Notes: <path or link>`
49
+ - [ ] for `major` releases, `specs/release/migration-notes-pre-v1.md` is updated in the same PR
50
+
51
+ ## 5) Documentation and Changelog
52
+
53
+ - [ ] changelog updated
54
+ - [ ] release notes include user-visible changes
55
+ - [ ] breaking changes section present if applicable
56
+ - [ ] migration notes included for incompatible changes
57
+
58
+ ## 6) Tag and Publish Preparation
59
+
60
+ - [ ] target version in package manifest is correct
61
+ - [ ] release tag format is `vX.Y.Z`
62
+ - [ ] release commit is finalized and reviewed
63
+ - [ ] package contents are validated before publish
64
+
65
+ ## 7) Publish and Post-Release
66
+
67
+ - [ ] publish completed from git-shard only
68
+ - [ ] tag pushed and release notes published
69
+ - [ ] post-release sync back to monorepo mirror created/tracked
70
+
71
+ ## Sign-off
72
+
73
+ - Release owner:
74
+ - Reviewer:
75
+ - Date:
76
+ - Result: Approved / Blocked
@@ -0,0 +1,88 @@
1
+ # Gap-to-Green Issue Pack
2
+
3
+ ## Purpose
4
+
5
+ This file provides issue-ready tickets for the remaining work before stable release sign-off.
6
+
7
+ ## How to Use
8
+
9
+ - copy each issue into tracker as a standalone task
10
+ - keep scope limited to `packages/headless/**` unless issue says otherwise
11
+ - require evidence links for CI runs and docs updates
12
+
13
+ Common labels:
14
+
15
+ - `headless`
16
+ - `release`
17
+ - `governance`
18
+ - `tests`
19
+ - `docs`
20
+ - `ci`
21
+
22
+ ## HLS-GTG-001 - Enforce ADR-003 classification in release PRs
23
+
24
+ - **Status**: Open
25
+ - **Priority**: High
26
+ - **Labels**: `headless`, `release`, `governance`, `ci`
27
+ - **Scope**: add an automated gate that validates SemVer classification in release PR metadata
28
+ - **Deliverables**:
29
+ - release-governance check script in `packages/headless/scripts/`
30
+ - CI workflow job that runs the check for release PR context
31
+ - **Acceptance Criteria**:
32
+ - release PR without explicit SemVer classification fails CI
33
+ - supported values are `patch`, `minor`, `major`
34
+ - non-release PRs are not blocked by this check
35
+
36
+ ## HLS-GTG-002 - Enforce migration notes policy on behavior-breaking changes
37
+
38
+ - **Status**: Open
39
+ - **Priority**: High
40
+ - **Labels**: `headless`, `release`, `governance`, `docs`
41
+ - **Scope**: require migration-note evidence when release PR declares breaking behavior/API change
42
+ - **Deliverables**:
43
+ - governance check updated with migration-note requirement
44
+ - release checklist wording aligned to automation
45
+ - **Acceptance Criteria**:
46
+ - release PR classified as `major` fails if migration note reference is absent
47
+ - release PR classified as `major` fails if migration notes file is not changed
48
+ - check output includes actionable failure reason
49
+
50
+ ## HLS-GTG-003 - Add adapter integration test coverage
51
+
52
+ - **Status**: Open
53
+ - **Priority**: High
54
+ - **Labels**: `headless`, `tests`, `a11y`
55
+ - **Scope**: add component-level integration tests validating adapter-style bindings for keyboard/pointer flows
56
+ - **Deliverables**:
57
+ - adapter integration tests under `packages/headless/src/adapters/`
58
+ - **Acceptance Criteria**:
59
+ - tests validate `model -> bindings -> events -> state` flow
60
+ - tests cover keyboard and pointer paths
61
+ - tests run in existing `npm run test`
62
+
63
+ ## HLS-GTG-004 - Finalize docs for multi-component reality and ADR status
64
+
65
+ - **Status**: Open
66
+ - **Priority**: Medium
67
+ - **Labels**: `headless`, `docs`
68
+ - **Scope**: align README and ADR-001 metadata with implemented state of package
69
+ - **Deliverables**:
70
+ - `packages/headless/README.md` updated with all implemented components and structure
71
+ - `packages/headless/specs/ADR-001-headless-architecture.md` status/version update
72
+ - **Acceptance Criteria**:
73
+ - README no longer describes listbox as the only current example
74
+ - ADR-001 status reflects accepted architecture baseline
75
+ - docs remain consistent with current source tree
76
+
77
+ ## HLS-GTG-005 - Add monorepo CI guard for headless package gates
78
+
79
+ - **Status**: Open
80
+ - **Priority**: High
81
+ - **Labels**: `headless`, `ci`, `release`
82
+ - **Scope**: add root CI job that runs package-specific lint and tests for `packages/headless`
83
+ - **Deliverables**:
84
+ - root workflow update with headless package job
85
+ - **Acceptance Criteria**:
86
+ - root CI executes `npm run lint`
87
+ - root CI executes `npm run test`
88
+ - failures block merge
@@ -0,0 +1,54 @@
1
+ # API Freeze Candidate (Pre-v1)
2
+
3
+ ## Purpose
4
+
5
+ This document is the `HLS-070` API freeze candidate result.
6
+
7
+ It captures the current freeze scope, contract confidence level,
8
+ and pre-stable cleanup requirements before first stable release.
9
+
10
+ ## Candidate Scope
11
+
12
+ Included components:
13
+
14
+ - Listbox
15
+ - Combobox
16
+ - Menu
17
+ - Tabs
18
+ - Treeview
19
+
20
+ Included shared primitives:
21
+
22
+ - keyboard intents
23
+ - typeahead
24
+ - selection reducers
25
+
26
+ ## Freeze Criteria Checklist
27
+
28
+ - [x] each component has dedicated source directory and spec document
29
+ - [x] each component contract has unit tests
30
+ - [x] boundary checks prevent monorepo-only imports
31
+ - [x] package-level lint/test gates are green
32
+ - [x] SemVer/deprecation process documented (ADR-003)
33
+ - [x] release ownership documented (ADR-002)
34
+
35
+ ## Contract Stability Assessment
36
+
37
+ Current status: **freeze-candidate (not final freeze)**
38
+
39
+ Rationale:
40
+
41
+ - behavior contracts are implemented and tested for current APG subset
42
+ - public API is coherent across components (`createX`, state/actions, `get*Props`)
43
+ - release governance documents now exist
44
+
45
+ Outstanding pre-stable items:
46
+
47
+ 1. run shard-only release drill and close follow-ups
48
+ 2. finalize release notes format and changelog protocol in shard workflow
49
+ 3. validate migration docs against first external consumer integration
50
+
51
+ ## Recommended Freeze Decision
52
+
53
+ - Freeze candidate accepted for release rehearsal.
54
+ - Final freeze approval deferred until `HLS-072` follow-ups are resolved.