@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,149 @@
|
|
|
1
|
+
# Meter Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Meter` provides a headless APG-aligned model for a graphical display of a numeric value within a known range (e.g., disk usage, password strength).
|
|
6
|
+
|
|
7
|
+
Unlike a progress bar, a meter represents a static measurement rather than the progress of a task.
|
|
8
|
+
|
|
9
|
+
## Component Files
|
|
10
|
+
|
|
11
|
+
- `src/meter/index.ts` - model and public `createMeter` API
|
|
12
|
+
- `src/meter/meter.test.ts` - unit behavior tests
|
|
13
|
+
|
|
14
|
+
## Options
|
|
15
|
+
|
|
16
|
+
`createMeter(options?: CreateMeterOptions)` accepts:
|
|
17
|
+
|
|
18
|
+
| Option | Type | Default | Description |
|
|
19
|
+
| ----------------- | --------------------------- | --------- | -------------------------------------------- |
|
|
20
|
+
| `idBase` | `string` | `'meter'` | Prefix for generated element IDs |
|
|
21
|
+
| `value` | `number` | `min` | Initial measured value |
|
|
22
|
+
| `min` | `number` | `0` | Minimum of the range |
|
|
23
|
+
| `max` | `number` | `100` | Maximum of the range (percentage convention) |
|
|
24
|
+
| `low` | `number` | — | Low threshold boundary |
|
|
25
|
+
| `high` | `number` | — | High threshold boundary |
|
|
26
|
+
| `optimum` | `number` | — | Optimum value within the range |
|
|
27
|
+
| `ariaLabel` | `string` | — | Accessible label |
|
|
28
|
+
| `ariaLabelledBy` | `string` | — | ID of labelling element |
|
|
29
|
+
| `ariaDescribedBy` | `string` | — | ID of describing element |
|
|
30
|
+
| `formatValueText` | `(value: number) => string` | — | Custom formatter for `aria-valuetext` |
|
|
31
|
+
|
|
32
|
+
## Public API
|
|
33
|
+
|
|
34
|
+
- `createMeter(options)` returns `MeterModel`
|
|
35
|
+
- `state` (signal-backed):
|
|
36
|
+
- `value`: `Atom<number>` — current measured value
|
|
37
|
+
- `min`: `Atom<number>` — minimum of the range
|
|
38
|
+
- `max`: `Atom<number>` — maximum of the range
|
|
39
|
+
- `percentage`: `Computed<number>` — `(value - min) / (max - min) * 100`, rounded to 4 decimal places; 0 when span is zero
|
|
40
|
+
- `status`: `Computed<MeterStatus>` — derived zone: `'low' | 'high' | 'optimum' | 'normal'`
|
|
41
|
+
- `actions`:
|
|
42
|
+
- `setValue(value: number)`: updates the measured value (clamped to `[min, max]`)
|
|
43
|
+
- `contracts`:
|
|
44
|
+
- `getMeterProps()`: returns a spread-ready ARIA attribute map (`MeterProps`)
|
|
45
|
+
|
|
46
|
+
### `MeterProps` Shape
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
{
|
|
50
|
+
id: string // `${idBase}-root`
|
|
51
|
+
role: 'meter'
|
|
52
|
+
'aria-valuenow': string // String(value)
|
|
53
|
+
'aria-valuemin': string // String(min)
|
|
54
|
+
'aria-valuemax': string // String(max)
|
|
55
|
+
'aria-valuetext'?: string // formatValueText?.(value) if provided
|
|
56
|
+
'aria-label'?: string // from options
|
|
57
|
+
'aria-labelledby'?: string // from options
|
|
58
|
+
'aria-describedby'?: string // from options
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## APG and A11y Contract
|
|
63
|
+
|
|
64
|
+
- role: `meter`
|
|
65
|
+
- `aria-valuenow`: current value (string)
|
|
66
|
+
- `aria-valuemin`: minimum value (string)
|
|
67
|
+
- `aria-valuemax`: maximum value (string)
|
|
68
|
+
- `aria-valuetext`: optional, produced by `formatValueText` callback
|
|
69
|
+
- linkage: supports `aria-label`, `aria-labelledby`, and `aria-describedby`
|
|
70
|
+
|
|
71
|
+
## Behavior Contract
|
|
72
|
+
|
|
73
|
+
- The component is read-only from a user interaction perspective (no keyboard/pointer input).
|
|
74
|
+
- Values are clamped between `min` and `max`.
|
|
75
|
+
- If `min >= max` is passed, values are swapped to enforce `min < max`.
|
|
76
|
+
- Thresholds (`low`, `high`, `optimum`) are normalized: clamped to `[min, max]`, and if `low > high` they are swapped.
|
|
77
|
+
- Status is calculated based on `low`, `high`, and `optimum` thresholds if provided in options.
|
|
78
|
+
|
|
79
|
+
### Status Derivation
|
|
80
|
+
|
|
81
|
+
Status is computed by `getMeterStatus(value, low, high, optimum)`:
|
|
82
|
+
|
|
83
|
+
1. If neither `low` nor `high` is set: return `'normal'`.
|
|
84
|
+
2. Determine region: `isInLowRegion = low != null && value < low`; `isInHighRegion = high != null && value > high`.
|
|
85
|
+
3. If `optimum` is set, check which region the optimum falls in:
|
|
86
|
+
- If optimum is in the low region and value is also in the low region: `'optimum'`.
|
|
87
|
+
- If optimum is in the high region and value is also in the high region: `'optimum'`.
|
|
88
|
+
- If optimum is in the normal region and value is also in the normal region: `'optimum'`.
|
|
89
|
+
4. If value is in the low region: `'low'`.
|
|
90
|
+
5. If value is in the high region: `'high'`.
|
|
91
|
+
6. Otherwise: `'normal'`.
|
|
92
|
+
|
|
93
|
+
## Transitions Table
|
|
94
|
+
|
|
95
|
+
| Trigger | Action | State Change |
|
|
96
|
+
| ------------------------- | --------------------- | ----------------------------------------------------------------- |
|
|
97
|
+
| Programmatic value update | `actions.setValue(n)` | `value` = clamp(n, min, max); `percentage` and `status` recompute |
|
|
98
|
+
|
|
99
|
+
Note: Meter has no user-interactive transitions (no keyboard/pointer). All state changes are programmatic via `setValue`.
|
|
100
|
+
|
|
101
|
+
## Invariants
|
|
102
|
+
|
|
103
|
+
- `min <= value <= max`.
|
|
104
|
+
- `min < max` (enforced by swapping if needed).
|
|
105
|
+
- If thresholds are provided: `min <= low <= high <= max` (enforced by normalization).
|
|
106
|
+
- `percentage` is always in `[0, 100]`.
|
|
107
|
+
- `status` is always one of `'low' | 'high' | 'optimum' | 'normal'`.
|
|
108
|
+
|
|
109
|
+
## Adapter Expectations
|
|
110
|
+
|
|
111
|
+
UIKit binds to the headless model as follows:
|
|
112
|
+
|
|
113
|
+
| Binding | Kind | Usage |
|
|
114
|
+
| --------------------------- | --------------- | ------------------------------------------------------ |
|
|
115
|
+
| `state.value` | signal read | Display current value |
|
|
116
|
+
| `state.min` | signal read | Range boundary |
|
|
117
|
+
| `state.max` | signal read | Range boundary |
|
|
118
|
+
| `state.percentage` | signal read | Indicator width (`width: ${percentage}%`) |
|
|
119
|
+
| `state.status` | signal read | Zone color mapping via CSS custom properties or class |
|
|
120
|
+
| `actions.setValue(n)` | action call | Update value from host property/attribute |
|
|
121
|
+
| `contracts.getMeterProps()` | contract spread | Spread onto the root meter element for ARIA compliance |
|
|
122
|
+
|
|
123
|
+
UIKit should **never** compute percentage, status, or ARIA attributes itself. All derived state comes from the headless model.
|
|
124
|
+
|
|
125
|
+
A default slot is supported in the UIKit layer for custom label content inside the indicator (visual concern only, no headless contract needed).
|
|
126
|
+
|
|
127
|
+
## Minimum Test Matrix
|
|
128
|
+
|
|
129
|
+
- value clamping at boundaries
|
|
130
|
+
- percentage calculation accuracy
|
|
131
|
+
- status derivation based on thresholds (low/high/optimum)
|
|
132
|
+
- correct `aria-valuenow/min/max` mapping
|
|
133
|
+
- `aria-valuetext` via `formatValueText` callback
|
|
134
|
+
- reactive updates when value changes
|
|
135
|
+
- threshold normalization (swap when low > high)
|
|
136
|
+
- min/max swap when min >= max
|
|
137
|
+
|
|
138
|
+
## ADR-001 Compliance
|
|
139
|
+
|
|
140
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
141
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
142
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
143
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
144
|
+
|
|
145
|
+
## Out of Scope (Current)
|
|
146
|
+
|
|
147
|
+
- animation orchestration (handled by visual layer)
|
|
148
|
+
- multi-segment meters
|
|
149
|
+
- vertical vs horizontal orientation (semantic parity)
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Number Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Number` is a headless contract for a numeric input field that **composes** `createSpinbutton` with additional features: clearable behavior, focus tracking, draft text management (transient editing state before numeric commit), placeholder support, and required semantics. It provides a unified API surface that proxies all spinbutton state, actions, and contracts while layering on input-style features needed by UIKit's `cv-number` component.
|
|
6
|
+
|
|
7
|
+
The `createNumber` factory internally instantiates a `createSpinbutton` model and exposes its capabilities through a single coherent interface, adding the following on top:
|
|
8
|
+
|
|
9
|
+
- **Draft text management** -- while the user types, the raw text is held in `draftText` without affecting the numeric `value`. On commit (blur, Enter), the draft is parsed and applied via spinbutton's `setValue`.
|
|
10
|
+
- **Clearable** -- an optional clear button that resets the value to the default and invokes an `onClear` callback.
|
|
11
|
+
- **Focus tracking** -- `focused` signal reflecting native focus/blur events.
|
|
12
|
+
- **Placeholder** -- a placeholder string displayed when the input is empty.
|
|
13
|
+
- **Required** -- marks the field as required for accessibility.
|
|
14
|
+
- **Stepper visibility** -- controls whether increment/decrement buttons are rendered.
|
|
15
|
+
|
|
16
|
+
## Component Files
|
|
17
|
+
|
|
18
|
+
- `src/number/index.ts` -- model and public `createNumber` API
|
|
19
|
+
- `src/number/number.test.ts` -- unit behavior tests
|
|
20
|
+
|
|
21
|
+
## Public API
|
|
22
|
+
|
|
23
|
+
- `createNumber(options)`
|
|
24
|
+
- `CreateNumberOptions`:
|
|
25
|
+
- `idBase?: string` -- base for generated IDs (default: `"number"`)
|
|
26
|
+
- `value?: number` -- initial numeric value (passed to spinbutton)
|
|
27
|
+
- `defaultValue?: number` -- the value to reset to on `clear()`; defaults to `min ?? 0`
|
|
28
|
+
- `min?: number` -- minimum bound (passed to spinbutton)
|
|
29
|
+
- `max?: number` -- maximum bound (passed to spinbutton)
|
|
30
|
+
- `step?: number` -- step size (passed to spinbutton)
|
|
31
|
+
- `largeStep?: number` -- large step size (passed to spinbutton)
|
|
32
|
+
- `disabled?: boolean` -- initial disabled state (default: `false`)
|
|
33
|
+
- `readonly?: boolean` -- initial readonly state (default: `false`)
|
|
34
|
+
- `required?: boolean` -- initial required state (default: `false`)
|
|
35
|
+
- `clearable?: boolean` -- whether the clear button is available (default: `false`)
|
|
36
|
+
- `stepper?: boolean` -- whether stepper buttons are visible (default: `false`)
|
|
37
|
+
- `placeholder?: string` -- initial placeholder text (default: `""`)
|
|
38
|
+
- `ariaLabel?: string` -- accessible label
|
|
39
|
+
- `ariaLabelledBy?: string` -- ID reference for labelling element
|
|
40
|
+
- `ariaDescribedBy?: string` -- ID reference for description element
|
|
41
|
+
- `formatValueText?: (value: number) => string` -- custom `aria-valuetext` formatter (passed to spinbutton)
|
|
42
|
+
- `onValueChange?: (value: number) => void` -- callback on committed value change
|
|
43
|
+
- `onClear?: () => void` -- callback when the value is cleared
|
|
44
|
+
- **Types**:
|
|
45
|
+
- `NumberKeyboardEventLike = Pick<KeyboardEvent, 'key'> & { preventDefault?: () => void }`
|
|
46
|
+
|
|
47
|
+
## State Signal Surface
|
|
48
|
+
|
|
49
|
+
All signals are reactive (Reatom atoms/computed).
|
|
50
|
+
|
|
51
|
+
### Proxied from spinbutton
|
|
52
|
+
|
|
53
|
+
| Signal | Type | Source |
|
|
54
|
+
| -------------- | --------------------- | ----------------------------- |
|
|
55
|
+
| `value()` | `number` | spinbutton `state.value` |
|
|
56
|
+
| `min()` | `number \| undefined` | spinbutton `state.min` |
|
|
57
|
+
| `max()` | `number \| undefined` | spinbutton `state.max` |
|
|
58
|
+
| `step()` | `number` | spinbutton `state.step` |
|
|
59
|
+
| `largeStep()` | `number` | spinbutton `state.largeStep` |
|
|
60
|
+
| `isDisabled()` | `boolean` | spinbutton `state.isDisabled` |
|
|
61
|
+
| `isReadOnly()` | `boolean` | spinbutton `state.isReadOnly` |
|
|
62
|
+
| `hasMin()` | `boolean` | spinbutton `state.hasMin` |
|
|
63
|
+
| `hasMax()` | `boolean` | spinbutton `state.hasMax` |
|
|
64
|
+
|
|
65
|
+
### Number-specific
|
|
66
|
+
|
|
67
|
+
| Signal | Type | Description |
|
|
68
|
+
| ------------------- | ---------------- | ---------------------------------------------------------------------- |
|
|
69
|
+
| `focused()` | `boolean` | Whether the input currently has focus |
|
|
70
|
+
| `filled()` | `boolean` | **Derived**: `true` when `value !== defaultValue` |
|
|
71
|
+
| `clearable()` | `boolean` | Whether the clear button feature is enabled |
|
|
72
|
+
| `showClearButton()` | `boolean` | **Derived**: `clearable && filled && !isDisabled && !isReadOnly` |
|
|
73
|
+
| `stepper()` | `boolean` | Whether stepper (increment/decrement) buttons are visible |
|
|
74
|
+
| `draftText()` | `string \| null` | Transient editing text before commit; `null` when not actively editing |
|
|
75
|
+
| `placeholder()` | `string` | Placeholder text for the input |
|
|
76
|
+
| `required()` | `boolean` | Whether the field is required |
|
|
77
|
+
| `defaultValue()` | `number` | The value to reset to on clear |
|
|
78
|
+
|
|
79
|
+
## Actions
|
|
80
|
+
|
|
81
|
+
All state transitions go through actions. UIKit must only call actions, never mutate state directly.
|
|
82
|
+
|
|
83
|
+
### Proxied from spinbutton
|
|
84
|
+
|
|
85
|
+
| Action | Description |
|
|
86
|
+
| ------------------------- | ---------------------------------------------------------------------------------- |
|
|
87
|
+
| `setValue(value: number)` | Sets the numeric value with clamping and snapping; clears draft text |
|
|
88
|
+
| `increment()` | Increments value by `step` |
|
|
89
|
+
| `decrement()` | Decrements value by `step` |
|
|
90
|
+
| `incrementLarge()` | Increments value by `largeStep` |
|
|
91
|
+
| `decrementLarge()` | Decrements value by `largeStep` |
|
|
92
|
+
| `setFirst()` | Jumps to `min` (if defined) |
|
|
93
|
+
| `setLast()` | Jumps to `max` (if defined) |
|
|
94
|
+
| `handleKeyDown(event)` | Handles spinbutton keys (ArrowUp/Down, PageUp/Down, Home/End) AND Escape for clear |
|
|
95
|
+
|
|
96
|
+
### Number-specific
|
|
97
|
+
|
|
98
|
+
| Action | Description |
|
|
99
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
100
|
+
| `setDisabled(v: boolean)` | Updates disabled state; delegates to spinbutton |
|
|
101
|
+
| `setReadOnly(v: boolean)` | Updates readonly state; delegates to spinbutton |
|
|
102
|
+
| `setRequired(v: boolean)` | Updates required state |
|
|
103
|
+
| `setClearable(v: boolean)` | Updates clearable state |
|
|
104
|
+
| `setStepper(v: boolean)` | Updates stepper visibility state |
|
|
105
|
+
| `setFocused(v: boolean)` | Updates focus state; on `false` (blur), auto-commits draft |
|
|
106
|
+
| `setPlaceholder(v: string)` | Updates placeholder text |
|
|
107
|
+
| `setDraftText(v: string \| null)` | Updates transient draft text directly |
|
|
108
|
+
| `commitDraft()` | Parses `draftText` as a number; if valid, calls `setValue`; if empty, calls `clear()`; if invalid, reverts draft to current value display; always clears draft to `null` |
|
|
109
|
+
| `clear()` | Resets value to `defaultValue`, calls `onClear` callback; no-op when `isDisabled` or `isReadOnly` |
|
|
110
|
+
| `handleInput(text: string)` | Processes native input event; updates `draftText` to `text` |
|
|
111
|
+
|
|
112
|
+
### Action semantics
|
|
113
|
+
|
|
114
|
+
- `setValue` bypasses the disabled/readonly guard (programmatic/controlled update), consistent with spinbutton behavior. It also clears `draftText` to `null`.
|
|
115
|
+
- `increment`, `decrement`, `incrementLarge`, `decrementLarge`, `setFirst`, `setLast` all clear `draftText` to `null` after modifying the value.
|
|
116
|
+
- `handleKeyDown` extends spinbutton's handler: if `key === 'Escape'` and `clearable && filled && !isDisabled && !isReadOnly`, calls `clear()` and prevents default. Otherwise delegates to spinbutton's `handleKeyDown`. If `key === 'Enter'`, calls `commitDraft()` and prevents default.
|
|
117
|
+
- `setFocused(false)` triggers `commitDraft()` automatically (blur commit).
|
|
118
|
+
|
|
119
|
+
## Contracts
|
|
120
|
+
|
|
121
|
+
Contracts return ready-to-spread attribute maps for UIKit to apply directly to DOM elements.
|
|
122
|
+
|
|
123
|
+
### `getInputProps()`
|
|
124
|
+
|
|
125
|
+
Returns attributes for the native `<input>` element:
|
|
126
|
+
|
|
127
|
+
| Attribute | Value |
|
|
128
|
+
| ------------------ | -------------------------------------------------------------- |
|
|
129
|
+
| `id` | `"{idBase}-input"` |
|
|
130
|
+
| `role` | `"spinbutton"` |
|
|
131
|
+
| `tabindex` | `"0"` when interactive, `"-1"` when `isDisabled` |
|
|
132
|
+
| `inputmode` | `"decimal"` |
|
|
133
|
+
| `aria-valuenow` | `String(value)` |
|
|
134
|
+
| `aria-valuemin` | `String(min)` when defined, otherwise omitted |
|
|
135
|
+
| `aria-valuemax` | `String(max)` when defined, otherwise omitted |
|
|
136
|
+
| `aria-valuetext` | Custom formatted text via `formatValueText`, otherwise omitted |
|
|
137
|
+
| `aria-disabled` | `"true"` when `isDisabled`, otherwise omitted |
|
|
138
|
+
| `aria-readonly` | `"true"` when `isReadOnly`, otherwise omitted |
|
|
139
|
+
| `aria-required` | `"true"` when `required`, otherwise omitted |
|
|
140
|
+
| `aria-label` | From options, when provided |
|
|
141
|
+
| `aria-labelledby` | From options, when provided |
|
|
142
|
+
| `aria-describedby` | From options, when provided |
|
|
143
|
+
| `placeholder` | Current placeholder value, omitted when empty |
|
|
144
|
+
| `autocomplete` | `"off"` |
|
|
145
|
+
|
|
146
|
+
### `getIncrementButtonProps()`
|
|
147
|
+
|
|
148
|
+
Proxied from spinbutton's `getIncrementButtonProps()`, with `hidden` and `aria-hidden` added based on `stepper` state:
|
|
149
|
+
|
|
150
|
+
| Attribute | Value |
|
|
151
|
+
| --------------- | --------------------------------------------------- |
|
|
152
|
+
| `id` | `"{idBase}-increment"` |
|
|
153
|
+
| `tabindex` | `"-1"` |
|
|
154
|
+
| `aria-label` | `"Increment value"` |
|
|
155
|
+
| `aria-disabled` | `"true"` when disabled or at max, otherwise omitted |
|
|
156
|
+
| `hidden` | `true` when `stepper` is `false` |
|
|
157
|
+
| `aria-hidden` | `"true"` when `hidden` |
|
|
158
|
+
| `onClick` | `increment` handler |
|
|
159
|
+
|
|
160
|
+
### `getDecrementButtonProps()`
|
|
161
|
+
|
|
162
|
+
Proxied from spinbutton's `getDecrementButtonProps()`, with `hidden` and `aria-hidden` added based on `stepper` state:
|
|
163
|
+
|
|
164
|
+
| Attribute | Value |
|
|
165
|
+
| --------------- | --------------------------------------------------- |
|
|
166
|
+
| `id` | `"{idBase}-decrement"` |
|
|
167
|
+
| `tabindex` | `"-1"` |
|
|
168
|
+
| `aria-label` | `"Decrement value"` |
|
|
169
|
+
| `aria-disabled` | `"true"` when disabled or at min, otherwise omitted |
|
|
170
|
+
| `hidden` | `true` when `stepper` is `false` |
|
|
171
|
+
| `aria-hidden` | `"true"` when `hidden` |
|
|
172
|
+
| `onClick` | `decrement` handler |
|
|
173
|
+
|
|
174
|
+
### `getClearButtonProps()`
|
|
175
|
+
|
|
176
|
+
Returns attributes for the clear button:
|
|
177
|
+
|
|
178
|
+
| Attribute | Value |
|
|
179
|
+
| ------------- | ------------------------------------------------------------- |
|
|
180
|
+
| `role` | `"button"` |
|
|
181
|
+
| `aria-label` | `"Clear value"` |
|
|
182
|
+
| `tabindex` | `"-1"` (not in tab order; activated by Escape key or pointer) |
|
|
183
|
+
| `hidden` | `true` when `showClearButton` is `false` |
|
|
184
|
+
| `aria-hidden` | `"true"` when `hidden` |
|
|
185
|
+
| `onClick` | `clear` handler |
|
|
186
|
+
|
|
187
|
+
## Transitions Table
|
|
188
|
+
|
|
189
|
+
| Event / Action | Guard | Effect | Next State |
|
|
190
|
+
| ------------------- | -------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------ |
|
|
191
|
+
| `setValue(v)` | -- | spinbutton `setValue(v)`, clear draft | `value = clamped/snapped v`; `draftText = null`; `onValueChange(v)` if changed |
|
|
192
|
+
| `increment()` | `!isDisabled && !isReadOnly` | spinbutton `increment()`, clear draft | `value = value + step`; `draftText = null` |
|
|
193
|
+
| `decrement()` | `!isDisabled && !isReadOnly` | spinbutton `decrement()`, clear draft | `value = value - step`; `draftText = null` |
|
|
194
|
+
| `incrementLarge()` | `!isDisabled && !isReadOnly` | spinbutton `incrementLarge()`, clear draft | `value = value + largeStep`; `draftText = null` |
|
|
195
|
+
| `decrementLarge()` | `!isDisabled && !isReadOnly` | spinbutton `decrementLarge()`, clear draft | `value = value - largeStep`; `draftText = null` |
|
|
196
|
+
| `setFirst()` | `!isDisabled && !isReadOnly && hasMin` | spinbutton `setFirst()`, clear draft | `value = min`; `draftText = null` |
|
|
197
|
+
| `setLast()` | `!isDisabled && !isReadOnly && hasMax` | spinbutton `setLast()`, clear draft | `value = max`; `draftText = null` |
|
|
198
|
+
| `handleInput(text)` | `!isDisabled && !isReadOnly` | set draft | `draftText = text` |
|
|
199
|
+
| `handleInput(text)` | `isDisabled \|\| isReadOnly` | -- | no change |
|
|
200
|
+
| `setDraftText(v)` | -- | set draft | `draftText = v` |
|
|
201
|
+
| `commitDraft()` | `draftText !== null && parseable` | parse, `setValue(parsed)` | `value = parsed`; `draftText = null` |
|
|
202
|
+
| `commitDraft()` | `draftText !== null && empty` | `clear()` | `value = defaultValue`; `draftText = null`; `onClear()` called |
|
|
203
|
+
| `commitDraft()` | `draftText !== null && invalid` | revert draft | `draftText = null` |
|
|
204
|
+
| `commitDraft()` | `draftText === null` | -- | no change |
|
|
205
|
+
| `clear()` | `!isDisabled && !isReadOnly` | reset to default | `value = defaultValue`; `draftText = null`; `onClear()` called |
|
|
206
|
+
| `clear()` | `isDisabled \|\| isReadOnly` | -- | no change |
|
|
207
|
+
| `keydown Escape` | `clearable && filled && !isDisabled && !isReadOnly` | `clear()` | `value = defaultValue`; `draftText = null`; `onClear()` called |
|
|
208
|
+
| `keydown Escape` | `!(clearable && filled) \|\| isDisabled \|\| isReadOnly` | -- | no change |
|
|
209
|
+
| `keydown Enter` | `draftText !== null` | `commitDraft()` | draft committed or reverted |
|
|
210
|
+
| `keydown ArrowUp` | -- | delegate to spinbutton | `value = value + step` |
|
|
211
|
+
| `keydown ArrowDown` | -- | delegate to spinbutton | `value = value - step` |
|
|
212
|
+
| `keydown PageUp` | -- | delegate to spinbutton | `value = value + largeStep` |
|
|
213
|
+
| `keydown PageDown` | -- | delegate to spinbutton | `value = value - largeStep` |
|
|
214
|
+
| `keydown Home` | -- | delegate to spinbutton | `value = min` or unchanged |
|
|
215
|
+
| `keydown End` | -- | delegate to spinbutton | `value = max` or unchanged |
|
|
216
|
+
| `setFocused(true)` | -- | set focused | `focused = true` |
|
|
217
|
+
| `setFocused(false)` | -- | set focused, commit draft | `focused = false`; `commitDraft()` triggered |
|
|
218
|
+
| `setDisabled(d)` | -- | delegate to spinbutton | `isDisabled = d` |
|
|
219
|
+
| `setReadOnly(r)` | -- | delegate to spinbutton | `isReadOnly = r` |
|
|
220
|
+
| `setRequired(r)` | -- | set required | `required = r` |
|
|
221
|
+
| `setClearable(c)` | -- | set clearable | `clearable = c` |
|
|
222
|
+
| `setStepper(s)` | -- | set stepper | `stepper = s` |
|
|
223
|
+
| `setPlaceholder(p)` | -- | set placeholder | `placeholder = p` |
|
|
224
|
+
|
|
225
|
+
## Invariants
|
|
226
|
+
|
|
227
|
+
1. `value` must always satisfy spinbutton invariants (clamped, snapped, within bounds when bounds are defined).
|
|
228
|
+
2. `filled` must be `true` if and only if `value !== defaultValue`.
|
|
229
|
+
3. `showClearButton` must be `true` if and only if `clearable && filled && !isDisabled && !isReadOnly`.
|
|
230
|
+
4. `clear()` must be a no-op when `isDisabled` or `isReadOnly`.
|
|
231
|
+
5. `draftText` must be `null` when not actively editing (i.e., after any commit, clear, or spinbutton action).
|
|
232
|
+
6. `commitDraft()` with empty draft string must call `clear()` (reset to default).
|
|
233
|
+
7. `commitDraft()` with non-parseable text must revert silently (set `draftText` to `null` without changing `value`).
|
|
234
|
+
8. `setFocused(false)` must trigger `commitDraft()`.
|
|
235
|
+
9. `handleKeyDown` must handle `Escape` (clear) and `Enter` (commit) before delegating remaining keys to spinbutton.
|
|
236
|
+
10. `aria-disabled` on the input must be `"true"` when `isDisabled` is `true`.
|
|
237
|
+
11. `aria-readonly` on the input must be `"true"` when `isReadOnly` is `true`.
|
|
238
|
+
12. `aria-required` on the input must be `"true"` when `required` is `true`.
|
|
239
|
+
13. `tabindex` on the input must be `"-1"` when `isDisabled`, `"0"` otherwise.
|
|
240
|
+
14. Increment/decrement button contracts must have `hidden: true` when `stepper` is `false`.
|
|
241
|
+
15. Clear button must have `hidden: true` when `showClearButton` is `false`.
|
|
242
|
+
16. `defaultValue` must satisfy spinbutton normalization (clamped and snapped).
|
|
243
|
+
17. Spinbutton actions (`increment`, `decrement`, etc.) must clear `draftText` to prevent stale draft display.
|
|
244
|
+
18. `onValueChange` must not be called from `clear()` if the value is already equal to `defaultValue`.
|
|
245
|
+
19. `getInputProps()` must return `role: "spinbutton"` to match APG spinbutton semantics.
|
|
246
|
+
20. `inputmode` must be `"decimal"` to trigger numeric keyboard on mobile devices.
|
|
247
|
+
|
|
248
|
+
## Adapter Expectations
|
|
249
|
+
|
|
250
|
+
UIKit (`cv-number`) binds to the headless contract as follows:
|
|
251
|
+
|
|
252
|
+
### Signals read
|
|
253
|
+
|
|
254
|
+
- `state.value()` -- to display the formatted numeric value when not actively editing
|
|
255
|
+
- `state.isDisabled()` -- to reflect the `disabled` host attribute
|
|
256
|
+
- `state.isReadOnly()` -- to reflect the `readonly` host attribute
|
|
257
|
+
- `state.required()` -- to reflect the `required` host attribute
|
|
258
|
+
- `state.focused()` -- to apply `[focused]` styling on the host
|
|
259
|
+
- `state.filled()` -- to apply `[filled]` styling on the host (e.g., floating label position)
|
|
260
|
+
- `state.showClearButton()` -- to conditionally render the clear button
|
|
261
|
+
- `state.stepper()` -- to conditionally render increment/decrement buttons
|
|
262
|
+
- `state.draftText()` -- to display the raw editing text in the input; when `null`, display formatted `value`
|
|
263
|
+
- `state.placeholder()` -- read via contract, not recomputed
|
|
264
|
+
- `state.hasMin()`, `state.hasMax()` -- for conditional rendering or styling
|
|
265
|
+
- `state.min()`, `state.max()`, `state.step()` -- for display or validation hints
|
|
266
|
+
|
|
267
|
+
### Actions called
|
|
268
|
+
|
|
269
|
+
- `actions.setValue(v)` -- when syncing attribute/property changes into headless
|
|
270
|
+
- `actions.setDisabled(d)` -- when the `disabled` attribute changes
|
|
271
|
+
- `actions.setReadOnly(r)` -- when the `readonly` attribute changes
|
|
272
|
+
- `actions.setRequired(r)` -- when the `required` attribute changes
|
|
273
|
+
- `actions.setPlaceholder(p)` -- when the `placeholder` attribute changes
|
|
274
|
+
- `actions.setClearable(c)` -- when the `clearable` attribute changes
|
|
275
|
+
- `actions.setStepper(s)` -- when the `stepper` attribute changes
|
|
276
|
+
- `actions.setFocused(f)` -- on native `<input>` `focus`/`blur` events
|
|
277
|
+
- `actions.handleInput(text)` -- on native `<input>` `input` event (updates draft)
|
|
278
|
+
- `actions.handleKeyDown(e)` -- on native `<input>` `keydown` event
|
|
279
|
+
- `actions.commitDraft()` -- on native `<input>` `change` event (if needed beyond blur)
|
|
280
|
+
- `actions.clear()` -- on clear button click
|
|
281
|
+
- `actions.increment()` -- on increment button click (when stepper is visible)
|
|
282
|
+
- `actions.decrement()` -- on decrement button click (when stepper is visible)
|
|
283
|
+
|
|
284
|
+
### Contracts spread
|
|
285
|
+
|
|
286
|
+
- `contracts.getInputProps()` -- spread onto the native `<input>` element for `id`, `role`, `tabindex`, `inputmode`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, `aria-valuetext`, `aria-disabled`, `aria-readonly`, `aria-required`, `aria-label`, `aria-labelledby`, `aria-describedby`, `placeholder`, `autocomplete`
|
|
287
|
+
- `contracts.getIncrementButtonProps()` -- spread onto the increment button for `id`, `tabindex`, `aria-label`, `aria-disabled`, `hidden`, `aria-hidden`, `onClick`
|
|
288
|
+
- `contracts.getDecrementButtonProps()` -- spread onto the decrement button for `id`, `tabindex`, `aria-label`, `aria-disabled`, `hidden`, `aria-hidden`, `onClick`
|
|
289
|
+
- `contracts.getClearButtonProps()` -- spread onto the clear button for `role`, `aria-label`, `tabindex`, `hidden`, `aria-hidden`, `onClick`
|
|
290
|
+
|
|
291
|
+
### Events dispatched by UIKit
|
|
292
|
+
|
|
293
|
+
- `cv-change` CustomEvent on committed value changes (from user interaction, not programmatic `setValue`)
|
|
294
|
+
- `cv-clear` CustomEvent when the value is cleared via clear button or Escape key
|
|
295
|
+
|
|
296
|
+
### Input display logic (UIKit responsibility)
|
|
297
|
+
|
|
298
|
+
UIKit reads `state.draftText()` and `state.value()` to determine what to display in the native `<input>`:
|
|
299
|
+
|
|
300
|
+
- When `draftText !== null`: display `draftText` (user is actively editing)
|
|
301
|
+
- When `draftText === null`: display formatted `String(value)` (committed state)
|
|
302
|
+
|
|
303
|
+
## Minimum Test Matrix
|
|
304
|
+
|
|
305
|
+
### Value management
|
|
306
|
+
|
|
307
|
+
- Set initial value via options and verify `state.value()`
|
|
308
|
+
- Default value defaults to `min` when provided, otherwise `0`
|
|
309
|
+
- `setValue(v)` updates value with clamping and snapping
|
|
310
|
+
- `setValue(v)` clears `draftText` to `null`
|
|
311
|
+
- `setValue(v)` works even when disabled (programmatic/controlled update)
|
|
312
|
+
|
|
313
|
+
### Spinbutton behavior
|
|
314
|
+
|
|
315
|
+
- `increment()` / `decrement()` behavior with step
|
|
316
|
+
- `incrementLarge()` / `decrementLarge()` behavior with largeStep
|
|
317
|
+
- `Home` / `End` behavior with defined/undefined boundaries
|
|
318
|
+
- Value clamping and snapping
|
|
319
|
+
- Spinbutton actions clear `draftText` to `null`
|
|
320
|
+
- Disabled and readonly state prevent spinbutton mutations
|
|
321
|
+
|
|
322
|
+
### Draft text management
|
|
323
|
+
|
|
324
|
+
- `handleInput(text)` sets `draftText` to the provided text
|
|
325
|
+
- `handleInput(text)` is no-op when disabled
|
|
326
|
+
- `handleInput(text)` is no-op when readonly
|
|
327
|
+
- `commitDraft()` with valid numeric text parses and sets value
|
|
328
|
+
- `commitDraft()` with empty string calls `clear()`
|
|
329
|
+
- `commitDraft()` with invalid text reverts (sets `draftText` to `null`, value unchanged)
|
|
330
|
+
- `commitDraft()` with `draftText === null` is no-op
|
|
331
|
+
- `setFocused(false)` triggers `commitDraft()`
|
|
332
|
+
|
|
333
|
+
### Clearable
|
|
334
|
+
|
|
335
|
+
- `clear()` sets value to `defaultValue` and calls `onClear`
|
|
336
|
+
- `clear()` is no-op when disabled
|
|
337
|
+
- `clear()` is no-op when readonly
|
|
338
|
+
- `clear()` clears `draftText` to `null`
|
|
339
|
+
- `Escape` key clears value when clearable and filled
|
|
340
|
+
- `Escape` key does nothing when not clearable
|
|
341
|
+
- `Escape` key does nothing when value equals defaultValue (not filled)
|
|
342
|
+
|
|
343
|
+
### Keyboard
|
|
344
|
+
|
|
345
|
+
- `Enter` key calls `commitDraft()`
|
|
346
|
+
- `ArrowUp` / `ArrowDown` delegate to spinbutton increment/decrement
|
|
347
|
+
- `PageUp` / `PageDown` delegate to spinbutton large step
|
|
348
|
+
- `Home` / `End` delegate to spinbutton setFirst/setLast
|
|
349
|
+
|
|
350
|
+
### Focus
|
|
351
|
+
|
|
352
|
+
- `setFocused(true)` sets focused to `true`
|
|
353
|
+
- `setFocused(false)` sets focused to `false` and commits draft
|
|
354
|
+
|
|
355
|
+
### Derived state
|
|
356
|
+
|
|
357
|
+
- `filled` is `true` when value differs from defaultValue, `false` when equal
|
|
358
|
+
- `showClearButton` reflects `clearable && filled && !isDisabled && !isReadOnly`
|
|
359
|
+
|
|
360
|
+
### Contracts
|
|
361
|
+
|
|
362
|
+
- `getInputProps()` returns correct `role`, `inputmode`, `aria-valuenow`, `aria-disabled`, `aria-readonly`, `aria-required`
|
|
363
|
+
- `getInputProps()` returns `tabindex "-1"` when disabled, `"0"` otherwise
|
|
364
|
+
- `getInputProps()` returns placeholder when non-empty, omitted when empty
|
|
365
|
+
- `getIncrementButtonProps()` returns `hidden: true` when stepper is `false`
|
|
366
|
+
- `getDecrementButtonProps()` returns `hidden: true` when stepper is `false`
|
|
367
|
+
- `getClearButtonProps()` returns `hidden: true` when `showClearButton` is `false`
|
|
368
|
+
- `getClearButtonProps()` returns correct `aria-label`
|
|
369
|
+
|
|
370
|
+
### Stepper
|
|
371
|
+
|
|
372
|
+
- Stepper buttons hidden by default (`stepper` defaults to `false`)
|
|
373
|
+
- `setStepper(true)` makes stepper buttons visible
|
|
374
|
+
- `setStepper(false)` hides stepper buttons
|
|
375
|
+
|
|
376
|
+
## ADR-001 Compliance
|
|
377
|
+
|
|
378
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
379
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings. The number module sits in the interactions layer, composing the spinbutton core.
|
|
380
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules. Only imports from `../spinbutton` (intra-package) and `@reatom/core`.
|
|
381
|
+
- **Composition**: `createNumber` internally creates a `createSpinbutton` instance. It does NOT duplicate spinbutton logic.
|
|
382
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
383
|
+
|
|
384
|
+
## Out of Scope (Current)
|
|
385
|
+
|
|
386
|
+
- Locale-aware number formatting and parsing (handled by adapters/UIKit)
|
|
387
|
+
- Thousand separators or currency formatting
|
|
388
|
+
- Input masking or restricted character filtering
|
|
389
|
+
- Validation / error state management (future form-level spec)
|
|
390
|
+
- Acceleration (increasing step size when holding stepper buttons)
|
|
391
|
+
- Native form submission integration (handled by adapters/wrappers)
|
|
392
|
+
- Prefix / suffix slot content management (UIKit layout concern)
|
|
393
|
+
- Custom parsers beyond `parseFloat` (adapters may override)
|