@agent-ui-kit/components 0.0.3

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 (271) hide show
  1. package/dist/base-styles.css +8 -0
  2. package/dist/component-styles.css +147 -0
  3. package/dist/controls/_base/index.d.ts +3 -0
  4. package/dist/controls/_base/index.js +9 -0
  5. package/dist/controls/_base/indicator-element.d.ts +132 -0
  6. package/dist/controls/_base/indicator-element.js +158 -0
  7. package/dist/controls/_base/listbox-element.d.ts +70 -0
  8. package/dist/controls/_base/listbox-element.js +74 -0
  9. package/dist/controls/_base/range-element.d.ts +120 -0
  10. package/dist/controls/_base/range-element.js +158 -0
  11. package/dist/controls/_surface/container-box.css +301 -0
  12. package/dist/controls/_surface/container.css +156 -0
  13. package/dist/controls/_token-surface/token-surface.d.ts +35 -0
  14. package/dist/controls/_token-surface/token-surface.js +97 -0
  15. package/dist/controls/attachment/attachment-meta.d.ts +8 -0
  16. package/dist/controls/attachment/attachment-meta.js +84 -0
  17. package/dist/controls/attachment/attachment.css +91 -0
  18. package/dist/controls/attachment/attachment.d.ts +40 -0
  19. package/dist/controls/attachment/attachment.js +70 -0
  20. package/dist/controls/avatar/avatar-initials.d.ts +4 -0
  21. package/dist/controls/avatar/avatar-initials.js +30 -0
  22. package/dist/controls/avatar/avatar.css +87 -0
  23. package/dist/controls/avatar/avatar.d.ts +40 -0
  24. package/dist/controls/avatar/avatar.js +77 -0
  25. package/dist/controls/badge/badge.css +175 -0
  26. package/dist/controls/badge/badge.d.ts +36 -0
  27. package/dist/controls/badge/badge.js +71 -0
  28. package/dist/controls/bar-chart/bar-chart.css +110 -0
  29. package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
  30. package/dist/controls/bar-chart/bar-chart.js +80 -0
  31. package/dist/controls/bar-chart/bar-math.d.ts +31 -0
  32. package/dist/controls/bar-chart/bar-math.js +106 -0
  33. package/dist/controls/button/button.css +263 -0
  34. package/dist/controls/button/button.d.ts +61 -0
  35. package/dist/controls/button/button.js +115 -0
  36. package/dist/controls/calendar/calendar.css +495 -0
  37. package/dist/controls/calendar/calendar.d.ts +161 -0
  38. package/dist/controls/calendar/calendar.js +1004 -0
  39. package/dist/controls/card/card-content.d.ts +24 -0
  40. package/dist/controls/card/card-content.js +133 -0
  41. package/dist/controls/card/card-footer.d.ts +3 -0
  42. package/dist/controls/card/card-footer.js +16 -0
  43. package/dist/controls/card/card-header.d.ts +3 -0
  44. package/dist/controls/card/card-header.js +18 -0
  45. package/dist/controls/card/card.css +445 -0
  46. package/dist/controls/card/card.d.ts +51 -0
  47. package/dist/controls/card/card.js +61 -0
  48. package/dist/controls/checkbox/checkbox.css +204 -0
  49. package/dist/controls/checkbox/checkbox.d.ts +114 -0
  50. package/dist/controls/checkbox/checkbox.js +130 -0
  51. package/dist/controls/code/code.css +72 -0
  52. package/dist/controls/code/code.d.ts +23 -0
  53. package/dist/controls/code/code.js +40 -0
  54. package/dist/controls/color-picker/color-picker.css +209 -0
  55. package/dist/controls/color-picker/color-picker.d.ts +84 -0
  56. package/dist/controls/color-picker/color-picker.js +545 -0
  57. package/dist/controls/color-picker/color.d.ts +40 -0
  58. package/dist/controls/color-picker/color.js +159 -0
  59. package/dist/controls/column/column.css +151 -0
  60. package/dist/controls/column/column.d.ts +107 -0
  61. package/dist/controls/column/column.js +57 -0
  62. package/dist/controls/combo-box/combo-box.css +397 -0
  63. package/dist/controls/combo-box/combo-box.d.ts +133 -0
  64. package/dist/controls/combo-box/combo-box.js +680 -0
  65. package/dist/controls/command-modal/command-modal.css +226 -0
  66. package/dist/controls/command-modal/command-modal.d.ts +42 -0
  67. package/dist/controls/command-modal/command-modal.js +315 -0
  68. package/dist/controls/disclosure/disclosure.css +162 -0
  69. package/dist/controls/disclosure/disclosure.d.ts +37 -0
  70. package/dist/controls/disclosure/disclosure.js +207 -0
  71. package/dist/controls/field/field.css +140 -0
  72. package/dist/controls/field/field.d.ts +27 -0
  73. package/dist/controls/field/field.js +228 -0
  74. package/dist/controls/form-provider/form-provider.css +40 -0
  75. package/dist/controls/form-provider/form-provider.d.ts +42 -0
  76. package/dist/controls/form-provider/form-provider.js +105 -0
  77. package/dist/controls/grid/grid.css +72 -0
  78. package/dist/controls/grid/grid.d.ts +60 -0
  79. package/dist/controls/grid/grid.js +56 -0
  80. package/dist/controls/icon/icon.css +37 -0
  81. package/dist/controls/icon/icon.d.ts +25 -0
  82. package/dist/controls/icon/icon.js +55 -0
  83. package/dist/controls/index.d.ts +57 -0
  84. package/dist/controls/index.js +136 -0
  85. package/dist/controls/ladder/ladder.css +89 -0
  86. package/dist/controls/ladder/ladder.d.ts +27 -0
  87. package/dist/controls/ladder/ladder.js +78 -0
  88. package/dist/controls/list/list.css +118 -0
  89. package/dist/controls/list/list.d.ts +84 -0
  90. package/dist/controls/list/list.js +48 -0
  91. package/dist/controls/menu/menu.css +195 -0
  92. package/dist/controls/menu/menu.d.ts +44 -0
  93. package/dist/controls/menu/menu.js +248 -0
  94. package/dist/controls/modal/modal.css +99 -0
  95. package/dist/controls/modal/modal.d.ts +61 -0
  96. package/dist/controls/modal/modal.js +155 -0
  97. package/dist/controls/popover/popover.css +83 -0
  98. package/dist/controls/popover/popover.d.ts +44 -0
  99. package/dist/controls/popover/popover.js +164 -0
  100. package/dist/controls/progress/progress.css +106 -0
  101. package/dist/controls/progress/progress.d.ts +28 -0
  102. package/dist/controls/progress/progress.js +80 -0
  103. package/dist/controls/radio/radio-group.css +84 -0
  104. package/dist/controls/radio/radio-group.d.ts +152 -0
  105. package/dist/controls/radio/radio-group.js +350 -0
  106. package/dist/controls/radio/radio.css +219 -0
  107. package/dist/controls/radio/radio.d.ts +19 -0
  108. package/dist/controls/radio/radio.js +75 -0
  109. package/dist/controls/ramp/ramp.css +75 -0
  110. package/dist/controls/ramp/ramp.d.ts +29 -0
  111. package/dist/controls/ramp/ramp.js +72 -0
  112. package/dist/controls/row/row.css +152 -0
  113. package/dist/controls/row/row.d.ts +95 -0
  114. package/dist/controls/row/row.js +45 -0
  115. package/dist/controls/segment/segment.css +66 -0
  116. package/dist/controls/segment/segment.d.ts +3 -0
  117. package/dist/controls/segment/segment.js +26 -0
  118. package/dist/controls/segmented-control/segmented-control.css +189 -0
  119. package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
  120. package/dist/controls/segmented-control/segmented-control.js +64 -0
  121. package/dist/controls/select/select.css +467 -0
  122. package/dist/controls/select/select.d.ts +147 -0
  123. package/dist/controls/select/select.js +538 -0
  124. package/dist/controls/slider/slider.css +231 -0
  125. package/dist/controls/slider/slider.d.ts +111 -0
  126. package/dist/controls/slider/slider.js +60 -0
  127. package/dist/controls/slider-multi/slider-multi.css +197 -0
  128. package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
  129. package/dist/controls/slider-multi/slider-multi.js +380 -0
  130. package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
  131. package/dist/controls/sparkline/sparkline-math.js +79 -0
  132. package/dist/controls/sparkline/sparkline.css +61 -0
  133. package/dist/controls/sparkline/sparkline.d.ts +27 -0
  134. package/dist/controls/sparkline/sparkline.js +74 -0
  135. package/dist/controls/split/constrain.d.ts +44 -0
  136. package/dist/controls/split/constrain.js +126 -0
  137. package/dist/controls/split/split-pane.css +63 -0
  138. package/dist/controls/split/split-pane.d.ts +49 -0
  139. package/dist/controls/split/split-pane.js +42 -0
  140. package/dist/controls/split/split.css +127 -0
  141. package/dist/controls/split/split.d.ts +42 -0
  142. package/dist/controls/split/split.js +364 -0
  143. package/dist/controls/stat/stat-model.d.ts +22 -0
  144. package/dist/controls/stat/stat-model.js +69 -0
  145. package/dist/controls/stat/stat.css +101 -0
  146. package/dist/controls/stat/stat.d.ts +30 -0
  147. package/dist/controls/stat/stat.js +73 -0
  148. package/dist/controls/status-stream/status-stream.css +151 -0
  149. package/dist/controls/status-stream/status-stream.d.ts +108 -0
  150. package/dist/controls/status-stream/status-stream.js +417 -0
  151. package/dist/controls/swatch/swatch.css +66 -0
  152. package/dist/controls/swatch/swatch.d.ts +28 -0
  153. package/dist/controls/swatch/swatch.js +74 -0
  154. package/dist/controls/swiper/swiper-item.css +17 -0
  155. package/dist/controls/swiper/swiper-item.d.ts +31 -0
  156. package/dist/controls/swiper/swiper-item.js +47 -0
  157. package/dist/controls/swiper/swiper-label.css +10 -0
  158. package/dist/controls/swiper/swiper-label.d.ts +4 -0
  159. package/dist/controls/swiper/swiper-label.js +20 -0
  160. package/dist/controls/swiper/swiper-paddles.css +26 -0
  161. package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
  162. package/dist/controls/swiper/swiper-paddles.js +62 -0
  163. package/dist/controls/swiper/swiper-pagination.css +60 -0
  164. package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
  165. package/dist/controls/swiper/swiper-pagination.js +78 -0
  166. package/dist/controls/swiper/swiper.css +153 -0
  167. package/dist/controls/swiper/swiper.d.ts +158 -0
  168. package/dist/controls/swiper/swiper.js +659 -0
  169. package/dist/controls/switch/switch.css +194 -0
  170. package/dist/controls/switch/switch.d.ts +5 -0
  171. package/dist/controls/switch/switch.js +18 -0
  172. package/dist/controls/table/table-model.d.ts +43 -0
  173. package/dist/controls/table/table-model.js +124 -0
  174. package/dist/controls/table/table.css +90 -0
  175. package/dist/controls/table/table.d.ts +29 -0
  176. package/dist/controls/table/table.js +178 -0
  177. package/dist/controls/tabs/tab-panel.d.ts +10 -0
  178. package/dist/controls/tabs/tab-panel.js +89 -0
  179. package/dist/controls/tabs/tab.d.ts +26 -0
  180. package/dist/controls/tabs/tab.js +67 -0
  181. package/dist/controls/tabs/tabs.css +181 -0
  182. package/dist/controls/tabs/tabs.d.ts +61 -0
  183. package/dist/controls/tabs/tabs.js +176 -0
  184. package/dist/controls/text/href.d.ts +26 -0
  185. package/dist/controls/text/href.js +48 -0
  186. package/dist/controls/text/text.css +328 -0
  187. package/dist/controls/text/text.d.ts +85 -0
  188. package/dist/controls/text/text.js +273 -0
  189. package/dist/controls/text-field/text-field.css +498 -0
  190. package/dist/controls/text-field/text-field.d.ts +226 -0
  191. package/dist/controls/text-field/text-field.js +905 -0
  192. package/dist/controls/textarea/textarea.css +251 -0
  193. package/dist/controls/textarea/textarea.d.ts +136 -0
  194. package/dist/controls/textarea/textarea.js +312 -0
  195. package/dist/controls/theme-provider/theme-provider.css +48 -0
  196. package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
  197. package/dist/controls/theme-provider/theme-provider.js +40 -0
  198. package/dist/controls/timeline/timeline.css +33 -0
  199. package/dist/controls/timeline/timeline.d.ts +39 -0
  200. package/dist/controls/timeline/timeline.js +52 -0
  201. package/dist/controls/timeline-item/timeline-item.css +495 -0
  202. package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
  203. package/dist/controls/timeline-item/timeline-item.js +360 -0
  204. package/dist/controls/toast/toast-region.css +56 -0
  205. package/dist/controls/toast/toast-region.d.ts +20 -0
  206. package/dist/controls/toast/toast-region.js +113 -0
  207. package/dist/controls/toast/toast.css +96 -0
  208. package/dist/controls/toast/toast.d.ts +28 -0
  209. package/dist/controls/toast/toast.js +191 -0
  210. package/dist/controls/toolbar/toolbar.css +125 -0
  211. package/dist/controls/toolbar/toolbar.d.ts +108 -0
  212. package/dist/controls/toolbar/toolbar.js +68 -0
  213. package/dist/controls/tooltip/tooltip.css +80 -0
  214. package/dist/controls/tooltip/tooltip.d.ts +57 -0
  215. package/dist/controls/tooltip/tooltip.js +205 -0
  216. package/dist/descriptor/component-descriptor.d.ts +112 -0
  217. package/dist/descriptor/component-descriptor.js +492 -0
  218. package/dist/descriptor/index.d.ts +1 -0
  219. package/dist/descriptor/index.js +5 -0
  220. package/dist/dom/container.d.ts +45 -0
  221. package/dist/dom/container.js +60 -0
  222. package/dist/dom/element.d.ts +98 -0
  223. package/dist/dom/element.js +221 -0
  224. package/dist/dom/form.d.ts +240 -0
  225. package/dist/dom/form.js +399 -0
  226. package/dist/dom/index.d.ts +10 -0
  227. package/dist/dom/index.js +31 -0
  228. package/dist/dom/props.d.ts +56 -0
  229. package/dist/dom/props.js +231 -0
  230. package/dist/dom/repeat.d.ts +10 -0
  231. package/dist/dom/repeat.js +202 -0
  232. package/dist/dom/template.d.ts +161 -0
  233. package/dist/dom/template.js +634 -0
  234. package/dist/dom/watch.d.ts +11 -0
  235. package/dist/dom/watch.js +82 -0
  236. package/dist/foundation-styles.css +9 -0
  237. package/dist/index.d.ts +2 -0
  238. package/dist/index.js +3 -0
  239. package/dist/reactive/graph.d.ts +65 -0
  240. package/dist/reactive/graph.js +310 -0
  241. package/dist/reactive/index.d.ts +3 -0
  242. package/dist/reactive/index.js +4 -0
  243. package/dist/reactive/scheduler.d.ts +13 -0
  244. package/dist/reactive/scheduler.js +49 -0
  245. package/dist/traits/area-drag.d.ts +12 -0
  246. package/dist/traits/area-drag.js +70 -0
  247. package/dist/traits/form-registry.d.ts +36 -0
  248. package/dist/traits/form-registry.js +108 -0
  249. package/dist/traits/index.d.ts +24 -0
  250. package/dist/traits/index.js +19 -0
  251. package/dist/traits/overlay.d.ts +50 -0
  252. package/dist/traits/overlay.js +277 -0
  253. package/dist/traits/pane-resize.d.ts +26 -0
  254. package/dist/traits/pane-resize.js +103 -0
  255. package/dist/traits/press-activation.d.ts +11 -0
  256. package/dist/traits/press-activation.js +55 -0
  257. package/dist/traits/roving-focus.d.ts +52 -0
  258. package/dist/traits/roving-focus.js +214 -0
  259. package/dist/traits/scroll-fade.d.ts +28 -0
  260. package/dist/traits/scroll-fade.js +120 -0
  261. package/dist/traits/selection-commit.d.ts +30 -0
  262. package/dist/traits/selection-commit.js +146 -0
  263. package/dist/traits/tabbable.d.ts +12 -0
  264. package/dist/traits/tabbable.js +59 -0
  265. package/dist/traits/track-user-invalid.d.ts +36 -0
  266. package/dist/traits/track-user-invalid.js +52 -0
  267. package/dist/traits/value-codec.d.ts +120 -0
  268. package/dist/traits/value-codec.js +497 -0
  269. package/dist/traits/value-drag.d.ts +18 -0
  270. package/dist/traits/value-drag.js +84 -0
  271. package/package.json +263 -0
