@typed/ui 1.0.0-beta.0 → 1.0.0-beta.10

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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -26
  3. package/dist/Alert.d.ts +72 -0
  4. package/dist/Alert.d.ts.map +1 -0
  5. package/dist/Alert.js +40 -0
  6. package/dist/Button.d.ts +100 -0
  7. package/dist/Button.d.ts.map +1 -0
  8. package/dist/Button.js +42 -0
  9. package/dist/Carousel.d.ts +346 -0
  10. package/dist/Carousel.d.ts.map +1 -0
  11. package/dist/Carousel.js +264 -0
  12. package/dist/Checkbox.d.ts +168 -0
  13. package/dist/Checkbox.d.ts.map +1 -0
  14. package/dist/Checkbox.js +146 -0
  15. package/dist/Collection.d.ts +254 -0
  16. package/dist/Collection.d.ts.map +1 -0
  17. package/dist/Collection.js +218 -0
  18. package/dist/Combobox.d.ts +367 -0
  19. package/dist/Combobox.d.ts.map +1 -0
  20. package/dist/Combobox.js +300 -0
  21. package/dist/Composite.d.ts +823 -0
  22. package/dist/Composite.d.ts.map +1 -0
  23. package/dist/Composite.js +615 -0
  24. package/dist/Dialog.d.ts +544 -0
  25. package/dist/Dialog.d.ts.map +1 -0
  26. package/dist/Dialog.js +357 -0
  27. package/dist/Disclosure.d.ts +219 -0
  28. package/dist/Disclosure.d.ts.map +1 -0
  29. package/dist/Disclosure.js +128 -0
  30. package/dist/Dom/Events.d.ts +122 -0
  31. package/dist/Dom/Events.d.ts.map +1 -0
  32. package/dist/Dom/Events.js +192 -0
  33. package/dist/Dom/Props.d.ts +161 -0
  34. package/dist/Dom/Props.d.ts.map +1 -0
  35. package/dist/Dom/Props.js +110 -0
  36. package/dist/Dom/Refs.d.ts +58 -0
  37. package/dist/Dom/Refs.d.ts.map +1 -0
  38. package/dist/Dom/Refs.js +61 -0
  39. package/dist/Dom/Render.d.ts +59 -0
  40. package/dist/Dom/Render.d.ts.map +1 -0
  41. package/dist/Dom/Render.js +71 -0
  42. package/dist/Dom/Types.d.ts +570 -0
  43. package/dist/Dom/Types.d.ts.map +1 -0
  44. package/dist/Dom/Types.js +1 -0
  45. package/dist/Dom/index.d.ts +20 -0
  46. package/dist/Dom/index.d.ts.map +1 -0
  47. package/dist/Dom/index.js +8 -0
  48. package/dist/Dom.d.ts +14 -0
  49. package/dist/Dom.d.ts.map +1 -0
  50. package/dist/Dom.js +13 -0
  51. package/dist/Focusable.d.ts +85 -0
  52. package/dist/Focusable.d.ts.map +1 -0
  53. package/dist/Focusable.js +35 -0
  54. package/dist/Form.d.ts +1727 -0
  55. package/dist/Form.d.ts.map +1 -0
  56. package/dist/Form.js +1142 -0
  57. package/dist/Grid.d.ts +388 -0
  58. package/dist/Grid.d.ts.map +1 -0
  59. package/dist/Grid.js +284 -0
  60. package/dist/Group.d.ts +128 -0
  61. package/dist/Group.d.ts.map +1 -0
  62. package/dist/Group.js +71 -0
  63. package/dist/Heading.d.ts +87 -0
  64. package/dist/Heading.d.ts.map +1 -0
  65. package/dist/Heading.js +58 -0
  66. package/dist/Hovercard.d.ts +297 -0
  67. package/dist/Hovercard.d.ts.map +1 -0
  68. package/dist/Hovercard.js +188 -0
  69. package/dist/HttpRouter.d.ts +129 -6
  70. package/dist/HttpRouter.d.ts.map +1 -1
  71. package/dist/HttpRouter.js +199 -56
  72. package/dist/Link.d.ts +63 -28
  73. package/dist/Link.d.ts.map +1 -1
  74. package/dist/Link.js +84 -37
  75. package/dist/Listbox.d.ts +305 -0
  76. package/dist/Listbox.d.ts.map +1 -0
  77. package/dist/Listbox.js +245 -0
  78. package/dist/Menu.d.ts +663 -0
  79. package/dist/Menu.d.ts.map +1 -0
  80. package/dist/Menu.js +569 -0
  81. package/dist/Menubar.d.ts +249 -0
  82. package/dist/Menubar.d.ts.map +1 -0
  83. package/dist/Menubar.js +207 -0
  84. package/dist/Meter.d.ts +157 -0
  85. package/dist/Meter.d.ts.map +1 -0
  86. package/dist/Meter.js +87 -0
  87. package/dist/NativeDetails.d.ts +41 -0
  88. package/dist/NativeDetails.d.ts.map +1 -0
  89. package/dist/NativeDetails.js +40 -0
  90. package/dist/NativeDialog.d.ts +66 -0
  91. package/dist/NativeDialog.d.ts.map +1 -0
  92. package/dist/NativeDialog.js +88 -0
  93. package/dist/NativePopover.d.ts +43 -0
  94. package/dist/NativePopover.d.ts.map +1 -0
  95. package/dist/NativePopover.js +84 -0
  96. package/dist/Popover.d.ts +240 -0
  97. package/dist/Popover.d.ts.map +1 -0
  98. package/dist/Popover.js +140 -0
  99. package/dist/RadioGroup.d.ts +330 -0
  100. package/dist/RadioGroup.d.ts.map +1 -0
  101. package/dist/RadioGroup.js +241 -0
  102. package/dist/Role.d.ts +64 -0
  103. package/dist/Role.d.ts.map +1 -0
  104. package/dist/Role.js +27 -0
  105. package/dist/Select.d.ts +418 -0
  106. package/dist/Select.d.ts.map +1 -0
  107. package/dist/Select.js +357 -0
  108. package/dist/Separator.d.ts +58 -0
  109. package/dist/Separator.d.ts.map +1 -0
  110. package/dist/Separator.js +32 -0
  111. package/dist/Slider.d.ts +141 -0
  112. package/dist/Slider.d.ts.map +1 -0
  113. package/dist/Slider.js +101 -0
  114. package/dist/SpinButton.d.ts +141 -0
  115. package/dist/SpinButton.d.ts.map +1 -0
  116. package/dist/SpinButton.js +101 -0
  117. package/dist/Storybook.d.ts +76 -0
  118. package/dist/Storybook.d.ts.map +1 -0
  119. package/dist/Storybook.js +102 -0
  120. package/dist/Switch.d.ts +148 -0
  121. package/dist/Switch.d.ts.map +1 -0
  122. package/dist/Switch.js +110 -0
  123. package/dist/Tab.d.ts +26 -0
  124. package/dist/Tab.d.ts.map +1 -0
  125. package/dist/Tab.js +25 -0
  126. package/dist/Tabs.d.ts +411 -0
  127. package/dist/Tabs.d.ts.map +1 -0
  128. package/dist/Tabs.js +262 -0
  129. package/dist/Toolbar.d.ts +248 -0
  130. package/dist/Toolbar.d.ts.map +1 -0
  131. package/dist/Toolbar.js +187 -0
  132. package/dist/Tooltip.d.ts +296 -0
  133. package/dist/Tooltip.d.ts.map +1 -0
  134. package/dist/Tooltip.js +172 -0
  135. package/dist/Tree.d.ts +405 -0
  136. package/dist/Tree.d.ts.map +1 -0
  137. package/dist/Tree.js +333 -0
  138. package/dist/TreeGrid.d.ts +426 -0
  139. package/dist/TreeGrid.d.ts.map +1 -0
  140. package/dist/TreeGrid.js +308 -0
  141. package/dist/VisuallyHidden.d.ts +68 -0
  142. package/dist/VisuallyHidden.d.ts.map +1 -0
  143. package/dist/VisuallyHidden.js +44 -0
  144. package/dist/WindowSplitter.d.ts +336 -0
  145. package/dist/WindowSplitter.d.ts.map +1 -0
  146. package/dist/WindowSplitter.js +305 -0
  147. package/dist/index.d.ts +48 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +48 -0
  150. package/package.json +47 -14
  151. package/src/HttpRouter.test.ts +0 -294
  152. package/src/HttpRouter.ts +0 -168
  153. package/src/Link.test.ts +0 -85
  154. package/src/Link.ts +0 -107
  155. package/src/index.ts +0 -2
  156. package/tsconfig.json +0 -6
