@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ChromVoid Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ The goal of this package is to provide a standalone headless foundation for a future UI kit,
2
+ built on Reatom v1000 and WAI-ARIA APG behavior contracts.
3
+
4
+ The visual UI kit is intentionally out of scope for this package.
5
+
6
+ ## Import contract
7
+
8
+ The package ships from `dist/` and supports both a convenience root barrel and lean leaf imports.
9
+
10
+ Use the root barrel when convenience matters more than bundle shape:
11
+
12
+ ```ts
13
+ import {createButton, createTabs} from '@chromvoid/headless-ui'
14
+ ```
15
+
16
+ Use leaf subpaths in perf-sensitive code when you want an intentionally narrow dependency graph:
17
+
18
+ ```ts
19
+ import {createButton} from '@chromvoid/headless-ui/button'
20
+ import {CompositeNavigationOrientation} from '@chromvoid/headless-ui/interactions/composite-navigation'
21
+ import {toggleSelection} from '@chromvoid/headless-ui/core/selection'
22
+ ```
23
+
24
+ The root barrel remains supported, but subpaths are the stable lean-import contract for applications and higher-level packages such as `@chromvoid/uikit`.
25
+
26
+ Architecture decisions and delivery artifacts:
27
+
28
+ - `./specs/ADR-001-headless-architecture.md`
29
+ - `./specs/IMPLEMENTATION-ROADMAP.md`
30
+ - `./specs/ISSUE-BACKLOG.md`
31
+ - `./specs/RELEASE-CANDIDATE.md`
32
+ - `./specs/release/mvp-changelog.md`
33
+
34
+ ## Package-local workflow
35
+
36
+ Run all package checks from the package root:
37
+
38
+ ```sh
39
+ npm ci
40
+ npm run lint
41
+ npm run test
42
+ npm run build
43
+ npm pack --dry-run
44
+ ```
45
+
46
+ ## Implemented components
47
+
48
+ - `listbox` (`src/listbox/`, `specs/components/listbox.md`)
49
+ - `combobox` (`src/combobox/`, `specs/components/combobox.md`)
50
+ - `menu` (`src/menu/`, `specs/components/menu.md`)
51
+ - `tabs` (`src/tabs/`, `specs/components/tabs.md`)
52
+ - `treeview` (`src/treeview/`, `specs/components/treeview.md`)
53
+ - `alert` (`src/alert/`, `specs/components/alert.md`)
54
+ - `breadcrumb` (`src/breadcrumb/`, `specs/components/breadcrumb.md`)
55
+ - `landmarks` (`src/landmarks/`, `specs/components/landmarks.md`)
56
+ - `meter` (`src/meter/`, `specs/components/meter.md`)
57
+ - `link` (`src/link/`, `specs/components/link.md`)
58
+ - `table` (`src/table/`, `specs/components/table.md`)
59
+ - `button` (`src/button/`, `specs/components/button.md`)
60
+ - `checkbox` (`src/checkbox/`, `specs/components/checkbox.md`)
61
+ - `switch` (`src/switch/`, `specs/components/switch.md`)
62
+ - `radio-group` (`src/radio-group/`, `specs/components/radio-group.md`)
63
+ - `slider` (`src/slider/`, `specs/components/slider.md`)
64
+ - `spinbutton` (`src/spinbutton/`, `specs/components/spinbutton.md`)
65
+ - `slider-multi-thumb` (`src/slider-multi-thumb/`, `specs/components/slider-multi-thumb.md`)
66
+ - `disclosure` (`src/disclosure/`, `specs/components/disclosure.md`)
67
+ - `accordion` (`src/accordion/`, `specs/components/accordion.md`)
68
+ - `dialog` (`src/dialog/`, `specs/components/dialog.md`)
69
+ - `alert-dialog` (`src/alert-dialog/`, `specs/components/alert-dialog.md`)
70
+ - `tooltip` (`src/tooltip/`, `specs/components/tooltip.md`)
71
+ - `menu-button` (`src/menu-button/`, `specs/components/menu-button.md`)
72
+ - `toolbar` (`src/toolbar/`, `specs/components/toolbar.md`)
73
+ - `grid` (`src/grid/`, `specs/components/grid.md`)
74
+ - `treegrid` (`src/treegrid/`, `specs/components/treegrid.md`)
75
+ - `feed` (`src/feed/`, `specs/components/feed.md`)
76
+ - `carousel` (`src/carousel/`, `specs/components/carousel.md`)
77
+ - `window-splitter` (`src/window-splitter/`, `specs/components/window-splitter.md`)
78
+
79
+ ## MVP-next scaffolds (not finalized)
80
+
81
+ - `popover` (`src/popover/`, `specs/components/popover.md`)
82
+ - `select` (`src/select/`, `specs/components/select.md`)
83
+ - `context-menu` (`src/context-menu/`, `specs/components/context-menu.md`)
84
+ - `command-palette` (`src/command-palette/`, `specs/components/command-palette.md`)
85
+ - `toast` (`src/toast/`, `specs/components/toast.md`)
86
+ - `progress` (`src/progress/`, `specs/components/progress.md`)
87
+
88
+ ## Shared layers
89
+
90
+ - `src/core/` - selection and value-range state primitives
91
+ - `src/interactions/` - keyboard intents, typeahead, composite navigation, overlay focus
92
+ - `src/a11y-contracts/` - typed aria/role contracts
93
+ - `src/adapters/` - adapter contracts and integration coverage
94
+
95
+ ## Conventions
96
+
97
+ - each component lives in a dedicated directory: `src/<component>/`
98
+ - each component has a dedicated contract spec: `specs/components/<component>.md`
99
+ - package must remain independent from monorepo-only imports (`@project/*`, `apps/*`)
@@ -0,0 +1,23 @@
1
+ import type { HeadlessId } from '../core/index.js';
2
+ export type FocusStrategy = 'roving-tabindex' | 'aria-activedescendant';
3
+ export interface RootA11yProps {
4
+ role: string;
5
+ tabindex: '0' | '-1';
6
+ 'aria-label'?: string;
7
+ 'aria-orientation'?: 'vertical' | 'horizontal';
8
+ 'aria-multiselectable'?: 'true' | 'false';
9
+ 'aria-activedescendant'?: string;
10
+ }
11
+ export interface ItemA11yProps {
12
+ id: string;
13
+ role: string;
14
+ tabindex: '0' | '-1';
15
+ 'aria-disabled'?: 'true';
16
+ 'aria-selected'?: 'true' | 'false';
17
+ 'data-active'?: 'true' | 'false';
18
+ }
19
+ export interface A11yContract {
20
+ strategy: FocusStrategy;
21
+ getRootProps(): RootA11yProps;
22
+ getItemProps(itemId: HeadlessId): ItemA11yProps;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import { type Atom, type Computed } from '@reatom/core';
2
+ export interface AccordionSection {
3
+ id: string;
4
+ disabled?: boolean;
5
+ }
6
+ export interface CreateAccordionOptions {
7
+ sections: readonly AccordionSection[];
8
+ idBase?: string;
9
+ allowMultiple?: boolean;
10
+ allowZeroExpanded?: boolean;
11
+ initialExpandedIds?: readonly string[];
12
+ ariaLabel?: string;
13
+ headingLevel?: number;
14
+ }
15
+ export interface AccordionState {
16
+ expandedIds: Atom<Set<string>>;
17
+ focusedId: Atom<string | null>;
18
+ value: Computed<string | null>;
19
+ expandedValues: Computed<string[]>;
20
+ sections: Atom<readonly AccordionSection[]>;
21
+ allowMultiple: Atom<boolean>;
22
+ allowZeroExpanded: Atom<boolean>;
23
+ headingLevel: Atom<number>;
24
+ ariaLabel: Atom<string | undefined>;
25
+ }
26
+ export interface AccordionActions {
27
+ toggle(id: string): void;
28
+ expand(id: string): void;
29
+ collapse(id: string): void;
30
+ setFocused(id: string): void;
31
+ moveNext(): void;
32
+ movePrev(): void;
33
+ moveFirst(): void;
34
+ moveLast(): void;
35
+ handleKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
36
+ setSections(sections: readonly AccordionSection[]): void;
37
+ setAllowMultiple(value: boolean): void;
38
+ setAllowZeroExpanded(value: boolean): void;
39
+ setHeadingLevel(level: number): void;
40
+ setAriaLabel(label: string | undefined): void;
41
+ setExpandedIds(ids: readonly string[]): void;
42
+ }
43
+ export interface AccordionRootProps {
44
+ id: string;
45
+ 'aria-label'?: string;
46
+ }
47
+ export interface AccordionHeaderProps {
48
+ id: string;
49
+ }
50
+ export interface AccordionTriggerProps {
51
+ id: string;
52
+ role: 'button';
53
+ tabindex: '0' | '-1';
54
+ 'aria-expanded': 'true' | 'false';
55
+ 'aria-controls': string;
56
+ 'aria-disabled': 'true' | 'false';
57
+ onClick: () => void;
58
+ onFocus: () => void;
59
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
60
+ }
61
+ export interface AccordionPanelProps {
62
+ id: string;
63
+ role: 'region';
64
+ 'aria-labelledby': string;
65
+ hidden: boolean;
66
+ }
67
+ export interface AccordionContracts {
68
+ getRootProps(): AccordionRootProps;
69
+ getHeaderProps(id: string): AccordionHeaderProps;
70
+ getTriggerProps(id: string): AccordionTriggerProps;
71
+ getPanelProps(id: string): AccordionPanelProps;
72
+ }
73
+ export interface AccordionModel {
74
+ readonly state: AccordionState;
75
+ readonly actions: AccordionActions;
76
+ readonly contracts: AccordionContracts;
77
+ }
78
+ export declare function createAccordion(options: CreateAccordionOptions): AccordionModel;
@@ -0,0 +1,264 @@
1
+ import { action, atom, computed } from '@reatom/core';
2
+ import { createCompositeNavigation } from '../interactions/composite-navigation.js';
3
+ const isSpaceKey = (key) => key === ' ' || key === 'Spacebar';
4
+ export function createAccordion(options) {
5
+ const idBase = options.idBase ?? 'accordion';
6
+ const allowMultipleAtom = atom(options.allowMultiple ?? false, `${idBase}.allowMultiple`);
7
+ const allowZeroExpandedAtom = atom(options.allowZeroExpanded ?? true, `${idBase}.allowZeroExpanded`);
8
+ const headingLevelAtom = atom(options.headingLevel ?? 3, `${idBase}.headingLevel`);
9
+ const ariaLabelAtom = atom(options.ariaLabel, `${idBase}.ariaLabel`);
10
+ const sectionsAtom = atom([...options.sections], `${idBase}.sections`);
11
+ const sectionIdsComputed = computed(() => new Set(sectionsAtom().map((s) => s.id)), `${idBase}.sectionIds`);
12
+ const sectionByIdComputed = computed(() => new Map(sectionsAtom().map((s) => [s.id, s])), `${idBase}.sectionById`);
13
+ const normalizeInitialExpandedIds = () => {
14
+ const sectionIdSet = new Set(options.sections.map((s) => s.id));
15
+ const unique = [...new Set(options.initialExpandedIds ?? [])].filter((id) => sectionIdSet.has(id));
16
+ const allowMultiple = options.allowMultiple ?? false;
17
+ const allowZeroExpanded = options.allowZeroExpanded ?? true;
18
+ if (!allowMultiple && unique.length > 1) {
19
+ const first = unique[0];
20
+ return first == null ? new Set() : new Set([first]);
21
+ }
22
+ if (!allowZeroExpanded && unique.length === 0) {
23
+ const first = options.sections[0]?.id;
24
+ return first == null ? new Set() : new Set([first]);
25
+ }
26
+ return new Set(unique);
27
+ };
28
+ const expandedIdsAtom = atom(normalizeInitialExpandedIds(), `${idBase}.expandedIds`);
29
+ const valueComputed = computed(() => {
30
+ const ids = expandedIdsAtom();
31
+ const first = ids.values().next();
32
+ return first.done ? null : first.value;
33
+ }, `${idBase}.value`);
34
+ const expandedValuesComputed = computed(() => [...expandedIdsAtom()], `${idBase}.expandedValues`);
35
+ const navigation = createCompositeNavigation({
36
+ idBase: `${idBase}.nav`,
37
+ orientation: 'vertical',
38
+ focusStrategy: 'roving-tabindex',
39
+ wrapMode: 'wrap',
40
+ items: options.sections,
41
+ });
42
+ const canExpand = (id) => sectionIdsComputed().has(id) && sectionByIdComputed().get(id)?.disabled !== true;
43
+ const isOnlyExpanded = (id, expandedIds) => expandedIds.size === 1 && expandedIds.has(id);
44
+ const updateExpanded = (updater) => {
45
+ const current = expandedIdsAtom();
46
+ const next = updater(current);
47
+ expandedIdsAtom.set(next);
48
+ };
49
+ const enforceExpandedInvariants = () => {
50
+ const allowMultiple = allowMultipleAtom();
51
+ const allowZeroExpanded = allowZeroExpandedAtom();
52
+ const sectionIds = sectionIdsComputed();
53
+ const current = expandedIdsAtom();
54
+ let next = new Set([...current].filter((id) => sectionIds.has(id)));
55
+ if (!allowMultiple && next.size > 1) {
56
+ const first = next.values().next().value;
57
+ next = first != null ? new Set([first]) : new Set();
58
+ }
59
+ if (!allowZeroExpanded && next.size === 0) {
60
+ const sections = sectionsAtom();
61
+ const firstEnabled = sections.find((s) => !s.disabled);
62
+ if (firstEnabled) {
63
+ next = new Set([firstEnabled.id]);
64
+ }
65
+ }
66
+ expandedIdsAtom.set(next);
67
+ };
68
+ const expand = action((id) => {
69
+ if (!canExpand(id))
70
+ return;
71
+ updateExpanded((current) => {
72
+ if (current.has(id))
73
+ return current;
74
+ if (!allowMultipleAtom()) {
75
+ return new Set([id]);
76
+ }
77
+ const next = new Set(current);
78
+ next.add(id);
79
+ return next;
80
+ });
81
+ }, `${idBase}.expand`);
82
+ const collapse = action((id) => {
83
+ if (!canExpand(id))
84
+ return;
85
+ updateExpanded((current) => {
86
+ if (!current.has(id))
87
+ return current;
88
+ if (!allowZeroExpandedAtom() && isOnlyExpanded(id, current)) {
89
+ return current;
90
+ }
91
+ const next = new Set(current);
92
+ next.delete(id);
93
+ return next;
94
+ });
95
+ }, `${idBase}.collapse`);
96
+ const toggle = action((id) => {
97
+ if (!canExpand(id))
98
+ return;
99
+ if (expandedIdsAtom().has(id)) {
100
+ collapse(id);
101
+ }
102
+ else {
103
+ expand(id);
104
+ }
105
+ }, `${idBase}.toggle`);
106
+ const setFocused = action((id) => {
107
+ navigation.actions.setActive(id);
108
+ }, `${idBase}.setFocused`);
109
+ const moveNext = action(() => {
110
+ navigation.actions.moveNext();
111
+ }, `${idBase}.moveNext`);
112
+ const movePrev = action(() => {
113
+ navigation.actions.movePrev();
114
+ }, `${idBase}.movePrev`);
115
+ const moveFirst = action(() => {
116
+ navigation.actions.moveFirst();
117
+ }, `${idBase}.moveFirst`);
118
+ const moveLast = action(() => {
119
+ navigation.actions.moveLast();
120
+ }, `${idBase}.moveLast`);
121
+ const handleKeyDown = action((event) => {
122
+ switch (event.key) {
123
+ case 'ArrowDown':
124
+ moveNext();
125
+ return;
126
+ case 'ArrowUp':
127
+ movePrev();
128
+ return;
129
+ case 'Home':
130
+ moveFirst();
131
+ return;
132
+ case 'End':
133
+ moveLast();
134
+ return;
135
+ default: {
136
+ if (event.key === 'Enter' || isSpaceKey(event.key)) {
137
+ const focusedId = navigation.state.activeId();
138
+ if (focusedId != null) {
139
+ toggle(focusedId);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }, `${idBase}.handleKeyDown`);
145
+ const setSections = action((sections) => {
146
+ sectionsAtom.set([...sections]);
147
+ navigation.actions.setItems(sections);
148
+ enforceExpandedInvariants();
149
+ }, `${idBase}.setSections`);
150
+ const setAllowMultiple = action((value) => {
151
+ allowMultipleAtom.set(value);
152
+ enforceExpandedInvariants();
153
+ }, `${idBase}.setAllowMultiple`);
154
+ const setAllowZeroExpanded = action((value) => {
155
+ allowZeroExpandedAtom.set(value);
156
+ enforceExpandedInvariants();
157
+ }, `${idBase}.setAllowZeroExpanded`);
158
+ const setHeadingLevel = action((level) => {
159
+ headingLevelAtom.set(Math.max(1, Math.min(6, level)));
160
+ }, `${idBase}.setHeadingLevel`);
161
+ const setAriaLabel = action((label) => {
162
+ ariaLabelAtom.set(label);
163
+ }, `${idBase}.setAriaLabel`);
164
+ const setExpandedIds = action((ids) => {
165
+ const sectionIds = sectionIdsComputed();
166
+ const allowMultiple = allowMultipleAtom();
167
+ const valid = ids.filter((id) => sectionIds.has(id));
168
+ if (!allowMultiple && valid.length > 1) {
169
+ expandedIdsAtom.set(new Set(valid.slice(0, 1)));
170
+ }
171
+ else {
172
+ expandedIdsAtom.set(new Set(valid));
173
+ }
174
+ if (!allowZeroExpandedAtom() && expandedIdsAtom().size === 0) {
175
+ const firstEnabled = sectionsAtom().find((s) => !s.disabled);
176
+ if (firstEnabled) {
177
+ expandedIdsAtom.set(new Set([firstEnabled.id]));
178
+ }
179
+ }
180
+ }, `${idBase}.setExpandedIds`);
181
+ const triggerId = (id) => `${idBase}-trigger-${id}`;
182
+ const panelId = (id) => `${idBase}-panel-${id}`;
183
+ const actions = {
184
+ toggle,
185
+ expand,
186
+ collapse,
187
+ setFocused,
188
+ moveNext,
189
+ movePrev,
190
+ moveFirst,
191
+ moveLast,
192
+ handleKeyDown,
193
+ setSections,
194
+ setAllowMultiple,
195
+ setAllowZeroExpanded,
196
+ setHeadingLevel,
197
+ setAriaLabel,
198
+ setExpandedIds,
199
+ };
200
+ const contracts = {
201
+ getRootProps() {
202
+ return {
203
+ id: `${idBase}-root`,
204
+ 'aria-label': ariaLabelAtom(),
205
+ };
206
+ },
207
+ getHeaderProps(id) {
208
+ if (!sectionIdsComputed().has(id)) {
209
+ throw new Error(`Unknown accordion header id: ${id}`);
210
+ }
211
+ return {
212
+ id: `${idBase}-header-${id}`,
213
+ };
214
+ },
215
+ getTriggerProps(id) {
216
+ const section = sectionByIdComputed().get(id);
217
+ if (!section) {
218
+ throw new Error(`Unknown accordion trigger id: ${id}`);
219
+ }
220
+ const expandedIds = expandedIdsAtom();
221
+ const expanded = expandedIds.has(id);
222
+ const forcedExpanded = !allowZeroExpandedAtom() && isOnlyExpanded(id, expandedIds);
223
+ const disabled = section.disabled === true || forcedExpanded;
224
+ return {
225
+ id: triggerId(id),
226
+ role: 'button',
227
+ tabindex: (navigation.state.activeId() === id && section.disabled !== true ? '0' : '-1'),
228
+ 'aria-expanded': (expanded ? 'true' : 'false'),
229
+ 'aria-controls': panelId(id),
230
+ 'aria-disabled': (disabled ? 'true' : 'false'),
231
+ onClick: () => toggle(id),
232
+ onFocus: () => setFocused(id),
233
+ onKeyDown: handleKeyDown,
234
+ };
235
+ },
236
+ getPanelProps(id) {
237
+ if (!sectionIdsComputed().has(id)) {
238
+ throw new Error(`Unknown accordion panel id: ${id}`);
239
+ }
240
+ return {
241
+ id: panelId(id),
242
+ role: 'region',
243
+ 'aria-labelledby': triggerId(id),
244
+ hidden: !expandedIdsAtom().has(id),
245
+ };
246
+ },
247
+ };
248
+ const state = {
249
+ expandedIds: expandedIdsAtom,
250
+ focusedId: navigation.state.activeId,
251
+ value: valueComputed,
252
+ expandedValues: expandedValuesComputed,
253
+ sections: sectionsAtom,
254
+ allowMultiple: allowMultipleAtom,
255
+ allowZeroExpanded: allowZeroExpandedAtom,
256
+ headingLevel: headingLevelAtom,
257
+ ariaLabel: ariaLabelAtom,
258
+ };
259
+ return {
260
+ state,
261
+ actions,
262
+ contracts,
263
+ };
264
+ }
@@ -0,0 +1,9 @@
1
+ export interface AdapterBindings<TRootProps, TItemProps> {
2
+ rootProps: TRootProps;
3
+ getItemProps(itemId: string): TItemProps;
4
+ onKeyDown(event: KeyboardEvent): void;
5
+ onClick(event: MouseEvent): void;
6
+ }
7
+ export interface HeadlessAdapter<TModel, TRootProps, TItemProps> {
8
+ bind(model: TModel): AdapterBindings<TRootProps, TItemProps>;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { type Atom } from '@reatom/core';
2
+ export type AlertAriaLive = 'assertive' | 'polite';
3
+ export interface CreateAlertOptions {
4
+ idBase?: string;
5
+ ariaLive?: AlertAriaLive;
6
+ ariaAtomic?: boolean;
7
+ durationMs?: number;
8
+ initialMessage?: string;
9
+ initialVisible?: boolean;
10
+ }
11
+ export interface AlertState {
12
+ isVisible: Atom<boolean>;
13
+ message: Atom<string>;
14
+ }
15
+ export interface AlertActions {
16
+ show(message: string): void;
17
+ hide(): void;
18
+ }
19
+ export interface AlertProps {
20
+ id: string;
21
+ role: 'alert';
22
+ 'aria-live': AlertAriaLive;
23
+ 'aria-atomic': 'true' | 'false';
24
+ }
25
+ export interface AlertContracts {
26
+ getAlertProps(): AlertProps;
27
+ }
28
+ export interface AlertModel {
29
+ readonly state: AlertState;
30
+ readonly actions: AlertActions;
31
+ readonly contracts: AlertContracts;
32
+ }
33
+ export declare function createAlert(options?: CreateAlertOptions): AlertModel;
@@ -0,0 +1,54 @@
1
+ import { action, atom } from '@reatom/core';
2
+ export function createAlert(options = {}) {
3
+ const idBase = options.idBase ?? 'alert';
4
+ const durationMs = options.durationMs;
5
+ const ariaLive = options.ariaLive ?? 'assertive';
6
+ const ariaAtomic = options.ariaAtomic ?? true;
7
+ const isVisibleAtom = atom(options.initialVisible ?? false, `${idBase}.isVisible`);
8
+ const messageAtom = atom(options.initialMessage ?? '', `${idBase}.message`);
9
+ const alertId = `${idBase}-region`;
10
+ let dismissTimer = null;
11
+ const clearDismissTimer = () => {
12
+ if (dismissTimer == null)
13
+ return;
14
+ clearTimeout(dismissTimer);
15
+ dismissTimer = null;
16
+ };
17
+ const hide = action(() => {
18
+ clearDismissTimer();
19
+ isVisibleAtom.set(false);
20
+ }, `${idBase}.hide`);
21
+ const show = action((message) => {
22
+ clearDismissTimer();
23
+ messageAtom.set(message);
24
+ isVisibleAtom.set(true);
25
+ if (durationMs != null && Number.isFinite(durationMs) && durationMs > 0) {
26
+ dismissTimer = setTimeout(() => {
27
+ hide();
28
+ }, durationMs);
29
+ }
30
+ }, `${idBase}.show`);
31
+ const actions = {
32
+ show,
33
+ hide,
34
+ };
35
+ const contracts = {
36
+ getAlertProps() {
37
+ return {
38
+ id: alertId,
39
+ role: 'alert',
40
+ 'aria-live': ariaLive,
41
+ 'aria-atomic': ariaAtomic ? 'true' : 'false',
42
+ };
43
+ },
44
+ };
45
+ const state = {
46
+ isVisible: isVisibleAtom,
47
+ message: messageAtom,
48
+ };
49
+ return {
50
+ state,
51
+ actions,
52
+ contracts,
53
+ };
54
+ }
@@ -0,0 +1,69 @@
1
+ import { type Atom, type Computed } from '@reatom/core';
2
+ export interface CreateAlertDialogOptions {
3
+ idBase?: string;
4
+ initialOpen?: boolean;
5
+ triggerId?: string;
6
+ initialFocusId?: string;
7
+ closeOnEscape?: boolean;
8
+ closeOnOutsidePointer?: boolean;
9
+ closeOnOutsideFocus?: boolean;
10
+ ariaLabelledBy?: string;
11
+ ariaDescribedBy?: string;
12
+ closeOnAction?: boolean;
13
+ onCancel?: () => void;
14
+ onAction?: () => void;
15
+ }
16
+ export interface AlertDialogState {
17
+ isOpen: Atom<boolean>;
18
+ restoreTargetId: Atom<string | null>;
19
+ isFocusTrapped: Computed<boolean>;
20
+ initialFocusTargetId: Atom<string | null>;
21
+ }
22
+ export interface AlertDialogActions {
23
+ open(): void;
24
+ close(): void;
25
+ handleKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
26
+ }
27
+ export interface AlertDialogOverlayProps {
28
+ id: string;
29
+ hidden: boolean;
30
+ 'data-open': 'true' | 'false';
31
+ onPointerDownOutside: () => void;
32
+ onFocusOutside: () => void;
33
+ }
34
+ export interface AlertDialogContentProps {
35
+ id: string;
36
+ role: 'alertdialog';
37
+ tabindex: '-1';
38
+ 'aria-modal': 'true';
39
+ 'aria-labelledby': string;
40
+ 'aria-describedby': string;
41
+ 'data-initial-focus'?: string;
42
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
43
+ }
44
+ export interface AlertDialogTitleProps {
45
+ id: string;
46
+ }
47
+ export interface AlertDialogDescriptionProps {
48
+ id: string;
49
+ }
50
+ export interface AlertDialogButtonProps {
51
+ id: string;
52
+ role: 'button';
53
+ tabindex: '0';
54
+ onClick: () => void;
55
+ }
56
+ export interface AlertDialogContracts {
57
+ getDialogProps(): AlertDialogContentProps;
58
+ getOverlayProps(): AlertDialogOverlayProps;
59
+ getTitleProps(): AlertDialogTitleProps;
60
+ getDescriptionProps(): AlertDialogDescriptionProps;
61
+ getCancelButtonProps(): AlertDialogButtonProps;
62
+ getActionButtonProps(): AlertDialogButtonProps;
63
+ }
64
+ export interface AlertDialogModel {
65
+ readonly state: AlertDialogState;
66
+ readonly actions: AlertDialogActions;
67
+ readonly contracts: AlertDialogContracts;
68
+ }
69
+ export declare function createAlertDialog(options?: CreateAlertDialogOptions): AlertDialogModel;