@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,95 @@
1
+ import { type Atom, type Computed } from '@reatom/core';
2
+ import { type OverlayDismissIntent, type OverlayOpenSource } from '../interactions/overlay-focus.js';
3
+ export interface CreateDialogOptions {
4
+ idBase?: string;
5
+ type?: 'dialog' | 'alertdialog';
6
+ initialOpen?: boolean;
7
+ isModal?: boolean;
8
+ closeOnEscape?: boolean;
9
+ closeOnOutsidePointer?: boolean;
10
+ closeOnOutsideFocus?: boolean;
11
+ initialFocusId?: string;
12
+ ariaLabelledBy?: string;
13
+ ariaDescribedBy?: string;
14
+ }
15
+ export interface DialogState {
16
+ isOpen: Atom<boolean>;
17
+ isModal: Atom<boolean>;
18
+ type: Atom<'dialog' | 'alertdialog'>;
19
+ restoreTargetId: Atom<string | null>;
20
+ isFocusTrapped: Computed<boolean>;
21
+ shouldLockScroll: Computed<boolean>;
22
+ initialFocusTargetId: Atom<string | null>;
23
+ }
24
+ export interface DialogActions {
25
+ setTriggerId(id: string | null): void;
26
+ open(source?: OverlayOpenSource): void;
27
+ close(intent?: OverlayDismissIntent): void;
28
+ toggle(source?: OverlayOpenSource): void;
29
+ handleTriggerClick(): void;
30
+ handleTriggerKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
31
+ handleKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
32
+ handleOutsidePointer(): void;
33
+ handleOutsideFocus(): void;
34
+ }
35
+ export interface DialogTriggerProps {
36
+ id: string;
37
+ role: 'button';
38
+ tabindex: '0';
39
+ 'aria-haspopup': 'dialog';
40
+ 'aria-expanded': 'true' | 'false';
41
+ 'aria-controls': string;
42
+ onClick: () => void;
43
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
44
+ }
45
+ export interface DialogOverlayProps {
46
+ id: string;
47
+ hidden: boolean;
48
+ 'data-open': 'true' | 'false';
49
+ onPointerDownOutside: () => void;
50
+ onFocusOutside: () => void;
51
+ }
52
+ export interface DialogContentProps {
53
+ id: string;
54
+ role: 'dialog' | 'alertdialog';
55
+ tabindex: '-1';
56
+ 'aria-modal': 'true' | 'false';
57
+ 'aria-labelledby'?: string;
58
+ 'aria-describedby'?: string;
59
+ 'data-initial-focus'?: string;
60
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
61
+ }
62
+ export interface DialogTitleProps {
63
+ id: string;
64
+ }
65
+ export interface DialogDescriptionProps {
66
+ id: string;
67
+ }
68
+ export interface DialogCloseButtonProps {
69
+ id: string;
70
+ role: 'button';
71
+ tabindex: '0';
72
+ onClick: () => void;
73
+ }
74
+ export interface DialogHeaderCloseButtonProps {
75
+ id: string;
76
+ role: 'button';
77
+ tabindex: '0';
78
+ 'aria-label': 'Close';
79
+ onClick: () => void;
80
+ }
81
+ export interface DialogContracts {
82
+ getTriggerProps(): DialogTriggerProps;
83
+ getOverlayProps(): DialogOverlayProps;
84
+ getContentProps(): DialogContentProps;
85
+ getTitleProps(): DialogTitleProps;
86
+ getDescriptionProps(): DialogDescriptionProps;
87
+ getCloseButtonProps(): DialogCloseButtonProps;
88
+ getHeaderCloseButtonProps(): DialogHeaderCloseButtonProps;
89
+ }
90
+ export interface DialogModel {
91
+ readonly state: DialogState;
92
+ readonly actions: DialogActions;
93
+ readonly contracts: DialogContracts;
94
+ }
95
+ export declare function createDialog(options?: CreateDialogOptions): DialogModel;
@@ -0,0 +1,153 @@
1
+ import { action, atom, computed } from '@reatom/core';
2
+ import { createOverlayFocus, } from '../interactions/overlay-focus.js';
3
+ const isSpaceKey = (key) => key === ' ' || key === 'Spacebar';
4
+ export function createDialog(options = {}) {
5
+ const idBase = options.idBase ?? 'dialog';
6
+ const closeOnEscape = options.closeOnEscape ?? true;
7
+ const closeOnOutsidePointer = options.closeOnOutsidePointer ?? true;
8
+ const closeOnOutsideFocus = options.closeOnOutsideFocus ?? true;
9
+ const typeAtom = atom(options.type ?? 'dialog', `${idBase}.type`);
10
+ const isModalAtom = atom(options.isModal ?? true, `${idBase}.isModal`);
11
+ const initialFocusTargetIdAtom = atom(options.initialFocusId ?? null, `${idBase}.initialFocusId`);
12
+ const defaultTriggerId = `${idBase}-trigger`;
13
+ const contentId = `${idBase}-content`;
14
+ const titleId = options.ariaLabelledBy ?? `${idBase}-title`;
15
+ const descriptionId = options.ariaDescribedBy ?? `${idBase}-description`;
16
+ const overlay = createOverlayFocus({
17
+ idBase: `${idBase}.overlay`,
18
+ initialOpen: options.initialOpen,
19
+ initialTriggerId: defaultTriggerId,
20
+ trapFocus: isModalAtom(),
21
+ restoreFocus: true,
22
+ });
23
+ const shouldLockScrollAtom = computed(() => overlay.state.isOpen() && isModalAtom(), `${idBase}.shouldLockScroll`);
24
+ const open = action((source = 'programmatic') => {
25
+ overlay.actions.open(source, overlay.state.triggerId() ?? defaultTriggerId);
26
+ }, `${idBase}.open`);
27
+ const close = action((intent = 'programmatic') => {
28
+ overlay.actions.close(intent);
29
+ }, `${idBase}.close`);
30
+ const toggle = action((source = 'programmatic') => {
31
+ if (overlay.state.isOpen()) {
32
+ close('programmatic');
33
+ return;
34
+ }
35
+ open(source);
36
+ }, `${idBase}.toggle`);
37
+ const setTriggerId = action((id) => {
38
+ overlay.actions.setTrigger(id);
39
+ }, `${idBase}.setTriggerId`);
40
+ const handleTriggerClick = action(() => {
41
+ toggle('pointer');
42
+ }, `${idBase}.handleTriggerClick`);
43
+ const handleTriggerKeyDown = action((event) => {
44
+ if (event.key === 'Enter' || isSpaceKey(event.key)) {
45
+ toggle('keyboard');
46
+ }
47
+ }, `${idBase}.handleTriggerKeyDown`);
48
+ const handleKeyDown = action((event) => {
49
+ if (!closeOnEscape && event.key === 'Escape') {
50
+ return;
51
+ }
52
+ overlay.actions.handleKeyDown(event);
53
+ }, `${idBase}.handleContentKeyDown`);
54
+ const handleOutsidePointer = action(() => {
55
+ if (!closeOnOutsidePointer) {
56
+ return;
57
+ }
58
+ overlay.actions.handleOutsidePointer();
59
+ }, `${idBase}.handleOutsidePointer`);
60
+ const handleOutsideFocus = action(() => {
61
+ if (!closeOnOutsideFocus) {
62
+ return;
63
+ }
64
+ overlay.actions.handleOutsideFocus();
65
+ }, `${idBase}.handleOutsideFocus`);
66
+ const actions = {
67
+ setTriggerId,
68
+ open,
69
+ close,
70
+ toggle,
71
+ handleTriggerClick,
72
+ handleTriggerKeyDown,
73
+ handleKeyDown,
74
+ handleOutsidePointer,
75
+ handleOutsideFocus,
76
+ };
77
+ const contracts = {
78
+ getTriggerProps() {
79
+ return {
80
+ id: overlay.state.triggerId() ?? defaultTriggerId,
81
+ role: 'button',
82
+ tabindex: '0',
83
+ 'aria-haspopup': 'dialog',
84
+ 'aria-expanded': overlay.state.isOpen() ? 'true' : 'false',
85
+ 'aria-controls': contentId,
86
+ onClick: handleTriggerClick,
87
+ onKeyDown: handleTriggerKeyDown,
88
+ };
89
+ },
90
+ getOverlayProps() {
91
+ return {
92
+ id: `${idBase}-overlay`,
93
+ hidden: !overlay.state.isOpen(),
94
+ 'data-open': overlay.state.isOpen() ? 'true' : 'false',
95
+ onPointerDownOutside: handleOutsidePointer,
96
+ onFocusOutside: handleOutsideFocus,
97
+ };
98
+ },
99
+ getContentProps() {
100
+ return {
101
+ id: contentId,
102
+ role: typeAtom(),
103
+ tabindex: '-1',
104
+ 'aria-modal': isModalAtom() ? 'true' : 'false',
105
+ 'aria-labelledby': titleId,
106
+ 'aria-describedby': descriptionId,
107
+ 'data-initial-focus': initialFocusTargetIdAtom() ?? undefined,
108
+ onKeyDown: handleKeyDown,
109
+ };
110
+ },
111
+ getTitleProps() {
112
+ return {
113
+ id: titleId,
114
+ };
115
+ },
116
+ getDescriptionProps() {
117
+ return {
118
+ id: descriptionId,
119
+ };
120
+ },
121
+ getCloseButtonProps() {
122
+ return {
123
+ id: `${idBase}-close`,
124
+ role: 'button',
125
+ tabindex: '0',
126
+ onClick: () => close('programmatic'),
127
+ };
128
+ },
129
+ getHeaderCloseButtonProps() {
130
+ return {
131
+ id: `${idBase}-header-close`,
132
+ role: 'button',
133
+ tabindex: '0',
134
+ 'aria-label': 'Close',
135
+ onClick: () => close('programmatic'),
136
+ };
137
+ },
138
+ };
139
+ const state = {
140
+ isOpen: overlay.state.isOpen,
141
+ isModal: isModalAtom,
142
+ type: typeAtom,
143
+ restoreTargetId: overlay.state.restoreTargetId,
144
+ isFocusTrapped: overlay.state.isFocusTrapped,
145
+ shouldLockScroll: shouldLockScrollAtom,
146
+ initialFocusTargetId: initialFocusTargetIdAtom,
147
+ };
148
+ return {
149
+ state,
150
+ actions,
151
+ contracts,
152
+ };
153
+ }
@@ -0,0 +1,52 @@
1
+ import { type Atom } from '@reatom/core';
2
+ export interface CreateDisclosureOptions {
3
+ idBase?: string;
4
+ isOpen?: boolean;
5
+ isDisabled?: boolean;
6
+ name?: string;
7
+ onOpenChange?: (isOpen: boolean) => void;
8
+ }
9
+ export interface DisclosureState {
10
+ isOpen: Atom<boolean>;
11
+ isDisabled: Atom<boolean>;
12
+ name: Atom<string | null>;
13
+ }
14
+ export interface DisclosureActions {
15
+ open(): void;
16
+ close(): void;
17
+ toggle(): void;
18
+ setDisabled(value: boolean): void;
19
+ setName(value: string | null): void;
20
+ handleClick(): void;
21
+ handleKeyDown(event: Pick<KeyboardEvent, 'key'> & {
22
+ preventDefault?: () => void;
23
+ }): void;
24
+ destroy(): void;
25
+ }
26
+ export interface DisclosureTriggerProps {
27
+ id: string;
28
+ role: 'button';
29
+ tabindex: '0' | '-1';
30
+ 'aria-expanded': 'true' | 'false';
31
+ 'aria-controls': string;
32
+ 'aria-disabled'?: 'true';
33
+ onClick: () => void;
34
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'> & {
35
+ preventDefault?: () => void;
36
+ }) => void;
37
+ }
38
+ export interface DisclosurePanelProps {
39
+ id: string;
40
+ 'aria-labelledby': string;
41
+ hidden: boolean;
42
+ }
43
+ export interface DisclosureContracts {
44
+ getTriggerProps(): DisclosureTriggerProps;
45
+ getPanelProps(): DisclosurePanelProps;
46
+ }
47
+ export interface DisclosureModel {
48
+ readonly state: DisclosureState;
49
+ readonly actions: DisclosureActions;
50
+ readonly contracts: DisclosureContracts;
51
+ }
52
+ export declare function createDisclosure(options?: CreateDisclosureOptions): DisclosureModel;
@@ -0,0 +1,159 @@
1
+ import { action, atom } from '@reatom/core';
2
+ const groupRegistry = new Map();
3
+ function registerInGroup(name, model) {
4
+ let group = groupRegistry.get(name);
5
+ if (!group) {
6
+ group = new Set();
7
+ groupRegistry.set(name, group);
8
+ }
9
+ group.add(model);
10
+ }
11
+ function unregisterFromGroup(name, model) {
12
+ const group = groupRegistry.get(name);
13
+ if (!group)
14
+ return;
15
+ group.delete(model);
16
+ if (group.size === 0) {
17
+ groupRegistry.delete(name);
18
+ }
19
+ }
20
+ function closeOthersInGroup(name, self) {
21
+ const group = groupRegistry.get(name);
22
+ if (!group)
23
+ return;
24
+ for (const member of group) {
25
+ if (member !== self) {
26
+ member.actions.close();
27
+ }
28
+ }
29
+ }
30
+ const isSpaceKey = (key) => key === ' ' || key === 'Spacebar';
31
+ export function createDisclosure(options = {}) {
32
+ const idBase = options.idBase ?? 'disclosure';
33
+ const isOpenAtom = atom(options.isOpen ?? false, `${idBase}.isOpen`);
34
+ const isDisabledAtom = atom(options.isDisabled ?? false, `${idBase}.isDisabled`);
35
+ const nameAtom = atom(options.name ?? null, `${idBase}.name`);
36
+ const notifyOpenChange = (next) => {
37
+ if (isOpenAtom() === next)
38
+ return;
39
+ isOpenAtom.set(next);
40
+ options.onOpenChange?.(next);
41
+ };
42
+ let self;
43
+ const open = action(() => {
44
+ if (isDisabledAtom())
45
+ return;
46
+ notifyOpenChange(true);
47
+ const currentName = nameAtom();
48
+ if (currentName != null) {
49
+ closeOthersInGroup(currentName, self);
50
+ }
51
+ }, `${idBase}.open`);
52
+ const close = action(() => {
53
+ if (isDisabledAtom())
54
+ return;
55
+ notifyOpenChange(false);
56
+ }, `${idBase}.close`);
57
+ const toggle = action(() => {
58
+ if (isDisabledAtom())
59
+ return;
60
+ if (isOpenAtom()) {
61
+ close();
62
+ }
63
+ else {
64
+ open();
65
+ }
66
+ }, `${idBase}.toggle`);
67
+ const setDisabled = action((value) => {
68
+ isDisabledAtom.set(value);
69
+ }, `${idBase}.setDisabled`);
70
+ const setName = action((value) => {
71
+ const oldName = nameAtom();
72
+ if (oldName === value)
73
+ return;
74
+ if (oldName != null) {
75
+ unregisterFromGroup(oldName, self);
76
+ }
77
+ nameAtom.set(value);
78
+ if (value != null) {
79
+ registerInGroup(value, self);
80
+ }
81
+ }, `${idBase}.setName`);
82
+ const handleClick = action(() => {
83
+ toggle();
84
+ }, `${idBase}.handleClick`);
85
+ const handleKeyDown = action((event) => {
86
+ if (isDisabledAtom())
87
+ return;
88
+ if (event.key === 'Enter' || isSpaceKey(event.key)) {
89
+ event.preventDefault?.();
90
+ toggle();
91
+ return;
92
+ }
93
+ if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
94
+ event.preventDefault?.();
95
+ open();
96
+ return;
97
+ }
98
+ if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
99
+ event.preventDefault?.();
100
+ close();
101
+ return;
102
+ }
103
+ }, `${idBase}.handleKeyDown`);
104
+ const destroy = action(() => {
105
+ const currentName = nameAtom();
106
+ if (currentName != null) {
107
+ unregisterFromGroup(currentName, self);
108
+ }
109
+ }, `${idBase}.destroy`);
110
+ const triggerId = `${idBase}-trigger`;
111
+ const panelId = `${idBase}-panel`;
112
+ const actions = {
113
+ open,
114
+ close,
115
+ toggle,
116
+ setDisabled,
117
+ setName,
118
+ handleClick,
119
+ handleKeyDown,
120
+ destroy,
121
+ };
122
+ const contracts = {
123
+ getTriggerProps() {
124
+ return {
125
+ id: triggerId,
126
+ role: 'button',
127
+ tabindex: isDisabledAtom() ? '-1' : '0',
128
+ 'aria-expanded': isOpenAtom() ? 'true' : 'false',
129
+ 'aria-controls': panelId,
130
+ 'aria-disabled': isDisabledAtom() ? 'true' : undefined,
131
+ onClick: handleClick,
132
+ onKeyDown: handleKeyDown,
133
+ };
134
+ },
135
+ getPanelProps() {
136
+ return {
137
+ id: panelId,
138
+ 'aria-labelledby': triggerId,
139
+ hidden: !isOpenAtom(),
140
+ };
141
+ },
142
+ };
143
+ const state = {
144
+ isOpen: isOpenAtom,
145
+ isDisabled: isDisabledAtom,
146
+ name: nameAtom,
147
+ };
148
+ const model = {
149
+ state,
150
+ actions,
151
+ contracts,
152
+ };
153
+ self = model;
154
+ const initialName = nameAtom();
155
+ if (initialName != null) {
156
+ registerInGroup(initialName, model);
157
+ }
158
+ return model;
159
+ }
@@ -0,0 +1,30 @@
1
+ import { type Atom } from '@reatom/core';
2
+ import { type CreateDialogOptions, type DialogState, type DialogActions, type DialogTriggerProps, type DialogOverlayProps, type DialogContentProps, type DialogTitleProps, type DialogDescriptionProps, type DialogCloseButtonProps, type DialogHeaderCloseButtonProps } from '../dialog/index.js';
3
+ export type DrawerPlacement = 'start' | 'end' | 'top' | 'bottom';
4
+ export interface CreateDrawerOptions extends CreateDialogOptions {
5
+ placement?: DrawerPlacement;
6
+ }
7
+ export interface DrawerPanelProps extends DialogContentProps {
8
+ 'data-placement': DrawerPlacement;
9
+ }
10
+ export interface DrawerState extends DialogState {
11
+ placement: Atom<DrawerPlacement>;
12
+ }
13
+ export interface DrawerActions extends DialogActions {
14
+ setPlacement(placement: DrawerPlacement): void;
15
+ }
16
+ export interface DrawerContracts {
17
+ getTriggerProps(): DialogTriggerProps;
18
+ getOverlayProps(): DialogOverlayProps;
19
+ getPanelProps(): DrawerPanelProps;
20
+ getTitleProps(): DialogTitleProps;
21
+ getDescriptionProps(): DialogDescriptionProps;
22
+ getCloseButtonProps(): DialogCloseButtonProps;
23
+ getHeaderCloseButtonProps(): DialogHeaderCloseButtonProps;
24
+ }
25
+ export interface DrawerModel {
26
+ readonly state: DrawerState;
27
+ readonly actions: DrawerActions;
28
+ readonly contracts: DrawerContracts;
29
+ }
30
+ export declare function createDrawer(options?: CreateDrawerOptions): DrawerModel;
@@ -0,0 +1,39 @@
1
+ import { atom } from '@reatom/core';
2
+ import { createDialog, } from '../dialog/index.js';
3
+ export function createDrawer(options = {}) {
4
+ const idBase = options.idBase ?? 'drawer';
5
+ const dialog = createDialog({
6
+ ...options,
7
+ idBase,
8
+ });
9
+ const placementAtom = atom(options.placement ?? 'end', `${idBase}.placement`);
10
+ const state = {
11
+ ...dialog.state,
12
+ placement: placementAtom,
13
+ };
14
+ const actions = {
15
+ ...dialog.actions,
16
+ setPlacement(placement) {
17
+ placementAtom.set(placement);
18
+ },
19
+ };
20
+ const contracts = {
21
+ getTriggerProps: dialog.contracts.getTriggerProps,
22
+ getOverlayProps: dialog.contracts.getOverlayProps,
23
+ getPanelProps() {
24
+ return {
25
+ ...dialog.contracts.getContentProps(),
26
+ 'data-placement': placementAtom(),
27
+ };
28
+ },
29
+ getTitleProps: dialog.contracts.getTitleProps,
30
+ getDescriptionProps: dialog.contracts.getDescriptionProps,
31
+ getCloseButtonProps: dialog.contracts.getCloseButtonProps,
32
+ getHeaderCloseButtonProps: dialog.contracts.getHeaderCloseButtonProps,
33
+ };
34
+ return {
35
+ state,
36
+ actions,
37
+ contracts,
38
+ };
39
+ }
@@ -0,0 +1,77 @@
1
+ import { type Atom, type Computed } from '@reatom/core';
2
+ export interface FeedArticle {
3
+ id: string;
4
+ disabled?: boolean;
5
+ }
6
+ export interface FeedKeyboardEventLike {
7
+ key: string;
8
+ ctrlKey?: boolean;
9
+ metaKey?: boolean;
10
+ }
11
+ export type FeedKeyboardResult = 'next' | 'prev' | 'exit-after' | 'exit-before' | null;
12
+ export interface CreateFeedOptions {
13
+ articles: readonly FeedArticle[];
14
+ idBase?: string;
15
+ ariaLabel?: string;
16
+ ariaLabelledBy?: string;
17
+ initialActiveArticleId?: string | null;
18
+ totalCount?: number;
19
+ onLoadMore?: () => readonly FeedArticle[] | Promise<readonly FeedArticle[]>;
20
+ onLoadNewer?: () => readonly FeedArticle[] | Promise<readonly FeedArticle[]>;
21
+ }
22
+ export interface FeedState {
23
+ articleIds: Computed<string[]>;
24
+ activeArticleId: Atom<string | null>;
25
+ isLoading: Atom<boolean>;
26
+ isBusy: Atom<boolean>;
27
+ totalCount: Atom<number>;
28
+ isEmpty: Computed<boolean>;
29
+ hasError: Computed<boolean>;
30
+ error: Atom<string | null>;
31
+ canLoadMore: Computed<boolean>;
32
+ canLoadNewer: Computed<boolean>;
33
+ }
34
+ export interface FeedActions {
35
+ focusNextArticle(): void;
36
+ focusPrevArticle(): void;
37
+ loadMore(): Promise<void>;
38
+ loadNewer(): Promise<void>;
39
+ setArticles(articles: FeedArticle[]): void;
40
+ appendArticles(articles: FeedArticle[]): void;
41
+ prependArticles(articles: FeedArticle[]): void;
42
+ removeArticle(articleId: string): void;
43
+ setBusy(value: boolean): void;
44
+ setError(message: string): void;
45
+ clearError(): void;
46
+ setTotalCount(count: number): void;
47
+ handleKeyDown(event: FeedKeyboardEventLike): FeedKeyboardResult;
48
+ }
49
+ export interface FeedProps {
50
+ id: string;
51
+ role: 'feed';
52
+ 'aria-label'?: string;
53
+ 'aria-labelledby'?: string;
54
+ 'aria-busy': 'true' | 'false';
55
+ }
56
+ export interface FeedArticleProps {
57
+ id: string;
58
+ role: 'article';
59
+ tabindex: '0' | '-1';
60
+ 'aria-posinset': number;
61
+ 'aria-setsize': number;
62
+ 'aria-labelledby'?: string;
63
+ 'aria-describedby'?: string;
64
+ 'aria-disabled'?: 'true';
65
+ 'data-active': 'true' | 'false';
66
+ onFocus: () => void;
67
+ }
68
+ export interface FeedContracts {
69
+ getFeedProps(): FeedProps;
70
+ getArticleProps(articleId: string): FeedArticleProps;
71
+ }
72
+ export interface FeedModel {
73
+ readonly state: FeedState;
74
+ readonly actions: FeedActions;
75
+ readonly contracts: FeedContracts;
76
+ }
77
+ export declare function createFeed(options: CreateFeedOptions): FeedModel;