package/dist/Tree.js ADDED
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Tree stores active item and expanded ids independently. Its key policy follows hierarchical tree
3
+ * navigation, while Item and Group expose explicit levels, expansion, and visibility without
4
+ * reconstructing nodes.
5
+ *
6
+ * @remarks
7
+ * The module keeps policy, state transitions, and DOM rendering separable so applications can use
8
+ * the state and pure operations without mounting UI, or supply custom hosts without replacing native
9
+ * events and browser-owned focus.
10
+ *
11
+ * Learn the interaction in the [Tree guide](/explore/ui-tree).
12
+ *
13
+ * @since 1.0.0
14
+ * @category modules
15
+ * @packageDocumentation
16
+ */
17
+ import * as Effect from "effect/Effect";
18
+ import * as Schema from "effect/Schema";
19
+ import { RefSubject } from "@typed/fx";
20
+ import { EventHandler, html, } from "@typed/template";
21
+ import * as Collection from "./Collection.js";
22
+ import * as Composite from "./Composite.js";
23
+ import * as Dom from "./Dom.js";
24
+ /**
25
+ * Effect Schema used by makeState to encode, decode, and hydrate Tree state.
26
+ *
27
+ * @remarks
28
+ * @example
29
+ * ```ts
30
+ * import * as Schema from "effect/Schema";
31
+ * import * as Tree from "@typed/ui/Tree";
32
+ *
33
+ * const decodeState = Schema.decodeUnknownEffect(Tree.StateSchema);
34
+ * ```
35
+ * @since 1.0.0
36
+ * @category Hierarchy state
37
+ */
38
+ export const StateSchema = Schema.Struct({
39
+ activeId: Schema.NullOr(Schema.String),
40
+ expandedIds: Schema.Array(Schema.String),
41
+ loop: Schema.Boolean,
42
+ });
43
+ /**
44
+ * Creates hydrated Tree state. activeId defaults null and expandedIds defaults empty.
45
+ *
46
+ * @remarks
47
+ * The returned Effect creates the RefSubject when run. That state is renderer-independent;
48
+ * collection registrations belong to the separate Scope that runs register or ref, not to state
49
+ * creation.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import * as Effect from "effect/Effect";
54
+ * import * as Tree from "@typed/ui/Tree";
55
+ *
56
+ * const program = Effect.scoped(
57
+ * Effect.gen(function* () {
58
+ * const state = yield* Tree.makeState({});
59
+ * const collection = yield* Tree.makeCollection();
60
+ * return { state: yield* state, collection: yield* collection };
61
+ * }),
62
+ * );
63
+ * ```
64
+ * @since 1.0.0
65
+ * @category Hierarchy state
66
+ */
67
+ export function makeState(initial = {}) {
68
+ return RefSubject.hydrate(StateSchema, {
69
+ activeId: initial.activeId ?? null,
70
+ expandedIds: [...(initial.expandedIds ?? [])],
71
+ loop: initial.loop ?? true,
72
+ });
73
+ }
74
+ /**
75
+ * Creates a scoped Collection for Tree items.
76
+ *
77
+ * @remarks
78
+ * The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
79
+ * owned by the Scope that runs register, independently of this construction Effect.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * import * as Effect from "effect/Effect";
84
+ * import * as Tree from "@typed/ui/Tree";
85
+ *
86
+ * const program = Effect.scoped(
87
+ * Effect.gen(function* () {
88
+ * const collection = yield* Tree.makeCollection();
89
+ * return yield* collection;
90
+ * }),
91
+ * );
92
+ * ```
93
+ * @since 1.0.0
94
+ * @category Node registration
95
+ */
96
+ export const makeCollection = Collection.makeState;
97
+ /**
98
+ * Tests membership in expandedIds without touching the DOM.
99
+ *
100
+ * @remarks
101
+ * Separating this deterministic policy from event wiring lets applications test it directly and
102
+ * reuse it in custom composites.
103
+ *
104
+ * This is a synchronous calculation. It acquires no resources and does not mutate the input array,
105
+ * state, event, or DOM.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import * as Tree from "@typed/ui/Tree";
110
+ *
111
+ * const expanded = Tree.isExpanded({ activeId: null, expandedIds: ["parent"], loop: true }, "parent");
112
+ * ```
113
+ * @since 1.0.0
114
+ * @category Expansion
115
+ */
116
+ export function isExpanded(state, id) {
117
+ return state.expandedIds.includes(id);
118
+ }
119
+ /**
120
+ * Adds an id once to expandedIds.
121
+ *
122
+ * @remarks
123
+ * The operation exposes Tree's transition directly so callers can compose it in Effect programs
124
+ * and native event handlers.
125
+ *
126
+ * @since 1.0.0
127
+ * @category Expansion
128
+ */
129
+ export function expand(state, id) {
130
+ return RefSubject.update(state, (current) => isExpanded(current, id) ? current : { ...current, expandedIds: [...current.expandedIds, id] });
131
+ }
132
+ /**
133
+ * Removes an id from expandedIds.
134
+ *
135
+ * @remarks
136
+ * The operation exposes Tree's transition directly so callers can compose it in Effect programs
137
+ * and native event handlers.
138
+ *
139
+ * @since 1.0.0
140
+ * @category Expansion
141
+ */
142
+ export function collapse(state, id) {
143
+ return RefSubject.update(state, (current) => ({
144
+ ...current,
145
+ expandedIds: current.expandedIds.filter((expanded) => expanded !== id),
146
+ }));
147
+ }
148
+ /**
149
+ * Sets the active tree item id without changing expansion.
150
+ *
151
+ * @remarks
152
+ * The operation exposes Tree's transition directly so callers can compose it in Effect programs
153
+ * and native event handlers.
154
+ *
155
+ * @since 1.0.0
156
+ * @category Node focus
157
+ */
158
+ export function activate(state, activeId) {
159
+ return RefSubject.update(state, (current) => ({ ...current, activeId }));
160
+ }
161
+ function rootInternalProps(options) {
162
+ const onfocus = options.collection === undefined
163
+ ? undefined
164
+ : Effect.gen(function* () {
165
+ const current = yield* options.state;
166
+ if (current.activeId !== null)
167
+ return;
168
+ const first = Composite.moveActiveId(visibleItems(yield* options.collection, current), current, "first");
169
+ if (first === null)
170
+ return;
171
+ yield* activate(options.state, first);
172
+ yield* focusItem(options.collection, first);
173
+ });
174
+ const onkeydown = options.collection === undefined
175
+ ? undefined
176
+ : EventHandler.make(Effect.fn((event) => onKeyDown(options.state, options.collection, event)));
177
+ return ({ property }) => ({
178
+ role: "tree",
179
+ "aria-label": property("label", undefined),
180
+ tabindex: RefSubject.map(options.state, (state) => (state.activeId === null ? 0 : -1)),
181
+ onfocus,
182
+ onkeydown,
183
+ ref: options.state,
184
+ });
185
+ }
186
+ /**
187
+ * Renders the tree root and applies hierarchy-aware keyboard navigation to registered items.
188
+ *
189
+ * @remarks
190
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
191
+ * collection registrations only when rendered. The rendering Scope removes those resources;
192
+ * unrelated nodes and attributes remain caller-owned.
193
+ *
194
+ * @since 1.0.0
195
+ * @category Tree surface
196
+ */
197
+ export function Root(options, host) {
198
+ return Dom.renderHost()(options, host, rootInternalProps(options), options.content, (props, content) => {
199
+ return html `<div ...${props}>${content}</div>`;
200
+ });
201
+ }
202
+ function itemInternalProps(options) {
203
+ const hasChildren = options.hasChildren ?? false;
204
+ const register = options.collection === undefined
205
+ ? undefined
206
+ : Collection.ref(options.collection, {
207
+ id: options.id,
208
+ value: { parentId: options.parentId, hasChildren },
209
+ textValue: options.id,
210
+ disabled: options.disabled,
211
+ });
212
+ return ({ property }) => ({
213
+ id: options.id,
214
+ role: "treeitem",
215
+ "aria-level": property("level", 1),
216
+ "aria-expanded": hasChildren
217
+ ? RefSubject.map(options.state, (state) => isExpanded(state, options.id))
218
+ : undefined,
219
+ "aria-disabled": options.disabled ?? false,
220
+ tabindex: RefSubject.map(options.state, (state) => (state.activeId === options.id ? 0 : -1)),
221
+ onfocus: activate(options.state, options.id),
222
+ onclick: options.disabled === true ? Effect.void : activate(options.state, options.id),
223
+ ref: Dom.composeRefs(register, options.ref),
224
+ });
225
+ }
226
+ /**
227
+ * Renders and optionally registers a treeitem with level, expanded, disabled, and roving-focus
228
+ * state.
229
+ *
230
+ * @remarks
231
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
232
+ * collection registrations only when rendered. The rendering Scope removes those resources;
233
+ * unrelated nodes and attributes remain caller-owned.
234
+ *
235
+ * @since 1.0.0
236
+ * @category Tree nodes
237
+ */
238
+ export function Item(options, host) {
239
+ return Dom.renderHost()(options, host, itemInternalProps(options), options.content, (props, content) => {
240
+ return html `<div ...${props}>${content}</div>`;
241
+ });
242
+ }
243
+ function groupInternalProps(options) {
244
+ return () => ({
245
+ role: "group",
246
+ "?hidden": RefSubject.map(options.state, (state) => !isExpanded(state, options.parentId)),
247
+ });
248
+ }
249
+ /**
250
+ * Renders a child group and hides it while its parent id is not expanded.
251
+ *
252
+ * @remarks
253
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
254
+ * collection registrations only when rendered. The rendering Scope removes those resources;
255
+ * unrelated nodes and attributes remain caller-owned.
256
+ *
257
+ * @since 1.0.0
258
+ * @category Child visibility
259
+ */
260
+ export function Group(options, host) {
261
+ return Dom.renderHost()(options, host, groupInternalProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
262
+ }
263
+ function onKeyDown(state, collection, event) {
264
+ return Effect.gen(function* () {
265
+ const current = yield* state;
266
+ const items = visibleItems(yield* collection, current);
267
+ if (event.key === "Enter" || event.key === " ") {
268
+ const active = current.activeId === null
269
+ ? undefined
270
+ : items.find((item) => item.id === current.activeId && item.disabled !== true);
271
+ const element = active?.element;
272
+ const click = element === undefined ? undefined : Reflect.get(element, "click");
273
+ if (typeof click === "function") {
274
+ event.preventDefault();
275
+ yield* Effect.sync(() => click.call(element));
276
+ }
277
+ return yield* state;
278
+ }
279
+ const direction = Composite.keyMove(event, { orientation: "vertical" });
280
+ if (direction !== undefined) {
281
+ event.preventDefault();
282
+ const next = Composite.moveActiveId(items, current, direction);
283
+ if (next === null)
284
+ return current;
285
+ yield* activate(state, next);
286
+ yield* focusItem(collection, next);
287
+ return yield* state;
288
+ }
289
+ const active = current.activeId === null
290
+ ? undefined
291
+ : (yield* collection).find((item) => item.id === current.activeId);
292
+ if (active === undefined)
293
+ return current;
294
+ const value = active.value;
295
+ if (event.key === "ArrowRight" && value?.hasChildren) {
296
+ event.preventDefault();
297
+ if (!isExpanded(current, active.id))
298
+ return yield* expand(state, active.id);
299
+ const child = items.find((item) => item.value?.parentId === active.id);
300
+ if (child === undefined)
301
+ return current;
302
+ yield* activate(state, child.id);
303
+ yield* focusItem(collection, child.id);
304
+ return yield* state;
305
+ }
306
+ if (event.key === "ArrowLeft") {
307
+ event.preventDefault();
308
+ if (value?.hasChildren && isExpanded(current, active.id))
309
+ return yield* collapse(state, active.id);
310
+ if (value?.parentId === undefined)
311
+ return current;
312
+ yield* activate(state, value.parentId);
313
+ yield* focusItem(collection, value.parentId);
314
+ return yield* state;
315
+ }
316
+ return current;
317
+ });
318
+ }
319
+ function visibleItems(items, state) {
320
+ const ordered = Collection.byDomOrder(items);
321
+ return ordered.filter((item) => {
322
+ let parentId = item.value?.parentId;
323
+ while (parentId !== undefined) {
324
+ if (!isExpanded(state, parentId))
325
+ return false;
326
+ parentId = ordered.find((candidate) => candidate.id === parentId)?.value?.parentId;
327
+ }
328
+ return true;
329
+ });
330
+ }
331
+ function focusItem(collection, id) {
332
+ return Effect.flatMap(collection, (items) => Composite.focusElement(items.find((item) => item.id === id)?.element));
333
+ }