@@ -0,0 +1,84 @@
1
+ import type { ReactiveProps } from '../../dom/index.js';
2
+ import { UIContainerElement } from '../../dom/container.js';
3
+ declare const props: {
4
+ align: {
5
+ default: "stretch";
6
+ reflect: true;
7
+ type: import("../../index.js").PropType<"end" | "start" | "center" | "stretch" | "baseline">;
8
+ attribute?: string | false;
9
+ };
10
+ justify: {
11
+ reflect: true;
12
+ type: import("../../index.js").PropType<"end" | "start" | "center" | "between" | "around" | "evenly">;
13
+ default: "end" | "start" | "center" | "between" | "around" | "evenly";
14
+ attribute?: string | false;
15
+ };
16
+ gap: {
17
+ reflect: true;
18
+ type: import("../../index.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
19
+ default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
20
+ attribute?: string | false;
21
+ };
22
+ wrap: {
23
+ reflect: true;
24
+ type: import("../../index.js").PropType<boolean>;
25
+ default: boolean;
26
+ attribute?: string | false;
27
+ };
28
+ elevation: {
29
+ reflect: true;
30
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
31
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
32
+ attribute?: string | false;
33
+ };
34
+ brightness: {
35
+ reflect: true;
36
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
37
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
38
+ attribute?: string | false;
39
+ };
40
+ };
41
+ export interface UIListElement extends ReactiveProps<typeof props> {
42
+ }
43
+ export declare class UIListElement extends UIContainerElement {
44
+ static props: {
45
+ align: {
46
+ default: "stretch";
47
+ reflect: true;
48
+ type: import("../../index.js").PropType<"end" | "start" | "center" | "stretch" | "baseline">;
49
+ attribute?: string | false;
50
+ };
51
+ justify: {
52
+ reflect: true;
53
+ type: import("../../index.js").PropType<"end" | "start" | "center" | "between" | "around" | "evenly">;
54
+ default: "end" | "start" | "center" | "between" | "around" | "evenly";
55
+ attribute?: string | false;
56
+ };
57
+ gap: {
58
+ reflect: true;
59
+ type: import("../../index.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
60
+ default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
61
+ attribute?: string | false;
62
+ };
63
+ wrap: {
64
+ reflect: true;
65
+ type: import("../../index.js").PropType<boolean>;
66
+ default: boolean;
67
+ attribute?: string | false;
68
+ };
69
+ elevation: {
70
+ reflect: true;
71
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
72
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
73
+ attribute?: string | false;
74
+ };
75
+ brightness: {
76
+ reflect: true;
77
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
78
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
79
+ attribute?: string | false;
80
+ };
81
+ };
82
+ protected connected(): void;
83
+ }
84
+ export {};
@@ -0,0 +1,48 @@
1
+ // list.ts — UIListElement, the semantic vertical-stack layout primitive (goals.md §G9 / ADR-0016 cl.3;
2
+ // decomp g9-containers slice s5). BEHAVIOUR + props + the list role + self-define ONLY; the flex layout +
3
+ // surface seam live in list.css.
4
+ //
5
+ // `ui-list` is a `ui-column` SPECIALIZATION that adds LIST SEMANTICS: it is a `display:flex` vertical stack
6
+ // (the surface base `UIContainerElement`, ADR-0015 — NOT form-associated) whose host carries the ARIA
7
+ // `role="list"`. The role is set THROUGH the protected `internals` (the family discipline, ADR-0010 /
8
+ // element.ts) — NEVER a host `role` attribute, so the host stays free of `role`/`aria-*`. The list imposes
9
+ // NO item element: its children are the list items, composed by the agent as a ChildList (the A2UI child
10
+ // model) — so `render()` stays the inherited VOID (returning nothing → the host render effect commits
11
+ // nothing → the user's light-DOM children are never clobbered).
12
+ //
13
+ // It consumes the two shared spreadable prop sets the container family declares (no static-props prototype
14
+ // merge — the ADR-0013 `formProps` precedent): `surfaceProps` (elevation/brightness, the surface axes) and
15
+ // `flexProps` (align/justify/gap/wrap, the A2UI-faithful layout grammar). The literal-union → CSS-keyword
16
+ // mapping is a role-pure repoint in list.css's `@scope` block; the base owns no flex/surface CSS.
17
+ //
18
+ // IS an A2UI catalog type — `List` (ADR-0087 Fork A, Wave C; supersedes ADR-0016's earlier non-catalog
19
+ // exclusion) renders to `ui-list`. Reach for `List` over `Column` for a homogeneous, itemized collection where
20
+ // list semantics matter to assistive tech; reach for `Row`/`Column` for a heterogeneous arrangement, and
21
+ // `Grid` when the layout should reflow its column count responsively (a2ui-catalog.spec.md §5.2).
22
+ //
23
+ // Imports the surface base from the same-layer dom `../../dom/container.ts` (not re-exported from the dom
24
+ // barrel until s12) + the typed-prop authoring types from the dom barrel. `controls → dom` is the allowed
25
+ // import direction (the layering law: reactive ← dom ← controls).
26
+ import { UIContainerElement } from "../../dom/container.js";
27
+ // The full prop surface = the two shared spreadable sets folded into this control's own `static props` (each
28
+ // already carries `reflect: true` from the base, so the `[elevation]`/`[align]`/etc. attribute selectors in
29
+ // list.css apply to JS-set values too). No re-declaration of the surface/flex grammar — one home, ADR-0016.
30
+ // ADR-0030: `align` default overridden from `start` to `stretch` (parity with ui-column — a ui-list IS a
31
+ // ui-column specialization; its cross axis is inline/width; children should fill the width by default).
32
+ const props = {
33
+ ...UIContainerElement.surfaceProps, // elevation / brightness (ADR-0015)
34
+ ...UIContainerElement.flexProps, // align / justify / gap / wrap (ADR-0016) — a vertical stack
35
+ align: { ...UIContainerElement.flexProps.align, default: 'stretch' }, // ADR-0030: override default ONLY; `as const` preserves the literal-union type (prevents string widening)
36
+ };
37
+ export class UIListElement extends UIContainerElement {
38
+ static props = props;
39
+ connected() {
40
+ // LIST SEMANTICS — `role=list` rides the host's ElementInternals, never a host `role` attribute (the
41
+ // family discipline). A constant semantic role, so it is set DIRECTLY (not in an effect); re-set on each
42
+ // connect (idempotent). The children are the list items — no item element is imposed, and `render()`
43
+ // stays void so the ChildList children flow through untouched.
44
+ this.internals.role = 'list';
45
+ }
46
+ }
47
+ if (!customElements.get('ui-list'))
48
+ customElements.define('ui-list', UIListElement);
@@ -0,0 +1,195 @@
1
+ /* menu.css — ui-menu, the single-file overlay-menu stylesheet (ADR-0003 · overlay-controller.lld.md).
2
+ * Two clearly-SECTIONED blocks, mirroring popover.css / modal.css:
3
+ *
4
+ * [1] TOKEN BLOCK — `:where(ui-menu)` (specificity 0,0,0) sets the menu's OWN default surface
5
+ * tokens and DECLARES the `--ui-menu-*` chain from the colour roles (`--md-sys-color-{family}-{role}`)
6
+ * and the dimensions (`--md-sys-space-*` / `--md-sys-shape-corner-base`). Raw `--md-sys-color-*` roles enter ONLY here;
7
+ * `@scope` never reads them.
8
+ * [2] STYLES BLOCK — `@scope (ui-menu)` CONSUMES only the role-pure `--ui-menu-*` chain.
9
+ * The host is `display: contents` — a logical overlay wrapper that generates no box.
10
+ * The trigger child stays in the document flow (the author provides it; we mark it but do
11
+ * not style it — it is the author's to own). The panel part (`[data-part="panel"]`) is the
12
+ * Container/surface: bg from `--ui-menu-bg`, `--md-sys-shape-corner-base`, `--md-sys-space-xs` padding
13
+ * shell — NOT a control height (`--md-sys-height-*` is never read by the panel itself).
14
+ * Menu items (`[role="menuitem"]`) use the LEGACY ITEM-PAD geometry (§4.6/§5.1 of the
15
+ * listbox-roving LLD): vertical block padding (`--md-sys-space-xs`) + inline padding
16
+ * (`--md-sys-space-md`), giving a comfortable row height without a fixed --md-sys-height-* frame.
17
+ * A `min-inline-size` floor prevents panel collapse (ADR-0021 trap). A
18
+ * `forced-color-adjust` block keeps the surface and highlighted items visible in WHCM.
19
+ *
20
+ * Standalone stylesheet — NOT injected from menu.ts (behaviour-only `.ts`, no runtime injection
21
+ * per plan §2). Linked via the component-styles barrel (s12); the barrel @imports foundation
22
+ * roles FIRST.
23
+ */
24
+
25
+ /* ════════ [1] TOKEN BLOCK — :where(ui-menu) declares the surface default + --ui-menu-* ════════ */
26
+
27
+ :where(ui-menu) {
28
+ /* Surface plane — the popup panel is an opaque neutral surface (like popover / card). */
29
+ --ui-menu-bg: var(--md-sys-color-neutral-surface);
30
+ --ui-menu-ink: var(--md-sys-color-neutral-on-surface);
31
+
32
+ /* Frame */
33
+ --ui-menu-outline: var(--md-sys-color-neutral-outline-variant);
34
+ --ui-menu-radius: var(--md-sys-shape-corner-base);
35
+
36
+ /* Shell spacing — panel-class geometry (Container/surface; NOT a control height). */
37
+ --ui-menu-padding: var(--md-sys-space-xs);
38
+
39
+ /* Size floor — prevents collapse when the menu has no items or all items are narrow (ADR-0021
40
+ lesson: a surface that can be empty / narrow needs a min-inline-size; the JS positioning
41
+ controller measures bounds on open, so a zero-width panel would mis-position too). */
42
+ --ui-menu-min-inline-size: 10rem;
43
+
44
+ /* Menu-item row geometry — the LEGACY ITEM-PAD (§4.6/§5.1, NOT the widget-box height).
45
+ A row is: vertical block padding + inline padding, giving a comfortable reading row.
46
+ STRUCTURAL DIVERGENCE from ui-select/ui-combo-box (flagged, not forced to fit — 2026-07-06
47
+ size-carrying-model pass): ui-menu has NO `[size]` attribute and renders NO trigger geometry
48
+ at all — the trigger is entirely author-owned content (menu.ts marks it but never sizes it,
49
+ unlike select's own <button data-part=trigger>). There is no `--ui-menu-height` to derive
50
+ against and no h/2 alignment target, so the select "listbox-padding + option-inline == h/2"
51
+ law does not have an analogue here. The item-pad stays its own fixed (`--md-sys-space-*`-scaled)
52
+ register — correct per ADR-0043 cl.5 / geometry.md's Pattern-class row, not a gap to close.
53
+ Adding a `[size]` axis to ui-menu would be a new-attribute contract change (its own decomp/ADR),
54
+ not a CSS-only geometry fix. */
55
+ --ui-menu-item-pad-block: var(--md-sys-space-xs);
56
+ --ui-menu-item-pad-inline: var(--md-sys-space-md);
57
+ --ui-menu-item-font: var(--md-sys-font-md);
58
+
59
+ /* Item row block-size — the REAL rendered row height (font + 2×block-pad; `line-height: 1` on
60
+ the item rule below, the same local derivation choice select/combo-box's option rows already
61
+ make, is what keeps this exact rather than font-metric-dependent). Feeds --ui-menu-max-block-size
62
+ below (TKT-0027) — there is no --ui-menu-height to reuse (the note above), so this is the
63
+ control's OWN public row-height fact, the item-pad siblings' geometry class. */
64
+ --ui-menu-item-block-size: calc(var(--ui-menu-item-font) + 2 * var(--ui-menu-item-pad-block));
65
+
66
+ /* Panel max-block-size (TKT-0027) — public dial, default min(50vh, 12 real item rows): the
67
+ smaller of half the viewport or a scannable dozen rows, so a short list never over-reserves
68
+ and a long list caps at a page instead of the old flat 40vh magic number. 12 rows = 12 item
69
+ boxes + 13 inset margins (leading + 11 collapsed inter-row + trailing) — the panel is
70
+ `[data-box]` (a BFC, container-box.css), which collapses adjacent siblings' block margins to
71
+ ONE inset rather than doubling them. Rides density naturally: --ui-menu-item-block-size reads
72
+ --md-sys-space-xs (rhythm-family, density-linked), so a denser/spacious theme repoints this dial
73
+ for free without a special case here. + 2px — the panel's border-box: max-block-size caps the
74
+ BORDER box (box-sizing: border-box below), but the row math above computed the CONTENT box
75
+ (12 rows + 13 insets, no border term) — without this the 12th row overflowed its own cap by
76
+ exactly the 1px+1px border width (FIXED 2026-07-12, TKT-0027 browser-leg finding). */
77
+ --ui-menu-max-block-size: min(50vh, calc(12 * var(--ui-menu-item-block-size) + 13 * var(--ui-box-inset, 0.375rem) + 2px));
78
+
79
+ /* One-level nested radius: item corners inset from the panel corner (ADR-0018). FIXED
80
+ (2026-07-06): was subtracting --md-sys-space-xs (4px), a DIFFERENT value than the --ui-box-inset
81
+ (6px, container-box.css) actually used for the item's own margin below — the nested-radius
82
+ law (ADR-0018: child radius = parent radius − the REAL inset) was inset-inconsistent. Now
83
+ subtracts the same --ui-box-inset the row margin reads. */
84
+ --ui-menu-item-radius: calc(var(--md-sys-shape-corner-base) - var(--ui-box-inset, 0.375rem));
85
+
86
+ /* Item state colours — hover fill + disabled muted ink.
87
+ FIXED (2026-07-07, overlay-list family-consistency pass): was --md-sys-color-neutral-tint-dimmer
88
+ (a translucent 10% alpha wash) — the ONLY row-hover in the fleet using the tint ladder; every
89
+ other interactive-row/control hover (ui-select's option, ui-button, ui-text-field, ui-checkbox,
90
+ ui-switch, ui-calendar's cell) reads the solid, scheme-inverting --md-sys-color-neutral-surface-high
91
+ role. ui-menu's translucent wash read as a visibly weaker/different hover than ui-select's listbox
92
+ right next to it (the reported inconsistency) — converged onto the existing fleet-wide role,
93
+ no new token minted. */
94
+ --ui-menu-item-bg-hover: var(--md-sys-color-neutral-surface-high);
95
+ --ui-menu-item-ink-disabled: var(--md-sys-color-neutral-outline); /* muted, not invisible */
96
+
97
+ }
98
+
99
+ /* ════════ [2] STYLES BLOCK — @scope (ui-menu) consumes ONLY --ui-menu-* ════════ */
100
+
101
+ @scope (ui-menu) {
102
+ /* The host is a logical disclosure wrapper — it generates no box; only the trigger (in flow)
103
+ and the panel (in the top layer when open) render as visible surfaces. */
104
+ :scope {
105
+ display: contents;
106
+ }
107
+
108
+ /* The menu panel — Container/surface in the Popover API top layer.
109
+ `position: fixed` and the inset are set by the JS positioning controller (overlay.ts) on open;
110
+ we do NOT fix a position here. `margin: 0` prevents UA-default centering from conflicting with
111
+ the controller's explicit inset. The panel is hidden by the UA `[popover]` rule until
112
+ showPopover() places it in the top layer — our CSS only applies when it is visible. */
113
+ :scope > [data-part='panel'] {
114
+ box-sizing: border-box;
115
+ margin: 0;
116
+ min-inline-size: var(--ui-menu-min-inline-size);
117
+ max-block-size: var(--ui-menu-max-block-size); /* TKT-0027 — public dial, default min(50vh, 12 item rows) */
118
+ overflow-y: auto;
119
+ padding: 0; /* the box-model menuitem margins provide the inset (container-box.css); no shell padding */
120
+
121
+ background-color: var(--ui-menu-bg);
122
+ color: var(--ui-menu-ink);
123
+ border: 1px solid var(--ui-menu-outline);
124
+ border-radius: var(--ui-menu-radius);
125
+ }
126
+
127
+ /* Menu-item rows — the LEGACY ITEM-PAD row (§4.6/§5.1). NOT --md-sys-height-* (a menuitem is a
128
+ list row, not a single-line fixed-height control widget). cursor + user-select mirror the
129
+ interactive-row standard (pointer for items, not-allowed for disabled). */
130
+ :scope [role='menuitem'] {
131
+ box-sizing: border-box;
132
+ display: flex;
133
+ align-items: center;
134
+ margin: var(--ui-box-inset, 0.375rem); /* the box-model inset — 6px from the panel edge + between rows */
135
+ padding-block: var(--ui-menu-item-pad-block);
136
+ padding-inline: var(--ui-menu-item-pad-inline);
137
+ /* line-height: 1 makes the --ui-menu-item-block-size calc exact (font + 2×pad == the rendered
138
+ row) — the same local derivation choice select/combo-box's option rows already make for the
139
+ identical reason (TKT-0027: a font-metric-dependent 'normal' line-height would make the
140
+ 12-fit/13-scroll cross-engine proof flaky). */
141
+ line-height: 1;
142
+ border-radius: var(--ui-menu-item-radius);
143
+ font-size: var(--ui-menu-item-font);
144
+ color: var(--ui-menu-ink);
145
+ cursor: default;
146
+ user-select: none;
147
+ -webkit-user-select: none; /* WebKit honours selection-none only via the prefix */
148
+ }
149
+
150
+ /* Hover state — the fleet-wide solid elevated-surface fill (no border — like native menus). */
151
+ :scope [role='menuitem']:not([disabled]):not([aria-disabled='true']):hover {
152
+ background-color: var(--ui-menu-item-bg-hover);
153
+ }
154
+
155
+ /* Keyboard focus ring (ADR-0009) — reads fleet tokens directly (the documented family exception:
156
+ every control draws the identical ring; see button.css for the precedent). Outset (positive
157
+ offset), matching ui-select's option ring and the fleet-wide convention (button/checkbox/
158
+ text-field/select/etc. all use the positive offset; only ui-tabs' inline tab-strip is inset,
159
+ for a different reason — adjacent siblings with no margin). FIXED (2026-07-07,
160
+ family-consistency pass): was the inset (negative-offset) treatment — the item's own
161
+ --ui-box-inset margin (≥6px) comfortably clears an outset ring (width+offset = 4px) without
162
+ touching the panel edge or a neighbouring row. */
163
+ :scope [role='menuitem']:not([disabled]):not([aria-disabled='true']):focus-visible {
164
+ outline: var(--md-sys-state-focus-ring-width) solid var(--md-sys-color-focus-ring);
165
+ outline-offset: var(--md-sys-state-focus-ring-offset);
166
+ }
167
+
168
+ /* Disabled items — muted ink, no pointer, inert to hover/focus. */
169
+ :scope [role='menuitem'][disabled],
170
+ :scope [role='menuitem'][aria-disabled='true'] {
171
+ color: var(--ui-menu-item-ink-disabled);
172
+ cursor: not-allowed;
173
+ pointer-events: none;
174
+ }
175
+
176
+ /* Forced-colors (WHCM) — the surface, frame, and ink survive as system Canvas/CanvasText.
177
+ Hover + focus items use system Highlight/HighlightText so they remain visible.
178
+ `forced-color-adjust: none` on the item hover/focus block keeps the fill from being
179
+ overridden (the platform would discard the solid surface-high fill; we commit to Highlight instead). */
180
+ @media (forced-colors: active) {
181
+ :scope > [data-part='panel'] {
182
+ background-color: Canvas;
183
+ color: CanvasText;
184
+ border-color: CanvasText;
185
+ }
186
+
187
+ :scope [role='menuitem']:not([disabled]):not([aria-disabled='true']):hover,
188
+ :scope [role='menuitem']:not([disabled]):not([aria-disabled='true']):focus-visible {
189
+ forced-color-adjust: none;
190
+ background-color: Highlight;
191
+ color: HighlightText;
192
+ outline: none;
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,44 @@
1
+ import { type ReactiveProps } from '../../dom/index.js';
2
+ import { UIElement } from '../../dom/index.js';
3
+ import { type OverlayHandle } from '../../traits/overlay.js';
4
+ declare const props: {
5
+ open: {
6
+ reflect: true;
7
+ type: import("../../index.js").PropType<boolean>;
8
+ default: boolean;
9
+ attribute?: string | false;
10
+ };
11
+ placement: {
12
+ reflect: true;
13
+ type: import("../../index.js").PropType<"bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end">;
14
+ default: "bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end";
15
+ attribute?: string | false;
16
+ };
17
+ };
18
+ export interface UIMenuElement extends ReactiveProps<typeof props> {
19
+ }
20
+ export declare class UIMenuElement extends UIElement {
21
+ #private;
22
+ static props: {
23
+ open: {
24
+ reflect: true;
25
+ type: import("../../index.js").PropType<boolean>;
26
+ default: boolean;
27
+ attribute?: string | false;
28
+ };
29
+ placement: {
30
+ reflect: true;
31
+ type: import("../../index.js").PropType<"bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end">;
32
+ default: "bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end";
33
+ attribute?: string | false;
34
+ };
35
+ };
36
+ /**
37
+ * Protected overlay handle — accessible to test probes (C10 idempotent-cleanup DoD).
38
+ * Replaced on each reconnect (connected() re-runs); the old handle's cleanup fires via the
39
+ * scope effect disposer at disconnect before this is re-assigned.
40
+ */
41
+ protected _overlayHandle: OverlayHandle | null;
42
+ protected connected(): void;
43
+ }
44
+ export {};
@@ -0,0 +1,248 @@
1
+ // menu.ts — UIMenuElement, the Wave-4 S3 menu overlay control
2
+ // (control-suite-wave4-overlay.decomp.md S3 · overlay-controller.lld.md LLD-C1..C4 · ADR-0043).
3
+ //
4
+ // Composition: a `UIElement` host + `overlay()` (Popover API + JS positioning + two-way `open`) +
5
+ // `rovingFocus()` over [role=menuitem] items (Arrow roves focus; type-ahead; Enter/click commits).
6
+ // NOT form-associated — ui-menu emits an action (`select` with {value,index}), carrying NO form
7
+ // value. Like ui-tabs (non-value commit→select), but in an overlay. A trigger (first element child,
8
+ // marked `data-part="trigger"`) opens the panel; commit closes it.
9
+ //
10
+ // Anatomy (light-DOM, created once — idempotent across reconnect):
11
+ // <ui-menu>
12
+ // <button data-part="trigger" aria-expanded="…" aria-controls="ui-menu-panel-N"
13
+ // aria-haspopup="menu">…</button>
14
+ // <div data-part="panel" role="menu" popover="auto" id="ui-menu-panel-N" tabindex="-1">
15
+ // <!-- author-provided item children moved in at connect; auto-get role=menuitem if absent -->
16
+ // <div role="menuitem" tabindex="-1">Item 1</div>
17
+ // <div role="menuitem" tabindex="-1">Item 2</div>
18
+ // </div>
19
+ // </ui-menu>
20
+ //
21
+ // Two-way `open` (ADR-0019): a scope-owned effect drives model→overlay; the overlay controller's
22
+ // `close` event listener drives overlay→model (light-dismiss syncs the prop back). The overlay
23
+ // trait emits `close` + `toggle` on EVERY real open-state transition — platform dismiss (Escape /
24
+ // outside-click), a commit's programmatic close, or a model-driven `open=false` alike (ADR-0101 —
25
+ // native ToggleEvent timing fidelity; the trait is the sole, uniform announcer for the family).
26
+ //
27
+ // Events: `select` ({value, index}) on commit; `toggle` + `close` on every real close, including a
28
+ // commit's programmatic close (ADR-0101 — see traits/overlay.ts).
29
+ //
30
+ // ARIA: host has no explicit role (logical wrapper); panel has `role=menu` set on the div part
31
+ // directly (a created part, not the host — the FACE rule "internals.role, never host attributes"
32
+ // applies to the HOST element; direct parts use setAttribute). The trigger gets `aria-expanded`,
33
+ // `aria-controls`, `aria-haspopup="menu"`. Item children auto-get `role=menuitem` if absent.
34
+ //
35
+ // `controls → dom → traits` is the one allowed import direction.
36
+ import { prop } from "../../dom/index.js";
37
+ import { UIElement } from "../../dom/index.js";
38
+ import { overlay } from "../../traits/overlay.js";
39
+ import { rovingFocus } from "../../traits/roving-focus.js";
40
+ import { scrollFade } from "../../traits/scroll-fade.js";
41
+ // ── Placement enum values (mirrors the OverlayPlacement union from overlay.ts) ─────────────────
42
+ const PLACEMENTS = [
43
+ 'bottom-start',
44
+ 'bottom-end',
45
+ 'top-start',
46
+ 'top-end',
47
+ 'left-start',
48
+ 'left-end',
49
+ 'right-start',
50
+ 'right-end',
51
+ ];
52
+ // ── Props ────────────────────────────────────────────────────────────────────────────────────────
53
+ const props = {
54
+ // `open` — whether the menu panel is shown. Reflected + BINDABLE: the catalog declares
55
+ // value:{prop:'open',event:'toggle'} so the renderer two-way-binds it (ADR-0019). Drives the
56
+ // overlay handle via a scope-owned effect.
57
+ open: { ...prop.boolean(false), reflect: true },
58
+ // `placement` — the preferred panel placement relative to the trigger (captured once per
59
+ // connection; changing it after connect takes effect on the next reconnect).
60
+ placement: { ...prop.enum(PLACEMENTS, 'bottom-start'), reflect: true },
61
+ };
62
+ // ── Module-level stable-id counter (one per panel, never reused) ─────────────────────────────────
63
+ let _nextPanelId = 0;
64
+ export class UIMenuElement extends UIElement {
65
+ static props = props;
66
+ // The control-created panel PART — created ONCE (idempotent guard in #ensureParts()) and
67
+ // NEVER re-created. Persists through disconnect/reconnect (like the modal/popover panel parts).
68
+ #panel = null;
69
+ /**
70
+ * Protected overlay handle — accessible to test probes (C10 idempotent-cleanup DoD).
71
+ * Replaced on each reconnect (connected() re-runs); the old handle's cleanup fires via the
72
+ * scope effect disposer at disconnect before this is re-assigned.
73
+ */
74
+ _overlayHandle = null;
75
+ connected() {
76
+ const { panel, trigger, items } = this.#ensureParts();
77
+ // Wire the overlay controller (LLD-C1..C4) — popover=auto (Escape + outside-click
78
+ // light-dismiss), focusOnOpen=true (focus moves INTO the menu on open, restored to trigger
79
+ // on close by the overlay handle).
80
+ const handle = overlay(this, {
81
+ popup: panel,
82
+ anchor: trigger,
83
+ placement: this.placement,
84
+ auto: true,
85
+ focusOnOpen: true,
86
+ });
87
+ this._overlayHandle = handle;
88
+ // Trigger click → toggle the panel.
89
+ //
90
+ // ADR-0101 erratum fix: flip the PROP (`this.open`), not `handle.toggle()` directly — see
91
+ // select.ts's click handler for the full race trace (identical mechanics here: a raw
92
+ // `handle.toggle()` never writes `this.open`, so `#commit`'s later `this.open = false` was a
93
+ // same-value no-op and the panel stuck open after picking a menu item — ticket #28's residual).
94
+ // The prop is the single source of truth; the model→overlay effect below drives
95
+ // `handle.open()`/`handle.close()` and the trait announces (ADR-0101) — the combo-box pattern.
96
+ this.listen(trigger, 'click', () => {
97
+ this.open = !this.open;
98
+ });
99
+ // overlay→model: when the Popover API light-dismisses (Escape / outside-click), the overlay
100
+ // controller emits `close` on the host. Sync the prop back so the two-way bind stays
101
+ // consistent (ADR-0019 — the renderer reads `toggle`, but `open` must be false by then).
102
+ this.listen(this, 'close', () => {
103
+ this.open = false;
104
+ });
105
+ // model→overlay: a scope-owned effect drives open/close from the prop and keeps aria-expanded
106
+ // in sync. Runs immediately on creation (eager first run): open=false → handle.close() (no-op,
107
+ // not yet open) + aria-expanded='false'. Re-runs whenever this.open changes.
108
+ this.effect(() => {
109
+ const isOpen = this.open;
110
+ if (isOpen)
111
+ handle.open();
112
+ else
113
+ handle.close();
114
+ trigger.setAttribute('aria-expanded', String(isOpen));
115
+ });
116
+ // Delegated click on the panel → commit the clicked menuitem. Handles mouse clicks AND
117
+ // keyboard-activated button items (Enter/Space on a <button> fires a native click event).
118
+ this.listen(panel, 'click', (ev) => {
119
+ const target = ev.target;
120
+ const list = items();
121
+ const index = list.findIndex((item) => item === target || item.contains(target));
122
+ if (index === -1)
123
+ return;
124
+ const item = list[index];
125
+ if (item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true')
126
+ return;
127
+ this.#commit(index, item);
128
+ });
129
+ // Keydown on the panel → Enter/Space on focused menuitems that are NOT native buttons or
130
+ // links (those convert Enter/Space → click natively, which our click listener then commits).
131
+ // For non-interactive elements (`<div role=menuitem>`), we must commit on keydown.
132
+ // DELIBERATE selectionCommit bypass (ratified, TKT-0065 lateral review): that trait reflects
133
+ // `aria-selected` on items — invalid on `role=menuitem` (action semantics, no selected state) —
134
+ // and carries no Space leg; this hand-rolled click+Enter/Space path IS the menu commit contract.
135
+ this.listen(panel, 'keydown', (ev) => {
136
+ const e = ev;
137
+ if (e.key !== 'Enter' && e.key !== ' ')
138
+ return;
139
+ const focused = document.activeElement;
140
+ if (!focused)
141
+ return;
142
+ // Buttons and links fire click on Enter/Space natively — the click listener handles them.
143
+ if (focused instanceof HTMLButtonElement || focused.tagName === 'A')
144
+ return;
145
+ const list = items();
146
+ const index = list.findIndex((item) => item === focused || item.contains(focused));
147
+ if (index === -1)
148
+ return;
149
+ const item = list[index];
150
+ if (item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true')
151
+ return;
152
+ e.preventDefault();
153
+ this.#commit(index, item);
154
+ });
155
+ // Roving keyboard focus — vertical (Arrow Up/Down), loop, type-ahead. Container = panel so
156
+ // only Arrow keys inside the panel are intercepted (not the trigger's own keyboard events).
157
+ rovingFocus(this, {
158
+ container: panel,
159
+ items,
160
+ orientation: 'vertical',
161
+ loop: true,
162
+ typeAhead: true,
163
+ });
164
+ // Edge-aware scroll fade (the gutter-exposure fix, 2026-07-04) — always on, no opt-in prop. The panel
165
+ // is now a bounded scroll viewport (menu.css: max-block-size: 40vh + overflow-y: auto, matching ui-select).
166
+ scrollFade(this, { viewport: panel });
167
+ }
168
+ /**
169
+ * Create the control's two light-DOM parts ONCE (idempotent across disconnect/reconnect):
170
+ * - trigger: the first element child, marked with `data-part="trigger"` + ARIA affordances.
171
+ * - panel: a `<div data-part="panel" role="menu">` appended to the host; non-trigger children
172
+ * are moved in and auto-assigned `role=menuitem` + `tabindex=-1` if they lack a role.
173
+ *
174
+ * The overlay controller sets `popover="auto"` on the panel. `render()` stays the inherited
175
+ * VOID — the parts are created once and never re-created.
176
+ */
177
+ #ensureParts() {
178
+ if (this.#panel) {
179
+ // Parts persist through disconnect/reconnect — return the existing ones.
180
+ const trigger = this.querySelector('[data-part="trigger"]');
181
+ if (!trigger)
182
+ throw new Error('ui-menu: trigger part lost on reconnect');
183
+ const panel = this.#panel;
184
+ return { panel, trigger, items: () => this.#itemsIn(panel) };
185
+ }
186
+ // Identify the menu trigger — the first element child provided by the author.
187
+ const trigger = this.firstElementChild;
188
+ if (!trigger) {
189
+ throw new Error('ui-menu: provide a trigger as the first child (e.g. <button>) before the menu items');
190
+ }
191
+ trigger.setAttribute('data-part', 'trigger');
192
+ // Create the menu panel. The overlay controller sets popover="auto" via setAttribute
193
+ // (single-ownership, ADR-0017 pattern — we do NOT set it here).
194
+ // `role=menu` is set on the div part directly (a created element, NOT the host — the FACE
195
+ // internals.role rule applies to the host element only).
196
+ const panel = document.createElement('div');
197
+ panel.setAttribute('data-part', 'panel');
198
+ panel.setAttribute('data-box', ''); // adopt the shared container box-model (inset margins)
199
+ panel.setAttribute('role', 'menu');
200
+ // tabindex="-1" lets moveFocusIn() (overlay LLD-C4) land on the panel itself when there
201
+ // are no enabled menuitems — the fallback panel.focus() requires the element to be focusable.
202
+ panel.setAttribute('tabindex', '-1');
203
+ // Stable id for aria-controls (created once, never reused).
204
+ panel.id = `ui-menu-panel-${++_nextPanelId}`;
205
+ // Move all non-trigger children into the panel (the menu items live in the top-layer surface,
206
+ // not beside the trigger in the document flow — the modal/popover child-move pattern).
207
+ let node = trigger.nextSibling;
208
+ while (node) {
209
+ const next = node.nextSibling;
210
+ panel.appendChild(node);
211
+ node = next;
212
+ }
213
+ // Auto-assign role=menuitem + tabindex=-1 to direct element children that do not already
214
+ // carry a role. The roving-focus trait then manages the tabindexes from this base state.
215
+ for (const child of panel.children) {
216
+ if (child instanceof HTMLElement) {
217
+ if (!child.hasAttribute('role'))
218
+ child.setAttribute('role', 'menuitem');
219
+ if (!child.hasAttribute('tabindex'))
220
+ child.setAttribute('tabindex', '-1');
221
+ }
222
+ }
223
+ this.appendChild(panel);
224
+ this.#panel = panel;
225
+ // Wire the trigger's ARIA affordances.
226
+ trigger.setAttribute('aria-controls', panel.id);
227
+ trigger.setAttribute('aria-haspopup', 'menu');
228
+ return { panel, trigger, items: () => this.#itemsIn(panel) };
229
+ }
230
+ /** Return the live ordered set of [role=menuitem] elements within the panel. */
231
+ #itemsIn(panel) {
232
+ return [...panel.querySelectorAll('[role="menuitem"]')];
233
+ }
234
+ /**
235
+ * Commit a user-driven item selection: emit `select` with {value, index}, then close the panel.
236
+ * The value is the item's `data-value` attribute, falling back to trimmed text content.
237
+ * Close is driven programmatically (open=false → effect → handle.close()); the overlay trait
238
+ * announces exactly one close+toggle pair for this transition too (ADR-0101), with `el.open`
239
+ * already `false` by the time a listener observes either event (the ordering invariant).
240
+ */
241
+ #commit(index, item) {
242
+ const value = item.dataset['value'] ?? item.textContent?.trim() ?? String(index);
243
+ this.emit('select', { value, index });
244
+ this.open = false;
245
+ }
246
+ }
247
+ if (!customElements.get('ui-menu'))
248
+ customElements.define('ui-menu', UIMenuElement);