@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,186 @@
1
+ import { atom, computed } from '@reatom/core';
2
+ import { createDialog } from '../dialog/index.js';
3
+ export function createSidebar(options = {}) {
4
+ const id = options.id ?? 'sidebar';
5
+ const defaultExpanded = options.defaultExpanded ?? true;
6
+ const closeOnEscape = options.closeOnEscape ?? true;
7
+ const closeOnOutsidePointer = options.closeOnOutsidePointer ?? true;
8
+ const ariaLabel = options.ariaLabel ?? 'Sidebar navigation';
9
+ const onExpandedChange = options.onExpandedChange;
10
+ const expandedAtom = atom(defaultExpanded, `${id}.expanded`);
11
+ const mobileAtom = atom(false, `${id}.mobile`);
12
+ const dialog = createDialog({
13
+ idBase: `${id}-dialog`,
14
+ initialOpen: false,
15
+ isModal: true,
16
+ closeOnEscape,
17
+ closeOnOutsidePointer,
18
+ closeOnOutsideFocus: true,
19
+ initialFocusId: options.initialFocusId,
20
+ });
21
+ dialog.actions.setTriggerId(`${id}-toggle`);
22
+ const isFocusTrappedAtom = computed(() => mobileAtom() && dialog.state.isOpen(), `${id}.isFocusTrapped`);
23
+ const shouldLockScrollAtom = computed(() => mobileAtom() && dialog.state.isOpen(), `${id}.shouldLockScroll`);
24
+ const setExpanded = (value) => {
25
+ const current = expandedAtom();
26
+ if (current === value)
27
+ return;
28
+ expandedAtom.set(value);
29
+ onExpandedChange?.(value);
30
+ };
31
+ const actions = {
32
+ toggle() {
33
+ if (mobileAtom()) {
34
+ dialog.actions.toggle();
35
+ }
36
+ else {
37
+ setExpanded(!expandedAtom());
38
+ }
39
+ },
40
+ expand() {
41
+ if (mobileAtom())
42
+ return;
43
+ setExpanded(true);
44
+ },
45
+ collapse() {
46
+ if (mobileAtom())
47
+ return;
48
+ setExpanded(false);
49
+ },
50
+ openOverlay() {
51
+ if (!mobileAtom())
52
+ return;
53
+ if (dialog.state.isOpen())
54
+ return;
55
+ dialog.actions.open();
56
+ },
57
+ closeOverlay(intent) {
58
+ if (!mobileAtom())
59
+ return;
60
+ if (!dialog.state.isOpen())
61
+ return;
62
+ dialog.actions.close(intent);
63
+ },
64
+ setMobile(value) {
65
+ if (mobileAtom() === value)
66
+ return;
67
+ mobileAtom.set(value);
68
+ if (value) {
69
+ if (dialog.state.isOpen()) {
70
+ dialog.actions.close();
71
+ }
72
+ }
73
+ else {
74
+ if (dialog.state.isOpen()) {
75
+ dialog.actions.close();
76
+ }
77
+ expandedAtom.set(defaultExpanded);
78
+ }
79
+ },
80
+ handleKeyDown(event) {
81
+ if (!mobileAtom())
82
+ return;
83
+ if (!dialog.state.isOpen())
84
+ return;
85
+ dialog.actions.handleKeyDown(event);
86
+ },
87
+ handleOutsidePointer() {
88
+ if (!mobileAtom())
89
+ return;
90
+ if (!dialog.state.isOpen())
91
+ return;
92
+ dialog.actions.handleOutsidePointer();
93
+ },
94
+ handleOutsideFocus() {
95
+ if (!mobileAtom())
96
+ return;
97
+ if (!dialog.state.isOpen())
98
+ return;
99
+ dialog.actions.handleOutsideFocus();
100
+ },
101
+ };
102
+ const contracts = {
103
+ getSidebarProps() {
104
+ const isMobile = mobileAtom();
105
+ const isOverlayOpen = dialog.state.isOpen();
106
+ if (isMobile && isOverlayOpen) {
107
+ return {
108
+ id: `${id}-panel`,
109
+ role: 'dialog',
110
+ 'aria-modal': 'true',
111
+ 'aria-label': ariaLabel,
112
+ 'data-collapsed': 'false',
113
+ 'data-mobile': 'true',
114
+ 'data-initial-focus': dialog.state.initialFocusTargetId() ?? undefined,
115
+ onKeyDown: actions.handleKeyDown,
116
+ };
117
+ }
118
+ return {
119
+ id: `${id}-panel`,
120
+ role: 'navigation',
121
+ 'aria-label': ariaLabel,
122
+ 'data-collapsed': expandedAtom() ? 'false' : 'true',
123
+ 'data-mobile': isMobile ? 'true' : 'false',
124
+ };
125
+ },
126
+ getToggleProps() {
127
+ const isMobile = mobileAtom();
128
+ const isExpanded = expandedAtom();
129
+ const isOverlayOpen = dialog.state.isOpen();
130
+ let ariaExpanded;
131
+ let toggleLabel;
132
+ if (isMobile) {
133
+ ariaExpanded = isOverlayOpen ? 'true' : 'false';
134
+ toggleLabel = isOverlayOpen ? 'Close sidebar' : 'Open sidebar';
135
+ }
136
+ else {
137
+ ariaExpanded = isExpanded ? 'true' : 'false';
138
+ toggleLabel = isExpanded ? 'Collapse sidebar' : 'Expand sidebar';
139
+ }
140
+ return {
141
+ id: `${id}-toggle`,
142
+ role: 'button',
143
+ tabindex: '0',
144
+ 'aria-expanded': ariaExpanded,
145
+ 'aria-controls': `${id}-panel`,
146
+ 'aria-label': toggleLabel,
147
+ onClick: actions.toggle,
148
+ };
149
+ },
150
+ getOverlayProps() {
151
+ const isMobile = mobileAtom();
152
+ const isOverlayOpen = dialog.state.isOpen();
153
+ const isHidden = !isMobile || !isOverlayOpen;
154
+ return {
155
+ id: `${id}-overlay`,
156
+ hidden: isHidden,
157
+ 'data-open': (!isHidden ? 'true' : 'false'),
158
+ onPointerDownOutside: actions.handleOutsidePointer,
159
+ onFocusOutside: actions.handleOutsideFocus,
160
+ };
161
+ },
162
+ getRailProps() {
163
+ const isVisible = !expandedAtom() && !mobileAtom();
164
+ return {
165
+ id: `${id}-rail`,
166
+ role: 'navigation',
167
+ 'aria-label': ariaLabel,
168
+ 'data-visible': isVisible ? 'true' : 'false',
169
+ };
170
+ },
171
+ };
172
+ const state = {
173
+ expanded: expandedAtom,
174
+ overlayOpen: dialog.state.isOpen,
175
+ mobile: mobileAtom,
176
+ isFocusTrapped: isFocusTrappedAtom,
177
+ shouldLockScroll: shouldLockScrollAtom,
178
+ restoreTargetId: dialog.state.restoreTargetId,
179
+ initialFocusTargetId: dialog.state.initialFocusTargetId,
180
+ };
181
+ return {
182
+ state,
183
+ actions,
184
+ contracts,
185
+ };
186
+ }
@@ -0,0 +1,61 @@
1
+ import { type Atom } from '@reatom/core';
2
+ import { type ValueRangeActions, type ValueRangeState } from '../core/value-range.js';
3
+ export type SliderOrientation = 'horizontal' | 'vertical';
4
+ export interface CreateSliderOptions {
5
+ idBase?: string;
6
+ value?: number;
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ largeStep?: number;
11
+ orientation?: SliderOrientation;
12
+ isDisabled?: boolean;
13
+ ariaLabel?: string;
14
+ ariaLabelledBy?: string;
15
+ ariaDescribedBy?: string;
16
+ formatValueText?: (value: number) => string;
17
+ onValueChange?: (value: number) => void;
18
+ }
19
+ export interface SliderState extends ValueRangeState {
20
+ isDisabled: Atom<boolean>;
21
+ orientation: SliderOrientation;
22
+ }
23
+ export interface SliderActions extends ValueRangeActions {
24
+ setDisabled(value: boolean): void;
25
+ handleKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
26
+ }
27
+ export interface SliderRootProps {
28
+ id: string;
29
+ 'data-orientation': SliderOrientation;
30
+ 'aria-disabled'?: 'true';
31
+ }
32
+ export interface SliderTrackProps {
33
+ id: string;
34
+ 'data-orientation': SliderOrientation;
35
+ }
36
+ export interface SliderThumbProps {
37
+ id: string;
38
+ role: 'slider';
39
+ tabindex: '0' | '-1';
40
+ 'aria-valuenow': string;
41
+ 'aria-valuemin': string;
42
+ 'aria-valuemax': string;
43
+ 'aria-valuetext'?: string;
44
+ 'aria-orientation': SliderOrientation;
45
+ 'aria-disabled'?: 'true';
46
+ 'aria-label'?: string;
47
+ 'aria-labelledby'?: string;
48
+ 'aria-describedby'?: string;
49
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
50
+ }
51
+ export interface SliderContracts {
52
+ getRootProps(): SliderRootProps;
53
+ getTrackProps(): SliderTrackProps;
54
+ getThumbProps(): SliderThumbProps;
55
+ }
56
+ export interface SliderModel {
57
+ readonly state: SliderState;
58
+ readonly actions: SliderActions;
59
+ readonly contracts: SliderContracts;
60
+ }
61
+ export declare function createSlider(options?: CreateSliderOptions): SliderModel;
@@ -0,0 +1,150 @@
1
+ import { action, atom } from '@reatom/core';
2
+ import { createValueRange } from '../core/value-range.js';
3
+ const updateValue = (isDisabled, valueAtom, update, onValueChange) => {
4
+ if (isDisabled())
5
+ return;
6
+ const previous = valueAtom();
7
+ update();
8
+ const next = valueAtom();
9
+ if (previous !== next) {
10
+ onValueChange?.(next);
11
+ }
12
+ };
13
+ export function createSlider(options = {}) {
14
+ const idBase = options.idBase ?? 'slider';
15
+ const orientation = options.orientation ?? 'horizontal';
16
+ const isDisabledAtom = atom(options.isDisabled ?? false, `${idBase}.isDisabled`);
17
+ const range = createValueRange({
18
+ idBase: `${idBase}.range`,
19
+ min: options.min ?? 0,
20
+ max: options.max ?? 100,
21
+ step: options.step,
22
+ largeStep: options.largeStep,
23
+ initialValue: options.value,
24
+ });
25
+ const setValue = action((value) => {
26
+ updateValue(isDisabledAtom, range.state.value, () => {
27
+ range.actions.setValue(value);
28
+ }, options.onValueChange);
29
+ }, `${idBase}.setValue`);
30
+ const increment = action(() => {
31
+ updateValue(isDisabledAtom, range.state.value, () => {
32
+ range.actions.increment();
33
+ }, options.onValueChange);
34
+ }, `${idBase}.increment`);
35
+ const decrement = action(() => {
36
+ updateValue(isDisabledAtom, range.state.value, () => {
37
+ range.actions.decrement();
38
+ }, options.onValueChange);
39
+ }, `${idBase}.decrement`);
40
+ const incrementLarge = action(() => {
41
+ updateValue(isDisabledAtom, range.state.value, () => {
42
+ range.actions.incrementLarge();
43
+ }, options.onValueChange);
44
+ }, `${idBase}.incrementLarge`);
45
+ const decrementLarge = action(() => {
46
+ updateValue(isDisabledAtom, range.state.value, () => {
47
+ range.actions.decrementLarge();
48
+ }, options.onValueChange);
49
+ }, `${idBase}.decrementLarge`);
50
+ const setFirst = action(() => {
51
+ updateValue(isDisabledAtom, range.state.value, () => {
52
+ range.actions.setFirst();
53
+ }, options.onValueChange);
54
+ }, `${idBase}.setFirst`);
55
+ const setLast = action(() => {
56
+ updateValue(isDisabledAtom, range.state.value, () => {
57
+ range.actions.setLast();
58
+ }, options.onValueChange);
59
+ }, `${idBase}.setLast`);
60
+ const setDisabled = action((value) => {
61
+ isDisabledAtom.set(value);
62
+ }, `${idBase}.setDisabled`);
63
+ const handleKeyDown = action((event) => {
64
+ if (isDisabledAtom())
65
+ return;
66
+ switch (event.key) {
67
+ case 'ArrowRight':
68
+ case 'ArrowUp':
69
+ increment();
70
+ return;
71
+ case 'ArrowLeft':
72
+ case 'ArrowDown':
73
+ decrement();
74
+ return;
75
+ case 'PageUp':
76
+ incrementLarge();
77
+ return;
78
+ case 'PageDown':
79
+ decrementLarge();
80
+ return;
81
+ case 'Home':
82
+ setFirst();
83
+ return;
84
+ case 'End':
85
+ setLast();
86
+ return;
87
+ default:
88
+ return;
89
+ }
90
+ }, `${idBase}.handleKeyDown`);
91
+ const actions = {
92
+ setValue,
93
+ increment,
94
+ decrement,
95
+ incrementLarge,
96
+ decrementLarge,
97
+ setFirst,
98
+ setLast,
99
+ setDisabled,
100
+ handleKeyDown,
101
+ };
102
+ const contracts = {
103
+ getRootProps() {
104
+ return {
105
+ id: `${idBase}-root`,
106
+ 'data-orientation': orientation,
107
+ 'aria-disabled': isDisabledAtom() ? 'true' : undefined,
108
+ };
109
+ },
110
+ getTrackProps() {
111
+ return {
112
+ id: `${idBase}-track`,
113
+ 'data-orientation': orientation,
114
+ };
115
+ },
116
+ getThumbProps() {
117
+ const value = range.state.value();
118
+ return {
119
+ id: `${idBase}-thumb`,
120
+ role: 'slider',
121
+ tabindex: isDisabledAtom() ? '-1' : '0',
122
+ 'aria-valuenow': String(value),
123
+ 'aria-valuemin': String(range.state.min()),
124
+ 'aria-valuemax': String(range.state.max()),
125
+ 'aria-valuetext': options.formatValueText?.(value),
126
+ 'aria-orientation': orientation,
127
+ 'aria-disabled': isDisabledAtom() ? 'true' : undefined,
128
+ 'aria-label': options.ariaLabel,
129
+ 'aria-labelledby': options.ariaLabelledBy,
130
+ 'aria-describedby': options.ariaDescribedBy,
131
+ onKeyDown: handleKeyDown,
132
+ };
133
+ },
134
+ };
135
+ const state = {
136
+ value: range.state.value,
137
+ min: range.state.min,
138
+ max: range.state.max,
139
+ step: range.state.step,
140
+ largeStep: range.state.largeStep,
141
+ percentage: range.state.percentage,
142
+ isDisabled: isDisabledAtom,
143
+ orientation,
144
+ };
145
+ return {
146
+ state,
147
+ actions,
148
+ contracts,
149
+ };
150
+ }
@@ -0,0 +1,70 @@
1
+ import { type Atom } from '@reatom/core';
2
+ export type SliderMultiThumbOrientation = 'horizontal' | 'vertical';
3
+ export interface CreateSliderMultiThumbOptions {
4
+ idBase?: string;
5
+ values: readonly number[];
6
+ min?: number;
7
+ max?: number;
8
+ step?: number;
9
+ largeStep?: number;
10
+ orientation?: SliderMultiThumbOrientation;
11
+ isDisabled?: boolean;
12
+ initialActiveThumbIndex?: number | null;
13
+ getThumbAriaLabel?: (index: number) => string | undefined;
14
+ formatValueText?: (value: number, index: number) => string;
15
+ onValuesChange?: (values: readonly number[]) => void;
16
+ }
17
+ export interface SliderMultiThumbState {
18
+ values: Atom<number[]>;
19
+ min: Atom<number>;
20
+ max: Atom<number>;
21
+ step: Atom<number>;
22
+ largeStep: Atom<number>;
23
+ activeThumbIndex: Atom<number | null>;
24
+ isDisabled: Atom<boolean>;
25
+ orientation: SliderMultiThumbOrientation;
26
+ }
27
+ export interface SliderMultiThumbActions {
28
+ setValue(index: number, value: number): void;
29
+ increment(index: number): void;
30
+ decrement(index: number): void;
31
+ incrementLarge(index: number): void;
32
+ decrementLarge(index: number): void;
33
+ setActiveThumb(index: number): void;
34
+ handleKeyDown(index: number, event: Pick<KeyboardEvent, 'key'>): void;
35
+ setDisabled(value: boolean): void;
36
+ }
37
+ export interface SliderMultiThumbRootProps {
38
+ id: string;
39
+ 'data-orientation': SliderMultiThumbOrientation;
40
+ 'aria-disabled'?: 'true';
41
+ }
42
+ export interface SliderMultiThumbTrackProps {
43
+ id: string;
44
+ 'data-orientation': SliderMultiThumbOrientation;
45
+ }
46
+ export interface SliderMultiThumbThumbProps {
47
+ id: string;
48
+ role: 'slider';
49
+ tabindex: '0' | '-1';
50
+ 'aria-valuenow': string;
51
+ 'aria-valuemin': string;
52
+ 'aria-valuemax': string;
53
+ 'aria-valuetext'?: string;
54
+ 'aria-orientation': SliderMultiThumbOrientation;
55
+ 'aria-disabled'?: 'true';
56
+ 'aria-label'?: string;
57
+ 'data-active': 'true' | 'false';
58
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
59
+ }
60
+ export interface SliderMultiThumbContracts {
61
+ getRootProps(): SliderMultiThumbRootProps;
62
+ getTrackProps(): SliderMultiThumbTrackProps;
63
+ getThumbProps(index: number): SliderMultiThumbThumbProps;
64
+ }
65
+ export interface SliderMultiThumbModel {
66
+ readonly state: SliderMultiThumbState;
67
+ readonly actions: SliderMultiThumbActions;
68
+ readonly contracts: SliderMultiThumbContracts;
69
+ }
70
+ export declare function createSliderMultiThumb(options: CreateSliderMultiThumbOptions): SliderMultiThumbModel;
@@ -0,0 +1,222 @@
1
+ import { action, atom } from '@reatom/core';
2
+ import { clampValue, decrementValue, decrementValueLarge, incrementValue, incrementValueLarge, normalizeValueRange, snapValueToStep, } from '../core/value-range.js';
3
+ const normalizeInitialValues = (values, range) => {
4
+ const snapped = values.map((value) => snapValueToStep(value, range));
5
+ const ordered = [...snapped].sort((left, right) => left - right);
6
+ return ordered.map((value, index, list) => {
7
+ const min = index === 0 ? range.min : (list[index - 1] ?? range.min);
8
+ const max = index === list.length - 1 ? range.max : (list[index + 1] ?? range.max);
9
+ return clampValue(value, min, max);
10
+ });
11
+ };
12
+ const isThumbIndex = (index, values) => Number.isInteger(index) && index >= 0 && index < values.length;
13
+ export function createSliderMultiThumb(options) {
14
+ const idBase = options.idBase ?? 'slider-multi-thumb';
15
+ const orientation = options.orientation ?? 'horizontal';
16
+ const range = normalizeValueRange({
17
+ min: options.min ?? 0,
18
+ max: options.max ?? 100,
19
+ step: options.step,
20
+ largeStep: options.largeStep,
21
+ });
22
+ const minAtom = atom(range.min, `${idBase}.min`);
23
+ const maxAtom = atom(range.max, `${idBase}.max`);
24
+ const stepAtom = atom(range.step, `${idBase}.step`);
25
+ const largeStepAtom = atom(range.largeStep, `${idBase}.largeStep`);
26
+ const getRange = () => ({
27
+ min: minAtom(),
28
+ max: maxAtom(),
29
+ step: stepAtom(),
30
+ largeStep: largeStepAtom(),
31
+ });
32
+ const valuesAtom = atom(normalizeInitialValues(options.values, getRange()), `${idBase}.values`);
33
+ const isDisabledAtom = atom(options.isDisabled ?? false, `${idBase}.isDisabled`);
34
+ const activeThumbIndexAtom = atom(options.initialActiveThumbIndex ?? (valuesAtom().length > 0 ? 0 : null), `${idBase}.activeThumbIndex`);
35
+ const getThumbBounds = (index, values = valuesAtom()) => {
36
+ const min = index === 0 ? minAtom() : (values[index - 1] ?? minAtom());
37
+ const max = index === values.length - 1 ? maxAtom() : (values[index + 1] ?? maxAtom());
38
+ return { min, max };
39
+ };
40
+ const applyValueAtIndex = (index, next) => {
41
+ const values = [...valuesAtom()];
42
+ if (!isThumbIndex(index, values))
43
+ return;
44
+ const bounds = getThumbBounds(index, values);
45
+ const localRange = {
46
+ ...getRange(),
47
+ min: bounds.min,
48
+ max: bounds.max,
49
+ };
50
+ values[index] = snapValueToStep(clampValue(next, bounds.min, bounds.max), localRange);
51
+ valuesAtom.set(values);
52
+ options.onValuesChange?.(values);
53
+ };
54
+ const setValue = action((index, value) => {
55
+ if (isDisabledAtom())
56
+ return;
57
+ applyValueAtIndex(index, value);
58
+ activeThumbIndexAtom.set(index);
59
+ }, `${idBase}.setValue`);
60
+ const increment = action((index) => {
61
+ if (isDisabledAtom())
62
+ return;
63
+ const values = valuesAtom();
64
+ if (!isThumbIndex(index, values))
65
+ return;
66
+ const bounds = getThumbBounds(index, values);
67
+ const localRange = {
68
+ ...getRange(),
69
+ min: bounds.min,
70
+ max: bounds.max,
71
+ };
72
+ applyValueAtIndex(index, incrementValue(values[index] ?? bounds.min, localRange));
73
+ activeThumbIndexAtom.set(index);
74
+ }, `${idBase}.increment`);
75
+ const decrement = action((index) => {
76
+ if (isDisabledAtom())
77
+ return;
78
+ const values = valuesAtom();
79
+ if (!isThumbIndex(index, values))
80
+ return;
81
+ const bounds = getThumbBounds(index, values);
82
+ const localRange = {
83
+ ...getRange(),
84
+ min: bounds.min,
85
+ max: bounds.max,
86
+ };
87
+ applyValueAtIndex(index, decrementValue(values[index] ?? bounds.max, localRange));
88
+ activeThumbIndexAtom.set(index);
89
+ }, `${idBase}.decrement`);
90
+ const incrementLarge = action((index) => {
91
+ if (isDisabledAtom())
92
+ return;
93
+ const values = valuesAtom();
94
+ if (!isThumbIndex(index, values))
95
+ return;
96
+ const bounds = getThumbBounds(index, values);
97
+ const localRange = {
98
+ ...getRange(),
99
+ min: bounds.min,
100
+ max: bounds.max,
101
+ };
102
+ applyValueAtIndex(index, incrementValueLarge(values[index] ?? bounds.min, localRange));
103
+ activeThumbIndexAtom.set(index);
104
+ }, `${idBase}.incrementLarge`);
105
+ const decrementLarge = action((index) => {
106
+ if (isDisabledAtom())
107
+ return;
108
+ const values = valuesAtom();
109
+ if (!isThumbIndex(index, values))
110
+ return;
111
+ const bounds = getThumbBounds(index, values);
112
+ const localRange = {
113
+ ...getRange(),
114
+ min: bounds.min,
115
+ max: bounds.max,
116
+ };
117
+ applyValueAtIndex(index, decrementValueLarge(values[index] ?? bounds.max, localRange));
118
+ activeThumbIndexAtom.set(index);
119
+ }, `${idBase}.decrementLarge`);
120
+ const setActiveThumb = action((index) => {
121
+ if (!isThumbIndex(index, valuesAtom()))
122
+ return;
123
+ activeThumbIndexAtom.set(index);
124
+ }, `${idBase}.setActiveThumb`);
125
+ const setDisabled = action((value) => {
126
+ isDisabledAtom.set(value);
127
+ }, `${idBase}.setDisabled`);
128
+ const handleKeyDown = action((index, event) => {
129
+ switch (event.key) {
130
+ case 'ArrowRight':
131
+ case 'ArrowUp':
132
+ increment(index);
133
+ return;
134
+ case 'ArrowLeft':
135
+ case 'ArrowDown':
136
+ decrement(index);
137
+ return;
138
+ case 'PageUp':
139
+ incrementLarge(index);
140
+ return;
141
+ case 'PageDown':
142
+ decrementLarge(index);
143
+ return;
144
+ case 'Home': {
145
+ const { min } = getThumbBounds(index);
146
+ setValue(index, min);
147
+ return;
148
+ }
149
+ case 'End': {
150
+ const { max } = getThumbBounds(index);
151
+ setValue(index, max);
152
+ return;
153
+ }
154
+ default:
155
+ return;
156
+ }
157
+ }, `${idBase}.handleKeyDown`);
158
+ const actions = {
159
+ setValue,
160
+ increment,
161
+ decrement,
162
+ incrementLarge,
163
+ decrementLarge,
164
+ setActiveThumb,
165
+ handleKeyDown,
166
+ setDisabled,
167
+ };
168
+ const contracts = {
169
+ getRootProps() {
170
+ return {
171
+ id: `${idBase}-root`,
172
+ 'data-orientation': orientation,
173
+ 'aria-disabled': isDisabledAtom() ? 'true' : undefined,
174
+ };
175
+ },
176
+ getTrackProps() {
177
+ return {
178
+ id: `${idBase}-track`,
179
+ 'data-orientation': orientation,
180
+ };
181
+ },
182
+ getThumbProps(index) {
183
+ const values = valuesAtom();
184
+ if (!isThumbIndex(index, values)) {
185
+ throw new Error(`Unknown slider thumb index: ${index}`);
186
+ }
187
+ const { min, max } = getThumbBounds(index, values);
188
+ const value = values[index] ?? min;
189
+ const activeIndex = activeThumbIndexAtom();
190
+ const isActive = activeIndex == null ? index === 0 : activeIndex === index;
191
+ return {
192
+ id: `${idBase}-thumb-${index}`,
193
+ role: 'slider',
194
+ tabindex: isDisabledAtom() ? '-1' : '0',
195
+ 'aria-valuenow': String(value),
196
+ 'aria-valuemin': String(min),
197
+ 'aria-valuemax': String(max),
198
+ 'aria-valuetext': options.formatValueText?.(value, index),
199
+ 'aria-orientation': orientation,
200
+ 'aria-disabled': isDisabledAtom() ? 'true' : undefined,
201
+ 'aria-label': options.getThumbAriaLabel?.(index),
202
+ 'data-active': isActive ? 'true' : 'false',
203
+ onKeyDown: (event) => handleKeyDown(index, event),
204
+ };
205
+ },
206
+ };
207
+ const state = {
208
+ values: valuesAtom,
209
+ min: minAtom,
210
+ max: maxAtom,
211
+ step: stepAtom,
212
+ largeStep: largeStepAtom,
213
+ activeThumbIndex: activeThumbIndexAtom,
214
+ isDisabled: isDisabledAtom,
215
+ orientation,
216
+ };
217
+ return {
218
+ state,
219
+ actions,
220
+ contracts,
221
+ };
222
+ }