@chromvoid/headless-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/a11y-contracts/index.d.ts +23 -0
- package/dist/a11y-contracts/index.js +1 -0
- package/dist/accordion/index.d.ts +78 -0
- package/dist/accordion/index.js +264 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +1 -0
- package/dist/alert/index.d.ts +33 -0
- package/dist/alert/index.js +54 -0
- package/dist/alert-dialog/index.d.ts +69 -0
- package/dist/alert-dialog/index.js +94 -0
- package/dist/badge/index.d.ts +48 -0
- package/dist/badge/index.js +89 -0
- package/dist/breadcrumb/index.d.ts +55 -0
- package/dist/breadcrumb/index.js +77 -0
- package/dist/button/index.d.ts +46 -0
- package/dist/button/index.js +86 -0
- package/dist/callout/index.d.ts +41 -0
- package/dist/callout/index.js +63 -0
- package/dist/card/index.d.ts +54 -0
- package/dist/card/index.js +103 -0
- package/dist/carousel/index.d.ts +98 -0
- package/dist/carousel/index.js +243 -0
- package/dist/checkbox/index.d.ts +50 -0
- package/dist/checkbox/index.js +87 -0
- package/dist/combobox/index.d.ts +114 -0
- package/dist/combobox/index.js +431 -0
- package/dist/command-palette/index.d.ts +73 -0
- package/dist/command-palette/index.js +147 -0
- package/dist/context-menu/index.d.ts +111 -0
- package/dist/context-menu/index.js +372 -0
- package/dist/copy-button/index.d.ts +62 -0
- package/dist/copy-button/index.js +183 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +2 -0
- package/dist/core/selection.d.ts +5 -0
- package/dist/core/selection.js +39 -0
- package/dist/core/value-range.d.ts +49 -0
- package/dist/core/value-range.js +134 -0
- package/dist/date-picker/index.d.ts +210 -0
- package/dist/date-picker/index.js +895 -0
- package/dist/dialog/index.d.ts +95 -0
- package/dist/dialog/index.js +153 -0
- package/dist/disclosure/index.d.ts +52 -0
- package/dist/disclosure/index.js +159 -0
- package/dist/drawer/index.d.ts +30 -0
- package/dist/drawer/index.js +39 -0
- package/dist/feed/index.d.ts +77 -0
- package/dist/feed/index.js +260 -0
- package/dist/grid/index.d.ts +103 -0
- package/dist/grid/index.js +415 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/input/index.d.ts +86 -0
- package/dist/input/index.js +156 -0
- package/dist/interactions/composite-navigation.d.ts +69 -0
- package/dist/interactions/composite-navigation.js +169 -0
- package/dist/interactions/index.d.ts +15 -0
- package/dist/interactions/index.js +4 -0
- package/dist/interactions/keyboard-intents.d.ts +16 -0
- package/dist/interactions/keyboard-intents.js +33 -0
- package/dist/interactions/overlay-focus.d.ts +40 -0
- package/dist/interactions/overlay-focus.js +93 -0
- package/dist/interactions/typeahead.d.ts +20 -0
- package/dist/interactions/typeahead.js +41 -0
- package/dist/landmarks/index.d.ts +39 -0
- package/dist/landmarks/index.js +58 -0
- package/dist/link/index.d.ts +34 -0
- package/dist/link/index.js +39 -0
- package/dist/listbox/index.d.ts +92 -0
- package/dist/listbox/index.js +337 -0
- package/dist/menu/index.d.ts +132 -0
- package/dist/menu/index.js +541 -0
- package/dist/menu-button/index.d.ts +71 -0
- package/dist/menu-button/index.js +121 -0
- package/dist/meter/index.d.ts +45 -0
- package/dist/meter/index.js +106 -0
- package/dist/number/index.d.ts +113 -0
- package/dist/number/index.js +252 -0
- package/dist/popover/index.d.ts +70 -0
- package/dist/popover/index.js +126 -0
- package/dist/progress/index.d.ts +49 -0
- package/dist/progress/index.js +79 -0
- package/dist/radio-group/index.d.ts +61 -0
- package/dist/radio-group/index.js +150 -0
- package/dist/select/index.d.ts +92 -0
- package/dist/select/index.js +239 -0
- package/dist/sidebar/index.d.ts +74 -0
- package/dist/sidebar/index.js +186 -0
- package/dist/slider/index.d.ts +61 -0
- package/dist/slider/index.js +150 -0
- package/dist/slider-multi-thumb/index.d.ts +70 -0
- package/dist/slider-multi-thumb/index.js +222 -0
- package/dist/spinbutton/index.d.ts +75 -0
- package/dist/spinbutton/index.js +214 -0
- package/dist/spinner/index.d.ts +1 -0
- package/dist/spinner/index.js +1 -0
- package/dist/spinner/spinner.d.ts +23 -0
- package/dist/spinner/spinner.js +25 -0
- package/dist/switch/index.d.ts +40 -0
- package/dist/switch/index.js +61 -0
- package/dist/table/index.d.ts +117 -0
- package/dist/table/index.js +377 -0
- package/dist/tabs/index.d.ts +63 -0
- package/dist/tabs/index.js +174 -0
- package/dist/textarea/index.d.ts +68 -0
- package/dist/textarea/index.js +137 -0
- package/dist/toast/index.d.ts +67 -0
- package/dist/toast/index.js +145 -0
- package/dist/toolbar/index.d.ts +59 -0
- package/dist/toolbar/index.js +139 -0
- package/dist/tooltip/index.d.ts +52 -0
- package/dist/tooltip/index.js +169 -0
- package/dist/treegrid/index.d.ts +101 -0
- package/dist/treegrid/index.js +463 -0
- package/dist/treeview/index.d.ts +68 -0
- package/dist/treeview/index.js +370 -0
- package/dist/window-splitter/index.d.ts +65 -0
- package/dist/window-splitter/index.js +204 -0
- package/package.json +92 -0
- package/specs/ADR-001-headless-architecture.md +461 -0
- package/specs/ADR-002-repo-release-model.md +108 -0
- package/specs/ADR-003-public-api-versioning.md +136 -0
- package/specs/ADR-004-focus-selection-policy.md +117 -0
- package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
- package/specs/ISSUE-BACKLOG.md +681 -0
- package/specs/RELEASE-CANDIDATE.md +30 -0
- package/specs/components/accordion.md +130 -0
- package/specs/components/alert-dialog.md +72 -0
- package/specs/components/alert.md +65 -0
- package/specs/components/badge.md +220 -0
- package/specs/components/breadcrumb.md +74 -0
- package/specs/components/button.md +115 -0
- package/specs/components/callout.md +195 -0
- package/specs/components/card.md +280 -0
- package/specs/components/carousel.md +140 -0
- package/specs/components/checkbox.md +172 -0
- package/specs/components/combobox.md +423 -0
- package/specs/components/command-palette.md +92 -0
- package/specs/components/context-menu.md +556 -0
- package/specs/components/copy-button.md +293 -0
- package/specs/components/date-picker.md +400 -0
- package/specs/components/dialog.md +298 -0
- package/specs/components/disclosure.md +257 -0
- package/specs/components/drawer.md +353 -0
- package/specs/components/feed.md +265 -0
- package/specs/components/grid.md +186 -0
- package/specs/components/input.md +254 -0
- package/specs/components/landmarks.md +136 -0
- package/specs/components/link.md +134 -0
- package/specs/components/listbox.md +351 -0
- package/specs/components/menu-button.md +76 -0
- package/specs/components/menu.md +623 -0
- package/specs/components/meter.md +149 -0
- package/specs/components/number.md +393 -0
- package/specs/components/popover.md +252 -0
- package/specs/components/progress.md +188 -0
- package/specs/components/radio-group.md +151 -0
- package/specs/components/select.md +144 -0
- package/specs/components/sidebar.md +321 -0
- package/specs/components/slider-multi-thumb.md +78 -0
- package/specs/components/slider.md +84 -0
- package/specs/components/spinbutton.md +140 -0
- package/specs/components/spinner.md +132 -0
- package/specs/components/switch.md +175 -0
- package/specs/components/table.md +403 -0
- package/specs/components/tabs.md +265 -0
- package/specs/components/textarea.md +185 -0
- package/specs/components/toast.md +198 -0
- package/specs/components/toolbar.md +278 -0
- package/specs/components/tooltip.md +252 -0
- package/specs/components/treegrid.md +281 -0
- package/specs/components/treeview.md +91 -0
- package/specs/components/window-splitter.md +297 -0
- package/specs/ops/git-shard-sync.md +107 -0
- package/specs/ops/release-checklist.md +76 -0
- package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
- package/specs/release/api-freeze-candidate.md +54 -0
- package/specs/release/changelog-automation.md +76 -0
- package/specs/release/changelog.generated.md +53 -0
- package/specs/release/changelog.patch.generated.md +46 -0
- package/specs/release/consumer-integration.md +53 -0
- package/specs/release/migration-notes-pre-v1.md +40 -0
- package/specs/release/mvp-changelog.md +57 -0
- package/specs/release/release-notes-template.md +61 -0
- package/specs/release/release-rehearsal.md +113 -0
- package/specs/release/semver-deprecation-dry-run.md +89 -0
- package/specs/release/shard-release-drill-report.md +50 -0
- package/specs/release/shard-release-follow-ups.md +31 -0
- package/specs/signals.md +208 -0
|
@@ -0,0 +1,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.
|