@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/Menu.js ADDED
@@ -0,0 +1,569 @@
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
+ * Learn the interaction in the [Menu guide](/explore/ui-menu).
12
+ *
13
+ * @since 1.0.0
14
+ * @category modules
15
+ * @packageDocumentation
16
+ */
17
+ import * as Effect from "effect/Effect";
18
+ import * as Scope from "effect/Scope";
19
+ import * as Schema from "effect/Schema";
20
+ import { RefSubject } from "@typed/fx";
21
+ import { EventHandler, html, } from "@typed/template";
22
+ import * as Collection from "./Collection.js";
23
+ import * as Composite from "./Composite.js";
24
+ import * as Dom from "./Dom.js";
25
+ import * as NativePopover from "./NativePopover.js";
26
+ const invokers = new WeakMap();
27
+ const submenuOwners = new WeakMap();
28
+ /**
29
+ * Effect Schema used by makeState to encode, decode, and hydrate Menu state.
30
+ *
31
+ * @remarks
32
+ * @example
33
+ * ```ts
34
+ * import * as Schema from "effect/Schema";
35
+ * import * as Menu from "@typed/ui/Menu";
36
+ *
37
+ * const decodeState = Schema.decodeUnknownEffect(Menu.StateSchema);
38
+ * ```
39
+ * @since 1.0.0
40
+ * @category Popup state
41
+ */
42
+ export const StateSchema = Schema.Struct({
43
+ id: Schema.String,
44
+ open: Schema.Boolean,
45
+ activeId: Schema.NullOr(Schema.String),
46
+ orientation: Schema.Literals(["vertical"]),
47
+ loop: Schema.Boolean,
48
+ rtl: Schema.Boolean,
49
+ virtualFocus: Schema.Boolean,
50
+ });
51
+ /**
52
+ * Creates hydrated Menu state. The caller supplies id; open defaults false, activeId null, loop
53
+ * true, and orientation vertical.
54
+ *
55
+ * @remarks
56
+ * The returned Effect creates the RefSubject when run. That state is renderer-independent;
57
+ * collection registrations belong to the separate Scope that runs register or ref, not to state
58
+ * creation.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import * as Effect from "effect/Effect";
63
+ * import * as Menu from "@typed/ui/Menu";
64
+ *
65
+ * const program = Effect.scoped(
66
+ * Effect.gen(function* () {
67
+ * const state = yield* Menu.makeState({ id: "file-menu" });
68
+ * const collection = yield* Menu.makeCollection();
69
+ * return { state: yield* state, collection: yield* collection };
70
+ * }),
71
+ * );
72
+ * ```
73
+ * @since 1.0.0
74
+ * @category Popup state
75
+ */
76
+ export function makeState(initial) {
77
+ return RefSubject.hydrate(StateSchema, {
78
+ id: initial.id,
79
+ open: initial.open ?? false,
80
+ activeId: initial.activeId ?? null,
81
+ orientation: "vertical",
82
+ loop: initial.loop ?? true,
83
+ rtl: false,
84
+ virtualFocus: false,
85
+ });
86
+ }
87
+ /**
88
+ * Creates a scoped Collection for Menu items.
89
+ *
90
+ * @remarks
91
+ * The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
92
+ * owned by the Scope that runs register, independently of this construction Effect.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import * as Effect from "effect/Effect";
97
+ * import * as Menu from "@typed/ui/Menu";
98
+ *
99
+ * const program = Effect.scoped(
100
+ * Effect.gen(function* () {
101
+ * const collection = yield* Menu.makeCollection();
102
+ * return yield* collection;
103
+ * }),
104
+ * );
105
+ * ```
106
+ * @since 1.0.0
107
+ * @category Command registration
108
+ */
109
+ export const makeCollection = Collection.makeState;
110
+ /**
111
+ * Updates only the menu's explicit open state.
112
+ *
113
+ * @remarks
114
+ * The operation exposes Menu's transition directly so callers can compose it in Effect programs
115
+ * and native event handlers.
116
+ *
117
+ * @since 1.0.0
118
+ * @category Popup state
119
+ */
120
+ export function setOpen(state, open) {
121
+ return RefSubject.update(state, (value) => ({ ...value, open }));
122
+ }
123
+ function triggerProps(options) {
124
+ const id = RefSubject.map(options.state, (state) => state.id);
125
+ const open = RefSubject.map(options.state, (state) => state.open);
126
+ return () => ({
127
+ type: "button",
128
+ popovertarget: id,
129
+ popovertargetaction: "toggle",
130
+ "aria-haspopup": "menu",
131
+ "aria-expanded": open,
132
+ onkeydown: EventHandler.make((event) => {
133
+ if (event.key !== "ArrowDown")
134
+ return;
135
+ event.preventDefault();
136
+ Dom.currentTarget(event).click();
137
+ }),
138
+ ref: Dom.composeRefs(options.state, invokerRef(options.state)),
139
+ });
140
+ }
141
+ function invokerRef(state) {
142
+ return Effect.fn(function* (element) {
143
+ invokers.set(state, element);
144
+ const scope = yield* Effect.scope;
145
+ yield* Scope.addFinalizer(scope, Effect.sync(() => {
146
+ if (invokers.get(state) === element)
147
+ invokers.delete(state);
148
+ }));
149
+ });
150
+ }
151
+ /**
152
+ * Renders a native button targeting menu popover content and opens the menu on ArrowDown.
153
+ *
154
+ * @remarks
155
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
156
+ * collection registrations only when rendered. The rendering Scope removes those resources;
157
+ * unrelated nodes and attributes remain caller-owned.
158
+ *
159
+ * @since 1.0.0
160
+ * @category Opening a menu
161
+ */
162
+ export function Trigger(options, host) {
163
+ return Dom.renderHost()(options, host, triggerProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
164
+ }
165
+ /**
166
+ * Consumer-facing alias of the canonical Menu component with identical behavior and lifetime.
167
+ *
168
+ * @remarks
169
+ * The alias acquires nothing. Rendering it has exactly the canonical component's Scope and DOM
170
+ * ownership contract.
171
+ *
172
+ * @since 1.0.0
173
+ * @category Opening a menu
174
+ */
175
+ export const Button = Trigger;
176
+ function contentProps(options) {
177
+ const id = RefSubject.map(options.state, (state) => state.id);
178
+ let typeahead = { value: "", updatedAt: 0 };
179
+ let restoreParentFocus = false;
180
+ let restoreOwnerFocus = false;
181
+ let restoreInvokerFocus = false;
182
+ let invoker;
183
+ const onkeydown = options.collection === undefined
184
+ ? undefined
185
+ : EventHandler.make(Effect.fn(function* (event) {
186
+ const direction = Composite.keyMove(event, { orientation: "vertical" });
187
+ if (direction !== undefined) {
188
+ event.preventDefault();
189
+ yield* Composite.moveAndFocus({ state: options.state, collection: options.collection, includeDisabled: true }, direction);
190
+ return;
191
+ }
192
+ if (event.key === "ArrowRight") {
193
+ const activeId = (yield* options.state).activeId;
194
+ const item = activeId === null
195
+ ? undefined
196
+ : (yield* options.collection).find((item) => item.id === activeId);
197
+ const element = item?.submenu === true ? item.element : undefined;
198
+ const click = element === undefined ? undefined : Reflect.get(element, "click");
199
+ if (typeof click === "function") {
200
+ event.preventDefault();
201
+ yield* Effect.sync(() => click.call(element));
202
+ }
203
+ return;
204
+ }
205
+ if (event.key === "ArrowLeft") {
206
+ event.preventDefault();
207
+ const element = Dom.currentTarget(event);
208
+ const hidePopover = Reflect.get(element, "hidePopover");
209
+ if (typeof hidePopover === "function") {
210
+ if (options.parent === undefined && submenuOwners.has(options.state))
211
+ restoreOwnerFocus = true;
212
+ else if (options.parent === undefined)
213
+ restoreInvokerFocus = true;
214
+ else
215
+ restoreParentFocus = true;
216
+ yield* Effect.sync(() => hidePopover.call(element));
217
+ }
218
+ else {
219
+ yield* setOpen(options.state, false);
220
+ if (options.parent === undefined && submenuOwners.has(options.state)) {
221
+ yield* submenuOwners.get(options.state)?.onArrowLeft ?? Effect.void;
222
+ }
223
+ else if (options.parent === undefined)
224
+ yield* Composite.focusElement(invoker ?? invokers.get(options.state));
225
+ else {
226
+ yield* RefSubject.update(options.parent.state, (state) => ({
227
+ ...state,
228
+ activeId: options.parent.triggerId,
229
+ }));
230
+ yield* Composite.focusActive({
231
+ state: options.parent.state,
232
+ collection: options.parent.collection,
233
+ });
234
+ }
235
+ }
236
+ return;
237
+ }
238
+ if (event.key === "Escape") {
239
+ restoreInvokerFocus = true;
240
+ yield* setOpen(options.state, false);
241
+ return;
242
+ }
243
+ if (event.key === "Tab") {
244
+ yield* setOpen(options.state, false);
245
+ return;
246
+ }
247
+ if (event.key === "Enter" || event.key === " ") {
248
+ const activeId = (yield* options.state).activeId;
249
+ const element = activeId === null
250
+ ? undefined
251
+ : (yield* options.collection).find((item) => item.id === activeId)?.element;
252
+ const click = element === undefined ? undefined : Reflect.get(element, "click");
253
+ if (typeof click === "function") {
254
+ event.preventDefault();
255
+ yield* Effect.sync(() => click.call(element));
256
+ }
257
+ return;
258
+ }
259
+ const key = Composite.typeaheadKey(event);
260
+ if (key === null)
261
+ return;
262
+ typeahead = Composite.updateTypeaheadBuffer(typeahead, key, Date.now());
263
+ const activeId = Composite.typeaheadFrom(yield* options.collection, typeahead.value, (yield* options.state).activeId, undefined, true);
264
+ if (activeId === null)
265
+ return;
266
+ event.preventDefault();
267
+ yield* RefSubject.update(options.state, (state) => ({ ...state, activeId }));
268
+ yield* Composite.focusActive({ state: options.state, collection: options.collection });
269
+ }));
270
+ const restoreFocus = Effect.gen(function* () {
271
+ if (restoreParentFocus && options.parent !== undefined) {
272
+ restoreParentFocus = false;
273
+ yield* RefSubject.update(options.parent.state, (state) => ({
274
+ ...state,
275
+ activeId: options.parent.triggerId,
276
+ }));
277
+ yield* Composite.focusActive({
278
+ state: options.parent.state,
279
+ collection: options.parent.collection,
280
+ });
281
+ }
282
+ if (restoreOwnerFocus) {
283
+ restoreOwnerFocus = false;
284
+ yield* submenuOwners.get(options.state)?.onArrowLeft ?? Effect.void;
285
+ }
286
+ if (restoreInvokerFocus) {
287
+ restoreInvokerFocus = false;
288
+ yield* Composite.focusElement(invoker ?? invokers.get(options.state));
289
+ }
290
+ });
291
+ const toggle = EventHandler.make(Effect.fn(function* (event) {
292
+ const open = Dom.toggleState(event) === "open";
293
+ const source = Reflect.get(event, "source");
294
+ if (isElement(source))
295
+ invoker = source;
296
+ const current = yield* options.state;
297
+ if (current.open === open) {
298
+ if (!open)
299
+ yield* restoreFocus;
300
+ return current;
301
+ }
302
+ if (open && options.collection !== undefined) {
303
+ const activeId = Composite.moveActiveId(yield* options.collection, current, "first", true);
304
+ const next = yield* RefSubject.update(options.state, (state) => ({
305
+ ...state,
306
+ open,
307
+ activeId,
308
+ }));
309
+ yield* Composite.focusActive({ state: options.state, collection: options.collection });
310
+ yield* Composite.scrollActive({ state: options.state, collection: options.collection });
311
+ return next;
312
+ }
313
+ const next = yield* setOpen(options.state, open);
314
+ if (!open)
315
+ yield* restoreFocus;
316
+ return next;
317
+ }));
318
+ return ({ property }) => ({
319
+ id,
320
+ role: "menu",
321
+ popover: "manual",
322
+ "aria-label": property("label", undefined),
323
+ onkeydown,
324
+ ontoggle: toggle,
325
+ ref: Dom.composeRefs(options.state, NativePopover.ref(options.state)),
326
+ });
327
+ }
328
+ function isElement(value) {
329
+ return typeof value === "object" && value !== null && "nodeType" in value;
330
+ }
331
+ /**
332
+ * Renders manual-popover menu content and coordinates focus, typeahead, Escape, and nested-menu
333
+ * traversal.
334
+ *
335
+ * @remarks
336
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
337
+ * collection registrations only when rendered. The rendering Scope removes those resources;
338
+ * unrelated nodes and attributes remain caller-owned.
339
+ *
340
+ * @since 1.0.0
341
+ * @category Menu surface
342
+ */
343
+ export function Content(options, host) {
344
+ return Dom.renderHost()(options, host, contentProps(options), options.content, (props, content) => {
345
+ return html `<div ...${props}>${content}</div>`;
346
+ });
347
+ }
348
+ /**
349
+ * Consumer-facing alias of the canonical Menu component with identical behavior and lifetime.
350
+ *
351
+ * @remarks
352
+ * The alias acquires nothing. Rendering it has exactly the canonical component's Scope and DOM
353
+ * ownership contract.
354
+ *
355
+ * @since 1.0.0
356
+ * @category Menu surface
357
+ */
358
+ export const Menu = Content;
359
+ function itemProps(options) {
360
+ const closesOnActivate = options.role === undefined || options.role === "menuitem";
361
+ const activate = options.disabled === true
362
+ ? Effect.void
363
+ : RefSubject.update(options.state, (state) => ({
364
+ ...state,
365
+ activeId: options.id,
366
+ open: closesOnActivate ? false : state.open,
367
+ }));
368
+ const focus = RefSubject.update(options.state, (state) => ({ ...state, activeId: options.id }));
369
+ const register = options.collection === undefined
370
+ ? undefined
371
+ : Collection.ref(options.collection, {
372
+ id: options.id,
373
+ value: options.id,
374
+ textValue: options.textValue ?? options.id,
375
+ disabled: options.disabled,
376
+ });
377
+ return () => ({
378
+ id: options.id,
379
+ role: options.role ?? "menuitem",
380
+ "aria-disabled": options.disabled ?? false,
381
+ "aria-checked": options.role === "menuitem" || options.role === undefined
382
+ ? undefined
383
+ : (options.checked ?? false),
384
+ tabindex: Composite.tabIndex(options.state, options.id),
385
+ onclick: activate,
386
+ onfocus: focus,
387
+ onmouseenter: focus,
388
+ ref: Dom.composeRefs(register, options.ref),
389
+ });
390
+ }
391
+ /**
392
+ * Renders and optionally registers a menu item; disabled items remain announced but do not
393
+ * activate.
394
+ *
395
+ * @remarks
396
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
397
+ * collection registrations only when rendered. The rendering Scope removes those resources;
398
+ * unrelated nodes and attributes remain caller-owned.
399
+ *
400
+ * @since 1.0.0
401
+ * @category Commands
402
+ */
403
+ export function Item(options, host) {
404
+ return Dom.renderHost()(options, host, itemProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
405
+ }
406
+ function submenuTriggerProps(options) {
407
+ const submenuId = RefSubject.map(options.submenu, (state) => state.id);
408
+ const expanded = RefSubject.map(options.submenu, (state) => state.open);
409
+ const focusEffect = RefSubject.update(options.state, (state) => ({
410
+ ...state,
411
+ activeId: options.id,
412
+ }));
413
+ const activateEffect = options.disabled === true ? Effect.void : focusEffect;
414
+ const register = options.collection === undefined
415
+ ? undefined
416
+ : Collection.ref(options.collection, {
417
+ id: options.id,
418
+ value: options.id,
419
+ textValue: options.textValue ?? options.id,
420
+ disabled: options.disabled,
421
+ submenu: true,
422
+ });
423
+ return () => ({
424
+ id: options.id,
425
+ type: "button",
426
+ role: "menuitem",
427
+ popovertarget: submenuId,
428
+ popovertargetaction: "toggle",
429
+ "aria-haspopup": "menu",
430
+ "aria-expanded": expanded,
431
+ "aria-disabled": options.disabled ?? false,
432
+ tabindex: Composite.tabIndex(options.state, options.id),
433
+ onclick: activateEffect,
434
+ onfocus: focusEffect,
435
+ onmouseenter: options.disabled === true
436
+ ? Effect.void
437
+ : Effect.andThen(activateEffect, setOpen(options.submenu, true)),
438
+ ref: Dom.composeRefs(register, Dom.composeRefs(options.ref, submenuOwnerRef(options.submenu, options.state, options.collection, options.id))),
439
+ });
440
+ }
441
+ function submenuOwnerRef(submenu, state, collection, triggerId) {
442
+ return Effect.fn(function* () {
443
+ const owner = {
444
+ onArrowLeft: Effect.gen(function* () {
445
+ if (collection === undefined)
446
+ return;
447
+ if ((yield* state).orientation === "horizontal") {
448
+ yield* Composite.moveAndFocus({ state, collection, includeDisabled: true }, "previous");
449
+ }
450
+ else {
451
+ yield* RefSubject.update(state, (value) => ({ ...value, activeId: triggerId }));
452
+ yield* Composite.focusActive({ state, collection });
453
+ }
454
+ }),
455
+ };
456
+ submenuOwners.set(submenu, owner);
457
+ const scope = yield* Effect.scope;
458
+ yield* Scope.addFinalizer(scope, Effect.sync(() => {
459
+ if (submenuOwners.get(submenu) === owner)
460
+ submenuOwners.delete(submenu);
461
+ }));
462
+ });
463
+ }
464
+ /**
465
+ * Renders a menuitem that targets nested popover content and coordinates parent/child directional
466
+ * focus.
467
+ *
468
+ * @remarks
469
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
470
+ * collection registrations only when rendered. The rendering Scope removes those resources;
471
+ * unrelated nodes and attributes remain caller-owned.
472
+ *
473
+ * @since 1.0.0
474
+ * @category Nested menus
475
+ */
476
+ export function SubmenuTrigger(options, host) {
477
+ return Dom.renderHost()(options, host, submenuTriggerProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
478
+ }
479
+ /**
480
+ * Renders Item with menuitemcheckbox semantics and caller-supplied checked state.
481
+ *
482
+ * @remarks
483
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
484
+ * collection registrations only when rendered. The rendering Scope removes those resources;
485
+ * unrelated nodes and attributes remain caller-owned.
486
+ *
487
+ * @since 1.0.0
488
+ * @category Checked commands
489
+ */
490
+ export function CheckboxItem(options, host) {
491
+ return Item({ ...options, role: "menuitemcheckbox" }, host);
492
+ }
493
+ /**
494
+ * Renders Item with menuitemradio semantics and caller-supplied checked state.
495
+ *
496
+ * @remarks
497
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
498
+ * collection registrations only when rendered. The rendering Scope removes those resources;
499
+ * unrelated nodes and attributes remain caller-owned.
500
+ *
501
+ * @since 1.0.0
502
+ * @category Checked commands
503
+ */
504
+ export function RadioItem(options, host) {
505
+ return Item({ ...options, role: "menuitemradio" }, host);
506
+ }
507
+ function groupProps(_options) {
508
+ return ({ property }) => ({
509
+ role: "group",
510
+ "aria-label": property("label", undefined),
511
+ });
512
+ }
513
+ /**
514
+ * Renders a labelled ARIA group inside menu content.
515
+ *
516
+ * @remarks
517
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
518
+ * collection registrations only when rendered. The rendering Scope removes those resources;
519
+ * unrelated nodes and attributes remain caller-owned.
520
+ *
521
+ * @example
522
+ * ```ts
523
+ * import * as Menu from "@typed/ui/Menu";
524
+ *
525
+ * const view = Menu.Group({ label: "View", content: "Choices" });
526
+ * ```
527
+ * @since 1.0.0
528
+ * @category Command grouping
529
+ */
530
+ export function Group(options, host) {
531
+ return Dom.renderHost()(options, host, groupProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
532
+ }
533
+ /**
534
+ * Consumer-facing alias of the canonical Menu component with identical behavior and lifetime.
535
+ *
536
+ * @remarks
537
+ * The alias acquires nothing. Rendering it has exactly the canonical component's Scope and DOM
538
+ * ownership contract.
539
+ *
540
+ * @since 1.0.0
541
+ * @category Commands
542
+ */
543
+ export const Dismiss = Item;
544
+ function separatorProps(_options) {
545
+ return ({ property }) => ({
546
+ role: "separator",
547
+ "aria-orientation": property("orientation", "horizontal"),
548
+ });
549
+ }
550
+ /**
551
+ * Renders a separator with explicit horizontal or vertical orientation.
552
+ *
553
+ * @remarks
554
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
555
+ * collection registrations only when rendered. The rendering Scope removes those resources;
556
+ * unrelated nodes and attributes remain caller-owned.
557
+ *
558
+ * @example
559
+ * ```ts
560
+ * import * as Menu from "@typed/ui/Menu";
561
+ *
562
+ * const view = Menu.Separator({ orientation: "horizontal" });
563
+ * ```
564
+ * @since 1.0.0
565
+ * @category Command grouping
566
+ */
567
+ export function Separator(options, host) {
568
+ return Dom.renderHost()(options, host, separatorProps(options), "", (props) => html `<hr ...${props} />`);
569
+ }