@chromvoid/headless-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/a11y-contracts/index.d.ts +23 -0
  4. package/dist/a11y-contracts/index.js +1 -0
  5. package/dist/accordion/index.d.ts +78 -0
  6. package/dist/accordion/index.js +264 -0
  7. package/dist/adapters/index.d.ts +9 -0
  8. package/dist/adapters/index.js +1 -0
  9. package/dist/alert/index.d.ts +33 -0
  10. package/dist/alert/index.js +54 -0
  11. package/dist/alert-dialog/index.d.ts +69 -0
  12. package/dist/alert-dialog/index.js +94 -0
  13. package/dist/badge/index.d.ts +48 -0
  14. package/dist/badge/index.js +89 -0
  15. package/dist/breadcrumb/index.d.ts +55 -0
  16. package/dist/breadcrumb/index.js +77 -0
  17. package/dist/button/index.d.ts +46 -0
  18. package/dist/button/index.js +86 -0
  19. package/dist/callout/index.d.ts +41 -0
  20. package/dist/callout/index.js +63 -0
  21. package/dist/card/index.d.ts +54 -0
  22. package/dist/card/index.js +103 -0
  23. package/dist/carousel/index.d.ts +98 -0
  24. package/dist/carousel/index.js +243 -0
  25. package/dist/checkbox/index.d.ts +50 -0
  26. package/dist/checkbox/index.js +87 -0
  27. package/dist/combobox/index.d.ts +114 -0
  28. package/dist/combobox/index.js +431 -0
  29. package/dist/command-palette/index.d.ts +73 -0
  30. package/dist/command-palette/index.js +147 -0
  31. package/dist/context-menu/index.d.ts +111 -0
  32. package/dist/context-menu/index.js +372 -0
  33. package/dist/copy-button/index.d.ts +62 -0
  34. package/dist/copy-button/index.js +183 -0
  35. package/dist/core/index.d.ts +20 -0
  36. package/dist/core/index.js +2 -0
  37. package/dist/core/selection.d.ts +5 -0
  38. package/dist/core/selection.js +39 -0
  39. package/dist/core/value-range.d.ts +49 -0
  40. package/dist/core/value-range.js +134 -0
  41. package/dist/date-picker/index.d.ts +210 -0
  42. package/dist/date-picker/index.js +895 -0
  43. package/dist/dialog/index.d.ts +95 -0
  44. package/dist/dialog/index.js +153 -0
  45. package/dist/disclosure/index.d.ts +52 -0
  46. package/dist/disclosure/index.js +159 -0
  47. package/dist/drawer/index.d.ts +30 -0
  48. package/dist/drawer/index.js +39 -0
  49. package/dist/feed/index.d.ts +77 -0
  50. package/dist/feed/index.js +260 -0
  51. package/dist/grid/index.d.ts +103 -0
  52. package/dist/grid/index.js +415 -0
  53. package/dist/index.d.ts +51 -0
  54. package/dist/index.js +51 -0
  55. package/dist/input/index.d.ts +86 -0
  56. package/dist/input/index.js +156 -0
  57. package/dist/interactions/composite-navigation.d.ts +69 -0
  58. package/dist/interactions/composite-navigation.js +169 -0
  59. package/dist/interactions/index.d.ts +15 -0
  60. package/dist/interactions/index.js +4 -0
  61. package/dist/interactions/keyboard-intents.d.ts +16 -0
  62. package/dist/interactions/keyboard-intents.js +33 -0
  63. package/dist/interactions/overlay-focus.d.ts +40 -0
  64. package/dist/interactions/overlay-focus.js +93 -0
  65. package/dist/interactions/typeahead.d.ts +20 -0
  66. package/dist/interactions/typeahead.js +41 -0
  67. package/dist/landmarks/index.d.ts +39 -0
  68. package/dist/landmarks/index.js +58 -0
  69. package/dist/link/index.d.ts +34 -0
  70. package/dist/link/index.js +39 -0
  71. package/dist/listbox/index.d.ts +92 -0
  72. package/dist/listbox/index.js +337 -0
  73. package/dist/menu/index.d.ts +132 -0
  74. package/dist/menu/index.js +541 -0
  75. package/dist/menu-button/index.d.ts +71 -0
  76. package/dist/menu-button/index.js +121 -0
  77. package/dist/meter/index.d.ts +45 -0
  78. package/dist/meter/index.js +106 -0
  79. package/dist/number/index.d.ts +113 -0
  80. package/dist/number/index.js +252 -0
  81. package/dist/popover/index.d.ts +70 -0
  82. package/dist/popover/index.js +126 -0
  83. package/dist/progress/index.d.ts +49 -0
  84. package/dist/progress/index.js +79 -0
  85. package/dist/radio-group/index.d.ts +61 -0
  86. package/dist/radio-group/index.js +150 -0
  87. package/dist/select/index.d.ts +92 -0
  88. package/dist/select/index.js +239 -0
  89. package/dist/sidebar/index.d.ts +74 -0
  90. package/dist/sidebar/index.js +186 -0
  91. package/dist/slider/index.d.ts +61 -0
  92. package/dist/slider/index.js +150 -0
  93. package/dist/slider-multi-thumb/index.d.ts +70 -0
  94. package/dist/slider-multi-thumb/index.js +222 -0
  95. package/dist/spinbutton/index.d.ts +75 -0
  96. package/dist/spinbutton/index.js +214 -0
  97. package/dist/spinner/index.d.ts +1 -0
  98. package/dist/spinner/index.js +1 -0
  99. package/dist/spinner/spinner.d.ts +23 -0
  100. package/dist/spinner/spinner.js +25 -0
  101. package/dist/switch/index.d.ts +40 -0
  102. package/dist/switch/index.js +61 -0
  103. package/dist/table/index.d.ts +117 -0
  104. package/dist/table/index.js +377 -0
  105. package/dist/tabs/index.d.ts +63 -0
  106. package/dist/tabs/index.js +174 -0
  107. package/dist/textarea/index.d.ts +68 -0
  108. package/dist/textarea/index.js +137 -0
  109. package/dist/toast/index.d.ts +67 -0
  110. package/dist/toast/index.js +145 -0
  111. package/dist/toolbar/index.d.ts +59 -0
  112. package/dist/toolbar/index.js +139 -0
  113. package/dist/tooltip/index.d.ts +52 -0
  114. package/dist/tooltip/index.js +169 -0
  115. package/dist/treegrid/index.d.ts +101 -0
  116. package/dist/treegrid/index.js +463 -0
  117. package/dist/treeview/index.d.ts +68 -0
  118. package/dist/treeview/index.js +370 -0
  119. package/dist/window-splitter/index.d.ts +65 -0
  120. package/dist/window-splitter/index.js +204 -0
  121. package/package.json +92 -0
  122. package/specs/ADR-001-headless-architecture.md +461 -0
  123. package/specs/ADR-002-repo-release-model.md +108 -0
  124. package/specs/ADR-003-public-api-versioning.md +136 -0
  125. package/specs/ADR-004-focus-selection-policy.md +117 -0
  126. package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
  127. package/specs/ISSUE-BACKLOG.md +681 -0
  128. package/specs/RELEASE-CANDIDATE.md +30 -0
  129. package/specs/components/accordion.md +130 -0
  130. package/specs/components/alert-dialog.md +72 -0
  131. package/specs/components/alert.md +65 -0
  132. package/specs/components/badge.md +220 -0
  133. package/specs/components/breadcrumb.md +74 -0
  134. package/specs/components/button.md +115 -0
  135. package/specs/components/callout.md +195 -0
  136. package/specs/components/card.md +280 -0
  137. package/specs/components/carousel.md +140 -0
  138. package/specs/components/checkbox.md +172 -0
  139. package/specs/components/combobox.md +423 -0
  140. package/specs/components/command-palette.md +92 -0
  141. package/specs/components/context-menu.md +556 -0
  142. package/specs/components/copy-button.md +293 -0
  143. package/specs/components/date-picker.md +400 -0
  144. package/specs/components/dialog.md +298 -0
  145. package/specs/components/disclosure.md +257 -0
  146. package/specs/components/drawer.md +353 -0
  147. package/specs/components/feed.md +265 -0
  148. package/specs/components/grid.md +186 -0
  149. package/specs/components/input.md +254 -0
  150. package/specs/components/landmarks.md +136 -0
  151. package/specs/components/link.md +134 -0
  152. package/specs/components/listbox.md +351 -0
  153. package/specs/components/menu-button.md +76 -0
  154. package/specs/components/menu.md +623 -0
  155. package/specs/components/meter.md +149 -0
  156. package/specs/components/number.md +393 -0
  157. package/specs/components/popover.md +252 -0
  158. package/specs/components/progress.md +188 -0
  159. package/specs/components/radio-group.md +151 -0
  160. package/specs/components/select.md +144 -0
  161. package/specs/components/sidebar.md +321 -0
  162. package/specs/components/slider-multi-thumb.md +78 -0
  163. package/specs/components/slider.md +84 -0
  164. package/specs/components/spinbutton.md +140 -0
  165. package/specs/components/spinner.md +132 -0
  166. package/specs/components/switch.md +175 -0
  167. package/specs/components/table.md +403 -0
  168. package/specs/components/tabs.md +265 -0
  169. package/specs/components/textarea.md +185 -0
  170. package/specs/components/toast.md +198 -0
  171. package/specs/components/toolbar.md +278 -0
  172. package/specs/components/tooltip.md +252 -0
  173. package/specs/components/treegrid.md +281 -0
  174. package/specs/components/treeview.md +91 -0
  175. package/specs/components/window-splitter.md +297 -0
  176. package/specs/ops/git-shard-sync.md +107 -0
  177. package/specs/ops/release-checklist.md +76 -0
  178. package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
  179. package/specs/release/api-freeze-candidate.md +54 -0
  180. package/specs/release/changelog-automation.md +76 -0
  181. package/specs/release/changelog.generated.md +53 -0
  182. package/specs/release/changelog.patch.generated.md +46 -0
  183. package/specs/release/consumer-integration.md +53 -0
  184. package/specs/release/migration-notes-pre-v1.md +40 -0
  185. package/specs/release/mvp-changelog.md +57 -0
  186. package/specs/release/release-notes-template.md +61 -0
  187. package/specs/release/release-rehearsal.md +113 -0
  188. package/specs/release/semver-deprecation-dry-run.md +89 -0
  189. package/specs/release/shard-release-drill-report.md +50 -0
  190. package/specs/release/shard-release-follow-ups.md +31 -0
  191. package/specs/signals.md +208 -0
