@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,228 @@
1
+ // field.ts — UIFieldElement, the visible label/description/error wrapper around ONE slotted form control
2
+ // (goals.md §G7 / decomp g7-field-form-provider slice s2, LLD-C4). BEHAVIOUR + props + the three parts +
3
+ // self-define ONLY; geometry/colour live in field.css (s3), the public contract in field.md (s4).
4
+ //
5
+ // `ui-field` extends `UIElement` (NOT `UIFormElement` — a field carries no form value of its own; wrapping
6
+ // it form-associated would double-submit) and reads the ADR-0050 `ui-form-connect` protocol event the base
7
+ // dispatches on EVERY `UIFormElement` connect. Labelling rides the ADR-0051 seam (`setFieldLabelling`) — the
8
+ // field never reaches into a control's internals or parts; the visible error rides the control's OWN
9
+ // user-invalid timing (`FormConnectDetail.userInvalid`), so there is exactly one timing source, never a
10
+ // second one duplicated here.
11
+ //
12
+ // Error-rendering history (two shipped bugs, same root smell — event-driven OBSERVATION of a timing
13
+ // signal, instead of just reading it reactively): (1) five DOM listeners (input/change/blur/invalid/reset)
14
+ // each calling a render — broke because `blur`/`invalid` are capture-only (they don't bubble): capture runs
15
+ // ancestor→target, so this field's listener (ancestor) could read the control's `userInvalid()` BEFORE the
16
+ // deeper `trackUserInvalid` listener (registered on the control itself) flipped `interacted` on the SAME
17
+ // dispatch — a stale read, first blur showed nothing. (2) a microtask-deferred re-render — passed every
18
+ // scripted/jsdom repro (a synchronous `dispatchEvent` call holds the JS stack through the WHOLE
19
+ // propagation, so a deferred read always lands after), but s11's real cross-engine browser probe falsified
20
+ // it: a genuine user-driven blur inserts a microtask CHECKPOINT between capture listeners (each listener
21
+ // return empties the call stack), so the deferred read still ran before the tracker's, in BOTH engines — no
22
+ // microtask-hop count survives a UA checkpoint. THIS (3rd) design has no event-listener race to have at
23
+ // all: `#associate` installs a scope-owned reactive EFFECT over the connect detail's tracked closures
24
+ // (`userInvalid()` / `validity()`, ADR-0050 §4) — every real trigger (typing, blur/change flipping
25
+ // `interacted`, a live `setCustomValidity` write, a reset writing `interacted` back to false) is a plain
26
+ // signal write the kernel wakes this effect for directly; there is no DOM dispatch order to depend on.
27
+ //
28
+ // Association is event-driven, first-wins, nearest-field-scoped: the field does NOT stop the connect event
29
+ // from bubbling (a provider ancestor still needs to see it), and accepts a control only when
30
+ // `control.closest('ui-field') === this` — the guard that also refuses a control belonging to a nested
31
+ // inner field. The option-A bridge (ADR-0051 cl.3) is the fallback for a control with no seam wire: when
32
+ // the associating control exposes an empty string `label` prop, a scope-owned effect writes this field's
33
+ // `label` text into it, disposed (and the write undone) on dissociation — never touching a consumer-set label.
34
+ //
35
+ // Upgrade order (define order ≠ tree order, ADR-0051 cl.5): a control can upgrade before this field does,
36
+ // so its one connect dispatch lands before any listener is live here. The complement is a one-shot catch-up
37
+ // scan at THIS field's own connect — `querySelectorAll('*')` → `instanceof UIFormElement` →
38
+ // `announceFormConnect()` — re-run again after a dissociation (while the field stays connected) so a second,
39
+ // still-slotted control can associate next (first-wins is per-tenure, not forever).
40
+ //
41
+ // Host carries no role/aria-* attribute (a structural wrapper); the only internals use is the
42
+ // `user-invalid` custom state, mirroring the associated control's own gate. `controls → dom` is the
43
+ // allowed import direction.
44
+ import { UIElement, UIFormElement, FORM_CONNECT_EVENT, prop, } from "../../dom/index.js";
45
+ // A per-instance id seed for the three parts (the text-field `messageSeq` precedent) — one sequence number
46
+ // shared by all three ids on a given instance, so `ui-field-label-3` / `-description-3` / `-error-3` read as
47
+ // a set at a glance.
48
+ let fieldSeq = 0;
49
+ function hasStringLabel(control) {
50
+ return typeof control.label === 'string';
51
+ }
52
+ const props = {
53
+ label: { ...prop.string(), reflect: true }, // the visible label text → [data-part=label] (reflect: false — text-field precedent)
54
+ description: prop.string(), // the visible description text → [data-part=description] (reflect: false)
55
+ };
56
+ export class UIFieldElement extends UIElement {
57
+ static props = props;
58
+ // The three parts, created ONCE (idempotent across reconnect — light-DOM children persist through
59
+ // disconnect, the text-field editor precedent). Nullable until `#ensureParts` runs; every other read
60
+ // happens after `connected()` has run, so a cast is safe there (the `#message as HTMLElement` precedent).
61
+ #label = null;
62
+ #description = null;
63
+ #error = null;
64
+ // The associated control's connect detail — `null` while unassociated. First-wins: set once per tenure,
65
+ // cleared by `#dissociate` (control removal, or the field's own teardown).
66
+ #assoc = null;
67
+ // The option-A bridge (ADR-0051 cl.3). `#bridgeControl` doubles as its own "armed" flag: non-null iff the
68
+ // bridge effect is live for THAT control, so `#dissociate` knows both whether to dispose the effect and
69
+ // whether to clear the control's label (never touching a consumer-set one).
70
+ #bridgeControl = null;
71
+ #bridgeDispose = null;
72
+ // The reactive error-render effect (design #3 — see the header history), installed per association
73
+ // (`#associate`) and disposed at `#dissociate` — the bridge-disposer pattern, applied twice.
74
+ #renderDispose = null;
75
+ connected() {
76
+ const { label, description } = this.#ensureParts();
77
+ // Visible text — reactive; the part ELEMENTS (and ids) stay stable across a prop change, so an
78
+ // associated control's labelling references never need a re-handoff.
79
+ this.effect(() => {
80
+ label.textContent = this.label;
81
+ });
82
+ this.effect(() => {
83
+ description.textContent = this.description;
84
+ });
85
+ // ── association (event-driven, the LLD-C1 event doing double duty) ──
86
+ // Deliberately NO stopPropagation — the event must keep bubbling to a provider ancestor.
87
+ this.listen(this, FORM_CONNECT_EVENT, (event) => {
88
+ const detail = event.detail;
89
+ if (!(detail?.control instanceof UIFormElement))
90
+ return; // defense — the event never fires without one
91
+ if (this.#assoc !== null)
92
+ return; // first-wins — a second control in this field stays un-associated
93
+ if (detail.control.closest('ui-field') !== this)
94
+ return; // nearest-field rule (also rejects a nested field's control)
95
+ this.#associate(detail);
96
+ });
97
+ // Error rendering is no longer a listener here at all — it rides a reactive effect installed per
98
+ // association (`#associate`, design #3 in the header history). No event listener, no dispatch-order
99
+ // to depend on: `assoc.control`'s own `formReset()` (text-field: writes `this.value` + the tracker's
100
+ // `interacted` back to false, both tracked signals) wakes the same effect, so a reset needs no separate
101
+ // observation path either.
102
+ // ── upgrade-path catch-up (LLD-C1 / ADR-0051 cl.5) — AFTER the listeners above install ──
103
+ this.#catchUp();
104
+ }
105
+ disconnected() {
106
+ this.#dissociate(); // resources still live — a field removed from around its control must not leave stale labelling
107
+ }
108
+ /**
109
+ * Accept a connect detail: store it, hand the parts to the control via the ADR-0051 seam, arm the
110
+ * option-A bridge, install the reactive error-render effect (design #3 — the header history), and watch
111
+ * the control's own connection signal so its removal dissociates this field.
112
+ */
113
+ #associate(detail) {
114
+ this.#assoc = detail;
115
+ detail.control.setFieldLabelling({ label: this.#label, description: this.#description, error: this.#error });
116
+ this.#armBridge(detail.control);
117
+ this.#renderDispose = this.effect(() => this.#renderValidity(detail));
118
+ this.listen(detail.signal, 'abort', () => this.#dissociate(), { once: true });
119
+ }
120
+ /**
121
+ * Idempotence insurance (the `#assoc === null` early return) — TWO paths reach this: the associated
122
+ * control's own abort listener (removed from the field) and this field's `disconnected()` (field itself
123
+ * removed). Clears the labelling handoff, the bridge, and the visible error; then, IFF the field itself
124
+ * stays connected, re-runs the catch-up scan so a remaining slotted control can associate next — first-wins
125
+ * is per-tenure, not forever. Skipped on the field-disconnect path: re-associating from a field being torn
126
+ * down would mint labelling refs into a detached subtree.
127
+ */
128
+ #dissociate() {
129
+ const assoc = this.#assoc;
130
+ if (assoc === null)
131
+ return;
132
+ this.#renderDispose?.(); // stop the reactive render — otherwise it outlives dissociation, reading a removed control
133
+ this.#renderDispose = null;
134
+ this.#bridgeDispose?.(); // stop the scope-owned effect — otherwise it outlives dissociation and keeps writing
135
+ this.#bridgeDispose = null;
136
+ if (this.#bridgeControl)
137
+ this.#bridgeControl.label = ''; // only the bridge's own write; a consumer-set label stands
138
+ this.#bridgeControl = null;
139
+ assoc.control.setFieldLabelling(null); // safe on a detached node — a plain signal write
140
+ this.#assoc = null;
141
+ const error = this.#error;
142
+ error.textContent = '';
143
+ error.hidden = true;
144
+ this.internals.states?.delete('user-invalid');
145
+ if (this.isConnected)
146
+ this.#catchUp(); // per-tenure re-arm — skipped when it is the field itself being removed
147
+ }
148
+ /**
149
+ * The option-A bridge (ADR-0051 cl.3): the fallback name path for a control with no seam wire. Arms ONLY
150
+ * when the control exposes a string `label` prop currently equal to `''` — a pre-set (consumer-owned)
151
+ * label is never touched, arriving or leaving. The effect tracks this field's `label` prop, so a later
152
+ * edit keeps the bridged control's name current.
153
+ */
154
+ #armBridge(control) {
155
+ if (!hasStringLabel(control) || control.label !== '')
156
+ return;
157
+ this.#bridgeControl = control;
158
+ this.#bridgeDispose = this.effect(() => {
159
+ control.label = this.label;
160
+ });
161
+ }
162
+ /**
163
+ * One-shot re-announce of every already-connected `UIFormElement` descendant (LLD-C1 / ADR-0051 cl.5) —
164
+ * covers a control that upgraded BEFORE this field did, whose own connect dispatch landed before any
165
+ * listener here was live. A not-yet-upgraded control is skipped (the `instanceof` filter) and covers
166
+ * itself: its own `connectedCallback` dispatches at upgrade, once this field is already listening.
167
+ */
168
+ #catchUp() {
169
+ for (const el of this.querySelectorAll('*')) {
170
+ if (el instanceof UIFormElement)
171
+ el.announceFormConnect();
172
+ }
173
+ }
174
+ /**
175
+ * The reactive error render (design #3 — the header history). Reads the associated control's OWN gates
176
+ * through the ADR-0050 connect closures — `assoc.validity()` / `assoc.userInvalid()` are TRACKED reads
177
+ * (an effect subscribes to whatever signals they close over: the control's `value`/`required` props, the
178
+ * `trackUserInvalid` tracker's `interacted` signal, `setCustomValidity`'s signal); `control.validationMessage`
179
+ * (the platform DOM getter) is deliberately NOT read here — an effect cannot subscribe to it, which is
180
+ * exactly the gap the two prior designs papered over with an event listener. Installed once per
181
+ * association (`this.effect(...)` in `#associate`) — its lifetime IS the association's, so there is no
182
+ * `#assoc` null-guard to write: `#dissociate` disposes this effect before anything else. An empty message
183
+ * while user-invalid is treated as not-showing — no empty-but-visible error box.
184
+ */
185
+ #renderValidity(assoc) {
186
+ const verdict = assoc.validity();
187
+ const message = !verdict.valid ? verdict.message : '';
188
+ const showing = assoc.userInvalid() && message !== '';
189
+ const error = this.#error;
190
+ error.textContent = showing ? message : '';
191
+ error.hidden = !showing;
192
+ if (showing)
193
+ this.internals.states?.add('user-invalid');
194
+ else
195
+ this.internals.states?.delete('user-invalid');
196
+ }
197
+ /**
198
+ * Create the three parts ONCE (idempotent across reconnect — light-DOM children persist through
199
+ * disconnect, the text-field editor precedent). DOM placement IS the reading order: label PREPENDED,
200
+ * description + error APPENDED (author children — the slotted control — sit between); column flow needs
201
+ * no CSS `order`. A `<div>` label, not a `<label>`: no `for` semantics apply here, and a bare `<label>`
202
+ * invites click-forwarding expectations this seam doesn't need. `error` starts hidden + empty.
203
+ */
204
+ #ensureParts() {
205
+ if (this.#label && this.#description && this.#error) {
206
+ return { label: this.#label, description: this.#description, error: this.#error };
207
+ }
208
+ const seq = ++fieldSeq;
209
+ const label = document.createElement('div');
210
+ label.setAttribute('data-part', 'label');
211
+ label.id = `ui-field-label-${seq}`;
212
+ this.#label = label;
213
+ this.prepend(label);
214
+ const description = document.createElement('div');
215
+ description.setAttribute('data-part', 'description');
216
+ description.id = `ui-field-description-${seq}`;
217
+ this.#description = description;
218
+ const error = document.createElement('div');
219
+ error.setAttribute('data-part', 'error');
220
+ error.id = `ui-field-error-${seq}`;
221
+ error.hidden = true;
222
+ this.#error = error;
223
+ this.append(description, error);
224
+ return { label, description, error };
225
+ }
226
+ }
227
+ if (!customElements.get('ui-field'))
228
+ customElements.define('ui-field', UIFieldElement);
@@ -0,0 +1,40 @@
1
+ /* form-provider.css — ui-form-provider, the single-file component stylesheet (ADR-0003). The degenerate
2
+ * minimal case of the fleet's two-block shape (text-field.css / column.css): a pure coordination element
3
+ * carries NO visual voice, so both sections collapse to their smallest legal form —
4
+ *
5
+ * [1] TOKEN BLOCK — `:where(ui-form-provider)` declares NOTHING. The provider discovers/aggregates
6
+ * `UIFormElement` descendants and composes with an optional native `<form>` (LLD-C7,
7
+ * field-form-provider.lld.md); it has no colour, geometry, or spacing opinion of its own, so there is
8
+ * no `--ui-form-provider-*` chain to declare. An EMPTY token block is deliberately NOT invented here
9
+ * (LLD-C8) — this comment stands in for the section, so the token-hygiene probes need no special
10
+ * case: an absent chain is still zero raw `--md-sys-color-*` leakage, zero non-`--ui-form-provider-*` consumption.
11
+ * [2] STYLES BLOCK — `@scope (ui-form-provider)` sets exactly one declaration: `display: block`. An
12
+ * unstyled custom element defaults to `inline`, which would lay its slotted block-level descendants
13
+ * (fields, buttons) out inline against the host — a first-paint layout defect. No further rule: no
14
+ * colour, no spacing, no geometry: the coordination host paints nothing, the slotted subtree owns
15
+ * every box.
16
+ *
17
+ * Standalone stylesheet — linked via the component-styles barrel (wired in s12); NEVER injected from
18
+ * form-provider.ts (behaviour-only `.ts`, no runtime style injection — plan §2).
19
+ *
20
+ * ADR-0103 re-confirms this contract UNCHANGED: unlike `ui-radio-group` (a value-owning group whose direct-
21
+ * children discovery makes a layout default the only fix, Lane A), the provider's descendants are discovered
22
+ * by event bubbling — arbitrary composition (grids, rows, cards) the provider must not fight — so vertical
23
+ * rhythm stays the page author's taught idiom (a `ui-column[gap]` wrap around the fields, Lane C), not a
24
+ * `display:flex` default here.
25
+ */
26
+
27
+ /* ════════════════ [1] TOKEN BLOCK — :where(ui-form-provider) declares NOTHING ════════════════ */
28
+ /* Deliberate absence (LLD-C8): a pure coordination element has no visual voice — there is no
29
+ --ui-form-provider-* token chain to declare, and nothing rides [size]/[tone]/state to repoint (LLD-C7:
30
+ NO static props on this element — attributes: []). */
31
+
32
+ /* ════════════════ [2] STYLES BLOCK — @scope (ui-form-provider) consumes NOTHING ════════════════ */
33
+
34
+ @scope (ui-form-provider) {
35
+ :scope {
36
+ display: block; /* the host's only rule: an unstyled custom element defaults to `inline`, which would
37
+ lay its slotted block-level descendants (fields, buttons) out inline against it — `block` is the
38
+ coordination host's structural default, not a styling opinion. */
39
+ }
40
+ }
@@ -0,0 +1,42 @@
1
+ import { UIElement, UIFormElement } from '../../dom/index.js';
2
+ import type { FormValue } from '../../dom/index.js';
3
+ /**
4
+ * The `submit()` aggregate detail (LLD-C7 — pinned event naming). The closed house event vocab has no
5
+ * `submit`; a provider-level submit IS a commit semantically, so it rides `change`. Disambiguation from a
6
+ * bubbled member `change` (which carries `detail: null` through the provider): `event.target === provider`.
7
+ */
8
+ export interface FormSubmitDetail {
9
+ entries: ReadonlyArray<readonly [string, FormValue]>;
10
+ values: Readonly<Record<string, FormValue>>;
11
+ }
12
+ export declare class UIFormProviderElement extends UIElement {
13
+ #private;
14
+ static props: {};
15
+ protected connected(): void;
16
+ protected disconnected(): void;
17
+ /** Live registered controls, registration order — a reactive read (projects `registry.members`). Empty while disconnected. */
18
+ get controls(): readonly UIFormElement[];
19
+ /** Submission entries — native FormData parity (see form-registry.ts). Empty while disconnected. */
20
+ entries(): ReadonlyArray<readonly [string, FormValue]>;
21
+ /** Keyed convenience view of entries() — last entry wins on a duplicate name. Empty while disconnected. */
22
+ values(): Readonly<Record<string, FormValue>>;
23
+ /** Members whose merged verdict is invalid, registration order. Empty while disconnected. */
24
+ invalid(): readonly UIFormElement[];
25
+ /** invalid().length === 0. Vacuously true while disconnected. */
26
+ valid(): boolean;
27
+ /**
28
+ * If invalid: `reportValidity()` on the FIRST invalid member (registration order — native "focus the
29
+ * first invalid control" parity; the UA anchors/announces it) and return `false`, no event. Else emit
30
+ * the aggregate as a `change` (this module's `FormSubmitDetail` — see the type doc for the
31
+ * disambiguation from a bubbled member `change`) and return `true`.
32
+ */
33
+ submit(): boolean;
34
+ /**
35
+ * Native composition (decomp `provider_and_native_form`): partition members by their public `.form` —
36
+ * each DISTINCT non-null owning `<form>` gets ONE `form.reset()` (the platform itself walks that form's
37
+ * FACE members' `formResetCallback`s — calling reset per-member here would double-reset); form-less
38
+ * members get a direct `formResetCallback()` call (the public platform callback). Both paths end in the
39
+ * base's `ui-form-reset` dispatch (LLD-C1, `dom/form.ts`) — this method never dispatches it itself.
40
+ */
41
+ reset(): void;
42
+ }
@@ -0,0 +1,105 @@
1
+ // form-provider.ts — UIFormProviderElement, the fleet's first context/provider primitive (ADR-0050;
2
+ // LLD-C7, decomp g7-field-form-provider slice s5). BEHAVIOUR + self-define ONLY — geometry lives in
3
+ // form-provider.css (s6), the public contract in form-provider.md (s7).
4
+ //
5
+ // A pure coordination element: NO static props (a coordination element takes no configuration — the
6
+ // descriptor's `attributes: []`), extends `UIElement` directly — NOT `UIFormElement` (the provider carries
7
+ // no value of its own; wrapping it `formAssociated` would double-submit, ADR-0050 alternatives) and NOT
8
+ // `UIContainerElement` (no surface paint). Discovery/aggregation is entirely the ADR-0050 connect-time
9
+ // registration event + `traits/form-registry.ts`'s reactive registry (the listener, the four aggregate
10
+ // computeds, teardown-by-abort all live there); this class is the thin public surface over that registry
11
+ // plus the two coordination methods, submit()/reset().
12
+ //
13
+ // Upgrade-path catch-up (LLD-C1 / ADR-0051 cl.5): "ancestors connect before descendants" holds for
14
+ // INSERTION order but not custom-element UPGRADE order — pre-existing DOM whose control module upgrades
15
+ // before this provider's does dispatches `ui-form-connect` into the void (no listener yet). `connected()`
16
+ // runs a one-shot `querySelectorAll('*')` AFTER the registry's listener installs and re-announces every
17
+ // already-connected `UIFormElement` it finds via `announceFormConnect()` — the registry's dup guard makes
18
+ // the re-announce idempotent, so this scan never double-registers a control that connected in time.
19
+ //
20
+ // `controls → dom + traits` is the allowed import direction.
21
+ import { UIElement, UIFormElement } from "../../dom/index.js";
22
+ import { formRegistry } from "../../traits/form-registry.js";
23
+ export class UIFormProviderElement extends UIElement {
24
+ // EMPTY by design — a coordination element takes no configuration (descriptor `attributes: []`). The
25
+ // field exists (rather than being omitted) for the fleet convention + the s10 descriptor trip-wire
26
+ // (`compareDescriptorToProps` needs a live PropsSchema object to compare against).
27
+ static props = {};
28
+ #registry = null; // created in connected(), nulled in disconnected()
29
+ connected() {
30
+ this.#registry = formRegistry(this);
31
+ // Upgrade-path catch-up (see header) — AFTER the registry's listener is live, so a re-announced
32
+ // control registers immediately rather than racing this scan.
33
+ for (const el of this.querySelectorAll('*')) {
34
+ if (el instanceof UIFormElement)
35
+ el.announceFormConnect();
36
+ }
37
+ }
38
+ disconnected() {
39
+ this.#registry?.release();
40
+ this.#registry = null;
41
+ }
42
+ /** Live registered controls, registration order — a reactive read (projects `registry.members`). Empty while disconnected. */
43
+ get controls() {
44
+ return this.#registry?.members.value.map((member) => member.control) ?? [];
45
+ }
46
+ /** Submission entries — native FormData parity (see form-registry.ts). Empty while disconnected. */
47
+ entries() {
48
+ return this.#registry?.entries.value ?? [];
49
+ }
50
+ /** Keyed convenience view of entries() — last entry wins on a duplicate name. Empty while disconnected. */
51
+ values() {
52
+ return this.#registry?.values.value ?? {};
53
+ }
54
+ /** Members whose merged verdict is invalid, registration order. Empty while disconnected. */
55
+ invalid() {
56
+ return this.#registry?.invalid.value ?? [];
57
+ }
58
+ /** invalid().length === 0. Vacuously true while disconnected. */
59
+ valid() {
60
+ return this.#registry?.valid.value ?? true;
61
+ }
62
+ /**
63
+ * If invalid: `reportValidity()` on the FIRST invalid member (registration order — native "focus the
64
+ * first invalid control" parity; the UA anchors/announces it) and return `false`, no event. Else emit
65
+ * the aggregate as a `change` (this module's `FormSubmitDetail` — see the type doc for the
66
+ * disambiguation from a bubbled member `change`) and return `true`.
67
+ */
68
+ submit() {
69
+ // Disconnected FIRST, ahead of the validity check: valid() is a read and degrades gracefully
70
+ // (vacuously true with no registry), but submit() is an ACTION — an empty aggregate from a
71
+ // disconnected provider means "I can't see the form," not "the form is empty," so it must refuse
72
+ // (no reportValidity, no emit), not report a hollow success.
73
+ if (this.#registry === null)
74
+ return false;
75
+ if (!this.valid()) {
76
+ this.invalid()[0]?.reportValidity();
77
+ return false;
78
+ }
79
+ this.emit('change', { entries: this.entries(), values: this.values() });
80
+ return true;
81
+ }
82
+ /**
83
+ * Native composition (decomp `provider_and_native_form`): partition members by their public `.form` —
84
+ * each DISTINCT non-null owning `<form>` gets ONE `form.reset()` (the platform itself walks that form's
85
+ * FACE members' `formResetCallback`s — calling reset per-member here would double-reset); form-less
86
+ * members get a direct `formResetCallback()` call (the public platform callback). Both paths end in the
87
+ * base's `ui-form-reset` dispatch (LLD-C1, `dom/form.ts`) — this method never dispatches it itself.
88
+ */
89
+ reset() {
90
+ const resetForms = new Set();
91
+ for (const control of this.controls) {
92
+ const form = control.form;
93
+ if (form === null) {
94
+ control.formResetCallback();
95
+ continue;
96
+ }
97
+ if (resetForms.has(form))
98
+ continue; // already reset via this form
99
+ resetForms.add(form);
100
+ form.reset();
101
+ }
102
+ }
103
+ }
104
+ if (!customElements.get('ui-form-provider'))
105
+ customElements.define('ui-form-provider', UIFormProviderElement);
@@ -0,0 +1,72 @@
1
+ /* grid.css — ui-grid, the single-file component stylesheet (ADR-0003). Two clearly-SECTIONED blocks, mirroring
2
+ * button.css / text-field.css:
3
+ *
4
+ * [1] TOKEN BLOCK — `:where(ui-grid)` (specificity 0,0,0) DECLARES the `--ui-grid-*` chain: the `--ui-grid-min`
5
+ * track FLOOR (a sensible default; grid.ts repoints it inline for a per-instance `min` override) and the
6
+ * `--ui-grid-gap` off the `--md-sys-space-{step}` density-responsive LAYOUT-spacing ladder (ADR-0015 cl.4 /
7
+ * geometry.md "--space-* is layout spacing, not control geometry"). `[gap=step]` repoints the gap (gap
8
+ * reflects, so the attribute is present). NO `--md-sys-height-*` — a Container/layout primitive has no control
9
+ * height (geometry.md "five size-classes"); spacing rides `--md-sys-space` × `[density]` only.
10
+ * [2] STYLES BLOCK — `@scope (ui-grid)` CONSUMES only `--ui-grid-*` for its layout: `display: grid` with the
11
+ * INTRINSIC auto-fit/`minmax` track model (ADR-0016 cl.3/4) — `repeat(auto-fit, minmax(var(--ui-grid-min),
12
+ * 1fr))` reflows by the grid's OWN rendered width (more tracks when wide, fewer when narrow) with NO
13
+ * explicit column-count prop and NO `@container` rule (auto-fit IS the responsiveness — unlike ui-row/
14
+ * ui-column, which wrap via `@container`). `gap` rides `--ui-grid-gap`.
15
+ *
16
+ * SURFACE + FORCED-COLORS are the shared sheet's job, NOT duplicated here: the `--ui-container-bg`/`-tint`
17
+ * elevation×brightness seam AND the `@media (forced-colors: active)` survival both cover `ui-grid` from
18
+ * controls/_surface/container.css (the `@import`-FIRST sheet in the component-styles barrel, s12). A bare
19
+ * ui-grid is transparent (an unset container is unchanged); `[elevation]`/`[brightness]` repoint the shared
20
+ * seam. `ui-grid` establishes NO `container-type` of its own (ADR-0100 — an intrinsically-sized primitive can
21
+ * never safely be a query container) — moot for its own responsiveness anyway, since the auto-fit/`minmax`
22
+ * track model below is TRACK SIZING, not a container query, so removing the establishment leaves it byte-for-
23
+ * byte unaffected. This sheet owns LAYOUT only.
24
+ *
25
+ * Standalone stylesheet — linked via the component-styles barrel (s12); NEVER injected from grid.ts
26
+ * (behaviour-only `.ts`, no runtime style injection — plan §2; the `min` token thread is the values-API, not a
27
+ * style rule). */
28
+
29
+ /* ════════════════ [1] TOKEN BLOCK — :where(ui-grid) declares --ui-grid-* ════════════════ */
30
+
31
+ :where(ui-grid) {
32
+ /* the auto-fit/minmax track FLOOR (ADR-0016 cl.3) — a sensible default; grid.ts repoints this INLINE for a
33
+ per-instance `min` override (inline custom property outranks this `:where()` declaration). */
34
+ --ui-grid-min: 16rem;
35
+ /* gap off the --md-sys-space density-responsive ladder (ADR-0015 cl.4); default = none (a bare grid has no gap).
36
+ [gap=step] repoints below — gap reflects, so the attribute is present for these selectors. */
37
+ --ui-grid-gap: var(--md-sys-space-none);
38
+ }
39
+
40
+ /* gap → the --md-sys-space ladder (the step vocabulary mirrors flexProps' gap enum 1:1) */
41
+ :where(ui-grid[gap='xs']) {
42
+ --ui-grid-gap: var(--md-sys-space-xs);
43
+ }
44
+ :where(ui-grid[gap='sm']) {
45
+ --ui-grid-gap: var(--md-sys-space-sm);
46
+ }
47
+ :where(ui-grid[gap='md']) {
48
+ --ui-grid-gap: var(--md-sys-space-md);
49
+ }
50
+ :where(ui-grid[gap='lg']) {
51
+ --ui-grid-gap: var(--md-sys-space-lg);
52
+ }
53
+ :where(ui-grid[gap='xl']) {
54
+ --ui-grid-gap: var(--md-sys-space-xl);
55
+ }
56
+ :where(ui-grid[gap='2xl']) {
57
+ --ui-grid-gap: var(--md-sys-space-2xl);
58
+ }
59
+
60
+ /* ════════════════ [2] STYLES BLOCK — @scope (ui-grid) consumes ONLY --ui-grid-* ════════════════ */
61
+
62
+ @scope (ui-grid) {
63
+ :scope {
64
+ display: grid;
65
+ /* the INTRINSIC reflow (ADR-0016 cl.3/4): auto-fit packs as many `--ui-grid-min`-floored tracks as the
66
+ grid's OWN width fits and reflows the column count by width — no explicit column-count prop, no
67
+ `@container` rule. Each track flexes 1fr between the floor and the available share. */
68
+ grid-template-columns: repeat(auto-fit, minmax(var(--ui-grid-min), 1fr));
69
+ gap: var(--ui-grid-gap);
70
+ box-sizing: border-box;
71
+ }
72
+ }
@@ -0,0 +1,60 @@
1
+ import { type ReactiveProps } from '../../dom/index.js';
2
+ import { UIContainerElement } from '../../dom/container.js';
3
+ declare const props: {
4
+ gap: {
5
+ reflect: true;
6
+ type: import("../../index.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
7
+ default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
8
+ attribute?: string | false;
9
+ };
10
+ min: {
11
+ reflect: true;
12
+ type: import("../../index.js").PropType<string>;
13
+ default: string;
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 UIGridElement extends ReactiveProps<typeof props> {
30
+ }
31
+ export declare class UIGridElement extends UIContainerElement {
32
+ static props: {
33
+ gap: {
34
+ reflect: true;
35
+ type: import("../../index.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
36
+ default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
37
+ attribute?: string | false;
38
+ };
39
+ min: {
40
+ reflect: true;
41
+ type: import("../../index.js").PropType<string>;
42
+ default: string;
43
+ attribute?: string | false;
44
+ };
45
+ elevation: {
46
+ reflect: true;
47
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
48
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
49
+ attribute?: string | false;
50
+ };
51
+ brightness: {
52
+ reflect: true;
53
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
54
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
55
+ attribute?: string | false;
56
+ };
57
+ };
58
+ protected connected(): void;
59
+ }
60
+ export {};
@@ -0,0 +1,56 @@
1
+ // grid.ts — UIGridElement, the auto-fit/minmax track-grid layout primitive (goals.md §G9 / ADR-0016 cl.3;
2
+ // decomp g9-containers slice s6). BEHAVIOUR + props + self-define ONLY; the layout/surface live in grid.css
3
+ // (the @scope block), the public contract in grid.md.
4
+ //
5
+ // `ui-grid` is a STRUCTURAL container — it extends `UIContainerElement` (the shared surface base, ADR-0015),
6
+ // is NOT form-associated (no value/validity), and has NO control height (geometry.md Container/layout class —
7
+ // spacing rides `--md-sys-space` × density, never `--md-sys-height-*`). Its children are arbitrary light-DOM grid
8
+ // items (the default cell); it `render()`s nothing (the inherited void), so the user's children are never
9
+ // clobbered (the host-as-grid pattern — the CSS lays them out).
10
+ //
11
+ // The RESPONSIVENESS is INTRINSIC (ADR-0016 cl.3/4): `grid-template-columns: repeat(auto-fit, minmax(min,
12
+ // 1fr))` reflows by the grid's OWN rendered width — more tracks when wide, fewer when narrow — with NO
13
+ // explicit column-count prop and NO `@container` rule. The one author lever is `min`, the minmax() track
14
+ // floor: an arbitrary CSS `<length>` that cannot ride an attribute selector (and `attr()` typed is not
15
+ // WebKit-safe), so it is threaded into the role-pure `--ui-grid-min` token via the values-API (the effect
16
+ // below) — the same token-repoint the CSS does, but with a runtime value. Unset ⇒ the grid.css default floor.
17
+ //
18
+ // `ui-grid` IS an A2UI catalog type — `Grid` (ADR-0087 Fork A, Wave C; supersedes ADR-0016's earlier
19
+ // non-catalog exclusion) renders to `ui-grid`. Reach for `Grid` when the layout should reflow its column count
20
+ // responsively with available width; prefer `Row`/`Column` (with an explicit `wrap`) for an author-controlled
21
+ // arrangement, and `List` when the children are an itemized collection needing list semantics (a2ui-catalog.
22
+ // spec.md §5.2). Imports the dom layer only (`controls → dom`); `UIContainerElement` is pulled from
23
+ // container.ts directly (the dom barrel re-exports it at s12, the integration slice).
24
+ import { prop } from "../../dom/index.js";
25
+ import { UIContainerElement } from "../../dom/container.js";
26
+ const props = {
27
+ // The two surface axes (elevation/brightness), spread from the shared base — props.ts has no static-props
28
+ // prototype merge, so the base exposes a SPREADABLE bag the subclass folds in (the ADR-0013 formProps pattern).
29
+ ...UIContainerElement.surfaceProps,
30
+ // `gap` — the ONE flex-grammar prop a track grid consumes (the `--md-sys-space` density-responsive ladder,
31
+ // ADR-0016 cl.1). Pulled as a single entry from the shared flexProps (a grid has no align/justify/wrap).
32
+ gap: UIContainerElement.flexProps.gap,
33
+ // `min` — the minmax() track FLOOR, an arbitrary CSS `<length>`. Threaded into the role-pure `--ui-grid-min`
34
+ // token (the connected() effect); unset ⇒ the grid.css default floor. Reflects so the attribute is the
35
+ // inspectable public API (the dimension itself rides the inline token, not an attribute selector).
36
+ min: { ...prop.string(), reflect: true },
37
+ };
38
+ export class UIGridElement extends UIContainerElement {
39
+ static props = props;
40
+ connected() {
41
+ // Thread the `min` `<length>` into the role-pure `--ui-grid-min` token seam — the one value the grid.css
42
+ // template's `minmax()` floor reads. An inline custom property outranks the `:where()` default, so a set
43
+ // `min` overrides the floor; unset ⇒ removeProperty ⇒ the grid.css default applies. Scope-owned: re-runs on
44
+ // `min` and re-applies on reconnect; disposed with the connection scope (zero LIVE residue — the inline
45
+ // token persists on the detached host like any light-DOM state, but the effect itself is torn down).
46
+ this.effect(() => {
47
+ const min = this.min;
48
+ if (min)
49
+ this.style.setProperty('--ui-grid-min', min);
50
+ else
51
+ this.style.removeProperty('--ui-grid-min');
52
+ });
53
+ }
54
+ }
55
+ if (!customElements.get('ui-grid'))
56
+ customElements.define('ui-grid', UIGridElement);
@@ -0,0 +1,37 @@
1
+ /* icon.css — ui-icon, the Display-class icon primitive (LLD-C5 §3, ADR-0065/0066). Two sectioned blocks:
2
+ *
3
+ * [1] TOKEN BLOCK — `:where(ui-icon)` declares --md-sys-icon-size: 1em. ui-icon has NO size/tone lever
4
+ * (no [size] ramp, no [variant] repoint) — it is a 1em×1em cell by design; a consumer sizes it via
5
+ * ambient `font-size` or an explicit `inline-size` override on the host (LLD §3, geometry.md Display
6
+ * class: "intrinsic structural sizing").
7
+ * [2] STYLES BLOCK — `@scope (ui-icon)` consumes ONLY --md-sys-icon-size. `color: inherit` so the injected
8
+ * <svg fill="currentColor"> (resolve.ts) tracks the surrounding ink with no per-icon color plumbing —
9
+ * including in forced-colors mode, where the CONSUMING context's ink already resolves to the platform
10
+ * system color, so ui-icon needs no dedicated `@media (forced-colors: active)` block of its own.
11
+ *
12
+ * The content-into-cell contract (LLD §3) is what makes this file this small: an injected <svg width=100%
13
+ * height=100%> fills whatever cell it lands in — an [data-role='icon'] cell at icon size, a
14
+ * [data-role='caret'] cell inset to font-rhythm by that cell's OWN existing padding — with ZERO change to
15
+ * button.css/select.css. This sheet only styles the STANDALONE <ui-icon> host itself.
16
+ */
17
+
18
+ /* ════════════════ [1] TOKEN BLOCK — :where(ui-icon) declares --md-sys-icon-* ════════════════ */
19
+
20
+ :where(ui-icon) {
21
+ --md-sys-icon-size: 1em;
22
+ }
23
+
24
+ /* ════════════════ [2] STYLES BLOCK — @scope (ui-icon) consumes ONLY --md-sys-icon-* ════════════════ */
25
+
26
+ @scope (ui-icon) {
27
+ :scope {
28
+ display: inline-flex;
29
+ inline-size: var(--md-sys-icon-size);
30
+ block-size: var(--md-sys-icon-size);
31
+ color: inherit;
32
+ }
33
+
34
+ :scope svg {
35
+ display: block;
36
+ }
37
+ }