@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,99 @@
1
+ /* modal.css — ui-modal, the single-file component stylesheet (ADR-0003 / ADR-0017). Two clearly-SECTIONED
2
+ * blocks, mirroring button.css / text-field.css:
3
+ *
4
+ * [1] TOKEN BLOCK — `:where(ui-modal)` (specificity 0,0,0) sets the modal's OWN default surface and DECLARES
5
+ * the `--ui-modal-*` chain from the colour roles (`--md-sys-color-{family}-{role}`, family = neutral) and the
6
+ * dimensions (`--md-sys-space-*` / `--md-sys-shape-corner-base`). This is where roles enter, exactly as button.css's
7
+ * `:where()` token block does. The container surface base defaults `--ui-container-bg` to TRANSPARENT (an
8
+ * unset layout primitive is unchanged — ADR-0015 cl.1); a modal is an OPAQUE plane, so it repoints its own
9
+ * default. The signed `elevation`/`brightness` axes still repoint `--ui-container-bg`/`-tint` on the HOST
10
+ * via container.css (higher specificity), and the value INHERITS to the dialog part below.
11
+ * [2] STYLES BLOCK — `@scope (ui-modal)` CONSUMES only the role-pure `--ui-container-*` surface seam + the own
12
+ * `--ui-modal-*` chain (never a raw `--md-sys-color-*`). The host is `display: contents` — a logical wrapper that
13
+ * generates no box; only the control-owned `<dialog data-part="dialog">` PART renders, in the platform TOP
14
+ * LAYER when `showModal()` is called (ADR-0017). The native UA dialog frame (1em padding, solid border,
15
+ * Canvas bg, centring margin) is fully repointed onto the surface seam + the space/radius tokens. The
16
+ * `::backdrop` scrim is the platform-painted blocking layer. Geometry is the Pattern/container size-class
17
+ * (geometry.md): the shell spaces off `--md-sys-space-*` × density — NO control height (`--md-sys-height-*`).
18
+ *
19
+ * Standalone stylesheet — linked via the component-styles barrel (s12); NEVER injected from modal.ts
20
+ * (behaviour-only `.ts`, no runtime style injection — plan §2). component-styles.css @imports container.css
21
+ * FIRST (the shared surface seam), then this sheet.
22
+ */
23
+
24
+ /* ════════════════ [1] TOKEN BLOCK — :where(ui-modal) declares the surface default + --ui-modal-* ════════════════ */
25
+
26
+ :where(ui-modal) {
27
+ /* The modal's OWN default surface (the container base defaults transparent — ADR-0015 cl.1; a modal is an
28
+ opaque plane). `[elevation]`/`[brightness]` on the host still win (container.css repoints at (0,1,0)), and
29
+ the seam value inherits down to the dialog part. */
30
+ --ui-container-bg: var(--md-sys-color-neutral-surface);
31
+
32
+ /* the own chain — @scope consumes ONLY these (+ the --ui-container-* seam), never a raw --md-sys-color-* role */
33
+ --ui-modal-ink: var(--md-sys-color-neutral-on-surface);
34
+ --ui-modal-outline: var(--md-sys-color-neutral-outline-variant); /* the faint dialog frame */
35
+ --ui-modal-radius: var(--md-sys-shape-corner-base); /* the shared fleet radius (ADR-0015 cl.5) */
36
+ --ui-modal-padding: var(--md-sys-space-lg); /* the shell pad off the density-responsive ladder (NOT a control height) */
37
+ --ui-modal-scrim: var(--md-sys-color-dialog-backdrop); /* the ::backdrop blocking wash — black 80% opacity (TKT-0019; was --md-sys-color-neutral-scrim, too light) */
38
+ /* The FRAME dials — ui-modal's public composition seam (TKT-0017): a composer that nests ui-modal (the
39
+ command-modal precedent) pins these via inline style on the element it owns (the slider --value-pct
40
+ JS-seam exemption, family-coherence B). Defaults reproduce the classic centered shrink-fit exactly. */
41
+ --ui-modal-inline-size: auto; /* set a length for a FIXED-width dialog (shrink-fit by default) */
42
+ --ui-modal-max-inline-size: min(92vw, 32rem);
43
+ --ui-modal-margin-block-start: auto; /* set a length (e.g. 15svh) to TOP-ANCHOR; auto = centered */
44
+ }
45
+
46
+ /* ════════════════ [2] STYLES BLOCK — @scope (ui-modal) consumes ONLY --ui-modal-* + the --ui-container-* seam ════════════════ */
47
+
48
+ @scope (ui-modal) {
49
+ /* the host is a logical wrapper — it generates no box; only the dialog PART renders (in the top layer when
50
+ shown). The surface custom props set on the host inherit through display:contents to the dialog. */
51
+ :scope {
52
+ display: contents;
53
+ }
54
+
55
+ /* the dialog surface — the modal plane. The base plane (elevation) is the solid --ui-container-bg; the tonal
56
+ wash (brightness composition) is the flat same-colour background-image stacked above it (ADR-0015 cl.3), so
57
+ the two surface axes composite. Everything else is the own --ui-modal-* chain. The native UA frame is fully
58
+ overridden. `margin: auto` keeps the showModal centring; the size caps keep a long modal scrollable. */
59
+ :scope > [data-part='dialog'] {
60
+ box-sizing: border-box;
61
+ /* centre in the top layer by default (the showModal default — restated so a UA reset cannot drop it);
62
+ the block-start dial top-anchors when a composer sets it (TKT-0017). */
63
+ margin-inline: auto;
64
+ margin-block-start: var(--ui-modal-margin-block-start);
65
+ margin-block-end: auto;
66
+ inline-size: var(--ui-modal-inline-size);
67
+ max-inline-size: var(--ui-modal-max-inline-size);
68
+ max-block-size: 85svh;
69
+ overflow: auto; /* a tall modal scrolls within the cap, not off-screen */
70
+
71
+ background-color: var(--ui-container-bg); /* the base plane (elevation) */
72
+ background-image: linear-gradient(var(--ui-container-tint), var(--ui-container-tint)); /* the tonal wash (composition) */
73
+ color: var(--ui-modal-ink);
74
+ border: 1px solid var(--ui-modal-outline);
75
+ border-radius: var(--ui-modal-radius);
76
+ /* Box-model (container-box.css, REVISED 2026-07-04): the dialog is a [data-box] — no shell padding; its
77
+ children carry the inset (author header/content/footer regions get the 12/6 region padding PLUS a 6px
78
+ positional margin — regions are inset, not full-bleed; loose content gets the 6px box inset margin). */
79
+ padding: 0;
80
+ }
81
+
82
+ /* the ::backdrop scrim — the platform-painted blocking layer behind the modal (top layer). A translucent
83
+ neutral scrim role, so the page dims but is not fully hidden. */
84
+ :scope > [data-part='dialog']::backdrop {
85
+ background-color: var(--ui-modal-scrim);
86
+ }
87
+
88
+ /* forced-colors (WHCM) — keep the dialog surface + frame + ink visible as system colours (the wash drops; a
89
+ translucent overlay would paint over the forced Canvas base and defeat it). The ::backdrop is left to the
90
+ scrim role / UA so the blocking layer still paints (it must not vanish — the modal stays distinguishable). */
91
+ @media (forced-colors: active) {
92
+ :scope > [data-part='dialog'] {
93
+ background-color: Canvas;
94
+ background-image: none;
95
+ border-color: CanvasText;
96
+ color: CanvasText;
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,61 @@
1
+ import { type ReactiveProps } from '../../dom/index.js';
2
+ import { UIContainerElement } from '../../dom/container.js';
3
+ declare const props: {
4
+ open: {
5
+ reflect: true;
6
+ type: import("../../index.js").PropType<boolean>;
7
+ default: boolean;
8
+ attribute?: string | false;
9
+ };
10
+ persistent: {
11
+ reflect: true;
12
+ type: import("../../index.js").PropType<boolean>;
13
+ default: boolean;
14
+ attribute?: string | false;
15
+ };
16
+ elevation: {
17
+ reflect: true;
18
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
19
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
20
+ attribute?: string | false;
21
+ };
22
+ brightness: {
23
+ reflect: true;
24
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
25
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
26
+ attribute?: string | false;
27
+ };
28
+ };
29
+ export interface UIModalElement extends ReactiveProps<typeof props> {
30
+ }
31
+ export declare class UIModalElement extends UIContainerElement {
32
+ #private;
33
+ static props: {
34
+ open: {
35
+ reflect: true;
36
+ type: import("../../index.js").PropType<boolean>;
37
+ default: boolean;
38
+ attribute?: string | false;
39
+ };
40
+ persistent: {
41
+ reflect: true;
42
+ type: import("../../index.js").PropType<boolean>;
43
+ default: boolean;
44
+ attribute?: string | false;
45
+ };
46
+ elevation: {
47
+ reflect: true;
48
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
49
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
50
+ attribute?: string | false;
51
+ };
52
+ brightness: {
53
+ reflect: true;
54
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
55
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
56
+ attribute?: string | false;
57
+ };
58
+ };
59
+ protected connected(): void;
60
+ }
61
+ export {};
@@ -0,0 +1,155 @@
1
+ // modal.ts — UIModalElement, the FACE modal dialog on the native `<dialog>` (goals.md §G9 / ADR-0017;
2
+ // decomp g9-containers slice s9). BEHAVIOUR + props + the control-owned `<dialog>` part + self-define ONLY;
3
+ // surface/backdrop live in modal.css (s9), the public contract in modal.md (s9).
4
+ //
5
+ // Native `<dialog>` showModal() supplies the four hard modal behaviours FREE, in the platform top layer:
6
+ // top-layer stacking (above any z-index/overflow/stacking context), a `::backdrop`, focus CONTAINMENT, and
7
+ // Escape-to-dismiss. ADR-0017 takes the platform element and adds only the gaps it leaves — focus RESTORE on
8
+ // close (cl.4) and the `open`↔platform sync (cl.2/3). A `<dialog>` is NOT a form widget (it submits nothing,
9
+ // carries no value, is not form-associated), so it honours the "no native form elements" rule under the
10
+ // ADR-0014 widgets-not-elements reading — `ui-modal` extends UIContainerElement (surface, NOT form-associated).
11
+ //
12
+ // The dialog is a control-owned light-DOM PART (`<dialog data-part="dialog">`), created ONCE (an idempotent
13
+ // guard) and NEVER re-rendered — `render()` stays the inherited VOID, because re-creating the dialog would drop
14
+ // the top-layer + focus state mid-session (ADR-0017 cl.1). Its three wires are scope-owned:
15
+ // • model→platform — an effect on `open`: true → `dialog.showModal()`, false → `dialog.close()` (idempotent
16
+ // guards; `showModal()` records the opener first so focus can be restored).
17
+ // • platform→model — the dialog's `close` event: a USER/platform close (Escape, backdrop, an external
18
+ // `.close()`) syncs `open=false` and emits the family `close` + `toggle` (the two-way bind, ADR-0019);
19
+ // a close WE drove (the prop already went false) only restores focus — no redundant emit. `this.open` is
20
+ // the discriminator (still true ⇒ the platform closed it), so no async flag is needed.
21
+ // • dismissal gate — `persistent` (default OFF); when set, the dialog's `cancel` event (Escape, the platform
22
+ // light-dismiss request) is preventDefault-ed, and a backdrop click is ignored. Backdrop click is detected
23
+ // rect-wise (a click whose target is the dialog box but lands OUTSIDE its content rect ⇒ the `::backdrop`).
24
+ //
25
+ // The HOST carries no role/aria-* attribute (the family `internals`-only ARIA discipline) — `aria-modal` is set
26
+ // by `showModal()`, and an author accessible name (`aria-label`/`aria-labelledby`) is FORWARDED onto the dialog
27
+ // PART (ADR-0017 cl.5), keeping the host attribute-clean.
28
+ //
29
+ // The dialog also wires `traits/scroll-fade.ts` (default-on, no separate opt-in prop) — the gutter-exposure
30
+ // fix, 2026-07-04: once a [data-box] header/footer became INSET rather than full-bleed, its own background no
31
+ // longer reaches the frame edge, so scrolled content can peek through the thin gutter around it. `controls →
32
+ // dom + traits` is the allowed import direction.
33
+ import { prop } from "../../dom/index.js";
34
+ import { UIContainerElement } from "../../dom/container.js";
35
+ import { scrollFade } from "../../traits/scroll-fade.js";
36
+ const props = {
37
+ // The two surface axes (elevation/brightness) — SPREAD from the container base, not inherited (props.ts has
38
+ // no static-props prototype merge; the ADR-0013 formProps precedent). They style the dialog PART's surface.
39
+ ...UIContainerElement.surfaceProps,
40
+ // `open` — whether the modal is shown. Reflected (the [open] attribute drives nothing in CSS — the dialog's
41
+ // own [open] is the platform's — but reflecting keeps the host's declared state inspectable/serializable) and
42
+ // BINDABLE: the catalog declares value:{prop:'open',event:'toggle'} so the renderer two-way-binds it (ADR-0019).
43
+ open: { ...prop.boolean(false), reflect: true },
44
+ // `persistent` (default OFF) — when present, the modal is NON-dismissable: the dialog's `cancel` event (Escape)
45
+ // is preventDefault-ed and a backdrop click is ignored, so the agent owns the close (set open=false). Reflected
46
+ // (presence-boolean: <ui-modal persistent> ⇒ true, absent ⇒ false) — the declarative override the old default-on
47
+ // `dismissable` could not express (attribute presence always coerced true). See ADR-0020.
48
+ persistent: { ...prop.boolean(false), reflect: true },
49
+ };
50
+ export class UIModalElement extends UIContainerElement {
51
+ static props = props;
52
+ // The control-owned dialog PART — a light-DOM child that persists across disconnect/reconnect, so it is
53
+ // created ONCE (the idempotent guard in #ensureDialog) and never re-appended.
54
+ #dialog = null;
55
+ // The element focused when the modal opened — restored on close (the one platform gap, ADR-0017 cl.4).
56
+ #opener = null;
57
+ connected() {
58
+ const dialog = this.#ensureDialog();
59
+ // ── platform→model — a USER/platform close syncs the prop + announces; a close WE drove only restores ──
60
+ this.listen(dialog, 'close', () => {
61
+ if (this.open) {
62
+ // The platform closed it (Escape, backdrop, an external .close()) while the prop still reads open —
63
+ // sync the state down and announce, so the agent learns the user dismissed it (the two-way bind).
64
+ this.open = false; // reflects (drops the attribute) + re-runs the effect → #closeDialog no-ops (already closed)
65
+ this.emit('close');
66
+ this.emit('toggle'); // the value:{event:'toggle'} two-way signal (ADR-0019)
67
+ }
68
+ this.#restoreFocus(); // restore on EVERY close (platform-driven or our own) — the platform omits this
69
+ });
70
+ // ── dismissal gate — Escape / the platform light-dismiss request fires `cancel`; block it when persistent ──
71
+ this.listen(dialog, 'cancel', (event) => {
72
+ if (this.persistent)
73
+ event.preventDefault(); // the dialog stays open; no `close` follows
74
+ });
75
+ // ── backdrop click — a click on the dialog box OUTSIDE its content rect is the `::backdrop`; dismiss if allowed ──
76
+ this.listen(dialog, 'click', (event) => {
77
+ if (this.persistent)
78
+ return;
79
+ const e = event;
80
+ if (e.target !== dialog)
81
+ return; // a click on content targets the child; the backdrop/box targets the dialog
82
+ const r = dialog.getBoundingClientRect();
83
+ const insideContent = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
84
+ if (!insideContent)
85
+ dialog.close(); // a true backdrop click → close (routes through the `close` user branch)
86
+ });
87
+ // ── model→platform — `open` drives showModal()/close() (scope-owned; re-arms on reconnect) ──
88
+ this.effect(() => {
89
+ if (this.open)
90
+ this.#openDialog(dialog);
91
+ else
92
+ this.#closeDialog(dialog);
93
+ });
94
+ // ── edge-aware scroll fade (the gutter-exposure fix, 2026-07-04) — always on ──
95
+ // The dialog IS the scroll viewport (modal.css: `overflow: auto` + `max-block-size: 85svh`) and a
96
+ // [data-box] region author may give it a sticky header/footer, whose inset background no longer reaches
97
+ // the frame edge. Unlike ui-card-content's `scroll-fade`, there is no separate opt-in prop here — a
98
+ // modal has no equivalent author-facing switch, so the fade is unconditionally wired (a no-op on a
99
+ // dialog that never overflows — scrollFade's own decision logic gates the flags, not this call site).
100
+ scrollFade(this, { viewport: dialog });
101
+ }
102
+ /** Open the dialog (idempotent) — record the opener FIRST (showModal moves focus in), then enter the top layer. */
103
+ #openDialog(dialog) {
104
+ if (dialog.open)
105
+ return; // already shown — do not re-enter the top layer / re-trap
106
+ this.#opener = document.activeElement; // the element to restore focus to on close
107
+ dialog.showModal(); // platform: top layer + ::backdrop + focus trap + Escape + aria-modal — all free
108
+ }
109
+ /** Close the dialog (idempotent). The `close` event fires with `open` already false ⇒ no redundant emit. */
110
+ #closeDialog(dialog) {
111
+ if (!dialog.open)
112
+ return;
113
+ dialog.close();
114
+ }
115
+ /** Restore focus to the recorded opener (ADR-0017 cl.4 — the platform traps but does not restore). */
116
+ #restoreFocus() {
117
+ const opener = this.#opener;
118
+ this.#opener = null;
119
+ if (opener && opener.isConnected && typeof opener.focus === 'function')
120
+ opener.focus();
121
+ }
122
+ /**
123
+ * Create the `<dialog data-part="dialog">` PART ONCE (idempotent across reconnect — it is a light-DOM child
124
+ * that persists through disconnect). The modal's children are MOVED into the dialog at creation (they render
125
+ * inside the top-layer surface, not beside it). An author accessible name (`aria-label`/`aria-labelledby`) is
126
+ * FORWARDED off the host onto the dialog part (ADR-0017 cl.5) so the host stays role/aria-clean and the name
127
+ * rides the semantic element. `render()` stays the inherited VOID — the dialog is never re-created.
128
+ */
129
+ #ensureDialog() {
130
+ if (this.#dialog)
131
+ return this.#dialog;
132
+ const dialog = document.createElement('dialog');
133
+ dialog.setAttribute('data-part', 'dialog');
134
+ dialog.setAttribute('data-box', ''); // adopt the shared container box-model (region padding + inset margins, ADR-0046 revised 2026-07-04)
135
+ // Forward the accessible name onto the dialog part, then strip it from the host (host carries no aria-*).
136
+ const labelledby = this.getAttribute('aria-labelledby');
137
+ if (labelledby !== null) {
138
+ dialog.setAttribute('aria-labelledby', labelledby);
139
+ this.removeAttribute('aria-labelledby');
140
+ }
141
+ const label = this.getAttribute('aria-label');
142
+ if (label !== null) {
143
+ dialog.setAttribute('aria-label', label);
144
+ this.removeAttribute('aria-label');
145
+ }
146
+ // Move the author content into the dialog (the dialog is detached here, so firstChild is real content).
147
+ while (this.firstChild)
148
+ dialog.appendChild(this.firstChild);
149
+ this.appendChild(dialog);
150
+ this.#dialog = dialog;
151
+ return dialog;
152
+ }
153
+ }
154
+ if (!customElements.get('ui-modal'))
155
+ customElements.define('ui-modal', UIModalElement);
@@ -0,0 +1,83 @@
1
+ /* popover.css — ui-popover, the single-file component stylesheet (ADR-0003 · overlay-controller.lld.md).
2
+ * Two clearly-SECTIONED blocks, mirroring modal.css / button.css:
3
+ *
4
+ * [1] TOKEN BLOCK — `:where(ui-popover)` (specificity 0,0,0) sets the popover's OWN default
5
+ * surface tokens and DECLARES the `--ui-popover-*` chain from the colour roles
6
+ * (`--md-sys-color-{family}-{role}`, family = neutral) and the dimensions (`--md-sys-space-*` /
7
+ * `--md-sys-shape-corner-base`). Raw `--md-sys-color-*` roles enter ONLY here; `@scope` never reads them.
8
+ * [2] STYLES BLOCK — `@scope (ui-popover)` CONSUMES only the role-pure `--ui-popover-*` chain.
9
+ * The host is `display: contents` — a logical disclosure wrapper that generates no box.
10
+ * The trigger child stays in the document flow (the author provides it; we mark it with
11
+ * `data-part="trigger"` but do not style it — it is the author's to own). The panel part
12
+ * (`[data-part="panel"]`) is the Container/surface: `--ui-container-bg` equivalent sourced
13
+ * from `--ui-popover-bg`, `--md-sys-shape-corner-base`, `--md-sys-space-md` padding — NOT a control height
14
+ * (`--md-sys-height-*` is never read). The panel needs a `min-inline-size` floor to prevent
15
+ * collapse when content is absent (the ui-text-field #74 / ui-slider trap, ADR-0021).
16
+ * A `forced-color-adjust: auto` block keeps the surface and frame in system colours.
17
+ *
18
+ * Standalone stylesheet — NOT injected from popover.ts (behaviour-only `.ts`, no runtime injection
19
+ * per plan §2). Linked via the component-styles barrel (s12); the barrel @imports foundation roles
20
+ * FIRST. The panel's `popover` attribute (set by the overlay controller) gives it `display: none`
21
+ * in the UA stylesheet until `showPopover()` places it in the top layer — our CSS only applies to
22
+ * the visible (open) panel, which is correct since `[popover]` elements are normally hidden.
23
+ */
24
+
25
+ /* ════════════════ [1] TOKEN BLOCK — :where(ui-popover) declares the surface default + --ui-popover-* ════════════════ */
26
+
27
+ :where(ui-popover) {
28
+ /* Surface plane — the popup panel is an opaque neutral surface (like a modal / card).
29
+ @scope consumes --ui-popover-bg, never the raw --md-sys-color-* role. */
30
+ --ui-popover-bg: var(--md-sys-color-neutral-surface);
31
+ --ui-popover-ink: var(--md-sys-color-neutral-on-surface);
32
+
33
+ /* Frame */
34
+ --ui-popover-outline: var(--md-sys-color-neutral-outline-variant);
35
+ --ui-popover-radius: var(--md-sys-shape-corner-base); /* the shared fleet radius (ADR-0015 cl.5) */
36
+
37
+ /* Shell spacing — panel-class geometry (Container/surface, NOT a control height) */
38
+ --ui-popover-padding: var(--md-sys-space-md);
39
+
40
+ /* Size floor — prevents collapse when content is absent or narrow (the ADR-0021 lesson:
41
+ a surface that can be empty needs a min-inline-size; the overlay JS positions against
42
+ the measured bounds so a zero-width panel would place at the wrong coords too). */
43
+ --ui-popover-min-inline-size: 8rem;
44
+ }
45
+
46
+ /* ════════════════ [2] STYLES BLOCK — @scope (ui-popover) consumes ONLY --ui-popover-* ════════════════ */
47
+
48
+ @scope (ui-popover) {
49
+ /* The host is a logical disclosure wrapper — it generates no box; only the trigger (in flow)
50
+ and the panel (in the top layer when open) render as visible surfaces. */
51
+ :scope {
52
+ display: contents;
53
+ }
54
+
55
+ /* The surface panel — a Container/surface in the top layer (the Popover API places it there).
56
+ The UA `[popover]` rule hides this with display:none until showPopover(); our padding / bg
57
+ only paint when the panel is in the top layer (already visible). `position: fixed` and the
58
+ inset are set by the JS positioning controller (overlay.ts) on open — we do NOT fix a position
59
+ here; the controller owns the placement completely. `margin: 0` prevents UA-default centering
60
+ from conflicting with the controller's explicit inset. */
61
+ :scope > [data-part='panel'] {
62
+ box-sizing: border-box;
63
+ margin: 0; /* the controller sets position:fixed + top/left — UA margin must not override */
64
+ min-inline-size: var(--ui-popover-min-inline-size);
65
+
66
+ background-color: var(--ui-popover-bg);
67
+ color: var(--ui-popover-ink);
68
+ border: 1px solid var(--ui-popover-outline);
69
+ border-radius: var(--ui-popover-radius);
70
+ padding: var(--ui-popover-padding);
71
+ }
72
+
73
+ /* forced-colors (WHCM) — the surface, frame, and ink stay visible as system colours.
74
+ The opacity-based overlay wash (if any) is not used here — the surface is a solid
75
+ Canvas plane so the paint cannot defeat the forced-color Canvas base. */
76
+ @media (forced-colors: active) {
77
+ :scope > [data-part='panel'] {
78
+ background-color: Canvas;
79
+ color: CanvasText;
80
+ border-color: CanvasText;
81
+ }
82
+ }
83
+ }
@@ -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 UIPopoverElement extends ReactiveProps<typeof props> {
19
+ }
20
+ export declare class UIPopoverElement 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 scope
39
+ * 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,164 @@
1
+ // popover.ts — UIPopoverElement, the Wave-4 S1 disclosure-popover overlay control
2
+ // (control-suite-wave4-overlay.decomp.md S1 · overlay-controller.lld.md LLD-C1..C4 · ADR-0043).
3
+ //
4
+ // Composition: a `UIElement` host + `overlay(this, { popup, anchor, placement })` — the direct trait-call
5
+ // pattern (there is no `host.use()`; traits are invoked directly, like `trackUserInvalid`/`rovingFocus`) — the BARE
6
+ // overlay (Popover API surface + JS positioning + light-dismiss + two-way `open`, ADR-0019). NOT
7
+ // form-associated — no value/validity. A disclosure trigger (first element child, default slot)
8
+ // toggles a control-created surface panel part (`[popover]`).
9
+ //
10
+ // Two-way `open` (ADR-0019): a scope-owned effect drives model→overlay (open/close the handle);
11
+ // a `close` event listener on the host drives overlay→model (light-dismiss syncs the prop back).
12
+ // The overlay trait emits `close` + `toggle` on the host for EVERY real transition — platform
13
+ // dismiss, a trigger-click close, or a model-driven `open=false` alike (ADR-0101); this control
14
+ // listens for `close` and sets `this.open = false` so the prop stays consistent on the light-dismiss
15
+ // path (the commit/model paths already set the prop first).
16
+ //
17
+ // Anatomy (light-DOM, created once — idempotent across reconnect):
18
+ // <ui-popover>
19
+ // <button data-part="trigger" aria-expanded="…" aria-controls="ui-popover-panel-N">…</button>
20
+ // <div data-part="panel" popover="auto" id="ui-popover-panel-N" tabindex="-1">…</div>
21
+ // </ui-popover>
22
+ //
23
+ // The trigger is the first element child provided by the author. Remaining children are moved
24
+ // into the panel at connect time (like the modal's child-move pattern, ADR-0017). The overlay
25
+ // controller sets `popover="auto"` on the panel. `render()` stays the inherited VOID — the parts
26
+ // are created once and never re-created (re-creating would drop top-layer / focus state).
27
+ //
28
+ // Placement is fixed per connection: the overlay controller captures `prefPlacement` at call time.
29
+ // Changing `placement` after connect takes effect on the NEXT reconnect (documented limitation).
30
+ //
31
+ // ARIA: minimal via internals — the host has no explicit role (a logical disclosure wrapper); the
32
+ // trigger gets `aria-expanded` + `aria-controls` set directly (child attributes, not host internals).
33
+ // The panel gets `tabindex="-1"` so `moveFocusIn()` can land on it when there are no interactive
34
+ // descendants. `controls → dom → traits` is the one allowed import direction.
35
+ import { prop } from "../../dom/index.js";
36
+ import { UIElement } from "../../dom/index.js";
37
+ import { overlay } from "../../traits/overlay.js";
38
+ // ── Placement enum values (mirrors the OverlayPlacement union from overlay.ts) ─────────────────
39
+ const PLACEMENTS = [
40
+ 'bottom-start',
41
+ 'bottom-end',
42
+ 'top-start',
43
+ 'top-end',
44
+ 'left-start',
45
+ 'left-end',
46
+ 'right-start',
47
+ 'right-end',
48
+ ];
49
+ // ── Props ────────────────────────────────────────────────────────────────────────────────────────
50
+ const props = {
51
+ // `open` — whether the popover panel is currently shown. Reflected (the [open] attribute keeps the
52
+ // host's declared state inspectable) and BINDABLE: the catalog declares value:{prop:'open',
53
+ // event:'toggle'} so the renderer two-way-binds it (ADR-0019). Drives the overlay handle.
54
+ open: { ...prop.boolean(false), reflect: true },
55
+ // `placement` — the preferred popup placement (flip + shift at viewport edges — LLD-C3). Captured
56
+ // once per connection; changing it after connect takes effect on the next reconnect. Reflected so
57
+ // `<ui-popover placement="top-end">` works declaratively.
58
+ placement: { ...prop.enum(PLACEMENTS, 'bottom-start'), reflect: true },
59
+ };
60
+ // ── Module-level stable-id counter (one per panel, never reused across instances) ──────────────
61
+ let _nextPanelId = 0;
62
+ export class UIPopoverElement extends UIElement {
63
+ static props = props;
64
+ // The control-created popover panel PART — created ONCE (idempotent guard in #ensureParts()) and
65
+ // NEVER re-created. Persists through disconnect/reconnect just like the modal dialog part.
66
+ #panel = null;
67
+ /**
68
+ * Protected overlay handle — accessible to test probes (C10 idempotent-cleanup DoD).
69
+ * Replaced on each reconnect (connected() re-runs); the old handle's cleanup fires via the scope
70
+ * effect disposer at disconnect before this is re-assigned.
71
+ */
72
+ _overlayHandle = null;
73
+ connected() {
74
+ const { panel, trigger } = this.#ensureParts();
75
+ // Wire the overlay controller (LLD-C1..C4) — this is the PROOF of the overlay controller.
76
+ // placement is read ONCE per connection (the controller captures prefPlacement at call time).
77
+ const handle = overlay(this, {
78
+ popup: panel,
79
+ anchor: trigger,
80
+ placement: this.placement,
81
+ auto: true, // popover=auto → Escape + outside-click light-dismiss (LLD-C2)
82
+ focusOnOpen: true, // disclosure popover: focus moves into the panel on open (LLD-C4)
83
+ });
84
+ this._overlayHandle = handle;
85
+ // Trigger click → toggle the panel (the disclosure interaction).
86
+ //
87
+ // ADR-0101 erratum fix: flip the PROP (`this.open`), not `handle.toggle()` directly — see
88
+ // select.ts's click handler for the full race trace (identical mechanics here: a raw
89
+ // `handle.toggle()` never writes `this.open`, so any later programmatic close via the prop was
90
+ // a same-value no-op, the model→overlay effect never re-ran, and the panel could stick open).
91
+ // The prop is the single source of truth; the model→overlay effect below drives
92
+ // `handle.open()`/`handle.close()` and the trait announces (ADR-0101) — the combo-box pattern.
93
+ this.listen(trigger, 'click', () => {
94
+ this.open = !this.open;
95
+ });
96
+ // overlay→model: when the Popover API light-dismisses (Escape / outside-click), the overlay
97
+ // controller emits `close` on the host. Sync the prop back so the two-way bind stays consistent
98
+ // (ADR-0019 — the renderer reads `toggle`, but we need `open` to already be false by then).
99
+ // At the time this listener fires, the overlay's internal `isOpen` is already false (the toggle
100
+ // listener in overlay.ts ran first), so the subsequent effect re-run calls `handle.close()` as
101
+ // a no-op (the idempotent guard in overlay.ts: `if (cleaned || !isOpen) return`).
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 in
106
+ // sync. Runs immediately on creation (eager first run): open=false → handle.close() (no-op, not
107
+ // 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
+ }
117
+ /**
118
+ * Create the control's two light-DOM parts ONCE (idempotent across disconnect/reconnect):
119
+ * - trigger: the first element child, marked with `data-part="trigger"` + ARIA affordances.
120
+ * - panel: a `<div data-part="panel">` appended to the host; non-trigger children are moved in.
121
+ * The overlay controller sets `popover="auto"` on the panel. `render()` stays the inherited VOID.
122
+ */
123
+ #ensureParts() {
124
+ if (this.#panel) {
125
+ // Parts persist through disconnect/reconnect — return the existing ones.
126
+ const trigger = this.querySelector('[data-part="trigger"]');
127
+ // The trigger must still be present (it's a light-DOM child, so it travels with the host).
128
+ if (!trigger)
129
+ throw new Error('ui-popover: trigger part lost on reconnect');
130
+ return { panel: this.#panel, trigger };
131
+ }
132
+ // Identify the disclosure trigger — the first element child provided by the author.
133
+ const trigger = this.firstElementChild;
134
+ if (!trigger) {
135
+ throw new Error('ui-popover: provide a trigger as the first child (e.g. <button>) before the panel content');
136
+ }
137
+ trigger.setAttribute('data-part', 'trigger');
138
+ // Create the surface panel. The overlay controller sets popover="auto" via
139
+ // `popup.setAttribute('popover', 'auto')` — we do NOT set it here (single-ownership, ADR-0017).
140
+ const panel = document.createElement('div');
141
+ panel.setAttribute('data-part', 'panel');
142
+ // tabindex="-1" lets `moveFocusIn()` (overlay LLD-C4) land on the panel itself when there are
143
+ // no interactive descendants — the fallback `popup.focus()` requires the element to be focusable.
144
+ panel.setAttribute('tabindex', '-1');
145
+ // Stable id for the trigger's `aria-controls` (created once, never reused).
146
+ panel.id = `ui-popover-panel-${++_nextPanelId}`;
147
+ // Move all non-trigger children into the panel (the disclosure content lives in the top-layer
148
+ // surface, not beside the trigger in the document flow — mirrors the modal child-move pattern).
149
+ let node = trigger.nextSibling;
150
+ while (node) {
151
+ const next = node.nextSibling;
152
+ panel.appendChild(node);
153
+ node = next;
154
+ }
155
+ this.appendChild(panel);
156
+ this.#panel = panel;
157
+ // Wire the trigger's ARIA affordances (present from the first render — the effect wires
158
+ // aria-expanded after connect, but aria-controls is stable and set here once).
159
+ trigger.setAttribute('aria-controls', panel.id);
160
+ return { panel, trigger };
161
+ }
162
+ }
163
+ if (!customElements.get('ui-popover'))
164
+ customElements.define('ui-popover', UIPopoverElement);