@typed/ui 1.0.0-beta.4 → 1.0.0-beta.5

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 (157) hide show
  1. package/README.md +53 -9
  2. package/dist/Alert.d.ts +78 -0
  3. package/dist/Alert.d.ts.map +1 -0
  4. package/dist/Alert.js +37 -0
  5. package/dist/Button.d.ts +144 -0
  6. package/dist/Button.d.ts.map +1 -0
  7. package/dist/Button.js +38 -0
  8. package/dist/Carousel.d.ts +547 -0
  9. package/dist/Carousel.d.ts.map +1 -0
  10. package/dist/Carousel.js +387 -0
  11. package/dist/Checkbox.d.ts +203 -0
  12. package/dist/Checkbox.d.ts.map +1 -0
  13. package/dist/Checkbox.js +140 -0
  14. package/dist/Collection.d.ts +254 -0
  15. package/dist/Collection.d.ts.map +1 -0
  16. package/dist/Collection.js +218 -0
  17. package/dist/Combobox.d.ts +516 -0
  18. package/dist/Combobox.d.ts.map +1 -0
  19. package/dist/Combobox.js +371 -0
  20. package/dist/Component.d.ts +127 -0
  21. package/dist/Component.d.ts.map +1 -0
  22. package/dist/Component.js +60 -0
  23. package/dist/Composite.d.ts +823 -0
  24. package/dist/Composite.d.ts.map +1 -0
  25. package/dist/Composite.js +615 -0
  26. package/dist/Dialog.d.ts +544 -0
  27. package/dist/Dialog.d.ts.map +1 -0
  28. package/dist/Dialog.js +355 -0
  29. package/dist/Disclosure.d.ts +219 -0
  30. package/dist/Disclosure.d.ts.map +1 -0
  31. package/dist/Disclosure.js +128 -0
  32. package/dist/Dom/Events.d.ts +122 -0
  33. package/dist/Dom/Events.d.ts.map +1 -0
  34. package/dist/Dom/Events.js +192 -0
  35. package/dist/Dom/Props.d.ts +161 -0
  36. package/dist/Dom/Props.d.ts.map +1 -0
  37. package/dist/Dom/Props.js +110 -0
  38. package/dist/Dom/Refs.d.ts +58 -0
  39. package/dist/Dom/Refs.d.ts.map +1 -0
  40. package/dist/Dom/Refs.js +61 -0
  41. package/dist/Dom/Render.d.ts +59 -0
  42. package/dist/Dom/Render.d.ts.map +1 -0
  43. package/dist/Dom/Render.js +71 -0
  44. package/dist/Dom/Types.d.ts +570 -0
  45. package/dist/Dom/Types.d.ts.map +1 -0
  46. package/dist/Dom/Types.js +1 -0
  47. package/dist/Dom/index.d.ts +20 -0
  48. package/dist/Dom/index.d.ts.map +1 -0
  49. package/dist/Dom/index.js +8 -0
  50. package/dist/Dom.d.ts +14 -0
  51. package/dist/Dom.d.ts.map +1 -0
  52. package/dist/Dom.js +13 -0
  53. package/dist/Focusable.d.ts +85 -0
  54. package/dist/Focusable.d.ts.map +1 -0
  55. package/dist/Focusable.js +35 -0
  56. package/dist/Form.d.ts +1695 -0
  57. package/dist/Form.d.ts.map +1 -0
  58. package/dist/Form.js +987 -0
  59. package/dist/Grid.d.ts +569 -0
  60. package/dist/Grid.d.ts.map +1 -0
  61. package/dist/Grid.js +379 -0
  62. package/dist/Group.d.ts +147 -0
  63. package/dist/Group.d.ts.map +1 -0
  64. package/dist/Group.js +63 -0
  65. package/dist/Heading.d.ts +86 -0
  66. package/dist/Heading.d.ts.map +1 -0
  67. package/dist/Heading.js +48 -0
  68. package/dist/Hovercard.d.ts +297 -0
  69. package/dist/Hovercard.d.ts.map +1 -0
  70. package/dist/Hovercard.js +188 -0
  71. package/dist/HttpRouter.d.ts +129 -6
  72. package/dist/HttpRouter.d.ts.map +1 -1
  73. package/dist/HttpRouter.js +196 -53
  74. package/dist/Link.d.ts +67 -28
  75. package/dist/Link.d.ts.map +1 -1
  76. package/dist/Link.js +91 -37
  77. package/dist/Listbox.d.ts +437 -0
  78. package/dist/Listbox.d.ts.map +1 -0
  79. package/dist/Listbox.js +316 -0
  80. package/dist/Menu.d.ts +972 -0
  81. package/dist/Menu.d.ts.map +1 -0
  82. package/dist/Menu.js +731 -0
  83. package/dist/Menubar.d.ts +367 -0
  84. package/dist/Menubar.d.ts.map +1 -0
  85. package/dist/Menubar.js +265 -0
  86. package/dist/Meter.d.ts +217 -0
  87. package/dist/Meter.d.ts.map +1 -0
  88. package/dist/Meter.js +94 -0
  89. package/dist/NativeDetails.d.ts +41 -0
  90. package/dist/NativeDetails.d.ts.map +1 -0
  91. package/dist/NativeDetails.js +40 -0
  92. package/dist/NativeDialog.d.ts +64 -0
  93. package/dist/NativeDialog.d.ts.map +1 -0
  94. package/dist/NativeDialog.js +50 -0
  95. package/dist/NativePopover.d.ts +41 -0
  96. package/dist/NativePopover.d.ts.map +1 -0
  97. package/dist/NativePopover.js +46 -0
  98. package/dist/Popover.d.ts +241 -0
  99. package/dist/Popover.d.ts.map +1 -0
  100. package/dist/Popover.js +141 -0
  101. package/dist/RadioGroup.d.ts +432 -0
  102. package/dist/RadioGroup.d.ts.map +1 -0
  103. package/dist/RadioGroup.js +291 -0
  104. package/dist/Role.d.ts +64 -0
  105. package/dist/Role.d.ts.map +1 -0
  106. package/dist/Role.js +27 -0
  107. package/dist/Select.d.ts +529 -0
  108. package/dist/Select.d.ts.map +1 -0
  109. package/dist/Select.js +407 -0
  110. package/dist/Separator.d.ts +54 -0
  111. package/dist/Separator.d.ts.map +1 -0
  112. package/dist/Separator.js +26 -0
  113. package/dist/Slider.d.ts +188 -0
  114. package/dist/Slider.d.ts.map +1 -0
  115. package/dist/Slider.js +96 -0
  116. package/dist/SpinButton.d.ts +188 -0
  117. package/dist/SpinButton.d.ts.map +1 -0
  118. package/dist/SpinButton.js +96 -0
  119. package/dist/Storybook.d.ts +76 -0
  120. package/dist/Storybook.d.ts.map +1 -0
  121. package/dist/Storybook.js +102 -0
  122. package/dist/Switch.d.ts +187 -0
  123. package/dist/Switch.d.ts.map +1 -0
  124. package/dist/Switch.js +116 -0
  125. package/dist/Tab.d.ts +26 -0
  126. package/dist/Tab.d.ts.map +1 -0
  127. package/dist/Tab.js +25 -0
  128. package/dist/Tabs.d.ts +591 -0
  129. package/dist/Tabs.d.ts.map +1 -0
  130. package/dist/Tabs.js +346 -0
  131. package/dist/Toolbar.d.ts +366 -0
  132. package/dist/Toolbar.d.ts.map +1 -0
  133. package/dist/Toolbar.js +245 -0
  134. package/dist/Tooltip.d.ts +296 -0
  135. package/dist/Tooltip.d.ts.map +1 -0
  136. package/dist/Tooltip.js +172 -0
  137. package/dist/Tree.d.ts +591 -0
  138. package/dist/Tree.d.ts.map +1 -0
  139. package/dist/Tree.js +434 -0
  140. package/dist/TreeGrid.d.ts +645 -0
  141. package/dist/TreeGrid.d.ts.map +1 -0
  142. package/dist/TreeGrid.js +420 -0
  143. package/dist/VisuallyHidden.d.ts +54 -0
  144. package/dist/VisuallyHidden.d.ts.map +1 -0
  145. package/dist/VisuallyHidden.js +27 -0
  146. package/dist/WindowSplitter.d.ts +376 -0
  147. package/dist/WindowSplitter.d.ts.map +1 -0
  148. package/dist/WindowSplitter.js +222 -0
  149. package/dist/index.d.ts +49 -0
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +49 -0
  152. package/package.json +45 -18
  153. package/src/HttpRouter.test.ts +0 -256
  154. package/src/HttpRouter.ts +0 -165
  155. package/src/Link.test.ts +0 -84
  156. package/src/Link.ts +0 -107
  157. package/src/index.ts +0 -2
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Listbox separates active and selected values, registers options in a Collection, and combines
3
+ * vertical keyboard movement with buffered typeahead. It supports roving focus by default and uses
4
+ * real focus, keydown, and click events.
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
+ * @since 1.0.0
12
+ * @category modules
13
+ * @packageDocumentation
14
+ */
15
+ import * as Effect from "effect/Effect";
16
+ import * as Schema from "effect/Schema";
17
+ import { RefSubject } from "@typed/fx";
18
+ import { EventHandler, html, } from "@typed/template";
19
+ import * as Collection from "./Collection.js";
20
+ import * as Composite from "./Composite.js";
21
+ import * as Dom from "./Dom.js";
22
+ /**
23
+ * Effect Schema used by makeState to encode, decode, and hydrate Listbox state.
24
+ *
25
+ * @remarks
26
+ * ## Why
27
+ *
28
+ * A public schema makes hydration and serialized state use the same runtime validation as direct
29
+ * construction.
30
+ *
31
+ * ## Ownership and lifetime
32
+ *
33
+ * This declaration is data or schema metadata and acquires no resources.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import * as Schema from "effect/Schema";
38
+ * import * as Listbox from "@typed/ui/Listbox";
39
+ *
40
+ * const decodeState = Schema.decodeUnknownEffect(Listbox.StateSchema);
41
+ * ```
42
+ * @since 1.0.0
43
+ * @category schemas
44
+ */
45
+ export const StateSchema = Schema.Struct({
46
+ value: Schema.NullOr(Schema.String),
47
+ activeId: Schema.NullOr(Schema.String),
48
+ orientation: Schema.Literals(["vertical"]),
49
+ loop: Schema.Boolean,
50
+ rtl: Schema.Boolean,
51
+ virtualFocus: Schema.Boolean,
52
+ });
53
+ /**
54
+ * Creates hydrated Listbox state. value and activeId default to null, loop defaults to true, and
55
+ * roving focus is used.
56
+ *
57
+ * @remarks
58
+ * ## Why
59
+ *
60
+ * State and collection ownership can be composed and tested independently from any renderer.
61
+ *
62
+ * ## Ownership and lifetime
63
+ *
64
+ * The returned Effect creates the RefSubject when run. That state is renderer-independent;
65
+ * collection registrations belong to the separate Scope that runs register or ref, not to state
66
+ * creation.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * import * as Effect from "effect/Effect";
71
+ * import * as Listbox from "@typed/ui/Listbox";
72
+ *
73
+ * const program = Effect.scoped(
74
+ * Effect.gen(function* () {
75
+ * const state = yield* Listbox.makeState({});
76
+ * const collection = yield* Listbox.makeCollection();
77
+ * return { state: yield* state, collection: yield* collection };
78
+ * }),
79
+ * );
80
+ * ```
81
+ * @since 1.0.0
82
+ * @category constructors
83
+ */
84
+ export function makeState(initial = {}) {
85
+ return RefSubject.hydrate(StateSchema, {
86
+ value: initial.value ?? null,
87
+ activeId: initial.activeId ?? null,
88
+ orientation: "vertical",
89
+ loop: initial.loop ?? true,
90
+ rtl: false,
91
+ virtualFocus: false,
92
+ });
93
+ }
94
+ /**
95
+ * Creates a scoped Collection for Listbox items.
96
+ *
97
+ * @remarks
98
+ * ## Why
99
+ *
100
+ * State and collection ownership can be composed and tested independently from any renderer.
101
+ *
102
+ * ## Ownership and lifetime
103
+ *
104
+ * The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
105
+ * owned by the Scope that runs register, independently of this construction Effect.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import * as Effect from "effect/Effect";
110
+ * import * as Listbox from "@typed/ui/Listbox";
111
+ *
112
+ * const program = Effect.scoped(
113
+ * Effect.gen(function* () {
114
+ * const collection = yield* Listbox.makeCollection();
115
+ * return yield* collection;
116
+ * }),
117
+ * );
118
+ * ```
119
+ * @since 1.0.0
120
+ * @category constructors
121
+ */
122
+ export const makeCollection = Collection.makeState;
123
+ /**
124
+ * Sets both activeId and selected value in one update.
125
+ *
126
+ * @remarks
127
+ * ## Why
128
+ *
129
+ * The operation exposes Listbox's transition directly so callers can compose it in Effect programs
130
+ * and native event handlers.
131
+ *
132
+ * ## Ownership and lifetime
133
+ *
134
+ * The returned Effect performs the update or DOM side effect only when run, preserves the declared
135
+ * error and service channels, and retains no resources after completion.
136
+ *
137
+ * ## Example
138
+ *
139
+ * Import with `import { select } from "@typed/ui/Listbox";` Extend the [Listbox.makeState runnable
140
+ * setup](/reference/%40typed%2Fui%2FListbox%23makeState). Inside the linked Effect program invoke
141
+ * `yield* select(state, "nyc", "New York")`, then read state to observe selection and active focus
142
+ * update together.
143
+ * @since 1.0.0
144
+ * @category combinators
145
+ */
146
+ export function select(state, id, value) {
147
+ return RefSubject.update(state, (current) => ({ ...current, activeId: id, value }));
148
+ }
149
+ /**
150
+ * Moves through the registered options, synchronizes value when the target has one, then focuses
151
+ * and scrolls the active option.
152
+ *
153
+ * @remarks
154
+ * ## Why
155
+ *
156
+ * The operation exposes Listbox's transition directly so callers can compose it in Effect programs
157
+ * and native event handlers.
158
+ *
159
+ * ## Ownership and lifetime
160
+ *
161
+ * The returned Effect performs the update or DOM side effect only when run, preserves the declared
162
+ * error and service channels, and retains no resources after completion.
163
+ *
164
+ * ## Example
165
+ *
166
+ * Import with `import { move } from "@typed/ui/Listbox";` Extend the [Listbox.makeState runnable
167
+ * setup](/reference/%40typed%2Fui%2FListbox%23makeState). Inside the linked Effect program invoke
168
+ * `yield* move(state, collection, "next")`, then read the state snapshot to observe the transition
169
+ * described above.
170
+ * @since 1.0.0
171
+ * @category combinators
172
+ */
173
+ export function move(state, collection, direction) {
174
+ return Effect.gen(function* () {
175
+ const current = yield* state;
176
+ const items = yield* collection;
177
+ const activeId = Composite.moveActiveId(items, current, direction);
178
+ const item = activeId === null ? undefined : items.find((item) => item.id === activeId);
179
+ const next = yield* RefSubject.update(state, (value) => ({
180
+ ...value,
181
+ activeId,
182
+ value: item?.value ?? value.value,
183
+ }));
184
+ yield* Composite.focusActive({ state, collection });
185
+ yield* Composite.scrollActive({ state, collection });
186
+ return next;
187
+ });
188
+ }
189
+ function rootProps(options) {
190
+ let typeahead = { value: "", updatedAt: 0 };
191
+ const onfocus = options.collection === undefined
192
+ ? undefined
193
+ : Effect.gen(function* () {
194
+ const current = yield* options.state;
195
+ if (current.activeId !== null)
196
+ return;
197
+ const item = Composite.moveActiveItem(yield* options.collection, current, "first");
198
+ if (item?.value === undefined)
199
+ return;
200
+ yield* select(options.state, item.id, item.value);
201
+ yield* Composite.focusActive({ state: options.state, collection: options.collection });
202
+ yield* Composite.scrollActive({ state: options.state, collection: options.collection });
203
+ });
204
+ const onkeydown = options.collection === undefined
205
+ ? undefined
206
+ : EventHandler.make(Effect.fn(function* (event) {
207
+ const key = Composite.typeaheadKey(event);
208
+ if (key !== null) {
209
+ typeahead = Composite.updateTypeaheadBuffer(typeahead, key, Date.now());
210
+ const id = Composite.typeaheadFrom(yield* options.collection, typeahead.value, (yield* options.state).activeId);
211
+ if (id !== null) {
212
+ event.preventDefault();
213
+ const item = (yield* options.collection).find((item) => item.id === id);
214
+ if (item?.value !== undefined)
215
+ yield* select(options.state, item.id, item.value);
216
+ yield* Composite.focusActive({
217
+ state: options.state,
218
+ collection: options.collection,
219
+ });
220
+ }
221
+ return;
222
+ }
223
+ const direction = Composite.keyMove(event, { orientation: "vertical" });
224
+ if (direction !== undefined) {
225
+ event.preventDefault();
226
+ yield* move(options.state, options.collection, direction);
227
+ }
228
+ }));
229
+ return ({ property }) => ({
230
+ role: "listbox",
231
+ "aria-label": property("label", undefined),
232
+ "aria-activedescendant": Composite.activeDescendant(options.state),
233
+ tabindex: Composite.rootTabIndex(options.state),
234
+ onfocus,
235
+ onkeydown,
236
+ ref: options.state,
237
+ });
238
+ }
239
+ /**
240
+ * Renders the listbox root, initializes selection on focus, and handles vertical movement and
241
+ * typeahead in DOM order.
242
+ *
243
+ * @remarks
244
+ * ## Why
245
+ *
246
+ * The component applies the family behavior while leaving callers free to supply a custom host
247
+ * through the shared DOM boundary.
248
+ *
249
+ * ## Ownership and lifetime
250
+ *
251
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
252
+ * collection registrations only when rendered. The rendering Scope removes those resources;
253
+ * unrelated nodes and attributes remain caller-owned.
254
+ *
255
+ * ## Example
256
+ *
257
+ * Import with `import { Root } from "@typed/ui/Listbox";` Extend the [Listbox.makeState runnable
258
+ * setup](/reference/%40typed%2Fui%2FListbox%23makeState). Replace the linked program's final
259
+ * snapshot read with `Root({ state, label: "City", content: "Options" })`; render that Fx before
260
+ * the same Scope closes.
261
+ * @since 1.0.0
262
+ * @category components
263
+ */
264
+ export function Root(options, host) {
265
+ return Dom.renderHost()(options, host, rootProps(options), options.content, (props, content) => {
266
+ return html `<div ...${props}>${content}</div>`;
267
+ });
268
+ }
269
+ function optionProps(options) {
270
+ const selected = RefSubject.map(options.state, (state) => state.value === options.value);
271
+ const register = options.collection === undefined
272
+ ? undefined
273
+ : Collection.ref(options.collection, {
274
+ id: options.id,
275
+ value: options.value,
276
+ textValue: options.textValue ?? options.value,
277
+ disabled: options.disabled,
278
+ });
279
+ return () => ({
280
+ id: options.id,
281
+ role: "option",
282
+ "aria-selected": selected,
283
+ "aria-disabled": options.disabled ?? false,
284
+ tabindex: Composite.tabIndex(options.state, options.id),
285
+ onclick: options.disabled === true ? Effect.void : select(options.state, options.id, options.value),
286
+ onfocus: options.disabled === true ? Effect.void : select(options.state, options.id, options.value),
287
+ ref: Dom.composeRefs(register, options.ref),
288
+ });
289
+ }
290
+ /**
291
+ * Renders and optionally registers an option; focus or click selects it unless disabled.
292
+ *
293
+ * @remarks
294
+ * ## Why
295
+ *
296
+ * The component applies the family behavior while leaving callers free to supply a custom host
297
+ * through the shared DOM boundary.
298
+ *
299
+ * ## Ownership and lifetime
300
+ *
301
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
302
+ * collection registrations only when rendered. The rendering Scope removes those resources;
303
+ * unrelated nodes and attributes remain caller-owned.
304
+ *
305
+ * ## Example
306
+ *
307
+ * Import with `import { Option } from "@typed/ui/Listbox";` Extend the [Listbox.makeState runnable
308
+ * setup](/reference/%40typed%2Fui%2FListbox%23makeState). Replace the linked program's final
309
+ * snapshot read with `Option({ state, id: "nyc", value: "New York", content: "New York" })`; render
310
+ * that Fx before the same Scope closes.
311
+ * @since 1.0.0
312
+ * @category components
313
+ */
314
+ export function Option(options, host) {
315
+ return Dom.renderHost()(options, host, optionProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
316
+ }