@typed/ui 1.0.0-beta.3 → 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 +197 -54
  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 +42 -15
  153. package/src/HttpRouter.test.ts +0 -294
  154. package/src/HttpRouter.ts +0 -168
  155. package/src/Link.test.ts +0 -84
  156. package/src/Link.ts +0 -107
  157. package/src/index.ts +0 -2
package/dist/Menu.js ADDED
@@ -0,0 +1,731 @@
1
+ /**
2
+ * Menu coordinates trigger, manual native popover content, registered items, and nested submenu
3
+ * state. Keyboard handling covers opening, Escape, directional parent/child traversal, roving
4
+ * focus, and typeahead without synthetic 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 Scope from "effect/Scope";
17
+ import * as Schema from "effect/Schema";
18
+ import { RefSubject } from "@typed/fx";
19
+ import { EventHandler, html, } from "@typed/template";
20
+ import * as Collection from "./Collection.js";
21
+ import * as Composite from "./Composite.js";
22
+ import * as Dom from "./Dom.js";
23
+ import * as NativePopover from "./NativePopover.js";
24
+ const invokers = new WeakMap();
25
+ const submenuOwners = new WeakMap();
26
+ /**
27
+ * Effect Schema used by makeState to encode, decode, and hydrate Menu state.
28
+ *
29
+ * @remarks
30
+ * ## Why
31
+ *
32
+ * A public schema makes hydration and serialized state use the same runtime validation as direct
33
+ * construction.
34
+ *
35
+ * ## Ownership and lifetime
36
+ *
37
+ * This declaration is data or schema metadata and acquires no resources.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import * as Schema from "effect/Schema";
42
+ * import * as Menu from "@typed/ui/Menu";
43
+ *
44
+ * const decodeState = Schema.decodeUnknownEffect(Menu.StateSchema);
45
+ * ```
46
+ * @since 1.0.0
47
+ * @category schemas
48
+ */
49
+ export const StateSchema = Schema.Struct({
50
+ id: Schema.String,
51
+ open: Schema.Boolean,
52
+ activeId: Schema.NullOr(Schema.String),
53
+ orientation: Schema.Literals(["vertical"]),
54
+ loop: Schema.Boolean,
55
+ rtl: Schema.Boolean,
56
+ virtualFocus: Schema.Boolean,
57
+ });
58
+ /**
59
+ * Creates hydrated Menu state. The caller supplies id; open defaults false, activeId null, loop
60
+ * true, and orientation vertical.
61
+ *
62
+ * @remarks
63
+ * ## Why
64
+ *
65
+ * State and collection ownership can be composed and tested independently from any renderer.
66
+ *
67
+ * ## Ownership and lifetime
68
+ *
69
+ * The returned Effect creates the RefSubject when run. That state is renderer-independent;
70
+ * collection registrations belong to the separate Scope that runs register or ref, not to state
71
+ * creation.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import * as Effect from "effect/Effect";
76
+ * import * as Menu from "@typed/ui/Menu";
77
+ *
78
+ * const program = Effect.scoped(
79
+ * Effect.gen(function* () {
80
+ * const state = yield* Menu.makeState({ id: "file-menu" });
81
+ * const collection = yield* Menu.makeCollection();
82
+ * return { state: yield* state, collection: yield* collection };
83
+ * }),
84
+ * );
85
+ * ```
86
+ * @since 1.0.0
87
+ * @category constructors
88
+ */
89
+ export function makeState(initial) {
90
+ return RefSubject.hydrate(StateSchema, {
91
+ id: initial.id,
92
+ open: initial.open ?? false,
93
+ activeId: initial.activeId ?? null,
94
+ orientation: "vertical",
95
+ loop: initial.loop ?? true,
96
+ rtl: false,
97
+ virtualFocus: false,
98
+ });
99
+ }
100
+ /**
101
+ * Creates a scoped Collection for Menu items.
102
+ *
103
+ * @remarks
104
+ * ## Why
105
+ *
106
+ * State and collection ownership can be composed and tested independently from any renderer.
107
+ *
108
+ * ## Ownership and lifetime
109
+ *
110
+ * The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
111
+ * owned by the Scope that runs register, independently of this construction Effect.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * import * as Effect from "effect/Effect";
116
+ * import * as Menu from "@typed/ui/Menu";
117
+ *
118
+ * const program = Effect.scoped(
119
+ * Effect.gen(function* () {
120
+ * const collection = yield* Menu.makeCollection();
121
+ * return yield* collection;
122
+ * }),
123
+ * );
124
+ * ```
125
+ * @since 1.0.0
126
+ * @category constructors
127
+ */
128
+ export const makeCollection = Collection.makeState;
129
+ /**
130
+ * Updates only the menu's explicit open state.
131
+ *
132
+ * @remarks
133
+ * ## Why
134
+ *
135
+ * The operation exposes Menu's transition directly so callers can compose it in Effect programs
136
+ * and native event handlers.
137
+ *
138
+ * ## Ownership and lifetime
139
+ *
140
+ * The returned Effect performs the update or DOM side effect only when run, preserves the declared
141
+ * error and service channels, and retains no resources after completion.
142
+ *
143
+ * ## Example
144
+ *
145
+ * Import with `import { setOpen } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
146
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Inside the linked Effect program invoke
147
+ * `yield* setOpen(state, true)`, then read state to observe `open: true` without changing focus or
148
+ * menu identity.
149
+ * @since 1.0.0
150
+ * @category combinators
151
+ */
152
+ export function setOpen(state, open) {
153
+ return RefSubject.update(state, (value) => ({ ...value, open }));
154
+ }
155
+ function triggerProps(options) {
156
+ const id = RefSubject.map(options.state, (state) => state.id);
157
+ const open = RefSubject.map(options.state, (state) => state.open);
158
+ return () => ({
159
+ type: "button",
160
+ popovertarget: id,
161
+ popovertargetaction: "toggle",
162
+ "aria-haspopup": "menu",
163
+ "aria-expanded": open,
164
+ onkeydown: EventHandler.make((event) => {
165
+ if (event.key !== "ArrowDown")
166
+ return;
167
+ event.preventDefault();
168
+ Dom.currentTarget(event).click();
169
+ }),
170
+ ref: Dom.composeRefs(options.state, invokerRef(options.state)),
171
+ });
172
+ }
173
+ function invokerRef(state) {
174
+ return Effect.fn(function* (element) {
175
+ invokers.set(state, element);
176
+ const scope = yield* Effect.scope;
177
+ yield* Scope.addFinalizer(scope, Effect.sync(() => {
178
+ if (invokers.get(state) === element)
179
+ invokers.delete(state);
180
+ }));
181
+ });
182
+ }
183
+ /**
184
+ * Renders a native button targeting menu popover content and opens the menu on ArrowDown.
185
+ *
186
+ * @remarks
187
+ * ## Why
188
+ *
189
+ * The component applies the family behavior while leaving callers free to supply a custom host
190
+ * through the shared DOM boundary.
191
+ *
192
+ * ## Ownership and lifetime
193
+ *
194
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
195
+ * collection registrations only when rendered. The rendering Scope removes those resources;
196
+ * unrelated nodes and attributes remain caller-owned.
197
+ *
198
+ * ## Example
199
+ *
200
+ * Import with `import { Trigger } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
201
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final snapshot
202
+ * read with `Trigger({ state, content: "File" })`; render that Fx before the same Scope closes.
203
+ * @since 1.0.0
204
+ * @category components
205
+ */
206
+ export function Trigger(options, host) {
207
+ return Dom.renderHost()(options, host, triggerProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
208
+ }
209
+ /**
210
+ * Consumer-facing alias of the canonical Menu component with identical behavior and lifetime.
211
+ *
212
+ * @remarks
213
+ * ## Why
214
+ *
215
+ * The component applies the family behavior while leaving callers free to supply a custom host
216
+ * through the shared DOM boundary.
217
+ *
218
+ * ## Ownership and lifetime
219
+ *
220
+ * The alias acquires nothing. Rendering it has exactly the canonical component's Scope and DOM
221
+ * ownership contract.
222
+ *
223
+ * ## Example
224
+ *
225
+ * Import with `import { Button } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
226
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final snapshot
227
+ * read with `Button({ state, content: "File" })`; render that Fx before the same Scope closes.
228
+ * @since 1.0.0
229
+ * @category components
230
+ */
231
+ export const Button = Trigger;
232
+ function contentProps(options) {
233
+ const id = RefSubject.map(options.state, (state) => state.id);
234
+ let typeahead = { value: "", updatedAt: 0 };
235
+ let restoreParentFocus = false;
236
+ let restoreOwnerFocus = false;
237
+ let restoreInvokerFocus = false;
238
+ let invoker;
239
+ const onkeydown = options.collection === undefined
240
+ ? undefined
241
+ : EventHandler.make(Effect.fn(function* (event) {
242
+ const direction = Composite.keyMove(event, { orientation: "vertical" });
243
+ if (direction !== undefined) {
244
+ event.preventDefault();
245
+ yield* Composite.moveAndFocus({ state: options.state, collection: options.collection, includeDisabled: true }, direction);
246
+ return;
247
+ }
248
+ if (event.key === "ArrowRight") {
249
+ const activeId = (yield* options.state).activeId;
250
+ const item = activeId === null
251
+ ? undefined
252
+ : (yield* options.collection).find((item) => item.id === activeId);
253
+ const element = item?.submenu === true ? item.element : undefined;
254
+ const click = element === undefined ? undefined : Reflect.get(element, "click");
255
+ if (typeof click === "function") {
256
+ event.preventDefault();
257
+ yield* Effect.sync(() => click.call(element));
258
+ }
259
+ return;
260
+ }
261
+ if (event.key === "ArrowLeft") {
262
+ event.preventDefault();
263
+ const element = Dom.currentTarget(event);
264
+ const hidePopover = Reflect.get(element, "hidePopover");
265
+ if (typeof hidePopover === "function") {
266
+ if (options.parent === undefined && submenuOwners.has(options.state))
267
+ restoreOwnerFocus = true;
268
+ else if (options.parent === undefined)
269
+ restoreInvokerFocus = true;
270
+ else
271
+ restoreParentFocus = true;
272
+ yield* Effect.sync(() => hidePopover.call(element));
273
+ }
274
+ else {
275
+ yield* setOpen(options.state, false);
276
+ if (options.parent === undefined && submenuOwners.has(options.state)) {
277
+ yield* submenuOwners.get(options.state)?.onArrowLeft ?? Effect.void;
278
+ }
279
+ else if (options.parent === undefined)
280
+ yield* Composite.focusElement(invoker ?? invokers.get(options.state));
281
+ else {
282
+ yield* RefSubject.update(options.parent.state, (state) => ({
283
+ ...state,
284
+ activeId: options.parent.triggerId,
285
+ }));
286
+ yield* Composite.focusActive({
287
+ state: options.parent.state,
288
+ collection: options.parent.collection,
289
+ });
290
+ }
291
+ }
292
+ return;
293
+ }
294
+ if (event.key === "Escape") {
295
+ restoreInvokerFocus = true;
296
+ yield* setOpen(options.state, false);
297
+ return;
298
+ }
299
+ if (event.key === "Tab") {
300
+ yield* setOpen(options.state, false);
301
+ return;
302
+ }
303
+ if (event.key === "Enter" || event.key === " ") {
304
+ const activeId = (yield* options.state).activeId;
305
+ const element = activeId === null
306
+ ? undefined
307
+ : (yield* options.collection).find((item) => item.id === activeId)?.element;
308
+ const click = element === undefined ? undefined : Reflect.get(element, "click");
309
+ if (typeof click === "function") {
310
+ event.preventDefault();
311
+ yield* Effect.sync(() => click.call(element));
312
+ }
313
+ return;
314
+ }
315
+ const key = Composite.typeaheadKey(event);
316
+ if (key === null)
317
+ return;
318
+ typeahead = Composite.updateTypeaheadBuffer(typeahead, key, Date.now());
319
+ const activeId = Composite.typeaheadFrom(yield* options.collection, typeahead.value, (yield* options.state).activeId, undefined, true);
320
+ if (activeId === null)
321
+ return;
322
+ event.preventDefault();
323
+ yield* RefSubject.update(options.state, (state) => ({ ...state, activeId }));
324
+ yield* Composite.focusActive({ state: options.state, collection: options.collection });
325
+ }));
326
+ const restoreFocus = Effect.gen(function* () {
327
+ if (restoreParentFocus && options.parent !== undefined) {
328
+ restoreParentFocus = false;
329
+ yield* RefSubject.update(options.parent.state, (state) => ({
330
+ ...state,
331
+ activeId: options.parent.triggerId,
332
+ }));
333
+ yield* Composite.focusActive({
334
+ state: options.parent.state,
335
+ collection: options.parent.collection,
336
+ });
337
+ }
338
+ if (restoreOwnerFocus) {
339
+ restoreOwnerFocus = false;
340
+ yield* submenuOwners.get(options.state)?.onArrowLeft ?? Effect.void;
341
+ }
342
+ if (restoreInvokerFocus) {
343
+ restoreInvokerFocus = false;
344
+ yield* Composite.focusElement(invoker ?? invokers.get(options.state));
345
+ }
346
+ });
347
+ const toggle = EventHandler.make(Effect.fn(function* (event) {
348
+ const open = Dom.toggleState(event) === "open";
349
+ const source = Reflect.get(event, "source");
350
+ if (isElement(source))
351
+ invoker = source;
352
+ const current = yield* options.state;
353
+ if (current.open === open) {
354
+ if (!open)
355
+ yield* restoreFocus;
356
+ return current;
357
+ }
358
+ if (open && options.collection !== undefined) {
359
+ const activeId = Composite.moveActiveId(yield* options.collection, current, "first", true);
360
+ const next = yield* RefSubject.update(options.state, (state) => ({
361
+ ...state,
362
+ open,
363
+ activeId,
364
+ }));
365
+ yield* Composite.focusActive({ state: options.state, collection: options.collection });
366
+ yield* Composite.scrollActive({ state: options.state, collection: options.collection });
367
+ return next;
368
+ }
369
+ const next = yield* setOpen(options.state, open);
370
+ if (!open)
371
+ yield* restoreFocus;
372
+ return next;
373
+ }));
374
+ return ({ property }) => ({
375
+ id,
376
+ role: "menu",
377
+ popover: "manual",
378
+ "aria-label": property("label", undefined),
379
+ onkeydown,
380
+ ontoggle: toggle,
381
+ ref: Dom.composeRefs(options.state, NativePopover.ref(options.state)),
382
+ });
383
+ }
384
+ function isElement(value) {
385
+ return typeof value === "object" && value !== null && "nodeType" in value;
386
+ }
387
+ /**
388
+ * Renders manual-popover menu content and coordinates focus, typeahead, Escape, and nested-menu
389
+ * traversal.
390
+ *
391
+ * @remarks
392
+ * ## Why
393
+ *
394
+ * The component applies the family behavior while leaving callers free to supply a custom host
395
+ * through the shared DOM boundary.
396
+ *
397
+ * ## Ownership and lifetime
398
+ *
399
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
400
+ * collection registrations only when rendered. The rendering Scope removes those resources;
401
+ * unrelated nodes and attributes remain caller-owned.
402
+ *
403
+ * ## Example
404
+ *
405
+ * Import with `import { Content } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
406
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final snapshot
407
+ * read with `Content({ state, label: "File", content: "Commands" })`; render that Fx before the
408
+ * same Scope closes.
409
+ * @since 1.0.0
410
+ * @category components
411
+ */
412
+ export function Content(options, host) {
413
+ return Dom.renderHost()(options, host, contentProps(options), options.content, (props, content) => {
414
+ return html `<div ...${props}>${content}</div>`;
415
+ });
416
+ }
417
+ /**
418
+ * Consumer-facing alias of the canonical Menu component with identical behavior and lifetime.
419
+ *
420
+ * @remarks
421
+ * ## Why
422
+ *
423
+ * The component applies the family behavior while leaving callers free to supply a custom host
424
+ * through the shared DOM boundary.
425
+ *
426
+ * ## Ownership and lifetime
427
+ *
428
+ * The alias acquires nothing. Rendering it has exactly the canonical component's Scope and DOM
429
+ * ownership contract.
430
+ *
431
+ * ## Example
432
+ *
433
+ * Import with `import { Menu } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
434
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final snapshot
435
+ * read with `Menu({ state, label: "File", content: "Commands" })`; render that Fx before the same
436
+ * Scope closes.
437
+ * @since 1.0.0
438
+ * @category components
439
+ */
440
+ export const Menu = Content;
441
+ function itemProps(options) {
442
+ const closesOnActivate = options.role === undefined || options.role === "menuitem";
443
+ const activate = options.disabled === true
444
+ ? Effect.void
445
+ : RefSubject.update(options.state, (state) => ({
446
+ ...state,
447
+ activeId: options.id,
448
+ open: closesOnActivate ? false : state.open,
449
+ }));
450
+ const focus = RefSubject.update(options.state, (state) => ({ ...state, activeId: options.id }));
451
+ const register = options.collection === undefined
452
+ ? undefined
453
+ : Collection.ref(options.collection, {
454
+ id: options.id,
455
+ value: options.id,
456
+ textValue: options.textValue ?? options.id,
457
+ disabled: options.disabled,
458
+ });
459
+ return () => ({
460
+ id: options.id,
461
+ role: options.role ?? "menuitem",
462
+ "aria-disabled": options.disabled ?? false,
463
+ "aria-checked": options.role === "menuitem" || options.role === undefined
464
+ ? undefined
465
+ : (options.checked ?? false),
466
+ tabindex: Composite.tabIndex(options.state, options.id),
467
+ onclick: activate,
468
+ onfocus: focus,
469
+ onmouseenter: focus,
470
+ ref: Dom.composeRefs(register, options.ref),
471
+ });
472
+ }
473
+ /**
474
+ * Renders and optionally registers a menu item; disabled items remain announced but do not
475
+ * activate.
476
+ *
477
+ * @remarks
478
+ * ## Why
479
+ *
480
+ * The component applies the family behavior while leaving callers free to supply a custom host
481
+ * through the shared DOM boundary.
482
+ *
483
+ * ## Ownership and lifetime
484
+ *
485
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
486
+ * collection registrations only when rendered. The rendering Scope removes those resources;
487
+ * unrelated nodes and attributes remain caller-owned.
488
+ *
489
+ * ## Example
490
+ *
491
+ * Import with `import { Item } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
492
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final snapshot
493
+ * read with `Item({ state, id: "save", content: "Save" })`; render that Fx before the same Scope
494
+ * closes.
495
+ * @since 1.0.0
496
+ * @category components
497
+ */
498
+ export function Item(options, host) {
499
+ return Dom.renderHost()(options, host, itemProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
500
+ }
501
+ function submenuTriggerProps(options) {
502
+ const submenuId = RefSubject.map(options.submenu, (state) => state.id);
503
+ const expanded = RefSubject.map(options.submenu, (state) => state.open);
504
+ const focusEffect = RefSubject.update(options.state, (state) => ({
505
+ ...state,
506
+ activeId: options.id,
507
+ }));
508
+ const activateEffect = options.disabled === true ? Effect.void : focusEffect;
509
+ const register = options.collection === undefined
510
+ ? undefined
511
+ : Collection.ref(options.collection, {
512
+ id: options.id,
513
+ value: options.id,
514
+ textValue: options.textValue ?? options.id,
515
+ disabled: options.disabled,
516
+ submenu: true,
517
+ });
518
+ return () => ({
519
+ id: options.id,
520
+ type: "button",
521
+ role: "menuitem",
522
+ popovertarget: submenuId,
523
+ popovertargetaction: "toggle",
524
+ "aria-haspopup": "menu",
525
+ "aria-expanded": expanded,
526
+ "aria-disabled": options.disabled ?? false,
527
+ tabindex: Composite.tabIndex(options.state, options.id),
528
+ onclick: activateEffect,
529
+ onfocus: focusEffect,
530
+ onmouseenter: options.disabled === true
531
+ ? Effect.void
532
+ : Effect.andThen(activateEffect, setOpen(options.submenu, true)),
533
+ ref: Dom.composeRefs(register, Dom.composeRefs(options.ref, submenuOwnerRef(options.submenu, options.state, options.collection, options.id))),
534
+ });
535
+ }
536
+ function submenuOwnerRef(submenu, state, collection, triggerId) {
537
+ return Effect.fn(function* () {
538
+ const owner = {
539
+ onArrowLeft: Effect.gen(function* () {
540
+ if (collection === undefined)
541
+ return;
542
+ if ((yield* state).orientation === "horizontal") {
543
+ yield* Composite.moveAndFocus({ state, collection, includeDisabled: true }, "previous");
544
+ }
545
+ else {
546
+ yield* RefSubject.update(state, (value) => ({ ...value, activeId: triggerId }));
547
+ yield* Composite.focusActive({ state, collection });
548
+ }
549
+ }),
550
+ };
551
+ submenuOwners.set(submenu, owner);
552
+ const scope = yield* Effect.scope;
553
+ yield* Scope.addFinalizer(scope, Effect.sync(() => {
554
+ if (submenuOwners.get(submenu) === owner)
555
+ submenuOwners.delete(submenu);
556
+ }));
557
+ });
558
+ }
559
+ /**
560
+ * Renders a menuitem that targets nested popover content and coordinates parent/child directional
561
+ * focus.
562
+ *
563
+ * @remarks
564
+ * ## Why
565
+ *
566
+ * The component applies the family behavior while leaving callers free to supply a custom host
567
+ * through the shared DOM boundary.
568
+ *
569
+ * ## Ownership and lifetime
570
+ *
571
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
572
+ * collection registrations only when rendered. The rendering Scope removes those resources;
573
+ * unrelated nodes and attributes remain caller-owned.
574
+ *
575
+ * ## Example
576
+ *
577
+ * Import with `import { SubmenuTrigger } from "@typed/ui/Menu";` Extend the [Menu.makeState
578
+ * runnable setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final
579
+ * snapshot read after `const submenu = yield* Menu.makeState({ id: "share-menu" })` with
580
+ * `SubmenuTrigger({ state, submenu, collection, id: "share", content: "Share" })`; render that Fx
581
+ * before the same Scope closes.
582
+ * @since 1.0.0
583
+ * @category components
584
+ */
585
+ export function SubmenuTrigger(options, host) {
586
+ return Dom.renderHost()(options, host, submenuTriggerProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
587
+ }
588
+ /**
589
+ * Renders Item with menuitemcheckbox semantics and caller-supplied checked state.
590
+ *
591
+ * @remarks
592
+ * ## Why
593
+ *
594
+ * The component applies the family behavior while leaving callers free to supply a custom host
595
+ * through the shared DOM boundary.
596
+ *
597
+ * ## Ownership and lifetime
598
+ *
599
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
600
+ * collection registrations only when rendered. The rendering Scope removes those resources;
601
+ * unrelated nodes and attributes remain caller-owned.
602
+ *
603
+ * ## Example
604
+ *
605
+ * Import with `import { CheckboxItem } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
606
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final snapshot
607
+ * read with `CheckboxItem({ state, id: "autosave", checked: true, content: "Autosave" })`; render
608
+ * that Fx before the same Scope closes.
609
+ * @since 1.0.0
610
+ * @category components
611
+ */
612
+ export function CheckboxItem(options, host) {
613
+ return Item({ ...options, role: "menuitemcheckbox" }, host);
614
+ }
615
+ /**
616
+ * Renders Item with menuitemradio semantics and caller-supplied checked state.
617
+ *
618
+ * @remarks
619
+ * ## Why
620
+ *
621
+ * The component applies the family behavior while leaving callers free to supply a custom host
622
+ * through the shared DOM boundary.
623
+ *
624
+ * ## Ownership and lifetime
625
+ *
626
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
627
+ * collection registrations only when rendered. The rendering Scope removes those resources;
628
+ * unrelated nodes and attributes remain caller-owned.
629
+ *
630
+ * ## Example
631
+ *
632
+ * Import with `import { RadioItem } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
633
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final snapshot
634
+ * read with `RadioItem({ state, id: "compact", checked: false, content: "Compact" })`; render that
635
+ * Fx before the same Scope closes.
636
+ * @since 1.0.0
637
+ * @category components
638
+ */
639
+ export function RadioItem(options, host) {
640
+ return Item({ ...options, role: "menuitemradio" }, host);
641
+ }
642
+ function groupProps(_options) {
643
+ return ({ property }) => ({
644
+ role: "group",
645
+ "aria-label": property("label", undefined),
646
+ });
647
+ }
648
+ /**
649
+ * Renders a labelled ARIA group inside menu content.
650
+ *
651
+ * @remarks
652
+ * ## Why
653
+ *
654
+ * The component applies the family behavior while leaving callers free to supply a custom host
655
+ * through the shared DOM boundary.
656
+ *
657
+ * ## Ownership and lifetime
658
+ *
659
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
660
+ * collection registrations only when rendered. The rendering Scope removes those resources;
661
+ * unrelated nodes and attributes remain caller-owned.
662
+ *
663
+ * @example
664
+ * ```ts
665
+ * import * as Menu from "@typed/ui/Menu";
666
+ *
667
+ * const view = Menu.Group({ label: "View", content: "Choices" });
668
+ * ```
669
+ * @since 1.0.0
670
+ * @category components
671
+ */
672
+ export function Group(options, host) {
673
+ return Dom.renderHost()(options, host, groupProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
674
+ }
675
+ /**
676
+ * Consumer-facing alias of the canonical Menu component with identical behavior and lifetime.
677
+ *
678
+ * @remarks
679
+ * ## Why
680
+ *
681
+ * The component applies the family behavior while leaving callers free to supply a custom host
682
+ * through the shared DOM boundary.
683
+ *
684
+ * ## Ownership and lifetime
685
+ *
686
+ * The alias acquires nothing. Rendering it has exactly the canonical component's Scope and DOM
687
+ * ownership contract.
688
+ *
689
+ * ## Example
690
+ *
691
+ * Import with `import { Dismiss } from "@typed/ui/Menu";` Extend the [Menu.makeState runnable
692
+ * setup](/reference/%40typed%2Fui%2FMenu%23makeState). Replace the linked program's final snapshot
693
+ * read with `Dismiss({ state, id: "close", content: "Close" })`; render that Fx before the same
694
+ * Scope closes.
695
+ * @since 1.0.0
696
+ * @category components
697
+ */
698
+ export const Dismiss = Item;
699
+ function separatorProps(_options) {
700
+ return ({ property }) => ({
701
+ role: "separator",
702
+ "aria-orientation": property("orientation", "horizontal"),
703
+ });
704
+ }
705
+ /**
706
+ * Renders a separator with explicit horizontal or vertical orientation.
707
+ *
708
+ * @remarks
709
+ * ## Why
710
+ *
711
+ * The component applies the family behavior while leaving callers free to supply a custom host
712
+ * through the shared DOM boundary.
713
+ *
714
+ * ## Ownership and lifetime
715
+ *
716
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
717
+ * collection registrations only when rendered. The rendering Scope removes those resources;
718
+ * unrelated nodes and attributes remain caller-owned.
719
+ *
720
+ * @example
721
+ * ```ts
722
+ * import * as Menu from "@typed/ui/Menu";
723
+ *
724
+ * const view = Menu.Separator({ orientation: "horizontal" });
725
+ * ```
726
+ * @since 1.0.0
727
+ * @category components
728
+ */
729
+ export function Separator(options, host) {
730
+ return Dom.renderHost()(options, host, separatorProps(options), "", (props) => html `<hr ...${props} />`);
731
+ }