@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,360 @@
1
+ // timeline-item.ts — UITimelineItemElement, the timeline family's shared INERT visual atom
2
+ // (timeline-family.lld.md §2 · SPEC-R1…R5 · ADR-0122 F1/F2/F3/F6; recursive nesting + the
3
+ // collapsed-summary preview — ADR-0143 F1-F3/F7, TKT-0091). BEHAVIOUR + props + the control-built
4
+ // marker/content/detail/nested anatomy + self-define ONLY. Anatomy/geometry per the LLD; styling lives
5
+ // in timeline-item.css, the public contract in timeline-item.md.
6
+ //
7
+ // One rail row = a marker (dot/ring/pulse via CSS, OR a built-in check/x glyph, OR a consumer-slotted
8
+ // marker) + the content roles (label · description · timestamp · trailing) + an optional collapsible
9
+ // detail/nested pair. Inert: holds no transport, no live-region role, emits ONLY `toggle` (the composed
10
+ // detail's own event — never a bespoke one). `internals.role = 'listitem'` is set in the CONSTRUCTOR
11
+ // (the toast role precedent — semantics before insertion), extends `UIElement` (NOT form-associated).
12
+ // Hosted by BOTH `ui-timeline` (durable) and `ui-status-stream` (live) — authored once, shared everywhere.
13
+ //
14
+ // Anatomy (#ensureAnatomy(), idempotent — ONCE, persists across reconnect, the toast/disclosure part-
15
+ // persistence precedent): a `<span data-part="marker">` (dot/connector painted by timeline-item.css;
16
+ // suppressed and replaced by a real glyph child when `icon` is set, a `[data-role="marker"]` consumer
17
+ // child is present, or `status` is `done`/`error` — SPEC-R3/R4) + four content cells keyed by
18
+ // `data-role` (`label`/`description`/`timestamp`/`trailing`). A pre-existing light-DOM child carrying one
19
+ // of those `[data-role]`s is ADOPTED (kept, never cloned — ADR-0022) and marked consumer-owned so
20
+ // `#renderContent()` never stamps over it (the adia wrapper-trap regression this build guards against);
21
+ // otherwise a fresh cell is created and the reactive effect stamps it from the matching prop. `trailing`
22
+ // has no prop source by default — it is consumer-content-only UNLESS the collapsed-summary preview
23
+ // auto-fills it (ADR-0143 F3, below), which backs off the instant a consumer adopts it themselves.
24
+ //
25
+ // A pre-existing `[data-role="detail"]` AND/OR `[data-role="nested"]` child are moved into ONE shared
26
+ // composed `ui-disclosure` (§2.3, ADR-0143 F1/F2) — NOT a bespoke caret+hidden reimplementation, and NOT
27
+ // two independent disclosures: `detail` (free consumer content) is appended first, `nested` (a genuine
28
+ // `<ui-timeline>` child — ADR-0143 F1, reusing `ui-timeline` itself rather than inventing a second
29
+ // nesting primitive) second, into the SAME disclosure host, which materializes only when either exists.
30
+ // A LIVE host (ui-status-stream's grouping, ADR-0146 F5) that needs a nested slot on an ALREADY-connected
31
+ // item calls `ensureNestedSlot(factory)` — the ONE public method the ADR-0143 2026-07-18 amendment adds
32
+ // (Kim's narrow, additive exception): it composes the EXACT SAME disclosure + nested slot + preview
33
+ // mechanism lazily, on first call, instead of eagerly at connect. Dead code for the authored-markup case.
34
+ // Recursion is authored-markup depth, not runtime-generated: a nested item can carry its OWN
35
+ // `[data-role="nested"]` child, arbitrary levels deep, with zero extra code — `ui-timeline`'s own
36
+ // `#markLastItem()` is already `:scope > ui-timeline-item`-scoped (verified against source, ADR-0143's
37
+ // own finding), so a nested timeline's terminal-connector marking self-scopes per level with NO change
38
+ // here; likewise `size` never cascades into a nested `<ui-timeline>` (ADR-0143 F7) — each level reads
39
+ // its OWN `[size]` attribute, no forwarding mechanism exists or is added.
40
+ // `this.append(marker, ...cells, [disclosure])` re-parents every node (fresh or adopted) into anatomy
41
+ // order in one call — `Node.append` on an already-connected child simply repositions it, so adoption and
42
+ // creation are handled uniformly by one assembly line.
43
+ //
44
+ // The marker glyph (#renderMarkerGlyph(), a SEPARATE effect from content — status/icon changes should
45
+ // never re-stamp label/description/timestamp, and vice versa): a consumer-adopted `[data-role="marker"]`
46
+ // child is NEVER touched. Otherwise: `icon` non-empty wins (F3, "a free marker coexists with status,
47
+ // orthogonal") — `resolveIcon(icon)` is injected and tagged `data-role="marker"` so the SAME CSS
48
+ // suppression rule fires; else a resolved-outcome `status` (`done`/`error`/`warning`, ADR-0146 F7) injects
49
+ // its built-in glyph (`check`/`x`/`warning` — @agent-ui-kit/icons, already fleet-adopted; disclosure's chevron
50
+ // / toast's close-x precedent) tagged the same way, each a DISTINCT shape (ADR-0057: warning's triangle is
51
+ // never error's `x` recoloured); else (`''`/`pending`/`active`) the marker is cleared and CSS
52
+ // `::before`/`::after` paint the dot/ring/pulse — the non-color SHAPE signifier (ADR-0057, SPEC-R4).
53
+ //
54
+ // The collapsed-summary preview (#renderTrailingPreview(), ADR-0143 F3): when `[data-role="nested"]`
55
+ // content exists, a `MutationObserver` on that nested subtree tracks the LAST `<ui-timeline-item>` in
56
+ // DOM order — recursing to the deepest leaf when THAT item itself nests further (last child wins, no
57
+ // status-priority, matching the ticket's own resolved rule) — and a reactive effect reads the shared
58
+ // disclosure's own `open` prop (a DIFFERENT element's reactive signal; reading it inside `this.effect()`
59
+ // still tracks it — signals are not scoped to their declaring element) to gate whether the resolved
60
+ // label+status is PAINTED into the item's own EXISTING `trailing` cell (closed) or left untouched
61
+ // (open, where the real nested content is already visible — no duplicate). `trailing` remains
62
+ // `#consumerOwned`-gated exactly as today: a consumer-authored `[data-role="trailing"]` child is NEVER
63
+ // overwritten by this effect, at any open/closed state.
64
+ //
65
+ // `controls → dom + controls/disclosure/disclosure.ts + @agent-ui-kit/icons` — the allowed import direction
66
+ // (cross-control, the segmented-control/radio + disclosure/icons precedents). The nested slot's content
67
+ // is treated as generic `Element`/DOM (querySelector/MutationObserver/attributes only) — never importing
68
+ // `ui-timeline`'s own module or type, so no `timeline-item → timeline → timeline-item` cycle is created
69
+ // (timeline.ts already imports timeline-item.ts for registration).
70
+ import { UIElement, prop } from "../../dom/index.js";
71
+ import "../disclosure/disclosure.js"; // the collapse mechanism (F6) — composed, not reinvented
72
+ import { resolveIcon } from '@agent-ui-kit/icons';
73
+ const STATUS = ['', 'pending', 'active', 'done', 'error', 'warning'];
74
+ const SIZE = ['sm', 'md', 'lg'];
75
+ // The resolved-outcome states that inject a built-in marker glyph (the in-progress '' / pending / active
76
+ // states paint a pure-CSS dot/ring/pulse instead). `warning` (ADR-0146 F7) joins done/error here with its
77
+ // OWN distinct triangle-exclamation glyph — shape-coded, never hue-alone (ADR-0057): the shape alone,
78
+ // never the colour, distinguishes warning from error's `x`.
79
+ const STATUS_GLYPH = { done: 'check', error: 'x', warning: 'warning' };
80
+ // A GROUP item's OWN glyph set (an item hosting a nested sub-timeline, `#nested` — ADR-0146 F5) — the
81
+ // Figma "Claude Code Gateway" reasoning-chain card (node 21:1641-1643): a card-level marker reads as a
82
+ // distinct, prominent status glyph (a spinning ring while working, a circled check when done, a circled
83
+ // X on error), never the small plain dot/bare-check/cross a LEAF step's marker uses. `error` gets its OWN
84
+ // `x-circle` glyph, NOT `warning`'s triangle reused (component-review finding, ADR-0057): the two states
85
+ // must stay SHAPE-distinct, not ink-hue-distinct only, the same law the leaf glyph set already honors —
86
+ // reusing `warning`'s shape for `error` at the group level would make them distinguishable by colour alone.
87
+ const GROUP_STATUS_GLYPH = { active: 'circle-notch', done: 'check-circle', error: 'x-circle', warning: 'warning' };
88
+ const props = {
89
+ // reflected (SPEC-R2 AC2: a JS-set status/size round-trips through getAttribute) — the LLD's frozen
90
+ // interface names the constructors; the CSS [status]/[size] repoint (and the AC2 round-trip itself)
91
+ // requires `reflect: true`, the container.ts/disclosure.ts convention for every attribute-selector-driven prop.
92
+ status: { ...prop.enum(STATUS, ''), reflect: true }, // '' = neutral marker (F3)
93
+ label: { ...prop.string(''), reflect: true },
94
+ description: prop.string(''),
95
+ timestamp: prop.string(''), // the consumer's string — NO codec (F6)
96
+ icon: prop.string(''), // a marker glyph name replacing the dot (adia icon-mode)
97
+ size: { ...prop.enum(SIZE, 'md'), reflect: true }, // first-class geometry (F2)
98
+ };
99
+ const CONTENT_ROLES = ['label', 'description', 'timestamp', 'trailing'];
100
+ const STAMPED_ROLES = new Set(['label', 'description', 'timestamp']);
101
+ export class UITimelineItemElement extends UIElement {
102
+ static props = props;
103
+ // The control-built parts — created ONCE (idempotent guard in #ensureAnatomy); persist through
104
+ // disconnect/reconnect (the toast/disclosure precedent). `#`-private: nothing outside can observe them.
105
+ #marker = null;
106
+ #cells = new Map();
107
+ #consumerOwned = new Set(); // roles adopted from a pre-existing consumer child — never stamped
108
+ #consumerMarker = false; // a consumer supplied a [data-role="marker"] child at connect — the item's own glyph logic backs off entirely
109
+ #disclosure = null;
110
+ #nested = null; // the adopted [data-role="nested"] child (ADR-0143 F1 — a genuine <ui-timeline>, treated as generic DOM)
111
+ #nestedObserver = null; // recomputes the collapsed-summary preview on any change to #nested's subtree
112
+ constructor() {
113
+ super();
114
+ this.internals.role = 'listitem'; // set in the CONSTRUCTOR (the toast role precedent) — semantics before insertion
115
+ }
116
+ connected() {
117
+ this.#ensureAnatomy(); // idempotent, ONCE (the toast/modal/disclosure part-persistence guard)
118
+ this.effect(() => this.#renderContent()); // re-stamps label/description/timestamp on prop change
119
+ this.effect(() => this.#renderMarkerGlyph()); // re-paints the marker glyph on status/icon change — a SEPARATE effect
120
+ if (this.#nested) {
121
+ this.#nestedObserver = new MutationObserver(() => this.#renderTrailingPreview());
122
+ this.#nestedObserver.observe(this.#nested, { subtree: true, childList: true, attributes: true, attributeFilter: ['status', 'label'], characterData: true });
123
+ // Reads `this.#disclosure!.open` — a DIFFERENT element's reactive prop — so this effect re-runs on
124
+ // every open/close (ADR-0143 F3's paint gate) in addition to every observer-triggered recompute above.
125
+ this.effect(() => this.#renderTrailingPreview());
126
+ }
127
+ }
128
+ disconnected() {
129
+ this.#nestedObserver?.disconnect();
130
+ this.#nestedObserver = null;
131
+ }
132
+ /** Reveal/collapse the detail region (used by ui-status-stream's update({detail})); no-op if no detail. */
133
+ toggleDetail(open) {
134
+ const disclosure = this.#disclosure;
135
+ if (!disclosure)
136
+ return;
137
+ disclosure.open = open ?? !disclosure.open;
138
+ }
139
+ /**
140
+ * ADR-0146 F5 (the ADR-0143 2026-07-18 amendment — Kim's narrow, additive exception) — LAZILY ensure a
141
+ * `[data-role="nested"]` slot exists on an ALREADY-CONNECTED item, for ui-status-stream's GROUPED entries.
142
+ * Returns the nested host, composing it into the SAME shared `ui-disclosure` `#ensureAnatomy()` uses at
143
+ * connect (ADR-0143 F1/F2) — never a second nesting mechanism (the family-coherence law the amendment
144
+ * preserves). The collapsed-summary preview + its `MutationObserver` (ADR-0143 F3) are armed here EXACTLY as
145
+ * `connected()` arms them for an authored nested slot, so the group's collapse + preview work with ZERO
146
+ * bespoke nesting code. This is the ONLY new public surface the amendment adds; it is a dead code path for
147
+ * the durable authored-markup host (which composes `nested` eagerly at connect) — a live host is its sole
148
+ * caller. `factory` mints the host (ui-status-stream passes a fresh `<ui-timeline>`) so THIS file stays
149
+ * import-free of `ui-timeline` (the cycle guard, file header). Idempotent: a second call returns the
150
+ * existing slot untouched. Must run while connected — it registers a scope-owned effect (`this.effect`
151
+ * throws outside the connected lifetime); ui-status-stream only calls it on an item already appended live.
152
+ */
153
+ ensureNestedSlot(factory) {
154
+ if (this.#nested)
155
+ return this.#nested;
156
+ const nested = factory();
157
+ nested.setAttribute('data-role', 'nested');
158
+ if (this.#disclosure) {
159
+ // The item already composed a disclosure (it was authored WITH `detail`) — append `nested` onto the
160
+ // disclosure host; its own childList heal observer (disclosure.ts `#heal`) relocates it into the body
161
+ // part (the ADR-0143 F2 two-hop adoption, just deferred to first-group time).
162
+ this.#disclosure.appendChild(nested);
163
+ }
164
+ else {
165
+ // No disclosure yet — compose one, adopting `nested` BEFORE the disclosure connects so its own
166
+ // `#ensureParts()` moves it into the body SYNCHRONOUSLY (the exact composition `#ensureAnatomy` runs at
167
+ // connect, deferred to the moment the first grouped child arrives).
168
+ const disclosure = document.createElement('ui-disclosure');
169
+ disclosure.setAttribute('data-part', 'detail');
170
+ disclosure.appendChild(nested);
171
+ this.#disclosure = disclosure;
172
+ this.append(disclosure);
173
+ }
174
+ this.#nested = nested;
175
+ // Re-evaluate the marker glyph NOW — this item just became a group parent, and GROUP_STATUS_GLYPH
176
+ // (#renderMarkerGlyph) only applies once `#nested` is set. The reactive effect that normally drives
177
+ // this reruns on a status/icon CHANGE, which may not happen at the moment nesting begins (a status
178
+ // unchanged by the new child's escalation would leave the OLD leaf-style glyph painted).
179
+ this.#renderMarkerGlyph();
180
+ // Arm the preview observer + paint-gate effect exactly as `connected()` does for an authored nested slot
181
+ // (ADR-0143 F3). On a later disconnect/reconnect, `connected()` re-arms both because `#nested` is now set
182
+ // — so the lazily-mounted slot survives reconnect identically to an authored one.
183
+ this.#nestedObserver = new MutationObserver(() => this.#renderTrailingPreview());
184
+ this.#nestedObserver.observe(nested, { subtree: true, childList: true, attributes: true, attributeFilter: ['status', 'label'], characterData: true });
185
+ this.effect(() => this.#renderTrailingPreview());
186
+ return nested;
187
+ }
188
+ /** Mark/unmark this item TRUNCATED — ui-status-stream's completion invariant (SPEC-R11): a distinct,
189
+ * non-color-only interrupted affordance via a real custom state (`:state(truncated)` in CSS), never a
190
+ * silent forever-spinner. `internals.states` is optional-chained (absent in jsdom, the fleet convention —
191
+ * button.ts/checkbox.ts/text-field.ts). Imperative/CSS-state, not a `static props` field (LLD §7/§8). */
192
+ markTruncated(truncated) {
193
+ if (truncated)
194
+ this.internals.states?.add('truncated');
195
+ else
196
+ this.internals.states?.delete('truncated');
197
+ }
198
+ /**
199
+ * Build the light-DOM anatomy ONCE (idempotent — the toast/disclosure part-persistence precedent).
200
+ * Pre-existing `[data-role]` children are ADOPTED (kept in place, marked consumer-owned); everything
201
+ * else is created fresh. `this.append(...)` re-parents every node — adopted or fresh — into the final
202
+ * anatomy order in one call (a no-op reposition for an already-connected child, ADR-0022's move
203
+ * semantics via the platform's own `Node.append`).
204
+ */
205
+ #ensureAnatomy() {
206
+ if (this.#marker)
207
+ return;
208
+ const marker = document.createElement('span');
209
+ marker.setAttribute('data-part', 'marker');
210
+ const consumerMarker = this.querySelector(':scope > [data-role="marker"]');
211
+ if (consumerMarker) {
212
+ marker.appendChild(consumerMarker); // adopted, moved (ADR-0022)
213
+ this.#consumerMarker = true;
214
+ }
215
+ for (const role of CONTENT_ROLES) {
216
+ const existing = this.querySelector(`:scope > [data-role="${role}"]`);
217
+ if (existing instanceof HTMLElement) {
218
+ this.#cells.set(role, existing); // adopted in place — the wrapper-trap guard: never re-stamped
219
+ this.#consumerOwned.add(role);
220
+ }
221
+ else {
222
+ const cell = document.createElement('span');
223
+ cell.setAttribute('data-role', role);
224
+ this.#cells.set(role, cell);
225
+ }
226
+ }
227
+ // ONE shared disclosure adopts detail THEN nested, in that order (ADR-0143 F1/F2) — materializes
228
+ // only when either exists; an item with neither composes no disclosure at all (unchanged from today).
229
+ const detailContent = this.querySelector(':scope > [data-role="detail"]');
230
+ const nestedContent = this.querySelector(':scope > [data-role="nested"]');
231
+ if (detailContent || nestedContent) {
232
+ const disclosure = document.createElement('ui-disclosure');
233
+ disclosure.setAttribute('data-part', 'detail');
234
+ if (detailContent)
235
+ disclosure.appendChild(detailContent); // moved (ADR-0022)
236
+ if (nestedContent instanceof HTMLElement) {
237
+ disclosure.appendChild(nestedContent); // moved (ADR-0022) — the SAME two-hop adoption `detail` already used
238
+ this.#nested = nestedContent;
239
+ }
240
+ this.#disclosure = disclosure;
241
+ // NO re-emit — the composed ui-disclosure's own `toggle` (bubbles+composed, element.ts's `emit()`)
242
+ // already surfaces on the item host via natural light-DOM bubbling (the SAME event object, not a
243
+ // second one); an explicit re-emit here would double-fire. Allowlisted, no new event name (F6).
244
+ }
245
+ this.append(marker, ...CONTENT_ROLES.map((r) => this.#cells.get(r)), ...(this.#disclosure ? [this.#disclosure] : []));
246
+ this.#marker = marker;
247
+ }
248
+ /**
249
+ * ADR-0143 F3 — resolve the LAST `<ui-timeline-item>` in DOM order under `#nested`, recursing to the
250
+ * deepest leaf when that item itself carries a further `[data-role="nested"]` child (last child wins,
251
+ * no status-priority — the ticket's own resolved rule). Returns `null` when `#nested` has no items yet
252
+ * (a plain no-op source, never an error). Pure DOM traversal — `#nested`'s content is never assumed to
253
+ * be a real `<ui-timeline>` beyond "an element whose direct children may be `ui-timeline-item`s."
254
+ */
255
+ #resolveLastDescendant(root) {
256
+ const items = root.querySelectorAll(':scope > ui-timeline-item');
257
+ const last = items[items.length - 1];
258
+ if (!(last instanceof HTMLElement))
259
+ return null;
260
+ // NOT :scope-restricted to a DIRECT child: by the time this runs, `last`'s OWN connected() may already
261
+ // have moved its [data-role="nested"] child into its composed <ui-disclosure data-part="detail"> body
262
+ // (connectedCallback fires parent-before-child on a bulk insert, but this resolution runs from a
263
+ // deferred effect, which fires after the whole subtree's synchronous connect pass — so a deeper item can
264
+ // already be relocated). The second branch below still finds it as a direct child of the disclosure's
265
+ // OWN `[data-part="body"]` — narrower than a bare subtree query, so a consumer who (misusing the
266
+ // reserved `nested` role) plants `[data-role="nested"]` somewhere inside free-form `detail` prose is
267
+ // never mistaken for `last`'s real nested slot.
268
+ const deeper = last.querySelector(':scope > [data-role="nested"], :scope > [data-part="detail"] [data-part="body"] > [data-role="nested"]');
269
+ if (deeper instanceof HTMLElement) {
270
+ const recursed = this.#resolveLastDescendant(deeper);
271
+ if (recursed)
272
+ return recursed;
273
+ }
274
+ return last;
275
+ }
276
+ /** The SAME text-based, non-color status-shape glyph the fleet already uses elsewhere for a compact
277
+ * inline status echo (the agent-admin client-message-echo precedent) — a distinct SHAPE per status,
278
+ * never color alone (ADR-0057), with no new CSS/icon-injection machinery for a one-line preview cell. */
279
+ #previewGlyph(status) {
280
+ if (status === 'done')
281
+ return '✓ ';
282
+ if (status === 'error')
283
+ return '✕ ';
284
+ if (status === 'warning')
285
+ return '▲ '; // ADR-0146 F7 — a distinct SHAPE (a triangle), never hue alone (ADR-0057)
286
+ if (status === 'active')
287
+ return '● ';
288
+ if (status === 'pending')
289
+ return '○ ';
290
+ return '';
291
+ }
292
+ /**
293
+ * ADR-0143 F3 — the collapsed-summary paint gate: while the shared disclosure is CLOSED (or absent —
294
+ * a no-op then, since there is nothing to summarize), paint the resolved last-descendant's status-shape
295
+ * glyph + label into the item's OWN `trailing` cell, unless a consumer has adopted `trailing`
296
+ * themselves (`#consumerOwned`, checked exactly as `#renderContent()` already does — never overwritten).
297
+ * While OPEN, clear the auto-fill (the real nested content is already visible; no duplicate). A no-op
298
+ * entirely when `#nested` is absent.
299
+ */
300
+ #renderTrailingPreview() {
301
+ const nested = this.#nested;
302
+ if (!nested)
303
+ return;
304
+ const trailing = this.#cells.get('trailing');
305
+ if (!trailing || this.#consumerOwned.has('trailing'))
306
+ return;
307
+ const isOpen = this.#disclosure?.open ?? false;
308
+ if (isOpen) {
309
+ trailing.textContent = '';
310
+ return;
311
+ }
312
+ const last = this.#resolveLastDescendant(nested);
313
+ trailing.textContent = last ? `${this.#previewGlyph(last.getAttribute('status') ?? '')}${last.getAttribute('label') ?? ''}` : '';
314
+ }
315
+ /** Stamp label/description/timestamp from props onto their cells — SKIPPING any consumer-owned cell
316
+ * (the wrapper-trap regression: a wrapped consumer child must suppress the default stamp, not duplicate). */
317
+ #renderContent() {
318
+ for (const role of STAMPED_ROLES) {
319
+ if (this.#consumerOwned.has(role))
320
+ continue;
321
+ this.#cells.get(role).textContent = this[role];
322
+ }
323
+ }
324
+ /** Paint the marker glyph: a consumer-adopted marker is untouched; `icon` wins over `status`; `done`/
325
+ * `error` get a built-in glyph; everything else clears to let CSS `::before`/`::after` paint the
326
+ * dot/ring/pulse (SPEC-R4). Every injected glyph is tagged `data-role="marker"` so ONE CSS suppression
327
+ * rule (`:has([data-role="marker"])`) fires uniformly for consumer/icon/status-glyph markers alike. */
328
+ /** Inject `name` as the marker glyph, tagged the way BOTH resolution paths below need it (`data-role`
329
+ * for the shared plain-dot suppression rule, `data-glyph` for the CSS spin-animation hook) — a single
330
+ * paint site so the icon-prop path and the status/group path can never silently diverge on how an
331
+ * injected glyph is tagged (code-review finding: the two branches had drifted into a hand-duplicated
332
+ * 4-line block each, already edited in lockstep once for `data-glyph`). */
333
+ #paintMarkerGlyph(name) {
334
+ const marker = this.#marker;
335
+ const svg = resolveIcon(name);
336
+ svg.setAttribute('data-role', 'marker');
337
+ svg.setAttribute('data-glyph', name);
338
+ marker.replaceChildren(svg);
339
+ }
340
+ #renderMarkerGlyph() {
341
+ const marker = this.#marker;
342
+ if (!marker || this.#consumerMarker)
343
+ return;
344
+ if (this.icon !== '') {
345
+ this.#paintMarkerGlyph(this.icon);
346
+ return;
347
+ }
348
+ // A group parent (`#nested` set — ADR-0146 F5) reads its OWN glyph set (GROUP_STATUS_GLYPH); a leaf
349
+ // step keeps the existing STATUS_GLYPH (bare check/x) + CSS dot/ring/pulse fallback.
350
+ const glyphMap = this.#nested ? GROUP_STATUS_GLYPH : STATUS_GLYPH;
351
+ const glyph = glyphMap[this.status];
352
+ if (glyph !== undefined) {
353
+ this.#paintMarkerGlyph(glyph);
354
+ return;
355
+ }
356
+ marker.replaceChildren(); // '' / pending / active(leaf) — pure CSS paints the dot/ring/pulse
357
+ }
358
+ }
359
+ if (!customElements.get('ui-timeline-item'))
360
+ customElements.define('ui-timeline-item', UITimelineItemElement);
@@ -0,0 +1,56 @@
1
+ /* toast-region.css — ui-toast-region, the single-file component stylesheet (ADR-0003; feed-family.
2
+ * lld.md LLD-C8 · SPEC-R12). Two clearly-SECTIONED blocks:
3
+ *
4
+ * [1] TOKEN BLOCK — `:where(ui-toast-region)` declares the `--ui-toast-region-*` chain: `inset`
5
+ * (block-end/inline-end fixed inset off `--md-sys-space-lg`) + `gap` (the inter-toast stacking gap
6
+ * off `--md-sys-space-sm`). No surface/radius/colour tokens AT ALL — the region paints NOTHING of
7
+ * its own (LLD-C8; the ui-form-provider "layout, no colour voice" precedent) — each `ui-toast`
8
+ * child supplies its own surface.
9
+ * [2] STYLES BLOCK — `@scope (ui-toast-region)` consumes ONLY `--ui-toast-region-*`. The host IS
10
+ * its own Popover-API top-layer element (`popover="manual"`, set in toast-region.ts) — the
11
+ * `:popover-open` state selector overrides the UA's default popover centering with a
12
+ * block-end/inline-end anchored FIXED position, then lays its `ui-toast` children out as a
13
+ * normal-flow flex column (oldest→newest, top→bottom — newest nearest the block-end inset edge,
14
+ * zero JS positioning; logical properties mirror RTL for free). `pointer-events: none` on the
15
+ * empty region area means it never intercepts page clicks (SPEC-R12) — re-enabled per-toast so
16
+ * each card (and its affordances) stays fully interactive.
17
+ *
18
+ * Standalone stylesheet — linked via the host page / component-styles barrel; NEVER injected from
19
+ * toast-region.ts (behaviour-only `.ts`, no runtime style injection — plan §2).
20
+ */
21
+
22
+ /* ══════════ [1] TOKEN BLOCK — :where(ui-toast-region) declares --ui-toast-region-* ══════════ */
23
+
24
+ :where(ui-toast-region) {
25
+ --ui-toast-region-inset: var(--md-sys-space-lg); /* block-end/inline-end fixed inset off the shell ramp */
26
+ --ui-toast-region-gap: var(--md-sys-space-sm); /* the inter-toast stacking gap */
27
+ }
28
+
29
+ /* ══════════ [2] STYLES BLOCK — @scope (ui-toast-region) consumes ONLY --ui-toast-region-* ══════════ */
30
+
31
+ @scope (ui-toast-region) {
32
+ /* The host IS the popover (not a separate part) — override the UA's default popover centering with
33
+ a block-end/inline-end anchored fixed position once it enters the top layer. */
34
+ :scope:popover-open {
35
+ position: fixed;
36
+ inset: auto; /* clear the UA popover default before setting the two edges below */
37
+ inset-block-end: var(--ui-toast-region-inset);
38
+ inset-inline-end: var(--ui-toast-region-inset);
39
+
40
+ display: flex;
41
+ flex-direction: column; /* normal-flow stacking: oldest→newest, top→bottom (SPEC-R12 AC3) */
42
+ gap: var(--ui-toast-region-gap);
43
+
44
+ margin: 0;
45
+ border: 0;
46
+ padding: 0;
47
+ background: transparent; /* the region paints nothing of its own — each ui-toast is its own surface */
48
+ overflow: visible;
49
+
50
+ pointer-events: none; /* the empty region area never intercepts page clicks (SPEC-R12) */
51
+ }
52
+
53
+ :scope:popover-open > ui-toast {
54
+ pointer-events: auto; /* re-enabled per-toast — each card stays fully interactive */
55
+ }
56
+ }
@@ -0,0 +1,20 @@
1
+ import { UIElement } from '../../dom/index.js';
2
+ import { UIToastElement } from './toast.js';
3
+ /** The `show()` input — a bare string shorthand normalizes to `{ message }`. */
4
+ export interface ToastOptions {
5
+ message: string;
6
+ urgent?: boolean;
7
+ duration?: number;
8
+ action?: string;
9
+ }
10
+ export declare class UIToastRegionElement extends UIElement {
11
+ #private;
12
+ static props: {};
13
+ protected connected(): void;
14
+ protected disconnected(): void;
15
+ /**
16
+ * Create and show a toast (SPEC-R13). See the class doc for the full sequence. Throws if the region
17
+ * is not connected — a dev error, never a silent queue (ledger #10).
18
+ */
19
+ show(options: ToastOptions | string): UIToastElement;
20
+ }
@@ -0,0 +1,113 @@
1
+ // toast-region.ts — UIToastRegionElement, the top-layer host `ui-toast` instances stack inside
2
+ // (feed-family.lld.md LLD-C8 · SPEC-R12/R13/R17 · ADR-0112 fork F1/F2). BEHAVIOUR + the `show()`
3
+ // convenience + self-define ONLY; stacking geometry lives in toast-region.css, the public contract in
4
+ // toast-region.md. Same folder as toast.ts (the radio/radio-group same-folder precedent) — NOT its own
5
+ // package. NOT catalogued (ADR-0112 cl.6).
6
+ //
7
+ // Surface (fork F1): a platform `popover="manual"` attribute (set on the HOST at connect if absent —
8
+ // a platform attribute, never ARIA) rides the top layer above ANY page stacking context — a completion
9
+ // arriving while a modal is open must still show — while toasts stack inside via plain CSS flex/gap,
10
+ // zero JS positioning (unlike the overlay-controller's anchored popups, this region has no anchor to
11
+ // measure against). A childList `MutationObserver` drives visibility off child COUNT: ≥1 ⇒
12
+ // showPopover() (guarded by a local `#isOpen` flag — the overlay.ts open()/close() idiom, jsdom-safe
13
+ // since `:popover-open` matching is unavailable there); 0 ⇒ hidePopover() (SPEC-R12 AC1).
14
+ //
15
+ // Ownership (fork F2, "the ownership ruling" — feed-family.lld.md §5, resolved not left open): v1
16
+ // ownership is CONSUMER-mounted — a page/app-shell declares `<ui-toast-region>` and holds the reference
17
+ // it calls `show()` on. NO static singleton exists anywhere (ADR-0082's per-instance isolation); the
18
+ // app-shell does NOT compose a default region this wave (a scope allocation inside F2's ratified
19
+ // architecture, not a new fork — no ADR owed).
20
+ //
21
+ // `show()` (SPEC-R13): normalizes a string shorthand to `{ message }`, creates a `<ui-toast>`, assigns
22
+ // urgent/duration/action, sets its message text BEFORE append (announcement-correct — SPEC-R15 AC2's
23
+ // pre-insertion role covers the live-region HALF; this covers the CONTENT half), re-asserts top-layer
24
+ // order (SPEC-R12 AC2 — a completion arriving while a LATER modal is open still paints above it:
25
+ // hidePopover();showPopover() back-to-back, synchronous, one frame, no focus implications — manual
26
+ // popovers never hold focus), appends, and returns the element. Throws on a disconnected region (a dev
27
+ // error, never a silent queue — ledger #10).
28
+ //
29
+ // `controls → dom + controls/toast/toast.ts` — the allowed import direction (same-folder sibling).
30
+ import { UIElement } from "../../dom/index.js";
31
+ import { UIToastElement } from "./toast.js";
32
+ export class UIToastRegionElement extends UIElement {
33
+ // EMPTY by design — placement is tokens in v1 (a `placement` prop is the named foreseen extension,
34
+ // LLD-C8). Present (not omitted) for the fleet convention + the descriptor trip-wire's empty
35
+ // bijection (the ui-form-provider precedent).
36
+ static props = {};
37
+ #isOpen = false;
38
+ #observer = null;
39
+ connected() {
40
+ if (!this.hasAttribute('popover'))
41
+ this.setAttribute('popover', 'manual'); // a platform attribute, never ARIA
42
+ this.#observer = new MutationObserver(() => this.#syncVisibility());
43
+ this.#observer.observe(this, { childList: true });
44
+ this.#syncVisibility(); // seed from any children already present (declarative markup)
45
+ }
46
+ disconnected() {
47
+ this.#observer?.disconnect();
48
+ this.#observer = null;
49
+ this.#isOpen = false;
50
+ }
51
+ /**
52
+ * Create and show a toast (SPEC-R13). See the class doc for the full sequence. Throws if the region
53
+ * is not connected — a dev error, never a silent queue (ledger #10).
54
+ */
55
+ show(options) {
56
+ if (!this.isConnected)
57
+ throw new Error('ui-toast-region: show() called while disconnected');
58
+ const opts = typeof options === 'string' ? { message: options } : options;
59
+ const toast = document.createElement('ui-toast');
60
+ if (opts.urgent !== undefined)
61
+ toast.urgent = opts.urgent;
62
+ if (opts.duration !== undefined)
63
+ toast.duration = opts.duration;
64
+ if (opts.action !== undefined)
65
+ toast.action = opts.action;
66
+ toast.textContent = opts.message; // BEFORE append — announcement-correct (SPEC-R13)
67
+ this.#reassertTopLayer();
68
+ this.appendChild(toast);
69
+ return toast;
70
+ }
71
+ #syncVisibility() {
72
+ if (this.childElementCount > 0)
73
+ this.#show();
74
+ else
75
+ this.#hide();
76
+ }
77
+ #show() {
78
+ if (this.#isOpen)
79
+ return;
80
+ this.#isOpen = true;
81
+ this.showPopover();
82
+ }
83
+ #hide() {
84
+ if (!this.#isOpen)
85
+ return;
86
+ this.#isOpen = false;
87
+ try {
88
+ this.hidePopover();
89
+ }
90
+ catch {
91
+ // already hidden — nothing more to do (a platform-drift guard, the overlay.ts idiom)
92
+ }
93
+ }
94
+ /**
95
+ * SPEC-R12 AC2 — when already open, re-enter the top layer so a completion arriving while a LATER
96
+ * modal is open still paints above it. A no-op when not currently open (the first-ever show path,
97
+ * where the childList observer's own showPopover() call — triggered by the append below — is the
98
+ * one that actually opens the region).
99
+ */
100
+ #reassertTopLayer() {
101
+ if (!this.#isOpen)
102
+ return;
103
+ try {
104
+ this.hidePopover();
105
+ }
106
+ catch {
107
+ // already hidden — proceed to re-show regardless
108
+ }
109
+ this.showPopover();
110
+ }
111
+ }
112
+ if (!customElements.get('ui-toast-region'))
113
+ customElements.define('ui-toast-region', UIToastRegionElement);
@@ -0,0 +1,96 @@
1
+ /* toast.css — ui-toast, the single-file component stylesheet (ADR-0003; feed-family.lld.md LLD-C7 ·
2
+ * SPEC-R14/R15/R19). Two clearly-SECTIONED blocks:
3
+ *
4
+ * [1] TOKEN BLOCK — `:where(ui-toast)` (specificity 0,0,0) declares the `--ui-toast-*` chain: a
5
+ * FIXED `--ui-toast-inline-size` (20em — a notification card is not a control on the height
6
+ * ramp; `max-inline-size: 100%` floors small viewports) + the surface roles (neutral
7
+ * surface-brightest / outline / an inline elevation shadow). No `[variant]`/`[size]` repoint —
8
+ * toast carries neither axis (SPEC-R19; a `[size]` selector would need a declared `size`
9
+ * attribute — family-coherence A2b).
10
+ * [2] STYLES BLOCK — `@scope (ui-toast)` consumes ONLY `--ui-toast-*` (∪ the shared `--md-sys-space-*`/
11
+ * `--md-sys-shape-corner-base` shell-spacing tokens, the ui-menu panel precedent). A three-column grid
12
+ * (`1fr auto auto` — message | action | close) laid out by the control-built parts in toast.ts;
13
+ * `[data-part='message']` gets `min-inline-size: 0` (the flex/grid text-overflow floor — an
14
+ * unbounded message must not blow out the fixed card width) + `overflow-wrap: anywhere` (an
15
+ * unbroken token, e.g. a URL, must wrap inside the fixed inline-size rather than overflow it).
16
+ * Forced-colors keeps the border in CanvasText once the box-shadow (a `rgb()` literal — WHCM
17
+ * flattens arbitrary alpha colours) vanishes under high-contrast — the bar-chart fill lesson:
18
+ * the border alone must carry the card's edge.
19
+ *
20
+ * No custom-state motion gate, no transitions — the LLD ships no entrance/exit animation in v1 (a
21
+ * fade/slide-in is a named foreseen extension, not this wave's contract). Standalone stylesheet —
22
+ * linked via the host page / component-styles barrel; NEVER injected from toast.ts (behaviour-only
23
+ * `.ts`, no runtime style injection — plan §2).
24
+ */
25
+
26
+ /* ════════════════ [1] TOKEN BLOCK — :where(ui-toast) declares --ui-toast-* ════════════════ */
27
+
28
+ :where(ui-toast) {
29
+ /* A notification card is a FIXED-width surface, not a control on the height ramp (geometry.md's
30
+ Pattern band — the ui-menu panel precedent, not a Control-band `--md-sys-height-*` lookup). */
31
+ --ui-toast-inline-size: 20em;
32
+
33
+ /* Surface — neutral roles (a toast carries no [intent]/[variant] colour channel in v1). */
34
+ --ui-toast-surface: var(--md-sys-color-neutral-surface-brightest);
35
+ --ui-toast-ink: var(--md-sys-color-neutral-on-surface); /* TKT-0066: was a direct role read in @scope */
36
+ --ui-toast-border: var(--md-sys-color-neutral-outline);
37
+ --ui-toast-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
38
+
39
+ /* TKT-0066 item 5 ruling: dimensional constants route through the own chain, never read raw in @scope. */
40
+ --ui-toast-gap: var(--md-sys-space-sm); /* shared shell-spacing step — the ui-menu panel precedent */
41
+ --ui-toast-pad-inline: var(--md-sys-space-md);
42
+ --ui-toast-pad-block: var(--md-sys-space-sm);
43
+ --ui-toast-radius: var(--md-sys-shape-corner-base); /* the shared fleet radius (ui-menu/ui-card precedent) */
44
+ }
45
+
46
+ /* ════════════════ [2] STYLES BLOCK — @scope (ui-toast) consumes ONLY --ui-toast-* ════════════════ */
47
+
48
+ @scope (ui-toast) {
49
+ :scope {
50
+ display: grid;
51
+ grid-template-columns: 1fr auto auto; /* message | action | close — the control-built parts (toast.ts) */
52
+ align-items: center;
53
+ gap: var(--ui-toast-gap);
54
+ box-sizing: border-box; /* the fixed inline-size below INCLUDES padding + border (button.css precedent) */
55
+
56
+ inline-size: var(--ui-toast-inline-size);
57
+ max-inline-size: 100%; /* floors small viewports — the card never overflows a narrow region */
58
+
59
+ padding-inline: var(--ui-toast-pad-inline);
60
+ padding-block: var(--ui-toast-pad-block);
61
+
62
+ background: var(--ui-toast-surface);
63
+ color: var(--ui-toast-ink);
64
+ border: 1px solid var(--ui-toast-border);
65
+ border-radius: var(--ui-toast-radius);
66
+ box-shadow: var(--ui-toast-shadow);
67
+ }
68
+
69
+ /* The message part — the fixed-width floor (an unbounded message must not blow out the card) + a
70
+ wrap escape for an unbroken token (e.g. a URL) that would otherwise overflow the fixed width. */
71
+ :scope [data-part='message'] {
72
+ min-inline-size: 0;
73
+ overflow-wrap: anywhere;
74
+ }
75
+
76
+ /* The close part is PINNED to the grid's third (last) column, always — the `action` part is only
77
+ appended when non-empty (toast.ts), so a non-actionable toast (the common case) has only TWO real
78
+ children (message, close) landing in auto-placement's columns 1 and 2, leaving the third (intended
79
+ close) column empty. Left unpinned, the close button rendered in the MIDDLE (action) track and the
80
+ genuinely-empty trailing track still consumed a `gap` between it and the padding edge — an extra
81
+ `--md-sys-space-sm` of phantom inset past the close button's own edge, reading as an off-standard
82
+ double-inset against the message's own left inset (TKT-0014). Explicit placement fixes both the
83
+ actionable AND non-actionable cases uniformly (a 3-child toast already lands here by auto-placement,
84
+ so this is a no-op for it). */
85
+ :scope [data-part='close'] {
86
+ grid-column: 3;
87
+ }
88
+
89
+ /* forced-colors — the box-shadow (an arbitrary-alpha rgb() literal) is flattened away under WHCM;
90
+ the border alone must carry the card's edge (the bar-chart fill lesson). */
91
+ @media (forced-colors: active) {
92
+ :scope {
93
+ border-color: CanvasText;
94
+ }
95
+ }
96
+ }