@@ -0,0 +1,370 @@
1
+ import { action, atom } from '@reatom/core';
2
+ import { selectOnly as selectOnlyPrimitive, toggleSelection } from '../core/selection.js';
3
+ import { mapListboxKeyboardIntent } from '../interactions/keyboard-intents.js';
4
+ const normalizeIds = (ids, allowedIds) => {
5
+ const deduped = [];
6
+ for (const id of ids) {
7
+ if (!allowedIds.has(id) || deduped.includes(id))
8
+ continue;
9
+ deduped.push(id);
10
+ }
11
+ return deduped;
12
+ };
13
+ const collectDescendants = (metaById, id) => {
14
+ const descendants = [];
15
+ const queue = [...(metaById.get(id)?.childIds ?? [])];
16
+ while (queue.length > 0) {
17
+ const current = queue.shift();
18
+ if (!current)
19
+ continue;
20
+ descendants.push(current);
21
+ const children = metaById.get(current)?.childIds ?? [];
22
+ queue.push(...children);
23
+ }
24
+ return descendants;
25
+ };
26
+ const buildTreeMeta = (nodes) => {
27
+ const metaById = new Map();
28
+ const rootIds = [];
29
+ const visit = (siblings, parentId, level) => {
30
+ const setSize = siblings.length;
31
+ siblings.forEach((node, index) => {
32
+ const childIds = (node.children ?? []).map((child) => child.id);
33
+ metaById.set(node.id, {
34
+ id: node.id,
35
+ parentId,
36
+ childIds,
37
+ level,
38
+ posInSet: index + 1,
39
+ setSize,
40
+ disabled: node.disabled === true,
41
+ });
42
+ if (parentId == null)
43
+ rootIds.push(node.id);
44
+ if ((node.children?.length ?? 0) > 0)
45
+ visit(node.children ?? [], node.id, level + 1);
46
+ });
47
+ };
48
+ visit(nodes, null, 1);
49
+ return { metaById, rootIds };
50
+ };
51
+ export function createTreeview(options) {
52
+ const idBase = options.idBase ?? 'tree';
53
+ const selectionMode = options.selectionMode ?? 'single';
54
+ const { metaById, rootIds } = buildTreeMeta(options.nodes);
55
+ const allIds = [...metaById.keys()];
56
+ const allIdSet = new Set(allIds);
57
+ const enabledIds = allIds.filter((id) => metaById.get(id)?.disabled !== true);
58
+ const enabledIdSet = new Set(enabledIds);
59
+ const isBranch = (id) => (metaById.get(id)?.childIds.length ?? 0) > 0;
60
+ const expandedIdsAtom = atom(normalizeIds(options.initialExpandedIds ?? [], allIdSet).filter((id) => isBranch(id)), `${idBase}.expandedIds`);
61
+ const activeIdAtom = atom(null, `${idBase}.activeId`);
62
+ const selectedIdsAtom = atom([], `${idBase}.selectedIds`);
63
+ const isVisible = (id, expandedSet) => {
64
+ let parentId = metaById.get(id)?.parentId ?? null;
65
+ while (parentId != null) {
66
+ if (!expandedSet.has(parentId)) {
67
+ return false;
68
+ }
69
+ parentId = metaById.get(parentId)?.parentId ?? null;
70
+ }
71
+ return true;
72
+ };
73
+ const getVisibleNodeIds = () => {
74
+ const expandedSet = new Set(expandedIdsAtom());
75
+ const visible = [];
76
+ const visit = (id) => {
77
+ if (!metaById.has(id))
78
+ return;
79
+ visible.push(id);
80
+ if (!expandedSet.has(id))
81
+ return;
82
+ const childIds = metaById.get(id)?.childIds ?? [];
83
+ for (const childId of childIds) {
84
+ visit(childId);
85
+ }
86
+ };
87
+ for (const rootId of rootIds) {
88
+ visit(rootId);
89
+ }
90
+ return visible;
91
+ };
92
+ const getVisibleEnabledNodeIds = () => getVisibleNodeIds().filter((id) => enabledIdSet.has(id));
93
+ const setActive = (id) => {
94
+ if (id == null) {
95
+ activeIdAtom.set(null);
96
+ return;
97
+ }
98
+ if (!enabledIdSet.has(id))
99
+ return;
100
+ const visibleIds = getVisibleEnabledNodeIds();
101
+ if (!visibleIds.includes(id))
102
+ return;
103
+ activeIdAtom.set(id);
104
+ };
105
+ const resolveInitialActive = () => {
106
+ if (options.initialActiveId != null && enabledIdSet.has(options.initialActiveId)) {
107
+ const visibleIds = getVisibleEnabledNodeIds();
108
+ if (visibleIds.includes(options.initialActiveId)) {
109
+ return options.initialActiveId;
110
+ }
111
+ }
112
+ return getVisibleEnabledNodeIds()[0] ?? null;
113
+ };
114
+ activeIdAtom.set(resolveInitialActive());
115
+ const initialSelectedIds = normalizeIds(options.initialSelectedIds ?? [], enabledIdSet);
116
+ selectedIdsAtom.set(selectionMode === 'single' ? initialSelectedIds.slice(0, 1) : initialSelectedIds);
117
+ const applySelectionFollowsFocus = (newActiveId) => {
118
+ if (selectionMode === 'single' && newActiveId != null && enabledIdSet.has(newActiveId)) {
119
+ selectedIdsAtom.set([newActiveId]);
120
+ }
121
+ };
122
+ const move = (direction) => {
123
+ const visibleEnabledIds = getVisibleEnabledNodeIds();
124
+ if (visibleEnabledIds.length === 0) {
125
+ activeIdAtom.set(null);
126
+ return;
127
+ }
128
+ const activeId = activeIdAtom();
129
+ if (activeId == null || !visibleEnabledIds.includes(activeId)) {
130
+ const newActiveId = visibleEnabledIds[0] ?? null;
131
+ activeIdAtom.set(newActiveId);
132
+ applySelectionFollowsFocus(newActiveId);
133
+ return;
134
+ }
135
+ const currentIndex = visibleEnabledIds.indexOf(activeId);
136
+ const nextIndex = (currentIndex + direction + visibleEnabledIds.length) % visibleEnabledIds.length;
137
+ const newActiveId = visibleEnabledIds[nextIndex] ?? null;
138
+ activeIdAtom.set(newActiveId);
139
+ applySelectionFollowsFocus(newActiveId);
140
+ };
141
+ const collapseWithInvariant = (id) => {
142
+ if (!isBranch(id))
143
+ return;
144
+ expandedIdsAtom.set(expandedIdsAtom().filter((item) => item !== id));
145
+ const activeId = activeIdAtom();
146
+ if (activeId == null || activeId === id)
147
+ return;
148
+ const descendants = collectDescendants(metaById, id);
149
+ if (descendants.includes(activeId)) {
150
+ activeIdAtom.set(enabledIdSet.has(id) ? id : (getVisibleEnabledNodeIds()[0] ?? null));
151
+ }
152
+ };
153
+ const setActiveAction = action((id) => {
154
+ setActive(id);
155
+ }, `${idBase}.setActive`);
156
+ const moveNext = action(() => {
157
+ move(1);
158
+ }, `${idBase}.moveNext`);
159
+ const movePrev = action(() => {
160
+ move(-1);
161
+ }, `${idBase}.movePrev`);
162
+ const moveFirst = action(() => {
163
+ const newActiveId = getVisibleEnabledNodeIds()[0] ?? null;
164
+ activeIdAtom.set(newActiveId);
165
+ applySelectionFollowsFocus(newActiveId);
166
+ }, `${idBase}.moveFirst`);
167
+ const moveLast = action(() => {
168
+ const visibleEnabledIds = getVisibleEnabledNodeIds();
169
+ const newActiveId = visibleEnabledIds[visibleEnabledIds.length - 1] ?? null;
170
+ activeIdAtom.set(newActiveId);
171
+ applySelectionFollowsFocus(newActiveId);
172
+ }, `${idBase}.moveLast`);
173
+ const expand = action((id) => {
174
+ if (!isBranch(id))
175
+ return;
176
+ if (!expandedIdsAtom().includes(id)) {
177
+ expandedIdsAtom.set([...expandedIdsAtom(), id]);
178
+ }
179
+ }, `${idBase}.expand`);
180
+ const collapse = action((id) => {
181
+ collapseWithInvariant(id);
182
+ }, `${idBase}.collapse`);
183
+ const toggleExpanded = action((id) => {
184
+ if (!isBranch(id))
185
+ return;
186
+ if (expandedIdsAtom().includes(id)) {
187
+ collapseWithInvariant(id);
188
+ }
189
+ else {
190
+ expand(id);
191
+ }
192
+ }, `${idBase}.toggleExpanded`);
193
+ const expandActive = action(() => {
194
+ const activeId = activeIdAtom();
195
+ if (activeId == null)
196
+ return;
197
+ expand(activeId);
198
+ }, `${idBase}.expandActive`);
199
+ const collapseActive = action(() => {
200
+ const activeId = activeIdAtom();
201
+ if (activeId == null)
202
+ return;
203
+ collapse(activeId);
204
+ }, `${idBase}.collapseActive`);
205
+ const select = action((id) => {
206
+ if (!enabledIdSet.has(id))
207
+ return;
208
+ if (selectionMode === 'single') {
209
+ selectedIdsAtom.set(selectOnlyPrimitive(id, enabledIdSet));
210
+ }
211
+ else {
212
+ selectedIdsAtom.set([id]);
213
+ }
214
+ activeIdAtom.set(id);
215
+ }, `${idBase}.select`);
216
+ const toggleSelected = action((id) => {
217
+ selectedIdsAtom.set(toggleSelection(selectedIdsAtom(), id, selectionMode, enabledIdSet));
218
+ if (enabledIdSet.has(id)) {
219
+ activeIdAtom.set(id);
220
+ }
221
+ }, `${idBase}.toggleSelected`);
222
+ const clearSelected = action(() => {
223
+ selectedIdsAtom.set([]);
224
+ }, `${idBase}.clearSelected`);
225
+ const handleKeyDown = action((event) => {
226
+ const activeId = activeIdAtom();
227
+ if (event.key === 'ArrowRight') {
228
+ if (activeId == null) {
229
+ moveFirst();
230
+ return;
231
+ }
232
+ const meta = metaById.get(activeId);
233
+ if (!meta)
234
+ return;
235
+ const isExpanded = expandedIdsAtom().includes(activeId);
236
+ if (meta.childIds.length > 0 && !isExpanded) {
237
+ expand(activeId);
238
+ return;
239
+ }
240
+ if (meta.childIds.length > 0 && isExpanded) {
241
+ const firstEnabledChild = meta.childIds.find((id) => enabledIdSet.has(id));
242
+ if (firstEnabledChild != null && isVisible(firstEnabledChild, new Set(expandedIdsAtom()))) {
243
+ activeIdAtom.set(firstEnabledChild);
244
+ }
245
+ }
246
+ return;
247
+ }
248
+ if (event.key === 'ArrowLeft') {
249
+ if (activeId == null) {
250
+ moveFirst();
251
+ return;
252
+ }
253
+ const meta = metaById.get(activeId);
254
+ if (!meta)
255
+ return;
256
+ if (meta.childIds.length > 0 && expandedIdsAtom().includes(activeId)) {
257
+ collapse(activeId);
258
+ return;
259
+ }
260
+ let parentId = meta.parentId;
261
+ while (parentId != null) {
262
+ if (enabledIdSet.has(parentId)) {
263
+ activeIdAtom.set(parentId);
264
+ return;
265
+ }
266
+ parentId = metaById.get(parentId)?.parentId ?? null;
267
+ }
268
+ return;
269
+ }
270
+ const intent = mapListboxKeyboardIntent(event, {
271
+ orientation: 'vertical',
272
+ selectionMode,
273
+ rangeSelectionEnabled: false,
274
+ });
275
+ if (intent == null)
276
+ return;
277
+ switch (intent) {
278
+ case 'NAV_NEXT':
279
+ moveNext();
280
+ return;
281
+ case 'NAV_PREV':
282
+ movePrev();
283
+ return;
284
+ case 'NAV_FIRST':
285
+ moveFirst();
286
+ return;
287
+ case 'NAV_LAST':
288
+ moveLast();
289
+ return;
290
+ case 'TOGGLE_SELECTION': {
291
+ const currentActiveId = activeIdAtom();
292
+ if (currentActiveId != null) {
293
+ toggleSelected(currentActiveId);
294
+ }
295
+ return;
296
+ }
297
+ case 'ACTIVATE': {
298
+ const currentActiveId = activeIdAtom();
299
+ if (currentActiveId != null) {
300
+ select(currentActiveId);
301
+ }
302
+ return;
303
+ }
304
+ case 'SELECT_ALL':
305
+ if (selectionMode === 'multiple') {
306
+ selectedIdsAtom.set([...enabledIds]);
307
+ }
308
+ return;
309
+ default:
310
+ return;
311
+ }
312
+ }, `${idBase}.handleKeyDown`);
313
+ const actions = {
314
+ setActive: setActiveAction,
315
+ moveNext,
316
+ movePrev,
317
+ moveFirst,
318
+ moveLast,
319
+ expand,
320
+ collapse,
321
+ toggleExpanded,
322
+ expandActive,
323
+ collapseActive,
324
+ select,
325
+ toggleSelected,
326
+ clearSelected,
327
+ handleKeyDown,
328
+ };
329
+ const contracts = {
330
+ getTreeProps() {
331
+ return {
332
+ role: 'tree',
333
+ tabindex: '0',
334
+ 'aria-label': options.ariaLabel,
335
+ 'aria-multiselectable': selectionMode === 'multiple' ? 'true' : undefined,
336
+ };
337
+ },
338
+ getItemProps(id) {
339
+ const meta = metaById.get(id);
340
+ if (!meta) {
341
+ throw new Error(`Unknown tree node id: ${id}`);
342
+ }
343
+ const expanded = meta.childIds.length > 0 ? expandedIdsAtom().includes(id) : undefined;
344
+ return {
345
+ id: `${idBase}-item-${id}`,
346
+ role: 'treeitem',
347
+ tabindex: activeIdAtom() === id ? '0' : '-1',
348
+ 'aria-level': meta.level,
349
+ 'aria-posinset': meta.posInSet,
350
+ 'aria-setsize': meta.setSize,
351
+ 'aria-expanded': expanded == null ? undefined : expanded ? 'true' : 'false',
352
+ 'aria-selected': selectedIdsAtom().includes(id) ? 'true' : 'false',
353
+ 'aria-disabled': meta.disabled ? 'true' : undefined,
354
+ 'data-active': activeIdAtom() === id ? 'true' : 'false',
355
+ 'data-expanded': expanded == null ? undefined : expanded ? 'true' : 'false',
356
+ };
357
+ },
358
+ getVisibleNodeIds,
359
+ };
360
+ const state = {
361
+ activeId: activeIdAtom,
362
+ selectedIds: selectedIdsAtom,
363
+ expandedIds: expandedIdsAtom,
364
+ };
365
+ return {
366
+ state,
367
+ actions,
368
+ contracts,
369
+ };
370
+ }
@@ -0,0 +1,65 @@
1
+ import { type Atom } from '@reatom/core';
2
+ export type WindowSplitterOrientation = 'horizontal' | 'vertical';
3
+ export interface CreateWindowSplitterOptions {
4
+ idBase?: string;
5
+ min?: number;
6
+ max?: number;
7
+ position?: number;
8
+ step?: number;
9
+ orientation?: WindowSplitterOrientation;
10
+ ariaLabel?: string;
11
+ ariaLabelledBy?: string;
12
+ primaryPaneId?: string;
13
+ secondaryPaneId?: string;
14
+ isFixed?: boolean;
15
+ formatValueText?: (value: number) => string;
16
+ onPositionChange?: (value: number) => void;
17
+ snap?: string;
18
+ snapThreshold?: number;
19
+ }
20
+ export interface WindowSplitterState {
21
+ position: Atom<number>;
22
+ min: Atom<number>;
23
+ max: Atom<number>;
24
+ orientation: Atom<WindowSplitterOrientation>;
25
+ isDragging: Atom<boolean>;
26
+ }
27
+ export interface WindowSplitterActions {
28
+ setPosition(value: number): void;
29
+ moveStep(direction: -1 | 1): void;
30
+ moveToMin(): void;
31
+ moveToMax(): void;
32
+ startDragging(): void;
33
+ stopDragging(): void;
34
+ handleKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
35
+ }
36
+ export interface WindowSplitterProps {
37
+ id: string;
38
+ role: 'separator';
39
+ tabindex: '0';
40
+ 'aria-valuenow': string;
41
+ 'aria-valuemin': string;
42
+ 'aria-valuemax': string;
43
+ 'aria-valuetext'?: string;
44
+ 'aria-orientation': WindowSplitterOrientation;
45
+ 'aria-controls': string;
46
+ 'aria-label'?: string;
47
+ 'aria-labelledby'?: string;
48
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
49
+ }
50
+ export interface WindowSplitterPaneProps {
51
+ id: string;
52
+ 'data-pane': 'primary' | 'secondary';
53
+ 'data-orientation': WindowSplitterOrientation;
54
+ }
55
+ export interface WindowSplitterContracts {
56
+ getSplitterProps(): WindowSplitterProps;
57
+ getPrimaryPaneProps(): WindowSplitterPaneProps;
58
+ getSecondaryPaneProps(): WindowSplitterPaneProps;
59
+ }
60
+ export interface WindowSplitterModel {
61
+ readonly state: WindowSplitterState;
62
+ readonly actions: WindowSplitterActions;
63
+ readonly contracts: WindowSplitterContracts;
64
+ }
65
+ export declare function createWindowSplitter(options?: CreateWindowSplitterOptions): WindowSplitterModel;
@@ -0,0 +1,204 @@
1
+ import { action, atom } from '@reatom/core';
2
+ import { createValueRange, clampValue } from '../core/value-range.js';
3
+ function parseSnapPoints(snap, min, max) {
4
+ return snap
5
+ .trim()
6
+ .split(/\s+/)
7
+ .map((s) => {
8
+ if (s.endsWith('%')) {
9
+ const pct = parseFloat(s) / 100;
10
+ return min + pct * (max - min);
11
+ }
12
+ return parseFloat(s);
13
+ })
14
+ .filter((n) => !isNaN(n));
15
+ }
16
+ function resolveSnap(value, snap, snapThreshold, min, max) {
17
+ if (!snap)
18
+ return value;
19
+ const points = parseSnapPoints(snap, min, max);
20
+ if (points.length === 0)
21
+ return value;
22
+ const firstPoint = points[0];
23
+ if (firstPoint === undefined)
24
+ return value;
25
+ let nearest = firstPoint;
26
+ let minDist = Math.abs(value - nearest);
27
+ for (const p of points) {
28
+ const d = Math.abs(value - p);
29
+ if (d < minDist) {
30
+ minDist = d;
31
+ nearest = p;
32
+ }
33
+ }
34
+ return minDist <= snapThreshold ? nearest : value;
35
+ }
36
+ const updatePosition = (valueAtom, update, onPositionChange) => {
37
+ const previous = valueAtom();
38
+ update();
39
+ const next = valueAtom();
40
+ if (next !== previous) {
41
+ onPositionChange?.(next);
42
+ }
43
+ };
44
+ export function createWindowSplitter(options = {}) {
45
+ const idBase = options.idBase ?? 'window-splitter';
46
+ const orientationAtom = atom(options.orientation ?? 'horizontal', `${idBase}.orientation`);
47
+ const isDraggingAtom = atom(false, `${idBase}.isDragging`);
48
+ const isFixed = options.isFixed ?? false;
49
+ const snapThreshold = options.snapThreshold ?? 12;
50
+ const min = options.min ?? 0;
51
+ const max = options.max ?? 100;
52
+ const range = createValueRange({
53
+ idBase: `${idBase}.range`,
54
+ min,
55
+ max,
56
+ step: options.step,
57
+ initialValue: options.position,
58
+ });
59
+ const applySnappedPosition = (value) => {
60
+ const currentMin = range.state.min();
61
+ const currentMax = range.state.max();
62
+ const clamped = clampValue(value, currentMin, currentMax);
63
+ const snapped = resolveSnap(clamped, options.snap, snapThreshold, currentMin, currentMax);
64
+ range.actions.setValue(snapped);
65
+ };
66
+ const setPosition = action((value) => {
67
+ updatePosition(range.state.value, () => {
68
+ applySnappedPosition(value);
69
+ }, options.onPositionChange);
70
+ }, `${idBase}.setPosition`);
71
+ const moveStep = action((direction) => {
72
+ updatePosition(range.state.value, () => {
73
+ if (direction > 0) {
74
+ range.actions.increment();
75
+ }
76
+ else {
77
+ range.actions.decrement();
78
+ }
79
+ }, options.onPositionChange);
80
+ }, `${idBase}.moveStep`);
81
+ const moveToMin = action(() => {
82
+ updatePosition(range.state.value, () => {
83
+ range.actions.setFirst();
84
+ }, options.onPositionChange);
85
+ }, `${idBase}.moveToMin`);
86
+ const moveToMax = action(() => {
87
+ updatePosition(range.state.value, () => {
88
+ range.actions.setLast();
89
+ }, options.onPositionChange);
90
+ }, `${idBase}.moveToMax`);
91
+ const toggleFixed = () => {
92
+ if (!isFixed)
93
+ return;
94
+ const currentMin = range.state.min();
95
+ const currentMax = range.state.max();
96
+ const midpoint = currentMin + (currentMax - currentMin) / 2;
97
+ if (range.state.value() <= midpoint) {
98
+ moveToMax();
99
+ }
100
+ else {
101
+ moveToMin();
102
+ }
103
+ };
104
+ const startDragging = action(() => {
105
+ isDraggingAtom.set(true);
106
+ }, `${idBase}.startDragging`);
107
+ const stopDragging = action(() => {
108
+ isDraggingAtom.set(false);
109
+ }, `${idBase}.stopDragging`);
110
+ const handleKeyDown = action((event) => {
111
+ const orientation = orientationAtom();
112
+ switch (event.key) {
113
+ case 'ArrowLeft':
114
+ if (!isFixed && orientation === 'vertical') {
115
+ moveStep(-1);
116
+ }
117
+ return;
118
+ case 'ArrowRight':
119
+ if (!isFixed && orientation === 'vertical') {
120
+ moveStep(1);
121
+ }
122
+ return;
123
+ case 'ArrowUp':
124
+ if (!isFixed && orientation === 'horizontal') {
125
+ moveStep(-1);
126
+ }
127
+ return;
128
+ case 'ArrowDown':
129
+ if (!isFixed && orientation === 'horizontal') {
130
+ moveStep(1);
131
+ }
132
+ return;
133
+ case 'Home':
134
+ moveToMin();
135
+ return;
136
+ case 'End':
137
+ moveToMax();
138
+ return;
139
+ case 'Enter':
140
+ toggleFixed();
141
+ return;
142
+ default:
143
+ return;
144
+ }
145
+ }, `${idBase}.handleKeyDown`);
146
+ const actions = {
147
+ setPosition,
148
+ moveStep,
149
+ moveToMin,
150
+ moveToMax,
151
+ startDragging,
152
+ stopDragging,
153
+ handleKeyDown,
154
+ };
155
+ const splitterId = `${idBase}-separator`;
156
+ const primaryPaneId = options.primaryPaneId ?? `${idBase}-pane-primary`;
157
+ const secondaryPaneId = options.secondaryPaneId ?? `${idBase}-pane-secondary`;
158
+ const controlsIds = `${primaryPaneId} ${secondaryPaneId}`;
159
+ const contracts = {
160
+ getSplitterProps() {
161
+ const value = range.state.value();
162
+ return {
163
+ id: splitterId,
164
+ role: 'separator',
165
+ tabindex: '0',
166
+ 'aria-valuenow': String(value),
167
+ 'aria-valuemin': String(range.state.min()),
168
+ 'aria-valuemax': String(range.state.max()),
169
+ 'aria-valuetext': options.formatValueText?.(value),
170
+ 'aria-orientation': orientationAtom(),
171
+ 'aria-controls': controlsIds,
172
+ 'aria-label': options.ariaLabel,
173
+ 'aria-labelledby': options.ariaLabelledBy,
174
+ onKeyDown: handleKeyDown,
175
+ };
176
+ },
177
+ getPrimaryPaneProps() {
178
+ return {
179
+ id: primaryPaneId,
180
+ 'data-pane': 'primary',
181
+ 'data-orientation': orientationAtom(),
182
+ };
183
+ },
184
+ getSecondaryPaneProps() {
185
+ return {
186
+ id: secondaryPaneId,
187
+ 'data-pane': 'secondary',
188
+ 'data-orientation': orientationAtom(),
189
+ };
190
+ },
191
+ };
192
+ const state = {
193
+ position: range.state.value,
194
+ min: range.state.min,
195
+ max: range.state.max,
196
+ orientation: orientationAtom,
197
+ isDragging: isDraggingAtom,
198
+ };
199
+ return {
200
+ state,
201
+ actions,
202
+ contracts,
203
+ };
204
+ }