@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,176 @@
1
+ // tabs.ts — UITabsElement, the tabs compound's coordinator (goals.md §G9 / decomp g9-containers s8 / ADR-0015
2
+ // surface · ADR-0019 the bindable `selected` two-way). BEHAVIOUR + props + the tablist part + roving/keyboard +
3
+ // the tab↔panel wiring + self-define ONLY; geometry/colour live in tabs.css, the contract in tabs.md.
4
+ //
5
+ // The container (the FIRST non-form family — extends UIContainerElement for the surface axes + the reused
6
+ // protected `internals`; NOT form-associated). It owns a control-created `[data-part=tablist]` strip
7
+ // (`role=tablist` on the PART div — allowed, like the text-field editor part; the HOST carries no role) into
8
+ // which it REPARENTS its `ui-tab` children (the panels stay as siblings of the strip — a tablist must wrap only
9
+ // the tabs). It then drives the whole widget from ONE place (a sibling cannot set another element's protected
10
+ // internals): each tab↔panel pair is wired once (`aria-controls`/`aria-labelledby` via internals element-
11
+ // reflection), and a single scope-owned effect re-applies selection — `aria-selected` + the roving tabindex
12
+ // (exactly the selected tab is tabindex=0) + `:state(selected)` on the tabs, and the `hidden` attribute on the
13
+ // panels (only the selected panel shows; the rest stay in the DOM). ArrowLeft/Right + Home/End move selection
14
+ // AND focus together (selection-follows-focus), committing through the same path as a click. The keyboard
15
+ // navigation is handled by the shared `rovingFocus` trait (listbox-roving LLD-C1).
16
+ //
17
+ // `selected` is a plain reflected string the renderer two-way-binds via LLD-C8 (ADR-0019): the agent SETS it
18
+ // (programmatic → the effect applies it, NO event echoed), a user gesture COMMITS it (the ONE `select` event
19
+ // emitted, the renderer reads the new tab back). The control stays renderer-agnostic — it knows nothing of A2UI.
20
+ // `controls → dom` is the allowed import direction; importing this module registers all three tags (it imports
21
+ // tab.ts/tab-panel.ts), so the s12 barrel needs only `export * from './tabs/tabs.ts'`.
22
+ import { UIContainerElement } from "../../dom/container.js";
23
+ import { prop } from "../../dom/index.js";
24
+ import { rovingFocus } from "../../traits/roving-focus.js";
25
+ import { UITabElement } from "./tab.js";
26
+ import { UITabPanelElement } from "./tab-panel.js";
27
+ // A per-instance id seed so each tabs' tab/panel pair gets unique IDREFs (the reverse aria-labelledby anchor).
28
+ let tabsSeq = 0;
29
+ const props = {
30
+ // The surface axes (ADR-0015) — elevation/brightness, spread from the base (no prototype merge; the ADR-0013
31
+ // formProps precedent). ui-tabs sets its OWN default --ui-container-bg in tabs.css (the base default is
32
+ // transparent), so a bare tabs still has a surface.
33
+ ...UIContainerElement.surfaceProps,
34
+ // `selected` — the active tab's identity (its `key`, or its DOM index as a string; '' ⇒ the first tab).
35
+ // OBSERVED + REFLECTED so the attribute mirrors the live selection, and BINDABLE: the renderer two-way-binds it
36
+ // via LLD-C8 (value:{prop:'selected',event:'select'}, ADR-0019). Typed `string` — the value crosses the
37
+ // attribute boundary as a string regardless (a numeric index is its string form); the descriptor records it so.
38
+ selected: { ...prop.string(), reflect: true },
39
+ // `fill` — ADR-0144 Q1 cl.1: ONE opt-in reflected boolean (the `ui-split-pane` `collapsible` shape), realized
40
+ // CSS-ONLY in tabs.css (`:scope[fill]`). Fills a height-bounded parent with a pinned tablist strip + an
41
+ // internally scrolling active panel — the exact composition `agent-admin.css` (TKT-0085) hand-rolled per
42
+ // consumer for lack of a shipped variant. Absent (the default), tabs stays byte-identical document-flow.
43
+ fill: { ...prop.boolean(false), reflect: true },
44
+ };
45
+ export class UITabsElement extends UIContainerElement {
46
+ static props = props;
47
+ // The control-created tablist strip + the captured tab/panel lists (light-DOM, persist across reconnect).
48
+ #tablist = null;
49
+ #tabs = [];
50
+ #panels = [];
51
+ #baseId = '';
52
+ // The resolved active index — the single source of truth for the keyboard delta; kept in sync by the effect
53
+ // and eagerly on commit (so a rapid second keypress before the effect flush still steps from the right place).
54
+ #activeIndex = -1;
55
+ connected() {
56
+ if (!this.#baseId)
57
+ this.#baseId = `ui-tabs-${++tabsSeq}`;
58
+ const strip = this.#ensureTablist();
59
+ // Reparent the tab children INTO the strip (idempotent — a tab already inside is skipped, so reconnect is a
60
+ // no-op). Recognised by instanceof so a probe subclass nests; the panels are left as strip siblings.
61
+ for (const child of [...this.children]) {
62
+ if (child instanceof UITabElement && child.parentNode !== strip)
63
+ strip.append(child);
64
+ }
65
+ this.#tabs = [...strip.children].filter((c) => c instanceof UITabElement);
66
+ this.#panels = [...this.children].filter((c) => c instanceof UITabPanelElement);
67
+ // Wire each tab↔panel pair ONCE (ids + the aria-controls/labelledby element-reflection). Pairs by DOM order.
68
+ this.#tabs.forEach((tab, i) => {
69
+ const panel = this.#panels[i];
70
+ if (!panel)
71
+ return;
72
+ const tabId = `${this.#baseId}-tab-${i}`;
73
+ const panelId = `${this.#baseId}-panel-${i}`;
74
+ tab.link(panel, tabId);
75
+ panel.link(tab, panelId);
76
+ });
77
+ // Click commit listener — delegated to the tablist strip (rides the connection AbortSignal).
78
+ // Keydown is handled by the rovingFocus trait below.
79
+ this.listen(strip, 'click', this.#onClick);
80
+ // The selection effect — re-applies on every `selected` change (and re-arms on reconnect). Runs once now
81
+ // (synchronously) so the initial roving tabindex + panel visibility are correct before first paint.
82
+ this.effect(() => {
83
+ const index = this.#resolveIndex(); // reads this.selected (tracked) + this.#tabs
84
+ this.#tabs.forEach((tab, i) => tab.setSelected(i === index));
85
+ this.#panels.forEach((panel, i) => {
86
+ panel.hidden = i !== index; // standard `hidden` (NOT ARIA) — only the selected panel shows; the rest stay in DOM
87
+ });
88
+ this.#activeIndex = index;
89
+ });
90
+ // Roving keyboard focus — the shared trait (listbox-roving LLD-C1) replaces the former inline #onKeydown.
91
+ // The effect above runs synchronously and sets #activeIndex before we get here, so initialIndex reads
92
+ // the correct position even after reconnect with a non-first tab selected. syncIndex reconciles on each
93
+ // keydown after a click or programmatic selection change that bypassed the trait's onMove.
94
+ rovingFocus(this, {
95
+ container: strip,
96
+ items: () => this.#tabs,
97
+ orientation: 'horizontal',
98
+ loop: true,
99
+ typeAhead: false,
100
+ initialIndex: () => this.#activeIndex,
101
+ syncIndex: () => this.#activeIndex,
102
+ onMove: (index) => this.#commit(index, false), // trait already moved focus; commit without re-focusing
103
+ });
104
+ // Motion gate (interaction-states standard) — arm `ready` ONE FRAME past first paint so the synchronous
105
+ // initial selection SNAPS and only later changes animate (tabs.css gates the transition behind
106
+ // :state(ready)). states optional-chained — jsdom has no CustomStateSet (the real motion is the browser smoke).
107
+ requestAnimationFrame(() => this.internals.states?.add('ready'));
108
+ }
109
+ /** Create the `[data-part=tablist]` strip ONCE (idempotent across reconnect — it is a persistent light-DOM
110
+ * child) and keep it as the first child. `role=tablist` rides the PART div (text-field editor-part precedent),
111
+ * so the HOST stays free of a role attribute. */
112
+ #ensureTablist() {
113
+ let strip = this.#tablist;
114
+ if (!strip) {
115
+ strip = document.createElement('div');
116
+ strip.setAttribute('data-part', 'tablist');
117
+ strip.setAttribute('role', 'tablist');
118
+ this.#tablist = strip;
119
+ }
120
+ if (strip.parentNode !== this)
121
+ this.insertBefore(strip, this.firstChild);
122
+ return strip;
123
+ }
124
+ /** Resolve `selected` → a tab index: '' ⇒ the first tab; a `key` match wins; else a numeric index in range;
125
+ * else fall back to the first tab. Reads `this.selected` so the selection effect tracks it. */
126
+ #resolveIndex() {
127
+ const tabs = this.#tabs;
128
+ if (tabs.length === 0)
129
+ return -1;
130
+ const sel = this.selected;
131
+ if (sel === '')
132
+ return 0;
133
+ const byKey = tabs.findIndex((t) => t.key !== '' && t.key === sel);
134
+ if (byKey !== -1)
135
+ return byKey;
136
+ if (/^\d+$/.test(sel)) {
137
+ const n = Number(sel);
138
+ if (n >= 0 && n < tabs.length)
139
+ return n;
140
+ }
141
+ return 0;
142
+ }
143
+ // ── click commit — delegated: find the clicked tab among ours (instanceof-safe across subclasses) ──
144
+ #onClick = (event) => {
145
+ const target = event.target;
146
+ const index = this.#tabs.findIndex((t) => t === target || t.contains(target));
147
+ if (index === -1)
148
+ return;
149
+ this.#commit(index, true);
150
+ };
151
+ /**
152
+ * Commit a user-driven selection: write `selected` (→ the effect re-applies aria/roving/panels), move focus to
153
+ * the tab (roving), and emit `select` ONLY when the selection actually changed — so a programmatic `selected`
154
+ * set by the agent (the renderer's two-way write) never echoes an event back (binding hygiene). The commit
155
+ * value is the tab's `key` when it has one, else its index as a string (the addressable identity).
156
+ */
157
+ #commit(index, moveFocus) {
158
+ const tab = this.#tabs[index];
159
+ if (!tab)
160
+ return;
161
+ const changed = index !== this.#activeIndex;
162
+ const identity = tab.key !== '' ? tab.key : String(index);
163
+ this.#activeIndex = index; // eager — keep the keyboard delta correct before the effect flush
164
+ this.selected = identity; // → reflects + wakes the selection effect
165
+ if (moveFocus)
166
+ tab.focus();
167
+ if (changed) {
168
+ // `select` is the ONE commit event (the event-vocab's selection event). The s11 catalog binds
169
+ // value:{prop:'selected',event:'select'} and the renderer's LLD-C8 controller listens to exactly it to
170
+ // write `selected` back into the A2UI data model — NOT `change` (which is value-commit-flavored).
171
+ this.emit('select', { value: identity, index });
172
+ }
173
+ }
174
+ }
175
+ if (!customElements.get('ui-tabs'))
176
+ customElements.define('ui-tabs', UITabsElement);
@@ -0,0 +1,26 @@
1
+ /** The fleet scheme allowlist — ONE constant, imported by every enforcement site (the component gate +
2
+ * the a2ui validator's first line). Widening it is a one-line, gate-visible edit (ADR-0114 fork F1). */
3
+ export declare const SAFE_HREF_SCHEMES: readonly ["https:", "http:", "mailto:"];
4
+ /**
5
+ * The component gate (SPEC-R8, normative verdict procedure). Validates; NEVER rewrites — returns the
6
+ * author's string byte-identical on allow, `null` on deny OR no-destination:
7
+ * - `raw.trim() === ''` → `null` (no destination — the default-`''`/whitespace self-link trap, SPEC-R8
8
+ * AC7: `new URL('', base)` would otherwise mint a self-link, so this check runs BEFORE parsing).
9
+ * - `new URL(raw, base)` throws → `null` (unparseable = denied, fail-closed).
10
+ * - the resolved `url.protocol` is not in `SAFE_HREF_SCHEMES` → `null` (denied).
11
+ * - else → `raw` (allowed — byte-identical, never rewritten; the anchor's own resolution equals this
12
+ * parse by construction, since both resolve against the same `base`).
13
+ *
14
+ * Parsing with `new URL` is load-bearing (SPEC-R8): it applies the SAME control-character/whitespace
15
+ * normalizations navigation itself would, so `"java\nscript:…"` and `" JAVASCRIPT:…"` are denied as their
16
+ * normalized selves — a string-prefix check is non-conformant and would miss exactly this smuggling class.
17
+ *
18
+ * `base` is a parameter (the component passes `document.baseURI`) so this module stays DOM-free — the
19
+ * validator side reuses `SAFE_HREF_SCHEMES` without inheriting a `document` dependency.
20
+ */
21
+ export declare function safeHref(raw: string, base: string): string | null;
22
+ /** The fixed anchor policy (ADR-0114 fork F3 — no props at v1): component-set constants, applied ONLY
23
+ * alongside an allowed `href`, never author-supplied. Exported alongside the gate so a future
24
+ * `ui-attachment` href leg shares this one module instead of minting a second copy. */
25
+ export declare const LINK_REL = "noopener noreferrer";
26
+ export declare const LINK_TARGET = "_blank";
@@ -0,0 +1,48 @@
1
+ // href.ts — the fleet's hyperlink SCHEME GATE (ADR-0114, SPEC-R8/R12; LLD-C1). DOM-free (no `document`
2
+ // import — `base` is a parameter): the canonical home BOTH text.ts's component gate AND a2ui's static
3
+ // validator (`conformance.ts`) import from, and the shared module a future feed-family `ui-attachment`
4
+ // href leg reuses (ADR-0114's cross-family reconciliation) — one policy constant, every enforcement site
5
+ // imports it rather than re-declaring it.
6
+ //
7
+ // Widening the scheme set is a ONE-LINE, gate-visible edit (ADR-0114 fork F1) — never a per-call-site
8
+ // change. Do not rename any export: `SAFE_HREF_SCHEMES` / `safeHref` / `LINK_REL` / `LINK_TARGET` are the
9
+ // frozen shape a concurrent build imports verbatim.
10
+ /** The fleet scheme allowlist — ONE constant, imported by every enforcement site (the component gate +
11
+ * the a2ui validator's first line). Widening it is a one-line, gate-visible edit (ADR-0114 fork F1). */
12
+ export const SAFE_HREF_SCHEMES = ['https:', 'http:', 'mailto:'];
13
+ /**
14
+ * The component gate (SPEC-R8, normative verdict procedure). Validates; NEVER rewrites — returns the
15
+ * author's string byte-identical on allow, `null` on deny OR no-destination:
16
+ * - `raw.trim() === ''` → `null` (no destination — the default-`''`/whitespace self-link trap, SPEC-R8
17
+ * AC7: `new URL('', base)` would otherwise mint a self-link, so this check runs BEFORE parsing).
18
+ * - `new URL(raw, base)` throws → `null` (unparseable = denied, fail-closed).
19
+ * - the resolved `url.protocol` is not in `SAFE_HREF_SCHEMES` → `null` (denied).
20
+ * - else → `raw` (allowed — byte-identical, never rewritten; the anchor's own resolution equals this
21
+ * parse by construction, since both resolve against the same `base`).
22
+ *
23
+ * Parsing with `new URL` is load-bearing (SPEC-R8): it applies the SAME control-character/whitespace
24
+ * normalizations navigation itself would, so `"java\nscript:…"` and `" JAVASCRIPT:…"` are denied as their
25
+ * normalized selves — a string-prefix check is non-conformant and would miss exactly this smuggling class.
26
+ *
27
+ * `base` is a parameter (the component passes `document.baseURI`) so this module stays DOM-free — the
28
+ * validator side reuses `SAFE_HREF_SCHEMES` without inheriting a `document` dependency.
29
+ */
30
+ export function safeHref(raw, base) {
31
+ if (raw.trim() === '')
32
+ return null; // no destination — never parsed (the self-link trap)
33
+ let url;
34
+ try {
35
+ url = new URL(raw, base);
36
+ }
37
+ catch {
38
+ return null; // unparseable — denied, fail-closed
39
+ }
40
+ if (!SAFE_HREF_SCHEMES.includes(url.protocol))
41
+ return null; // denied scheme
42
+ return raw; // allowed — byte-identical, never rewritten
43
+ }
44
+ /** The fixed anchor policy (ADR-0114 fork F3 — no props at v1): component-set constants, applied ONLY
45
+ * alongside an allowed `href`, never author-supplied. Exported alongside the gate so a future
46
+ * `ui-attachment` href leg shares this one module instead of minting a second copy. */
47
+ export const LINK_REL = 'noopener noreferrer';
48
+ export const LINK_TARGET = '_blank';
@@ -0,0 +1,328 @@
1
+ /* text.css — ui-text, the Display-class text primitive (ADR-0078, supersedes ADR-0025 cl.3/cl.3a's
2
+ * --ui-type-* single-axis ramp). Two clearly-SECTIONED blocks:
3
+ *
4
+ * [1] TOKEN BLOCK — `:where(ui-text)` (specificity 0,0,0) DECLARES the five `--ui-text-*` component
5
+ * tokens from the fleet `--md-sys-typescale-{role}-{size}-*` scale (`@agent-ui-kit/shared/dimensions.css`).
6
+ * Base = body-medium (`variant=body size=md`, the defaults). Then the 9×3 repoint matrix (cl.3): 8
7
+ * ROLE blocks (`[variant='display']` → display-medium, one per non-body role at its size=md default)
8
+ * + 18 SIZE-OVERRIDE blocks (`[variant='display'][size='lg']` → display-large, …; 9 roles × sm/lg).
9
+ * Attribute-absence law: `variant`/`size` do NOT auto-reflect their default, so bare `<ui-text
10
+ * size='lg'>` (no [variant] attribute) must ALSO hit the body row — the two body size-override
11
+ * blocks below cover the `:not([variant])` case. `--ui-text-color` ties to the AA-gated surface-text
12
+ * ink role. `:where()` keeps every repoint at specificity 0 — LATER declaration wins a tie, so this
13
+ * file's ORDER (base → role → size-override → [emphasis]) is load-bearing: do not reorder the blocks
14
+ * below. `[emphasis]` (ADR-0109, the fifth axis) is declared LAST — one line, `--ui-text-weight: 700`
15
+ * (the platform bold register), beating every role/size-override row on the tie.
16
+ * [2] STYLES BLOCK — `@scope (ui-text)` CONSUMES only `--ui-text-*` (font-size/-weight/line-height/
17
+ * letter-spacing/color) plus the variant TREATMENTS (structural, not scale, opinions): uppercase for
18
+ * kicker/overline, italic + a rule + an indent for quote, and the ADR-0078 cl.4 stamp-transparency
19
+ * reset (the light-DOM stamp inherits all typography from the host, so `as` changes semantics with
20
+ * zero geometry delta). `display: block`, `user-select: text`, and forced-colors stand unchanged.
21
+ * ADR-0106 adds a fourth axis, `[truncate]` — two legs mirroring the stamp-reset's own two-legged
22
+ * shape: the host leg (the 80% unstamped case) and a stamp leg (declared AFTER the stamp-
23
+ * transparency reset so its overflow/text-overflow win the cascade on the shared selector surface)
24
+ * — text-overflow/overflow must sit on whichever box holds the text; white-space: nowrap on the
25
+ * host is enough on its own, it inherits into the stamp for free. ADR-0109's `emphasis` (the fifth
26
+ * axis) needs ZERO styles-block change and no stamp leg — `font-weight` inherits, so the existing
27
+ * `font-weight: var(--ui-text-weight)` declaration + the cl.4 stamp-transparency reset already carry
28
+ * the [1]-block repoint into any stamped element for free.
29
+ *
30
+ * The hyperlink capability (ADR-0114, SPEC-R13; LLD-C3) — the stamp-transparency reset's `:is()` gains
31
+ * `a` (so an anchor stamp inherits typography/geometry, zero geometry delta — SPEC-R7 AC1); the link leg
32
+ * is declared AFTER it, attribute-gated on `:scope > a[href]` — a DENIED stamp (no `href`) keeps prose
33
+ * rendering, underline-free, by construction; the CSS needs no knowledge of the gate's verdict. Underline
34
+ * is ALWAYS present, never hue-only (ADR-0057) — `--ui-text-link-ink` is the one new component token.
35
+ *
36
+ * Standalone stylesheet — linked via the host page / component-styles barrel (Phase 2); NEVER injected
37
+ * from text.ts (behaviour-only `.ts`, no runtime style injection — plan §2).
38
+ */
39
+
40
+ /* ════════════════ [1] TOKEN BLOCK — :where(ui-text) declares --ui-text-* ════════════════ */
41
+
42
+ :where(ui-text) {
43
+ --ui-text-quote-border: var(--md-sys-color-neutral-outline-variant); /* TKT-0066: was a direct role read in @scope */
44
+ /* Typography tokens — default = body-medium (variant=body, size=md). The role/size-override blocks
45
+ below repoint these five to the matching --md-sys-typescale-{role}-{size}-* row (cl.3's role-pure
46
+ seam). The component reads only --ui-text-* (never --md-sys-typescale-* directly), so a fleet
47
+ type-scale value change never touches the control logic. */
48
+ --ui-text-size: var(--md-sys-typescale-body-medium-size);
49
+ --ui-text-weight: var(--md-sys-typescale-body-medium-weight);
50
+ --ui-text-line-height: var(--md-sys-typescale-body-medium-line-height);
51
+ --ui-text-tracking: var(--md-sys-typescale-body-medium-tracking);
52
+ /* Color — the AA-gated surface-text ink role (the ONLY --md-sys-color-* role ui-text needs). */
53
+ --ui-text-color: var(--md-sys-color-neutral-on-surface);
54
+
55
+ /* TKT-0066 item 5 ruling: dimensional constants route through the own chain, never read raw in @scope. */
56
+ --ui-text-quote-pad: var(--md-sys-space-md); /* the ≈12px rhythm step — density-responsive, cl.2b */
57
+ }
58
+
59
+ /* ── 8 ROLE blocks (cl.3) — [variant] alone, at the size=md default (…-medium). body is the base above,
60
+ so it has none here; its size overrides (sm/lg) are covered in the size-override group below. ── */
61
+
62
+ :where(ui-text[variant='display']) {
63
+ --ui-text-size: var(--md-sys-typescale-display-medium-size);
64
+ --ui-text-weight: var(--md-sys-typescale-display-medium-weight);
65
+ --ui-text-line-height: var(--md-sys-typescale-display-medium-line-height);
66
+ --ui-text-tracking: var(--md-sys-typescale-display-medium-tracking);
67
+ }
68
+ :where(ui-text[variant='headline']) {
69
+ --ui-text-size: var(--md-sys-typescale-headline-medium-size);
70
+ --ui-text-weight: var(--md-sys-typescale-headline-medium-weight);
71
+ --ui-text-line-height: var(--md-sys-typescale-headline-medium-line-height);
72
+ --ui-text-tracking: var(--md-sys-typescale-headline-medium-tracking);
73
+ }
74
+ :where(ui-text[variant='title']) {
75
+ --ui-text-size: var(--md-sys-typescale-title-medium-size);
76
+ --ui-text-weight: var(--md-sys-typescale-title-medium-weight);
77
+ --ui-text-line-height: var(--md-sys-typescale-title-medium-line-height);
78
+ --ui-text-tracking: var(--md-sys-typescale-title-medium-tracking);
79
+ }
80
+ :where(ui-text[variant='label']) {
81
+ --ui-text-size: var(--md-sys-typescale-label-medium-size);
82
+ --ui-text-weight: var(--md-sys-typescale-label-medium-weight);
83
+ --ui-text-line-height: var(--md-sys-typescale-label-medium-line-height);
84
+ --ui-text-tracking: var(--md-sys-typescale-label-medium-tracking);
85
+ }
86
+ :where(ui-text[variant='kicker']) {
87
+ --ui-text-size: var(--md-sys-typescale-kicker-medium-size);
88
+ --ui-text-weight: var(--md-sys-typescale-kicker-medium-weight);
89
+ --ui-text-line-height: var(--md-sys-typescale-kicker-medium-line-height);
90
+ --ui-text-tracking: var(--md-sys-typescale-kicker-medium-tracking);
91
+ }
92
+ :where(ui-text[variant='overline']) {
93
+ --ui-text-size: var(--md-sys-typescale-overline-medium-size);
94
+ --ui-text-weight: var(--md-sys-typescale-overline-medium-weight);
95
+ --ui-text-line-height: var(--md-sys-typescale-overline-medium-line-height);
96
+ --ui-text-tracking: var(--md-sys-typescale-overline-medium-tracking);
97
+ }
98
+ :where(ui-text[variant='quote']) {
99
+ --ui-text-size: var(--md-sys-typescale-quote-medium-size);
100
+ --ui-text-weight: var(--md-sys-typescale-quote-medium-weight);
101
+ --ui-text-line-height: var(--md-sys-typescale-quote-medium-line-height);
102
+ --ui-text-tracking: var(--md-sys-typescale-quote-medium-tracking);
103
+ }
104
+ :where(ui-text[variant='lead']) {
105
+ --ui-text-size: var(--md-sys-typescale-lead-medium-size);
106
+ --ui-text-weight: var(--md-sys-typescale-lead-medium-weight);
107
+ --ui-text-line-height: var(--md-sys-typescale-lead-medium-line-height);
108
+ --ui-text-tracking: var(--md-sys-typescale-lead-medium-tracking);
109
+ }
110
+
111
+ /* ── 18 SIZE-OVERRIDE blocks (cl.3) — 9 roles × {sm,lg}; declared AFTER the role blocks so a tied
112
+ specificity (:where() = 0) resolves to the override on declaration order. body's two covers ALSO match
113
+ the absent-[variant] case (:not([variant])) — the attribute-absence law. ── */
114
+
115
+ :where(ui-text[variant='display'][size='sm']) {
116
+ --ui-text-size: var(--md-sys-typescale-display-small-size);
117
+ --ui-text-weight: var(--md-sys-typescale-display-small-weight);
118
+ --ui-text-line-height: var(--md-sys-typescale-display-small-line-height);
119
+ --ui-text-tracking: var(--md-sys-typescale-display-small-tracking);
120
+ }
121
+ :where(ui-text[variant='display'][size='lg']) {
122
+ --ui-text-size: var(--md-sys-typescale-display-large-size);
123
+ --ui-text-weight: var(--md-sys-typescale-display-large-weight);
124
+ --ui-text-line-height: var(--md-sys-typescale-display-large-line-height);
125
+ --ui-text-tracking: var(--md-sys-typescale-display-large-tracking);
126
+ }
127
+ :where(ui-text[variant='headline'][size='sm']) {
128
+ --ui-text-size: var(--md-sys-typescale-headline-small-size);
129
+ --ui-text-weight: var(--md-sys-typescale-headline-small-weight);
130
+ --ui-text-line-height: var(--md-sys-typescale-headline-small-line-height);
131
+ --ui-text-tracking: var(--md-sys-typescale-headline-small-tracking);
132
+ }
133
+ :where(ui-text[variant='headline'][size='lg']) {
134
+ --ui-text-size: var(--md-sys-typescale-headline-large-size);
135
+ --ui-text-weight: var(--md-sys-typescale-headline-large-weight);
136
+ --ui-text-line-height: var(--md-sys-typescale-headline-large-line-height);
137
+ --ui-text-tracking: var(--md-sys-typescale-headline-large-tracking);
138
+ }
139
+ :where(ui-text[variant='title'][size='sm']) {
140
+ --ui-text-size: var(--md-sys-typescale-title-small-size);
141
+ --ui-text-weight: var(--md-sys-typescale-title-small-weight);
142
+ --ui-text-line-height: var(--md-sys-typescale-title-small-line-height);
143
+ --ui-text-tracking: var(--md-sys-typescale-title-small-tracking);
144
+ }
145
+ :where(ui-text[variant='title'][size='lg']) {
146
+ --ui-text-size: var(--md-sys-typescale-title-large-size);
147
+ --ui-text-weight: var(--md-sys-typescale-title-large-weight);
148
+ --ui-text-line-height: var(--md-sys-typescale-title-large-line-height);
149
+ --ui-text-tracking: var(--md-sys-typescale-title-large-tracking);
150
+ }
151
+ :where(ui-text[variant='label'][size='sm']) {
152
+ --ui-text-size: var(--md-sys-typescale-label-small-size);
153
+ --ui-text-weight: var(--md-sys-typescale-label-small-weight);
154
+ --ui-text-line-height: var(--md-sys-typescale-label-small-line-height);
155
+ --ui-text-tracking: var(--md-sys-typescale-label-small-tracking);
156
+ }
157
+ :where(ui-text[variant='label'][size='lg']) {
158
+ --ui-text-size: var(--md-sys-typescale-label-large-size);
159
+ --ui-text-weight: var(--md-sys-typescale-label-large-weight);
160
+ --ui-text-line-height: var(--md-sys-typescale-label-large-line-height);
161
+ --ui-text-tracking: var(--md-sys-typescale-label-large-tracking);
162
+ }
163
+ :where(ui-text[variant='kicker'][size='sm']) {
164
+ --ui-text-size: var(--md-sys-typescale-kicker-small-size);
165
+ --ui-text-weight: var(--md-sys-typescale-kicker-small-weight);
166
+ --ui-text-line-height: var(--md-sys-typescale-kicker-small-line-height);
167
+ --ui-text-tracking: var(--md-sys-typescale-kicker-small-tracking);
168
+ }
169
+ :where(ui-text[variant='kicker'][size='lg']) {
170
+ --ui-text-size: var(--md-sys-typescale-kicker-large-size);
171
+ --ui-text-weight: var(--md-sys-typescale-kicker-large-weight);
172
+ --ui-text-line-height: var(--md-sys-typescale-kicker-large-line-height);
173
+ --ui-text-tracking: var(--md-sys-typescale-kicker-large-tracking);
174
+ }
175
+ :where(ui-text[variant='overline'][size='sm']) {
176
+ --ui-text-size: var(--md-sys-typescale-overline-small-size);
177
+ --ui-text-weight: var(--md-sys-typescale-overline-small-weight);
178
+ --ui-text-line-height: var(--md-sys-typescale-overline-small-line-height);
179
+ --ui-text-tracking: var(--md-sys-typescale-overline-small-tracking);
180
+ }
181
+ :where(ui-text[variant='overline'][size='lg']) {
182
+ --ui-text-size: var(--md-sys-typescale-overline-large-size);
183
+ --ui-text-weight: var(--md-sys-typescale-overline-large-weight);
184
+ --ui-text-line-height: var(--md-sys-typescale-overline-large-line-height);
185
+ --ui-text-tracking: var(--md-sys-typescale-overline-large-tracking);
186
+ }
187
+ :where(ui-text[variant='quote'][size='sm']) {
188
+ --ui-text-size: var(--md-sys-typescale-quote-small-size);
189
+ --ui-text-weight: var(--md-sys-typescale-quote-small-weight);
190
+ --ui-text-line-height: var(--md-sys-typescale-quote-small-line-height);
191
+ --ui-text-tracking: var(--md-sys-typescale-quote-small-tracking);
192
+ }
193
+ :where(ui-text[variant='quote'][size='lg']) {
194
+ --ui-text-size: var(--md-sys-typescale-quote-large-size);
195
+ --ui-text-weight: var(--md-sys-typescale-quote-large-weight);
196
+ --ui-text-line-height: var(--md-sys-typescale-quote-large-line-height);
197
+ --ui-text-tracking: var(--md-sys-typescale-quote-large-tracking);
198
+ }
199
+ :where(ui-text[variant='lead'][size='sm']) {
200
+ --ui-text-size: var(--md-sys-typescale-lead-small-size);
201
+ --ui-text-weight: var(--md-sys-typescale-lead-small-weight);
202
+ --ui-text-line-height: var(--md-sys-typescale-lead-small-line-height);
203
+ --ui-text-tracking: var(--md-sys-typescale-lead-small-tracking);
204
+ }
205
+ :where(ui-text[variant='lead'][size='lg']) {
206
+ --ui-text-size: var(--md-sys-typescale-lead-large-size);
207
+ --ui-text-weight: var(--md-sys-typescale-lead-large-weight);
208
+ --ui-text-line-height: var(--md-sys-typescale-lead-large-line-height);
209
+ --ui-text-tracking: var(--md-sys-typescale-lead-large-tracking);
210
+ }
211
+ /* body's pair ALSO matches a bare <ui-text size='sm'|'lg'> (no [variant] attribute — the absence law). */
212
+ :where(ui-text[size='sm']:not([variant]), ui-text[variant='body'][size='sm']) {
213
+ --ui-text-size: var(--md-sys-typescale-body-small-size);
214
+ --ui-text-weight: var(--md-sys-typescale-body-small-weight);
215
+ --ui-text-line-height: var(--md-sys-typescale-body-small-line-height);
216
+ --ui-text-tracking: var(--md-sys-typescale-body-small-tracking);
217
+ }
218
+ :where(ui-text[size='lg']:not([variant]), ui-text[variant='body'][size='lg']) {
219
+ --ui-text-size: var(--md-sys-typescale-body-large-size);
220
+ --ui-text-weight: var(--md-sys-typescale-body-large-weight);
221
+ --ui-text-line-height: var(--md-sys-typescale-body-large-line-height);
222
+ --ui-text-tracking: var(--md-sys-typescale-body-large-tracking);
223
+ }
224
+
225
+ /* ── [emphasis] (ADR-0109, the fifth orthogonal axis) — declared LAST: :where() keeps every repoint at
226
+ specificity 0, so source order is what lets this one beat every role/size-override block above on a tie.
227
+ 700 is the platform's bold register (the CSS `bold` keyword) — a platform constant, not a new fleet
228
+ token and not a typescale-row edit; no-ops honestly on kicker (already 700). ── */
229
+ :where(ui-text[emphasis]) {
230
+ --ui-text-weight: 700;
231
+ }
232
+
233
+ /* ── The hyperlink capability (ADR-0114, LLD-C3) — the link ink role. Declared after [emphasis] (order
234
+ is not load-bearing here — this token is consumed only by the `a[href]` selector below, never by the
235
+ base :scope rule, so it cannot lose a specificity-0 tie against anything above). ── */
236
+ :where(ui-text) {
237
+ --ui-text-link-ink: var(--md-sys-color-primary);
238
+ }
239
+
240
+ /* ════════════════ [2] STYLES BLOCK — @scope (ui-text) consumes ONLY --ui-text-* ════════════════ */
241
+
242
+ @scope (ui-text) {
243
+ :scope {
244
+ display: block; /* a text primitive is a block element — lays out like <p>/<h*> */
245
+ font-size: var(--ui-text-size);
246
+ font-weight: var(--ui-text-weight);
247
+ line-height: var(--ui-text-line-height);
248
+ letter-spacing: var(--ui-text-tracking);
249
+ color: var(--ui-text-color);
250
+ /* user-select ENABLED — display text is selectable (the deliberate inverse of ui-button, which disables it) */
251
+ user-select: text;
252
+ /* NO block-size / padding-block / min-inline-size — Display class has no frame law (ADR-0025 cl.1) */
253
+ }
254
+
255
+ /* Editorial-extra TREATMENTS (cl.2b) — structural opinions, not scale; the four editorial roles each add
256
+ one on top of the token-driven size/weight/line-height/tracking above. */
257
+ :scope[variant='kicker'],
258
+ :scope[variant='overline'] {
259
+ text-transform: uppercase;
260
+ }
261
+ :scope[variant='quote'] {
262
+ font-style: italic;
263
+ border-inline-start: 3px solid var(--ui-text-quote-border);
264
+ padding-inline-start: var(--ui-text-quote-pad);
265
+ }
266
+
267
+ /* The stamp-transparency reset (ADR-0078 cl.4) — the `as` real element inherits ALL typography from the
268
+ host, so stamping/unstamping changes semantics with ZERO geometry delta (same rendered box either way,
269
+ asserted in the browser tests). Covers every tag `as` can stamp. */
270
+ :scope > :is(h1, h2, h3, h4, h5, h6, p, blockquote, span, a) {
271
+ margin: 0;
272
+ font: inherit;
273
+ letter-spacing: inherit;
274
+ color: inherit;
275
+ }
276
+
277
+ /* The hyperlink capability (ADR-0114, SPEC-R13; LLD-C3) — declared AFTER the stamp-transparency reset so
278
+ its `color`/`text-decoration` win the cascade on the shared `a` selector. Attribute-gated on `[href]`:
279
+ a DENIED stamp (no `href` — `#syncLink` never set one) keeps plain prose rendering; the CSS needs no
280
+ knowledge of the gate's verdict. Underline is ALWAYS present, never hue-only (ADR-0057). */
281
+ :scope > a[href] {
282
+ color: var(--ui-text-link-ink);
283
+ text-decoration-line: underline;
284
+ text-underline-offset: 0.15em;
285
+ text-decoration-thickness: from-font;
286
+ }
287
+ :scope > a[href]:hover {
288
+ text-decoration-thickness: 2px;
289
+ }
290
+ /* the fleet's shared focus treatment (ADR-0009) — the SAME ring every control draws, read directly from
291
+ the fleet tokens rather than repointed through the component chain (the button.css precedent). */
292
+ :scope > a[href]:focus-visible {
293
+ outline: var(--md-sys-state-focus-ring-width) solid var(--md-sys-color-focus-ring);
294
+ outline-offset: var(--md-sys-state-focus-ring-offset);
295
+ }
296
+ /* :visited shares the link ink at v1 — no visited role minted (SPEC-R13, stated not hidden). */
297
+
298
+ /* ADR-0106 — `[truncate]`, the fourth orthogonal axis: single-line + ellipsis, CSS-only (no
299
+ ResizeObserver, no clipped-state measurement — text.ts's unconditional `title` mirror is the reveal).
300
+ Two legs, the SAME two-legged shape as the stamp-transparency reset above: the host leg covers the 80%
301
+ unstamped case (`as="none"`); `white-space: nowrap` there is enough on its own — it inherits into the
302
+ stamp for free. The stamp leg re-applies `overflow`/`text-overflow` on whichever real element (h1…span)
303
+ is currently holding the text, since THAT box is what actually clips; it is declared after the
304
+ stamp-transparency reset above so it wins the shared-selector cascade. Toggling `as` under truncation
305
+ keeps the rendered box identical (the ADR-0078 cl.4 invariant extends to the clipped box). */
306
+ :scope[truncate] {
307
+ white-space: nowrap;
308
+ overflow: hidden;
309
+ text-overflow: ellipsis;
310
+ }
311
+ :scope[truncate] > :is(h1, h2, h3, h4, h5, h6, p, blockquote, span) {
312
+ overflow: hidden;
313
+ text-overflow: ellipsis;
314
+ }
315
+
316
+ /* forced-colors — keep the text visible (CanvasText maps to the platform high-contrast text ink) */
317
+ @media (forced-colors: active) {
318
+ :scope {
319
+ color: CanvasText;
320
+ }
321
+ /* the link keeps the platform's own link ink under forced-colors — underline survives above (SPEC-R13
322
+ AC2); LinkText is the system role every UA maps real anchors to, so this reads correctly even though
323
+ the stamp is a custom-element child, not a native navigation context. */
324
+ :scope > a[href] {
325
+ color: LinkText;
326
+ }
327
+ }
328
+ }