@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,399 @@
1
+ // form.ts — `UIFormElement`, the FACE form-associated base (ADR-0013; decomp g4-g6 slice s1).
2
+ //
3
+ // The base every value-carrying FACE control (`ui-text-field`, `ui-checkbox`, `ui-switch`, `ui-select`,
4
+ // `ui-listbox`) extends. It turns a `UIElement` into a form participant: `static formAssociated = true`
5
+ // activates the platform's form lifecycle, and the inherited `ElementInternals` — acquired ONCE in the
6
+ // `UIElement` constructor, reused here and NEVER re-`attachInternals()`d (a second acquisition throws) —
7
+ // carries the value + validity to the owning `<form>`. Two scope-owned effects publish the control's
8
+ // `formValue()` / `formValidity()` hooks to `internals.setFormValue` / `setValidity`, so a control declares
9
+ // WHAT its value + validity are and the base wires the reactive plumbing. The base owns the three universal
10
+ // form attributes via the SPREADABLE `formProps` (props.ts has no static-props prototype merge — the
11
+ // subclass folds them into its own `static props`); it deliberately does NOT own a typed `value` — the
12
+ // value's type + codec are the subclass's (string for text-field; a `checked` boolean for checkbox; …).
13
+ //
14
+ // Super-wrapped wiring keeps subclass authoring identical to a plain `UIElement` control: a subclass still
15
+ // overrides the clean, no-`super` `connected()` hook; this base wraps `connectedCallback` to add its own
16
+ // scope-owned effects after `super` has opened the scope.
17
+ //
18
+ // G7 growth (ADR-0050 + ADR-0051, the fleet's first context/provider primitive + its labelling seam):
19
+ // • ADR-0050 — at the END of `connectedCallback` (scope + effects all live) the base dispatches the
20
+ // `ui-form-connect` protocol event: a composed, bubbling, NOT-cancelable event (`dispatchEvent`, not
21
+ // `this.emit` — `emit`'s cancelable/public shape is for consumer events) whose detail is the control's
22
+ // PUBLIC reactive surface, closing over `formValue()`/the merged validity/`formUserInvalid()`, plus the
23
+ // live connection `AbortSignal` (the teardown handle a registry subscribes `abort` on — a disconnected
24
+ // control cannot dispatch, so deregistration rides the handle, never a second event). `formResetCallback`
25
+ // grows a matching `ui-form-reset` dispatch (no detail — the target IS the resetting control).
26
+ // • ADR-0051 — a small signal-backed field-labelling seam: `setFieldLabelling`/`fieldLabelling` are the
27
+ // handoff surface a `ui-field` calls (elements, not id strings — the internals path consumes elements);
28
+ // a base scope-owned effect forwards the current labelling through the overridable `applyFieldLabelling`
29
+ // hook, whose GUARDED default reflects via `ElementInternals` ARIA-element accessors ONLY when
30
+ // `internals.role` is set (feature-detected — the tabs `reflectAriaElements` precedent) — a part-role
31
+ // control (text-field) overrides it. `formUserInvalid()` is the parallel hook feeding the connect
32
+ // detail's `userInvalid` read; a control composing `trackUserInvalid` overrides it with the tracker gate.
33
+ // Both are additive: provider-less/field-less usage shows ZERO behavioral drift (the events bubble to
34
+ // nobody; `fieldLabelling` stays `null`, so `applyFieldLabelling`'s default no-op — jsdom has no
35
+ // `internals.role` reflection surface either — never fires a visible change).
36
+ // • F1 (review-caught upgrade-order hole): the one connect dispatch above assumes a listening
37
+ // provider/field is ALREADY live — true for insertion order, but NOT custom-element UPGRADE order
38
+ // (pre-existing DOM whose control module upgrades before the provider/field module does ⇒ the dispatch
39
+ // fires into the void). `announceFormConnect()` (public) re-dispatches the SAME event with a fresh
40
+ // detail on demand — a provider/field's one-shot catch-up scan calls it per already-connected control
41
+ // it discovers late. Blanket-safe: the registry/field acceptance guards are already idempotent.
42
+ // • Nested-member guard (defect repair — A2UI patterns-page-caught): a `UIFormElement` never announces
43
+ // when it has a `UIFormElement` ANCESTOR — it is an INTERNAL PART (e.g. text-field type=date's own
44
+ // `<ui-calendar>`; a radio inside its radio-group), never a registry/field member. Both dispatch sites
45
+ // route through the one `#dispatchFormConnect` choke point so the guard can never be bypassed by one
46
+ // of the two. See `#dispatchFormConnect`'s own doc for the write-loop this closes.
47
+ //
48
+ // Imports only `../reactive` (the kernel) + same-layer `./element.ts` / `./props.ts` (the layering holds).
49
+ // `ElementInternals` / `ValidityState` / `ValidityStateFlags` / `HTMLFormElement` / `HTMLElement` / `File` /
50
+ // `FormData` / `AbortSignal` / `CustomEvent` are ambient DOM globals, not imports.
51
+ var _a;
52
+ import { signal } from "../reactive/index.js";
53
+ import { UIElement } from "./element.js";
54
+ import { prop } from "./props.js";
55
+ // ── ADR-0050 — the protocol events ──────────────────────────────────────────────
56
+ // The `ui-` namespace: base↔provider/field plumbing, OUTSIDE the public component-event vocab
57
+ // (change·input·select·open·close·toggle). Both composed + bubbling, NOT cancelable (dispatched directly
58
+ // with `dispatchEvent`, not `this.emit` — `emit`'s cancelable/public shape is for consumer events).
59
+ /** Fired once, at the end of `connectedCallback` — the control announcing full connection to the nearest
60
+ * provider/field ancestor. */
61
+ export const FORM_CONNECT_EVENT = 'ui-form-connect';
62
+ /** Fired from `formResetCallback`, after `formReset()` — detail: none (the target IS the resetting
63
+ * control). Covers BOTH reset paths: native `form.reset()` and a provider's direct `formResetCallback()` call. */
64
+ export const FORM_RESET_EVENT = 'ui-form-reset';
65
+ // Reflect an ARIA element-list relation through `internals` (feature-detected — the reflection accessors
66
+ // landed in modern Chromium/WebKit but NOT jsdom, so this is a no-op under the jsdom inner loop and live in
67
+ // the real engines, verified by the s11 browser smokes; the tabs `reflectAriaElements` precedent — tab.ts /
68
+ // tab-panel.ts carry their own local copies too, the folder ships no shared module for this one-liner).
69
+ // `null` clears the relation.
70
+ function reflectAriaElements(internals, name, elements) {
71
+ if (name in internals)
72
+ internals[name] = elements;
73
+ }
74
+ // The three universal form attributes every value-carrying control shares (clause 2). All three REFLECT:
75
+ // `name` reflects for native parity (the HTML `name` IDL attribute reflects its content attribute) AND
76
+ // because FACE submission keys the entry by the `name` CONTENT attribute — so the imperative `el.name = …`
77
+ // path must update the attribute or the value submits unkeyed; `disabled` / `required` reflect so
78
+ // attribute-selector styling (`[disabled]` / `[required]`) applies to JS-set values too. `value` is ABSENT —
79
+ // its type/codec belong to the subclass. A subclass spreads this into its own `static props`:
80
+ // `static props = { ...UIFormElement.formProps, value: prop.string(), … }`.
81
+ const formProps = {
82
+ name: { ...prop.string(), reflect: true },
83
+ disabled: { ...prop.boolean(), reflect: true },
84
+ required: { ...prop.boolean(), reflect: true },
85
+ };
86
+ export class UIFormElement extends UIElement {
87
+ /** Activates platform form association — `setFormValue`/`setValidity` + the form lifecycle callbacks (clause 1). */
88
+ static formAssociated = true;
89
+ /** The spreadable universal form attributes — the no-static-props-inheritance workaround (clause 2). */
90
+ static formProps = formProps;
91
+ // Fieldset / `<form disabled>` state, pushed in by `formDisabledCallback`. A signal so `effectiveDisabled()`
92
+ // is reactive — a subclass effect reading it re-runs when an ancestor fieldset toggles disabled.
93
+ #formDisabled = signal(false);
94
+ // Renderer-driven custom validity (ADR-0029 §5 — the A2UI `checks` controller seam). A signal so the
95
+ // merged validity effect below re-runs when the renderer calls `setCustomValidity` (live validation).
96
+ // Empty string = no custom message = no custom-error contribution (native-parity clear, like `input.setCustomValidity('')`).
97
+ #customValidity = signal('');
98
+ // ADR-0051 — the field-labelling handoff, written by `setFieldLabelling`, read by the base's forwarding
99
+ // effect (below) + `fieldLabelling`. `null` = unassociated (the default; every existing control's behavior).
100
+ #fieldLabelling = signal(null);
101
+ /**
102
+ * Super-wrapped wiring (clause 4). `super.connectedCallback()` opens the connection scope + AbortController,
103
+ * runs the no-`super` `connected()` hook, and installs the render effect; the scope is now live, so the
104
+ * effects below are scope-owned and disposed on disconnect (zero residue). They install AFTER the render
105
+ * effect — harmless: the form family's `render()` is the inherited void. The subclass keeps the clean,
106
+ * no-`super` `connected()` hook — it never sees this wrapper.
107
+ */
108
+ connectedCallback() {
109
+ super.connectedCallback();
110
+ // Publish the control's value to the form, reactively: reading `formValue()` tracks whatever signals the
111
+ // subclass's hook reads (its `value` prop), so a value change re-runs ONLY this effect → one setFormValue.
112
+ this.effect(() => {
113
+ this.internals.setFormValue(this.formValue());
114
+ });
115
+ // Publish the control's MERGED validity verdict to the platform, reactively (same tracking discipline;
116
+ // `#mergedValidity` is the single source — also fed to the ADR-0050 connect detail below).
117
+ this.effect(() => {
118
+ this.#applyValidity(this.#mergedValidity());
119
+ });
120
+ // ADR-0051 — forward the current field labelling through the overridable hook. Base scope-owned, so
121
+ // association (a `ui-field`'s `setFieldLabelling` call) / clear re-apply reactively. Installed BEFORE
122
+ // the connect dispatch below (LLD-C2) — nothing can call `setFieldLabelling` before this control has
123
+ // even announced itself, so there is no real race, but the ordering is the documented contract.
124
+ this.effect(() => {
125
+ this.applyFieldLabelling(this.#fieldLabelling.value);
126
+ });
127
+ // ADR-0050 — announce full connection to the nearest provider/field ancestor. Dispatched LAST — the
128
+ // scope is open, `connected()` has run, and all three effects above are installed, so a listener sees
129
+ // a fully-wired control.
130
+ this.#dispatchFormConnect();
131
+ }
132
+ /**
133
+ * The ONE dispatch choke point — both the initial connect dispatch above and `announceFormConnect`
134
+ * (F1, below) route through this, so the nested-member guard (bug repair, below) and the dispatch shape
135
+ * itself never drift apart between the two call sites. `dispatchEvent` directly (not `this.emit`):
136
+ * composed + bubbling, NOT cancelable — this is base↔provider/field plumbing, not a consumer-facing
137
+ * semantic event.
138
+ *
139
+ * GUARD (defect repair — a nested `UIFormElement` is an INTERNAL PART, never a registry/field MEMBER):
140
+ * native form controls do not nest as members of each other (there is no such thing as an `<input>`
141
+ * inside an `<input>`'s own submission graph) — ADR-0050's intent was always to discover form-owning
142
+ * MEMBERS, not every `UIFormElement`-shaped node regardless of where it sits. `ui-text-field type=date`
143
+ * creates its OWN internal `<ui-calendar>` (itself a `UIFormElement`, for its own unrelated internal
144
+ * value-picking UI) and appends it inside itself — undetected, a wrapping `ui-form-provider`'s catch-up
145
+ * scan (`querySelectorAll('*')` + `instanceof`) discovers the calendar as a SECOND, phantom member,
146
+ * whose aggregate reads feed back into the field's own effects → an unbounded write-loop (the kernel's
147
+ * ~100-wave budget throws). The fix: a control with a `UIFormElement` ANCESTOR never announces at all —
148
+ * neither the initial dispatch nor a catch-up re-announce reaches a listener, so it can never register
149
+ * anywhere. `#hasFormElementAncestor` walks `parentElement` (O(depth), stops at the first hit) — cheap,
150
+ * connect-time-only, no observer. This also correctly covers `ui-radio` nested in `ui-radio-group`
151
+ * (`UIRadioGroupElement extends UIFormElement`) — the group is the aggregation owner of its own radios,
152
+ * which are internal parts of ITS submission graph the same way the calendar is the text-field's; each
153
+ * radio's own `formValidity()` was always trivially valid, so no outer aggregate observably changes.
154
+ */
155
+ #dispatchFormConnect() {
156
+ if (this.#hasFormElementAncestor())
157
+ return;
158
+ this.dispatchEvent(new CustomEvent(FORM_CONNECT_EVENT, {
159
+ bubbles: true,
160
+ composed: true,
161
+ detail: this.#connectDetail(),
162
+ }));
163
+ }
164
+ /** Walk the parentElement chain for the nearest `UIFormElement` ancestor (O(depth), stops at the first
165
+ * hit) — the nested-member guard `#dispatchFormConnect` applies. Ancestors only: `this` itself is
166
+ * always a `UIFormElement` and is deliberately excluded from the walk. */
167
+ #hasFormElementAncestor() {
168
+ let node = this.parentElement;
169
+ while (node !== null) {
170
+ if (node instanceof _a)
171
+ return true;
172
+ node = node.parentElement;
173
+ }
174
+ return false;
175
+ }
176
+ /**
177
+ * Mint a FRESH `FormConnectDetail` off the CURRENT connection signal — the single source
178
+ * `#dispatchFormConnect` constructs from (both the initial dispatch and `announceFormConnect` route
179
+ * through it), so the two never drift apart. Only meaningful while connected (`connectionSignal` is
180
+ * live); the non-null assertion holds because both call sites already guard on `connectionSignal !==
181
+ * null` before reaching here (not `isConnected` — see `announceFormConnect`'s doc for why that guard is
182
+ * the wrong one).
183
+ */
184
+ #connectDetail() {
185
+ return {
186
+ control: this,
187
+ signal: this.connectionSignal,
188
+ value: () => this.formValue(),
189
+ validity: () => this.#mergedValidity(),
190
+ userInvalid: () => this.formUserInvalid(),
191
+ };
192
+ }
193
+ /**
194
+ * Re-announce full connection (F1 — the review-caught upgrade-order hole). "Ancestors connect before
195
+ * descendants" holds for INSERTION order but NOT custom-element UPGRADE order: pre-existing DOM whose
196
+ * control module registers/upgrades before the provider/field module does means the ONE connect dispatch
197
+ * in `connectedCallback` fires into the void — no listener is live yet — and registration would be lost
198
+ * forever without a catch-up path. Public so a provider/field's one-shot catch-up scan (built at their
199
+ * own build slice) can call it on every already-connected `UIFormElement` it discovers late, re-dispatching
200
+ * `ui-form-connect` with a FRESH detail (the same minting `#connectDetail()` uses, so a reconnect since
201
+ * the original dispatch is reflected — a NEW connection signal, current prop values).
202
+ *
203
+ * Guard: `connectionSignal === null`, NOT `isConnected` (a real bug an s9 repro caught — the bulk-insert
204
+ * hazard). `isConnected` reflects DOM tree membership, set the instant a subtree is spliced in; a custom
205
+ * element's OWN `connectedCallback` (which opens the connection scope + mints `#ac`) fires ASYNC to that,
206
+ * in a separate reaction-queue pass, ancestor-first. So on a bulk insert of a pre-built offline subtree
207
+ * (assemble, then one `append()` — the house test idiom), every descendant is already `isConnected` the
208
+ * moment the PARENT's `connectedCallback` runs (and could catch-up-scan its already-`isConnected`
209
+ * children) even though THEIR OWN `connectedCallback` — and so `connectionSignal` — hasn't fired yet.
210
+ * NOT a jsdom accommodation: atomic-subtree-connect ordering (ancestor-`connectedCallback`-before-
211
+ * descendant, all descendants already `isConnected` throughout) is SPEC-MANDATED and holds identically in
212
+ * every real engine (s8 confirmed with a plain-HTMLElement probe) — this is a real-browser correctness
213
+ * fix, not a test-environment workaround. `connectionSignal === null` is the correct predicate for "has
214
+ * this control's OWN connect wiring run yet": it is null before that control's first `connectedCallback`
215
+ * (nothing to announce — that `connectedCallback` will dispatch for itself momentarily) AND after its
216
+ * `disconnectedCallback` (`#ac` is nulled there too, element.ts) — one guard covers both the not-yet-mine
217
+ * and no-longer-mine legs; `isConnected` is redundant once this holds and is dropped to avoid it masking
218
+ * the real invariant.
219
+ *
220
+ * This guard closes TWO bugs, not one. The obvious one: the pre-fix crash (a `null` `signal` in the
221
+ * minted detail, `detail.signal.aborted` throwing in a registry/field listener). The SILENT one (s8-found):
222
+ * in the same-cascade race, `ui-field`'s association still applies `aria-labelledby` (the labelling signal
223
+ * write does not ride the connection scope), but the affected control's OWN `connectedCallback` — and so
224
+ * its abort listener — had not yet run, so `this.listen(detail.signal, 'abort', …)` in `#associate` would
225
+ * have wired against a signal that was never live, and a later removal would never dissociate (stale
226
+ * labelling, forever). This guard skips the same-cascade announce entirely and lets the descendant's OWN
227
+ * end-of-`connectedCallback` dispatch (below the guard here — see that dispatch's own signal, always live
228
+ * by construction) carry the association instead, once the field/provider is already listening — so the
229
+ * abort listener installs correctly, every time.
230
+ *
231
+ * Also routes through `#dispatchFormConnect`'s nested-member guard — a catch-up scan's
232
+ * `querySelectorAll('*')` finds every `UIFormElement` descendant indiscriminately (it cannot itself
233
+ * distinguish an internal part like a text-field's calendar from a real member), so the guard MUST live
234
+ * here too, not only on the initial dispatch, or a late-discovered internal part would still register.
235
+ */
236
+ announceFormConnect() {
237
+ if (this.connectionSignal === null)
238
+ return;
239
+ this.#dispatchFormConnect();
240
+ }
241
+ /** Map a `ValidityResult` onto `internals.setValidity`: valid clears it; invalid sets flags + message (+ anchor). */
242
+ #applyValidity(result) {
243
+ if (result.valid)
244
+ this.internals.setValidity({});
245
+ else
246
+ this.internals.setValidity(result.flags, result.message, result.anchor);
247
+ }
248
+ /**
249
+ * The MERGED validity verdict — `formValidity()` ⊕ the renderer-driven `#customValidity` signal (ADR-0029
250
+ * §5): native wins when already invalid; a non-empty custom message produces `{valid:false,
251
+ * flags:{customError:true}, message}`; both empty → valid (clear). The SINGLE source feeding both the
252
+ * internals-publishing effect above and `FormConnectDetail.validity` (ADR-0050 — extracted so the two
253
+ * never drift apart).
254
+ */
255
+ #mergedValidity() {
256
+ const native = this.formValidity();
257
+ const custom = this.#customValidity.value;
258
+ if (!native.valid)
259
+ return native; // native wins (e.g. valueMissing overrides the custom message)
260
+ if (custom !== '')
261
+ return { valid: false, flags: { customError: true }, message: custom };
262
+ return native; // both valid: clear
263
+ }
264
+ /**
265
+ * Set a renderer-driven custom validity message (ADR-0029 §5 — the A2UI `checks` seam, native-parity name).
266
+ * An empty string clears the custom contribution (native parity: `el.setCustomValidity('')` = valid).
267
+ * A non-empty message drives `{valid:false, flags:{customError:true}, message}` unless `formValidity()`
268
+ * is already invalid (native wins — e.g. `valueMissing` takes the display slot over a custom message).
269
+ * The reactive effect in `connectedCallback` picks up the `#customValidity` signal change and re-publishes
270
+ * through `#applyValidity` without any direct caller → `internals.setValidity` call here.
271
+ */
272
+ setCustomValidity(message) {
273
+ this.#customValidity.value = message;
274
+ }
275
+ // ── overridable control hooks (clause 3) ──────────────────────────────────────
276
+ /** The value this control contributes to its form. Default: nothing (`null`); the subclass returns its value. */
277
+ formValue() {
278
+ return null;
279
+ }
280
+ /**
281
+ * This control's validity verdict, published to `internals.setValidity` by a scope-owned effect. Default:
282
+ * always valid; the subclass computes it from its props (e.g. `required && value === '' → valueMissing`).
283
+ *
284
+ * NAMED `formValidity`, NOT `validity` (parallel to `formValue`): the public IDL `validity` GETTER below
285
+ * returns the platform's `ValidityState`, so the verdict hook cannot share that name. ADR-0013 clauses 3/6
286
+ * both wrote `validity`; this base resolves the collision in favour of native IDL parity for `validity`.
287
+ */
288
+ formValidity() {
289
+ return { valid: true };
290
+ }
291
+ // ── effective-disabled (clause 5) ─────────────────────────────────────────────
292
+ /**
293
+ * The disabled state a control should obey: its OWN `disabled` prop OR an ancestor `<fieldset disabled>` /
294
+ * form-disabled state (`formDisabledCallback`). Reactive — read it inside an effect and it re-runs when
295
+ * either source flips. (The form-disabled channel is the ADR-0013 divergence from ADR-0010's `ariaDisabled`
296
+ * for non-form controls.)
297
+ */
298
+ effectiveDisabled() {
299
+ return this.disabled || this.#formDisabled.value;
300
+ }
301
+ // ── platform form lifecycle callbacks → overridable hooks (clause 5) ──────────
302
+ /**
303
+ * Platform reset → the `formReset()` hook (the subclass restores value ← its defaultValue; clause 2), THEN
304
+ * the ADR-0051 `ui-form-reset` protocol dispatch (composed, bubbling, no detail — the target IS the
305
+ * resetting control) so an observing `ui-field` re-suppresses its visible error. Covers BOTH reset paths:
306
+ * native `form.reset()` (the platform walks its FACE members' `formResetCallback`s) and a form-less
307
+ * provider's direct call to this same public platform callback.
308
+ */
309
+ formResetCallback() {
310
+ this.formReset();
311
+ this.dispatchEvent(new CustomEvent(FORM_RESET_EVENT, { bubbles: true, composed: true }));
312
+ }
313
+ /** Platform fieldset/form-disabled change → the reactive `#formDisabled` signal behind `effectiveDisabled()`. */
314
+ formDisabledCallback(disabled) {
315
+ this.#formDisabled.value = disabled;
316
+ }
317
+ /** Platform state restore (navigation/autofill) → the `formStateRestore(state)` hook. */
318
+ formStateRestoreCallback(state, _mode) {
319
+ this.formStateRestore(state);
320
+ }
321
+ /** Platform association change → the no-op `formAssociated(form)` hook (an extension point). */
322
+ formAssociatedCallback(form) {
323
+ this.formAssociated(form);
324
+ }
325
+ /** Restore value ← defaultValue on form reset. Default no-op — the subclass owns `value` + its defaultValue. */
326
+ formReset() { }
327
+ /** Restore value from a previously-submitted/autofilled form state. Default no-op — the subclass decodes `state`. */
328
+ formStateRestore(_state) { }
329
+ /** Called when the element associates with / disassociates from a form. Default no-op extension point. */
330
+ formAssociated(_form) { }
331
+ // ── IDL delegators (clause 6) — native form-control parity, via the inherited internals ───────
332
+ // `name` is NOT delegated here: it is the reactive `name` prop (spread from `formProps`), the same surface
333
+ // a consumer reads as `el.name` — and `internals` has no `name` member to delegate to.
334
+ /** The owning `<form>`, or null. */
335
+ get form() {
336
+ return this.internals.form;
337
+ }
338
+ /** The platform's live `ValidityState` (the IDL object; distinct from the `formValidity()` verdict hook). */
339
+ get validity() {
340
+ return this.internals.validity;
341
+ }
342
+ /** The platform validation message (the `setValidity` message; empty when valid). */
343
+ get validationMessage() {
344
+ return this.internals.validationMessage;
345
+ }
346
+ /** Whether this control is a candidate for constraint validation. */
347
+ get willValidate() {
348
+ return this.internals.willValidate;
349
+ }
350
+ /** Run constraint validation, firing an `invalid` event when invalid. */
351
+ checkValidity() {
352
+ return this.internals.checkValidity();
353
+ }
354
+ /** Like `checkValidity`, and additionally reports the problem to the user (focuses the anchor). */
355
+ reportValidity() {
356
+ return this.internals.reportValidity();
357
+ }
358
+ // ── ADR-0051 — the field-labelling seam ───────────────────────────────────────
359
+ /**
360
+ * Public — a `ui-field` calls this with its label/description/error part elements; `null` clears the
361
+ * association (control removed from its field, or the field itself torn down). Signal-backed (the
362
+ * `setCustomValidity` precedent, ADR-0029 §5) — deliberately NOT a prop: elements can't ride attributes,
363
+ * so this stays off the attributes-as-API descriptor surface. Safe on a disconnected control (a plain
364
+ * signal write); the base effect applies it on next connect.
365
+ */
366
+ setFieldLabelling(refs) {
367
+ this.#fieldLabelling.value = refs;
368
+ }
369
+ /** Protected reactive read — an override's forwarding effect (or a subclass's own label/message effects,
370
+ * e.g. text-field's wire) re-runs on association/clear. */
371
+ get fieldLabelling() {
372
+ return this.#fieldLabelling.value;
373
+ }
374
+ /**
375
+ * Protected forwarding hook (ADR-0051 cl.2), applied by the base scope-owned effect installed in
376
+ * `connectedCallback`. Default: internals ARIA element reflection, GUARDED on `internals.role != null` —
377
+ * every internals-role control (checkbox/switch/radio/slider/calendar) wires for FREE, while a role-less
378
+ * host deliberately no-ops (a half-attached name on a role-less AX node is silent lossiness that can fool
379
+ * a probe — a part-role control, e.g. text-field, MUST override instead). Contract for an override:
380
+ * idempotent; handle `null` (clear); guard a not-yet-created part.
381
+ */
382
+ applyFieldLabelling(refs) {
383
+ if (this.internals.role == null)
384
+ return; // role-less host — a part-role control MUST override (ADR-0051)
385
+ reflectAriaElements(this.internals, 'ariaLabelledByElements', refs?.label ? [refs.label] : null);
386
+ const described = refs ? [refs.description, refs.error].filter((el) => el !== null) : null;
387
+ reflectAriaElements(this.internals, 'ariaDescribedByElements', described && described.length > 0 ? described : null);
388
+ }
389
+ /**
390
+ * Protected user-invalid gate, feeding `FormConnectDetail.userInvalid` (ADR-0050). Default `false`. A
391
+ * control composing `trackUserInvalid` overrides it with its tracker's gate (`() =>
392
+ * tracker.userInvalid()`), keeping ONE timing source — the field's error observation rides this read
393
+ * (jsdom cannot match `:state(user-invalid)`; the browser gate asserts the equivalence).
394
+ */
395
+ formUserInvalid() {
396
+ return false;
397
+ }
398
+ }
399
+ _a = UIFormElement;
@@ -0,0 +1,10 @@
1
+ export { UIElement } from './element.js';
2
+ export { UIFormElement, FORM_CONNECT_EVENT, FORM_RESET_EVENT } from './form.js';
3
+ export type { FormValue, ValidityResult, FormConnectDetail, FieldLabelling } from './form.js';
4
+ export { UIContainerElement } from './container.js';
5
+ export { prop, Types } from './props.js';
6
+ export type { PropType, PropConfig, PropsSchema, ReactiveProps } from './props.js';
7
+ export { repeat } from './repeat.js';
8
+ export { watch } from './watch.js';
9
+ export { Directive, directive, NO_COMMIT, mount } from './template.js';
10
+ export type { RenderContext, DirectiveResult } from './template.js';
@@ -0,0 +1,31 @@
1
+ // dom — the element hosts (UIElement, the FACE form base UIFormElement, and the FACE container surface base
2
+ // UIContainerElement) + the typed-prop authoring API + the G3 child directives (repeat/watch). Imports only
3
+ // ../reactive (and same-layer ./element.ts / ./props.ts / ./form.ts / ./container.ts / ./repeat.ts / ./watch.ts).
4
+ //
5
+ // The PUBLIC dom surface (rubric D7): the element hosts and the typed-prop schema API a control author uses,
6
+ // the two child directives a template author drives, PLUS the directive-AUTHORING trio + the imperative
7
+ // `mount` host (ADR-0023). The wiring seams that connect them stay module-private and are deliberately NOT
8
+ // re-exported here:
9
+ // • props.ts/element.ts plumbing — `finalize`, `coerceAttribute`, `observedAttributesFor`, `propForAttribute`,
10
+ // and the codec factories behind `prop.*` (`enumType`/`jsonType`).
11
+ // • the template ENTRY — `render` / `html` / `svg` / `TemplateResult` / `ChildPart` / `prepare`. The template
12
+ // machinery is internal cross-module plumbing; a directive is the only PUBLIC render unit.
13
+ // PUBLIC since ADR-0023: directive-AUTHORING (`Directive` / `directive` / `NO_COMMIT`, plus the `RenderContext`
14
+ // and `DirectiveResult` types) and the imperative `mount(result, container, ctx?)` host that commits a kernel
15
+ // directive (e.g. `repeat`) into a container WITHOUT the private `html\`\`` entry — what an imperative consumer
16
+ // (the a2ui renderer) needs to invoke AND author directives. `repeat` / `watch` remain the shipped directives.
17
+ export { UIElement } from "./element.js";
18
+ // The ADR-0050/0051 protocol surface (G7): the `ui-form-connect`/`ui-form-reset` event names + the connect
19
+ // detail shape are PUBLIC so a registry controller (traits/form-registry.ts) and a consuming control
20
+ // (controls/field/) can listen/type against them without reaching into `./form.ts` directly.
21
+ export { UIFormElement, FORM_CONNECT_EVENT, FORM_RESET_EVENT } from "./form.js";
22
+ // The FACE container surface base (G9, ADR-0015/0016) — NOT form-associated. The shared spreadable surface +
23
+ // flex prop schemas ride its statics (`UIContainerElement.surfaceProps` / `.flexProps`), the ADR-0013 formProps
24
+ // precedent: a subclass folds them into its OWN `static props`, so there is no standalone schema value to
25
+ // re-export — the class IS the surface (matching how `UIFormElement` carries `formProps`).
26
+ export { UIContainerElement } from "./container.js";
27
+ export { prop, Types } from "./props.js";
28
+ export { repeat } from "./repeat.js";
29
+ export { watch } from "./watch.js";
30
+ // The directive-authoring trio + the imperative mount host (ADR-0023). `render`/`html`/`ChildPart` stay private.
31
+ export { Directive, directive, NO_COMMIT, mount } from "./template.js";
@@ -0,0 +1,56 @@
1
+ export interface PropType<T> {
2
+ from(attr: string | null): T;
3
+ to(value: T): string | null;
4
+ }
5
+ /** A codec for a fixed string-literal set; `from` snaps an unknown attribute back to the first member. */
6
+ export declare function enumType<T extends readonly string[]>(values: T): PropType<T[number]>;
7
+ /** A codec for an arbitrary JSON-serializable value of type `T` (round-trips via JSON). */
8
+ export declare function jsonType<T>(): PropType<T>;
9
+ /** The three fixed codecs (the parametric `enumType`/`jsonType` are factories above). */
10
+ export declare const Types: {
11
+ string: PropType<string>;
12
+ number: PropType<number | null>;
13
+ boolean: PropType<boolean>;
14
+ };
15
+ export interface PropConfig<T> {
16
+ type: PropType<T>;
17
+ default: T;
18
+ attribute?: string | false;
19
+ reflect?: boolean;
20
+ }
21
+ export declare const prop: {
22
+ string(def?: string): PropConfig<string>;
23
+ number(def?: number | null): PropConfig<number | null>;
24
+ boolean(def?: boolean): PropConfig<boolean>;
25
+ enum<const T extends readonly string[]>(values: T, def: T[number]): PropConfig<T[number]>;
26
+ json<T>(def: T): PropConfig<T>;
27
+ };
28
+ export type PropsSchema = Record<string, PropConfig<unknown>>;
29
+ export type ReactiveProps<S extends PropsSchema> = {
30
+ [K in keyof S]: S[K] extends PropConfig<infer T> ? T : never;
31
+ };
32
+ /** A constructor-like with a `static props` schema — what `finalize` installs accessors onto. */
33
+ export interface Finalizable {
34
+ prototype: object;
35
+ props?: PropsSchema;
36
+ }
37
+ /** Install signal-backed prototype accessors from `Ctor.props`. Idempotent (safe to call once per class). */
38
+ export declare function finalize(ctor: Finalizable): void;
39
+ /**
40
+ * Inbound seam (the first of the two boundary functions): cross an attribute STRING (or `null` for
41
+ * absence) into the typed prop signal via the prop's `PropType.from` codec, and return the coerced
42
+ * value. This is the primitive the element slice's `attributeChangedCallback` (e-attrs, later) composes
43
+ * on — p-install owns the per-instance store + the string→typed crossing; it does NOT own the platform
44
+ * callback. A no-op for an unknown prop name.
45
+ *
46
+ * Two directional-lock interactions (rubric D3): (1) if an outbound reflect for this prop is in flight,
47
+ * this call is that reflect's own platform echo — suppress it (return `undefined`) so it cannot loop.
48
+ * (2) Otherwise write through the installed accessor under the `inbound` lock, so the setter applies the
49
+ * value but does NOT reflect it straight back out. The write goes through the setter (not the raw signal)
50
+ * precisely so that symmetric guard is load-bearing: one write path, the lock decides whether it reflects.
51
+ */
52
+ export declare function coerceAttribute(instance: object, ctor: Finalizable, name: string, attr: string | null): unknown;
53
+ /** The attribute names to observe for a finalized ctor: each prop's attribute name, minus property-only props. */
54
+ export declare function observedAttributesFor(ctor: Finalizable): string[];
55
+ /** Reverse the mapping: the prop name owning a given attribute name (respecting `attribute` overrides), or `undefined`. */
56
+ export declare function propForAttribute(ctor: Finalizable, attr: string): string | undefined;