@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,115 @@
1
+ // button.ts — UIButtonElement, the reference FACE control (goals.md §G5 / Phase-1). BEHAVIOUR + props +
2
+ // role + self-define ONLY.
3
+ //
4
+ // Content model — host-as-grid (ADR-0006): the user's light-DOM children (an optional leading icon + the
5
+ // label) are placed by button.css's `:has()` grid (s7). `render()` stays the inherited no-op — there is no
6
+ // template, so the host render effect never commits anything of its own. ADR-0133 ADDS one adoption move
7
+ // beyond that: the label region (the non-adornment children — anything without `slot="leading"`/
8
+ // `slot="trailing"`) is wrapped in a real, persistent `<span data-part="label">` so button.css can carry
9
+ // `text-overflow` on it (anonymous grid text has no selector surface for the clip — button.css:99-103's own
10
+ // long-standing deferral comment). This is `ui-text`'s stamp/heal shape (ADR-0078 cl.4) adapted: nodes are
11
+ // MOVED, never cloned, and a `childList` MutationObserver re-adopts any stray label child that lands
12
+ // directly on the host later (parser streaming, or the A2UI `buttonFactory`'s `el.textContent = label`
13
+ // write, `factories.ts:117-119` — the identical clobbering pattern ui-text's heal observer exists to
14
+ // survive). The wrapper is UNCONDITIONAL — no opt-in prop — because the label is already forced to one line
15
+ // (`white-space: nowrap`, unconditional); ellipsis-on-overflow completes that existing default rather than
16
+ // adding a new axis. An `icon-only` button (no label content at all) never gets an empty wrapper — the heal
17
+ // pass only builds one when there is a stray non-adornment node to hold, so the fifth (square) structure is
18
+ // untouched. Geometry/colour otherwise live entirely in button.css.
19
+ //
20
+ // Keyboard activation is the `pressActivation` trait (Space/Enter → a native-parity `host.click()`),
21
+ // invoked from `connected()` so its listeners ride the connection AbortSignal. Focusability is the
22
+ // `tabbable` trait (ADR-0010): tabindex=0 while enabled, out of the tab order while disabled — the inert
23
+ // half that lives in a trait. ARIA `role` is set through `ElementInternals`, never a host attribute, and so
24
+ // is the disabled AX state (`ariaDisabled`) — a control-level effect because `internals` is protected and a
25
+ // trait cannot reach it. `controls → dom + traits` is the allowed import direction.
26
+ import { UIElement, prop } from "../../dom/index.js";
27
+ import { pressActivation } from "../../traits/press-activation.js";
28
+ import { tabbable } from "../../traits/tabbable.js";
29
+ /** An adornment child sits in a POSITION slot (ADR-0006/ADR-0012) — everything else is label content. */
30
+ const isAdornment = (node) => node instanceof Element && (node.getAttribute('slot') === 'leading' || node.getAttribute('slot') === 'trailing');
31
+ const props = {
32
+ // variant/size REFLECT so the attribute-selector styling (`[variant]`/`[size]` in button.css repointing
33
+ // the colour roles + dimensional ramp) applies to JS-set values too, not only author-set attributes.
34
+ variant: { ...prop.enum(['solid', 'soft', 'ghost'], 'solid'), reflect: true },
35
+ size: { ...prop.enum(['sm', 'md', 'lg'], 'md'), reflect: true },
36
+ // `disabled` reflects to a `disabled` attribute so CSS can render the host pointer-inert (s7); the
37
+ // trait already guards keyboard activation off `() => this.disabled`.
38
+ disabled: { ...prop.boolean(false), reflect: true },
39
+ // icon-only ⇒ the geometry law's fifth structure (references/geometry.md "icon-only (no label) →
40
+ // square"): a real slotted adornment (leading/trailing) with NO label content at all. CSS alone
41
+ // cannot detect an empty/text-node label — `:has()` only matches ELEMENTS, so a bare text-node label
42
+ // (the common `<svg slot=leading>…Download</ui-button>` pattern) is invisible to it — so this is an
43
+ // explicit author opt-in (button.css's `:scope[icon-only]` structure). The accessible name must then
44
+ // come from `aria-label` (there is no label text to read), the toast.ts close-button precedent.
45
+ iconOnly: { ...prop.boolean(false), reflect: true, attribute: 'icon-only' },
46
+ };
47
+ export class UIButtonElement extends UIElement {
48
+ static props = props;
49
+ // The label wrapper (ADR-0133) — the one real element the label region is adopted into, so button.css
50
+ // can carry `text-overflow` on it. Null whenever there is no label content to hold (e.g. `icon-only`).
51
+ #label = null;
52
+ // The childList observer that heals the wrapper invariant — the SAME shape as ui-text's `#observer`
53
+ // (ADR-0078 cl.4): re-adopts a stray label child (parser streaming) and rebuilds the wrapper fresh after
54
+ // a `textContent` clobber (the A2UI `buttonFactory` `label` write). Disconnected in `disconnected()` —
55
+ // the "zero residue after removal" discipline a raw platform observer needs by hand.
56
+ #observer = null;
57
+ connected() {
58
+ this.internals.role = 'button'; // ARIA via internals — never a host role/aria-* attribute
59
+ tabbable(this, { disabled: () => this.disabled }); // tabindex=0 enabled / out of tab order disabled (ADR-0010)
60
+ pressActivation(this, { disabled: () => this.disabled }); // Space/Enter → click; auto-cleans on disconnect
61
+ // The disabled AX state — control-level (internals is protected, unreachable from a trait); scope-owned,
62
+ // so it re-runs on the disabled signal and is disposed on disconnect (zero residue).
63
+ this.effect(() => {
64
+ this.internals.ariaDisabled = this.disabled ? 'true' : null;
65
+ });
66
+ // The label wrapper (ADR-0133) — an initial heal pass adopts whatever label content already exists
67
+ // (parser-streamed or set before connect), then the observer heals every later mutation. Installed
68
+ // AFTER the initial pass so it never observes its own synchronous setup.
69
+ this.#heal();
70
+ this.#observer = new MutationObserver(() => this.#heal());
71
+ this.#observer.observe(this, { childList: true });
72
+ // Motion gate (interaction-states standard) — flip the `ready` custom state ONE FRAME PAST first paint, so
73
+ // the upgrade/first-paint styling SNAPS in and only subsequent state changes animate (the button.css
74
+ // transition is gated behind `:state(ready)`). requestAnimationFrame — NOT updateComplete (a microtask,
75
+ // before paint) — to clear the first paint; `states` is optional-chained because jsdom has no CustomStateSet
76
+ // (the real motion behaviour is the cross-engine smoke). Idempotent (a Set), so reconnect is safe.
77
+ requestAnimationFrame(() => this.internals.states?.add('ready'));
78
+ }
79
+ disconnected() {
80
+ this.#observer?.disconnect();
81
+ this.#observer = null;
82
+ }
83
+ /**
84
+ * Restore the label-wrapper invariant (ADR-0133). A DETACHED wrapper (a `textContent`/`innerHTML`
85
+ * clobber that replaced every child, wrapper included) is forgotten; every stray non-adornment child
86
+ * currently on the host is then adopted into the wrapper, building one fresh only if there is at least
87
+ * one such child (an empty wrapper would pollute the `icon-only` single-column anatomy with a dead grid
88
+ * track). The wrapper's insert point is anchored on the TRAILING adornment element itself (never on
89
+ * `strays[0]`) — adornments are never moved, so inserting immediately before whatever trailing element
90
+ * currently exists (or appending, when there is none) places the wrapper after any leading adornment and
91
+ * before any trailing one regardless of interspersed whitespace text nodes (component-reviewer finding:
92
+ * an authored, pretty-printed `<ui-button>\n <svg slot="leading">…</svg>Label\n</ui-button>` has a
93
+ * whitespace text node BEFORE the leading adornment — anchoring on `strays[0]` put the wrapper ahead of
94
+ * it, inverting the anatomy). Adopting a node re-fires this same observer, but the next delivery finds
95
+ * the invariant satisfied — a no-op — so this self-converges within ≤2 passes (the ui-text `#heal`
96
+ * precedent).
97
+ */
98
+ #heal() {
99
+ if (this.#label && this.#label.parentNode !== this)
100
+ this.#label = null; // detached — a full clobber
101
+ const strays = Array.from(this.childNodes).filter((node) => node !== this.#label && !isAdornment(node));
102
+ if (strays.length === 0)
103
+ return; // nothing to wrap — keep icon-only (and empty) buttons untouched
104
+ if (!this.#label) {
105
+ this.#label = document.createElement('span');
106
+ this.#label.setAttribute('data-part', 'label');
107
+ const trailing = Array.from(this.children).find((el) => el.getAttribute('slot') === 'trailing') ?? null;
108
+ this.insertBefore(this.#label, trailing); // before the trailing adornment, or appended when there is none
109
+ }
110
+ for (const node of strays)
111
+ this.#label.appendChild(node);
112
+ }
113
+ }
114
+ if (!customElements.get('ui-button'))
115
+ customElements.define('ui-button', UIButtonElement);
@@ -0,0 +1,495 @@
1
+ /* calendar.css — ui-calendar, the standalone month-grid date picker control (ADR-0048 / Wave-5B-1;
2
+ * range mode ADR-0093; fluid tracks + two-layer cell ADR-0105).
3
+ *
4
+ * Two clearly-sectioned blocks:
5
+ *
6
+ * [1] TOKEN BLOCK — `:where(ui-calendar)` (specificity 0,0,0) DECLARES the `--ui-calendar-*`
7
+ * chain from colour roles (`--md-sys-color-{family}-{role}`) and the dimensional ramp. Fallback values
8
+ * are sensible until the tokens-specialist delivers the canonical token definitions. The
9
+ * `[size]` repoint triples follow the button/select/text-field precedent; `[disabled]` mutes
10
+ * the nav buttons. `--ui-calendar-range-fill`/`-range-ink` (ADR-0093 clause 4, ERRATUM
11
+ * 2026-07-07) alias `--md-sys-color-primary-surface-highest` / `-primary-on-surface` —
12
+ * 0.0655/0.0722 OKLCH-L separation from the panel bg in light/dark (was ~0.001, effectively
13
+ * invisible — see the erratum on ADR-0093) and 12.70:1 / 12.16:1 ink-vs-fill contrast, well
14
+ * past the AA 4.5:1 floor (verified against tokens.css's OKLCH primitives).
15
+ *
16
+ * [2] STYLES BLOCK — `@scope (ui-calendar)` CONSUMES only `--ui-calendar-*` for its OWN selectors.
17
+ * The panel (`[data-part=panel][data-box]`) adopts the shared container box-model (inset margins
18
+ * + sticky header) from `_surface/container-box.css` at zero specificity. The grid is a CSS
19
+ * grid of 7 FLUID columns (ADR-0105: `minmax(--ui-calendar-cell-size, 1fr)` — the cell-size ramp
20
+ * is the track FLOOR, not a fixed track; shrink-wrapped it resolves to the floor exactly, given
21
+ * surplus width the grid fills it); `[role=row]` divs use `display:contents` so cells participate
22
+ * directly in the parent grid (a clean flat-grid layout; `[role=row]`'s ARIA role is set via
23
+ * attribute, not CSS, so `display:contents` does not affect the accessibility tree). Day cells are
24
+ * a TWO-LAYER cell (ADR-0105, CSS-only — the single `<button role=gridcell>` of calendar.ts is
25
+ * unchanged): the BAND layer is the button box itself (stretches to its fluid track — the square
26
+ * `[data-in-range]` wash, ink/disabled/outside colours, the enlarged hit target) and the POINT
27
+ * layer is a fixed `--ui-calendar-cell-size` circle (`::before`, absolutely centred, negative
28
+ * `z-index` under an `isolation:isolate` stacking context on the button so it paints ABOVE the
29
+ * band wash and BELOW the numeral) carrying the aria-selected/hover/today-ring/focus-ring — the
30
+ * things that are legitimately circular, at every track width. A second pseudo (`::after`) on
31
+ * `[data-range-start]`/`[data-range-end]` (mutually exclusive with the OTHER endpoint attribute
32
+ * via `:not()`, so a same-day range paints neither) bridges a half-track wash from the circle's
33
+ * edge to the band interior — `calc(50% + cell-size/2)` to the far inline edge, which is exactly
34
+ * 0-width at the track floor (today's rendering, byte-for-byte) and only opens up under surplus
35
+ * width. The [data-today] ring, [aria-selected]/[data-in-range] fills, and the half-wash are
36
+ * load-bearing colour values protected under forced-colors via `forced-color-adjust:none`
37
+ * (inherited by the pseudos from the button rules that already set it — it's an inherited
38
+ * property).
39
+ *
40
+ * Standalone — NOT injected from calendar.ts (behaviour-only `.ts`, no runtime style injection
41
+ * per plan §2). Linked via component-styles.css; foundation roles FIRST in the load order.
42
+ */
43
+
44
+ /* ══════════════════════ [1] TOKEN BLOCK — :where(ui-calendar) ══════════════════════════════ */
45
+
46
+ :where(ui-calendar) {
47
+ /* ── Cell geometry (ADR-0105: re-stated as the track FLOOR + the point-layer diameter — the
48
+ value is unchanged, only its meaning widens). The day cell's block-size is always this; the
49
+ inline-size is this at the track floor (shrink-wrapped) and grows with surplus width (fluid
50
+ `minmax()` track) — the fixed circular POINT layer (::before) stays this diameter regardless.
51
+ Aliases --md-sys-height-md so the cell rides the explicit per-[scale] height ramp (ADR-0038),
52
+ tracking scale automatically without a multiplier. [size] repoints below.
53
+ --ui-calendar-gap is the inter-cell gap; aliases --md-sys-space-xs (4px × density). */
54
+ --ui-calendar-cell-size: var(--md-sys-height-md);
55
+ --ui-calendar-gap: var(--md-sys-space-xs);
56
+
57
+ /* ── Day-cell typography — font-size at a comfortable reading size inside the cell. */
58
+ --ui-calendar-font: var(--md-sys-font-md, 14px);
59
+
60
+ /* ── Selected-day fill (the committed value cell).
61
+ --md-sys-color-primary-selected is the AA primary-on-accent role (tokens-calendar); fall back to
62
+ --md-sys-color-primary so the build stays green if it lands a moment after this component. */
63
+ --ui-calendar-selected-fill: var(--md-sys-color-primary-selected, var(--md-sys-color-primary));
64
+ --ui-calendar-selected-ink: var(--md-sys-color-primary-on-primary); /* white — the on-primary pair */
65
+
66
+ /* ── Today's ring (the current date, whether selected or not).
67
+ Aliases --md-sys-color-primary-outline; drawn as an INSET box-shadow so it sits inside the cell
68
+ and stays distinct from the offset-outside focus outline (see forced-colors note below). */
69
+ --ui-calendar-today-ring: var(--md-sys-color-primary-outline);
70
+
71
+ /* ── Disabled / out-of-range day ink (muted; WCAG exempts inactive controls). */
72
+ --ui-calendar-disabled-ink: var(--md-sys-color-neutral-on-surface-variant);
73
+
74
+ /* ── Adjacent-month day ink (prev/next month overflow cells — dimmed to neutral mid). */
75
+ --ui-calendar-outside-ink: var(--md-sys-color-neutral);
76
+
77
+ /* ── Nav button geometry — the ‹ and › inline affordances are sized = font (§4.6 law).
78
+ They are plain buttons; no fixed icon-cell needed (they're typography glyphs). */
79
+ --ui-calendar-nav-font: var(--md-sys-font-md, 14px);
80
+
81
+ /* ── Panel surface — inherits the container/surface tokens for bg/border/radius.
82
+ The panel is a Container/surface (NOT a Control-class surface), so it uses bg + outline. */
83
+ --ui-calendar-panel-bg: var(--md-sys-color-neutral-surface, #fff);
84
+ --ui-calendar-panel-ink: var(--md-sys-color-neutral-on-surface, #1c1c1e);
85
+ --ui-calendar-panel-outline: var(--md-sys-color-neutral-outline-variant, #c4c4c4);
86
+ --ui-calendar-panel-radius: var(--md-sys-shape-corner-base, 0.375rem);
87
+
88
+ /* ── Weekday column-header ink (muted row above the cells). */
89
+ --ui-calendar-header-ink: var(--md-sys-color-neutral-on-surface-variant, #767676);
90
+
91
+ /* ── Cell hover background (non-selected, non-disabled). */
92
+ --ui-calendar-cell-hover-bg: var(--md-sys-color-neutral-surface-high, #f0f0f0);
93
+
94
+ /* ── Range interior wash (ADR-0093 clause 4, ERRATUM 2026-07-07) — the SQUARE fill for
95
+ [data-in-range] cells (endpoints reuse --ui-calendar-selected-fill/-ink above, unchanged).
96
+ ADR-0093 clause 4 originally aliased --md-sys-color-primary-surface / -primary-on-surface —
97
+ measured (gallery audit) to differ from the panel's --md-sys-color-neutral-surface by only
98
+ ~0.001 OKLCH L in BOTH schemes (light 0.9336 vs 0.9347; dark 0.2598 vs 0.2592): the band was
99
+ load-bearing colour with no real lightness separation, i.e. effectively invisible. Repointed
100
+ to --md-sys-color-primary-surface-highest, whose OKLCH L differs from the panel by 0.0655
101
+ (light) / 0.0722 (dark) — ~65×/~72× the old gap, a clearly-visible-but-soft wash (chroma
102
+ stays far below the solid endpoint fill's --md-sys-color-primary-550/600). Ink stays
103
+ --md-sys-color-primary-on-surface: ink-vs-fill contrast is 12.70:1 (light) / 12.16:1 (dark),
104
+ comfortably past the AA 4.5:1 floor a text/numeral pairing needs (see the erratum on
105
+ .claude/docs/adr/0093-calendar-range-selection.md for the full measurement). */
106
+ --ui-calendar-range-fill: var(--md-sys-color-primary-surface-highest, #dde6f4);
107
+ --ui-calendar-range-ink: var(--md-sys-color-primary-on-surface, #1c1c1e);
108
+
109
+ /* ── user-invalid (ADR-0051) — the danger border repoint, the text-field precedent. Appears only
110
+ AFTER the first interaction (the trackUserInvalid controller owns that timing; calendar.ts
111
+ toggles :state(user-invalid)). */
112
+ --ui-calendar-border-invalid: var(--md-sys-color-danger);
113
+ }
114
+
115
+ /* [size] repoints cell size onto the §1-ramp height entries (ADR-0038: explicit per-[scale] table,
116
+ 24/28/36px at the default ui-md scale). Pattern mirrors checkbox [size]→--md-sys-compact-* repoints. */
117
+ :where(ui-calendar[size='sm']) {
118
+ --ui-calendar-cell-size: var(--md-sys-height-sm); /* 24px default; retables under [scale] */
119
+ --ui-calendar-font: var(--md-sys-font-sm, 13px);
120
+ --ui-calendar-nav-font: var(--md-sys-font-sm, 13px);
121
+ }
122
+
123
+ :where(ui-calendar[size='lg']) {
124
+ --ui-calendar-cell-size: var(--md-sys-height-lg); /* 36px default ≈ 2.25rem target; retables under [scale] */
125
+ --ui-calendar-font: var(--md-sys-font-lg, 16px);
126
+ --ui-calendar-nav-font: var(--md-sys-font-lg, 16px);
127
+ }
128
+
129
+ /* Disabled host — mute the nav-button ink (day cells carry their own aria-disabled state). */
130
+ :where(ui-calendar[disabled]) {
131
+ --ui-calendar-panel-ink: var(--md-sys-color-neutral-on-surface-variant, #767676);
132
+ }
133
+
134
+ /* ══════════════════════ [2] STYLES BLOCK — @scope (ui-calendar) ════════════════════════════ */
135
+
136
+ @scope (ui-calendar) {
137
+ /* Host: inline-block so ui-calendar shrink-wraps its panel by default (like a compact form
138
+ widget) when NOTHING gives it width. ADR-0105 — the grid's tracks are fluid (`minmax()`
139
+ below), so a caller that DOES grant width (a stretched flex/grid cell — blockification
140
+ turns this inline-block into the flex/grid item the cross-axis `stretch` default sizes,
141
+ ADR-0030) gets a grid that fills it, not a fixed-width grid stranded in a wide, half-empty
142
+ panel. The escape for a caller that wants the OLD always-compact behaviour even when
143
+ stretched: `max-inline-size: max-content` (one line, page-author override — ADR-0102). */
144
+ :scope {
145
+ display: inline-block;
146
+ vertical-align: top;
147
+ }
148
+
149
+ /* ── Panel ([data-part=panel][data-box]) ────────────────────────────────────────────────
150
+ The [data-box] attribute opts this element into container-box.css's box-model:
151
+ • display: flow-root (BFC; child margins collapse to one inset)
152
+ • <header> children: INSET (not full-bleed), sticky, flex-row, padding inline=12/block=6, gap=8
153
+ • other direct children: margin: 6px (the uniform inset)
154
+ We add paint: background + border + radius. No position/size overrides needed — the
155
+ grid sizes the panel naturally to 7×cell-size + gaps. */
156
+ :scope [data-part='panel'] {
157
+ display: flow-root; /* BFC — child margins collapse; also set by container-box.css */
158
+ background-color: var(--ui-calendar-panel-bg);
159
+ color: var(--ui-calendar-panel-ink);
160
+ border: 1px solid var(--ui-calendar-panel-outline);
161
+ border-radius: var(--ui-calendar-panel-radius);
162
+ box-sizing: border-box;
163
+ }
164
+
165
+ /* user-invalid (ADR-0051) — the danger border, appearing only AFTER the first interaction (the
166
+ trackUserInvalid controller owns that timing; calendar.ts toggles :state(user-invalid) on the
167
+ host). The panel is the calendar's one bordered surface — the text-field precedent applied to
168
+ the Container/surface-class part instead of a Control-class frame. */
169
+ :scope:state(user-invalid) [data-part='panel'] {
170
+ border-color: var(--ui-calendar-border-invalid);
171
+ }
172
+
173
+ /* ── Nav header (<header data-part="nav">) ──────────────────────────────────────────────
174
+ container-box.css already gives the <header>: display:flex; align-items:center; gap=8px;
175
+ padding-inline=12px; padding-block=6px; margin=6px (INSET, not full-bleed); position:sticky;
176
+ inset-block-start=6px (the box's own inset — matches the at-rest margin, so nothing snaps
177
+ to 0 if the panel ever scrolls); z-index:1. We add: space-between so ‹ title › spreads,
178
+ and the title grows to fill. */
179
+ :scope [data-part='nav'] {
180
+ justify-content: space-between;
181
+ }
182
+
183
+ /* Month/year title: grows to fill the space between the two nav buttons. Centred text. */
184
+ :scope [data-part='title'] {
185
+ flex: 1;
186
+ text-align: center;
187
+ font-size: var(--ui-calendar-nav-font);
188
+ font-weight: 600;
189
+ line-height: 1;
190
+ }
191
+
192
+ /* Nav buttons (‹ ›): plain icon affordances — no background, no border, just the glyph. */
193
+ :scope [data-part='prev'],
194
+ :scope [data-part='next'] {
195
+ flex-shrink: 0;
196
+ padding: 0.25rem;
197
+ font-size: var(--ui-calendar-nav-font);
198
+ line-height: 1;
199
+ background: transparent;
200
+ border: none;
201
+ border-radius: var(--ui-calendar-panel-radius);
202
+ color: inherit;
203
+ cursor: pointer;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ }
208
+
209
+ /* The injected Phosphor caret-left/caret-right (setIcon, @agent-ui-kit/icons): 1em tracks the
210
+ button's own font-size (--ui-calendar-nav-font above) at every [size] tier — the same
211
+ rendered size as the previous ‹ › text glyphs. Overrides the svg's own width/height="100%"
212
+ presentational attributes (resolve.ts). */
213
+ :scope [data-part='prev'] svg,
214
+ :scope [data-part='next'] svg {
215
+ display: block;
216
+ inline-size: 1em;
217
+ block-size: 1em;
218
+ }
219
+
220
+ :scope [data-part='prev']:hover,
221
+ :scope [data-part='next']:hover {
222
+ background-color: var(--ui-calendar-cell-hover-bg);
223
+ }
224
+
225
+ :scope [data-part='prev']:focus-visible,
226
+ :scope [data-part='next']:focus-visible {
227
+ outline: var(--md-sys-state-focus-ring-width, 2px) solid var(--md-sys-color-focus-ring);
228
+ outline-offset: var(--md-sys-state-focus-ring-offset, 2px);
229
+ }
230
+
231
+ /* ── Grid container ([data-part="grid"]) ────────────────────────────────────────────────
232
+ A CSS grid of 7 FLUID columns (ADR-0105), one per day of the week. `minmax(cell-size, 1fr)`:
233
+ the cell-size ramp is the track FLOOR (shrink-wrapped/intrinsic sizing resolves `1fr` to its
234
+ base size when the grid's own inline-size is undetermined by an ancestor — today's exact
235
+ compact rendering, byte-identical); given a DEFINITE width from an ancestor (a stretched
236
+ flex/grid cell), the seven tracks share the surplus equally and the grid fills it. Row
237
+ heights are untouched — width adapts, the month never inflates vertically. The [role=row]
238
+ divs use display:contents so their children (columnheaders and gridcells) participate directly
239
+ in the flat 7-column grid — each week flows into the next row automatically. The
240
+ weekday header row and the 6 week rows are all part of the same flat grid, avoiding
241
+ a nested-flex layout that would need explicit sizing on every row. */
242
+ :scope [data-part='grid'] {
243
+ display: grid;
244
+ grid-template-columns: repeat(7, minmax(var(--ui-calendar-cell-size, 2rem), 1fr));
245
+ gap: var(--ui-calendar-gap, 0.125rem);
246
+ /* The direct-child margin from [data-box] applies here (6px all around, the box's uniform
247
+ inset). We keep the default inset margin — the grid sits slightly inside the panel. */
248
+ }
249
+
250
+ /* [role=row] is transparent to the parent CSS grid — cells participate directly.
251
+ The ARIA role is set via the HTML attribute, NOT by CSS, so display:contents does
252
+ NOT remove the row from the accessibility tree (the role attribute is authoritative). */
253
+ :scope [data-part='grid'] [role='row'] {
254
+ display: contents;
255
+ }
256
+
257
+ /* ── Weekday column headers ──────────────────────────────────────────────────────────────
258
+ Non-interactive; muted ink; centred. The <span role="columnheader"> elements sit in
259
+ the first grid row and establish the 7-column visual header. */
260
+ :scope [data-part='grid'] [role='columnheader'] {
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: center;
264
+ block-size: calc(var(--ui-calendar-cell-size, 2rem) * 0.75); /* slightly smaller than cells */
265
+ font-size: calc(var(--ui-calendar-font, 14px) * 0.85);
266
+ color: var(--ui-calendar-header-ink);
267
+ -webkit-user-select: none;
268
+ user-select: none;
269
+ }
270
+
271
+ /* ── Day cells ([role=gridcell]) — the TWO-LAYER cell (ADR-0105) ─────────────────────────
272
+ The single button IS the BAND layer: it stretches to its (fluid) track, carrying the square
273
+ [data-in-range] wash, ink/disabled/outside colours, and the enlarged hit target (a strict
274
+ a11y win — the click-delegation commit path already resolves through
275
+ closest('[role="gridcell"]'), so a wider button changes nothing behaviourally). `position:
276
+ relative` gives its two pseudo layers a containing block; `isolation: isolate` establishes a
277
+ stacking context so a NEGATIVE z-index on the pseudos still paints ABOVE the button's own
278
+ background (paint-order step 1) and BELOW the day-number text (an in-flow inline descendant,
279
+ which paints ahead of z-index:auto/0 descendants but AFTER negative-z ones — negative z-index
280
+ is the only way under it, CSS2.1 Appendix E). line-height:1 (ADR-0036: single-line control
281
+ glyph centred by the frame). */
282
+ :scope [data-part='grid'] [role='gridcell'] {
283
+ display: flex;
284
+ align-items: center;
285
+ justify-content: center;
286
+ box-sizing: border-box;
287
+ position: relative;
288
+ isolation: isolate;
289
+ block-size: var(--ui-calendar-cell-size, 2rem); /* rows never inflate — only inline-size is fluid */
290
+ border: none;
291
+ border-radius: 0; /* the BAND is square; circularity lives on the POINT layer below */
292
+ background: transparent;
293
+ color: inherit;
294
+ font-size: var(--ui-calendar-font, 14px);
295
+ line-height: var(--md-sys-control-line-height, 1);
296
+ cursor: pointer;
297
+ padding: 0;
298
+ -webkit-user-select: none;
299
+ user-select: none;
300
+ outline: none; /* the POINT layer's ::before carries the focus ring below — a
301
+ ring on the (now possibly wide) band itself would draw a
302
+ rectangle, not the circle ADR-0105 requires */
303
+ }
304
+
305
+ /* ── The POINT layer (::before) — a FIXED --ui-calendar-cell-size circle, always centred in the
306
+ button regardless of its (fluid) width. Carries: the aria-selected/single/endpoint fill, the
307
+ hover wash, the [data-today] inset ring, and the :focus-visible ring — the things that are
308
+ legitimately circular at every track width (ADR-0105 decision 3). `inset:0; margin:auto` +
309
+ a fixed inline/block-size is the "centre a fixed box inside an auto-sized positioned parent"
310
+ technique — it re-centres itself as the button's track grows, no JS/ResizeObserver needed. */
311
+ :scope [data-part='grid'] [role='gridcell']::before {
312
+ content: '';
313
+ position: absolute;
314
+ inset: 0;
315
+ margin: auto;
316
+ inline-size: var(--ui-calendar-cell-size, 2rem);
317
+ block-size: var(--ui-calendar-cell-size, 2rem);
318
+ border-radius: 50%;
319
+ z-index: -1; /* under the numeral, above the band background */
320
+ pointer-events: none; /* decorative — the button itself owns hit-testing */
321
+ }
322
+
323
+ :scope [data-part='grid'] [role='gridcell']:hover::before {
324
+ background-color: var(--ui-calendar-cell-hover-bg);
325
+ }
326
+
327
+ :scope [data-part='grid'] [role='gridcell']:focus-visible::before {
328
+ outline: var(--md-sys-state-focus-ring-width, 2px) solid var(--md-sys-color-focus-ring);
329
+ outline-offset: var(--md-sys-state-focus-ring-offset, 2px);
330
+ }
331
+
332
+ /* ── Today ring — an inset ring marking the current date (whether selected or not), on the
333
+ fixed-diameter POINT layer so it stays a true ring, never stretched into an oval. Uses
334
+ box-shadow so it stacks on top of the selected fill when the selected day = today (two
335
+ independent paint channels on the same pseudo — background-color + box-shadow never
336
+ conflict). `forced-color-adjust: none` ensures the ring survives WHCM (load-bearing colour). */
337
+ :scope [data-part='grid'] [role='gridcell'][data-today]::before {
338
+ box-shadow: inset 0 0 0 1.5px var(--ui-calendar-today-ring, currentcolor);
339
+ forced-color-adjust: none;
340
+ }
341
+
342
+ /* ── Selected fill — the committed value cell (single mode) OR a range endpoint (range mode:
343
+ [data-range-start]/[data-range-end] cells carry aria-selected="true" too — ADR-0093 clause 4
344
+ "endpoints reuse the existing circular fill"), painted on the fixed-diameter POINT layer so it
345
+ stays a circle at every width, never an ellipse (ADR-0105). `:not([data-in-range])` excludes
346
+ range mode's interior cells — they ALSO carry aria-selected="true" (clause 4's "the whole band
347
+ is selected") but must show the SQUARE band wash below, not an accent circle; endpoints never
348
+ carry [data-in-range] (rangeSelectionFor() computes them as mutually exclusive), so this
349
+ selector is a no-op narrowing for single mode and the real endpoint/interior split for range
350
+ mode. `color` stays on the BUTTON (inherited by the real day-number text, centred over the
351
+ circle) — `forced-color-adjust:none` keeps the fill in WHCM. */
352
+ :scope [data-part='grid'] [role='gridcell'][aria-selected='true']:not([data-in-range]) {
353
+ color: var(--ui-calendar-selected-ink, #fff);
354
+ forced-color-adjust: none;
355
+ }
356
+ :scope [data-part='grid'] [role='gridcell'][aria-selected='true']:not([data-in-range])::before {
357
+ background-color: var(--ui-calendar-selected-fill, #0057d8);
358
+ forced-color-adjust: none;
359
+ }
360
+
361
+ /* ── Range interior wash ([data-in-range], ADR-0093 clause 4) — SQUARE (radius:0), painted on
362
+ the BAND layer (the button itself) so it spans the FULL fluid track at any width — the wash
363
+ never opens holes between abutting interior cells (ADR-0105 decision 2). Never co-occurs with
364
+ [data-range-start]/[data-range-end] (rangeSelectionFor() computes them as mutually exclusive),
365
+ so this rule only ever touches true interior cells; under forced-colors the explicit override
366
+ below lands the SAME Highlight/HighlightText pair as the endpoints — the whole band is one
367
+ self-delimiting run (ADR-0093 clause 4) — with the circle-vs-square shape (POINT vs BAND
368
+ layer, untouched by forced-colors) left as the sole endpoint-vs-interior signifier (ADR-0057
369
+ non-colour-signifier rule). */
370
+ :scope [data-part='grid'] [role='gridcell'][data-in-range] {
371
+ background-color: var(--ui-calendar-range-fill, #dde6f4);
372
+ color: var(--ui-calendar-range-ink, #1c1c1e);
373
+ forced-color-adjust: none;
374
+ }
375
+
376
+ /* ── Range endpoint half-wash (::after, ADR-0105 decision 3) — bridges the fixed POINT circle to
377
+ the band interior on a [data-range-start]/[data-range-end] cell, so a wide track never opens a
378
+ gap between the endpoint circle and the neighbouring interior wash. Logical `inset-inline-*`
379
+ (no directional gradient) — RTL-safe by construction, the mirrored side follows for free. Spans
380
+ from the circle's OWN edge (`50% + cell-size/2` from centre) to the button's far inline edge —
381
+ at the track floor (button inline-size == cell-size) that span is exactly 0 (today's rendering,
382
+ byte-for-byte); it only opens under surplus width. `z-index:-2` (below the ::before circle,
383
+ which paints over its near portion, CSS2.1 Appendix E step 2 — most-negative-first) — only the
384
+ interior-facing half is ever visible. `:not([data-range-end])` / `:not([data-range-start])` on
385
+ the OTHER endpoint attribute excludes a same-day range (a cell that is BOTH start and end) —
386
+ with no interior cell on either side, no half-wash is drawn (nothing to bridge to). */
387
+ :scope [data-part='grid'] [role='gridcell'][data-range-start]:not([data-range-end])::after,
388
+ :scope [data-part='grid'] [role='gridcell'][data-range-end]:not([data-range-start])::after {
389
+ content: '';
390
+ position: absolute;
391
+ inset-block: 0;
392
+ background-color: var(--ui-calendar-range-fill, #dde6f4);
393
+ z-index: -2;
394
+ pointer-events: none;
395
+ forced-color-adjust: none;
396
+ }
397
+ :scope [data-part='grid'] [role='gridcell'][data-range-start]:not([data-range-end])::after {
398
+ inset-inline-start: calc(50% + var(--ui-calendar-cell-size, 2rem) / 2);
399
+ inset-inline-end: 0;
400
+ }
401
+ :scope [data-part='grid'] [role='gridcell'][data-range-end]:not([data-range-start])::after {
402
+ inset-inline-start: 0;
403
+ inset-inline-end: calc(50% + var(--ui-calendar-cell-size, 2rem) / 2);
404
+ }
405
+
406
+ /* ── Adjacent-month cells (data-outside) — muted ink; still clickable (navigates month). */
407
+ :scope [data-part='grid'] [role='gridcell'][data-outside] {
408
+ color: var(--ui-calendar-outside-ink, #767676);
409
+ }
410
+
411
+ /* ── Disabled / out-of-range cells (aria-disabled=true) — pointer-inert; muted. opacity applies
412
+ to the whole stacking context (button + both pseudo layers together), so a disabled endpoint's
413
+ circle/wash mute along with its numeral — no separate pseudo overrides needed. */
414
+ :scope [data-part='grid'] [role='gridcell'][aria-disabled='true'] {
415
+ color: var(--ui-calendar-disabled-ink, #767676);
416
+ cursor: default;
417
+ pointer-events: none;
418
+ opacity: 0.5;
419
+ }
420
+
421
+ /* ── forced-colors (WHCM) ────────────────────────────────────────────────────────────────
422
+ Four visually distinct states must survive system-colour flattening (ADR-0093 clause 4 —
423
+ the ADR-0048 three-state discipline, now four; ADR-0105 re-homes the paint per layer, the
424
+ mapping is unchanged):
425
+ focus = Highlight ring on the POINT layer (::before) — free via --md-sys-color-focus-ring
426
+ → Highlight; stays circular at every width because the ring lives on the fixed-
427
+ diameter pseudo, never the (possibly wide) band
428
+ selected = Highlight fill + HighlightText numeral — explicit below; in range mode this is
429
+ the WHOLE band (endpoints AND interior both carry aria-selected="true" — clause 4
430
+ "the band is self-delimiting, so endpoint-vs-interior needs no third colour"); the
431
+ half-wash (::after) carries the SAME Highlight so a bridged band reads as one
432
+ contiguous run under WHCM too
433
+ today = ButtonText INSET ring on the POINT layer (offset-inside, inside the circle)
434
+ (interior vs endpoint shape — circle (POINT layer) vs square (BAND layer) — survives
435
+ forced-colors unaided: border-radius is not a colour property, so it is never flattened)
436
+ today MUST NOT use Highlight here — it would merge with the selected/band fill AND with the
437
+ focus ring, collapsing every state into one indistinguishable Highlight blob.
438
+ The button rules above already carry `forced-color-adjust:none` per state, unconditionally
439
+ (not gated by this media query); it is an INHERITED property, so ::before/::after pick it up
440
+ from their originating button automatically — no separate declaration needed on the pseudos
441
+ (only the explicit system-colour paint below needs restating on the pseudo selectors, since
442
+ background-color/box-shadow are NOT inherited). */
443
+ @media (forced-colors: active) {
444
+ /* Panel surface: Canvas / CanvasText (standard surface mapping). */
445
+ :scope [data-part='panel'] {
446
+ background-color: Canvas;
447
+ color: CanvasText;
448
+ border-color: CanvasText;
449
+ }
450
+
451
+ /* Selected fill: Highlight / HighlightText (system selection pair — WCAG AA guaranteed by OS).
452
+ Matches range-mode endpoints too (they carry aria-selected="true"); the circle (::before) —
453
+ NOT the band — carries the fill, so the shape signifier survives forced-colors. */
454
+ :scope [data-part='grid'] [role='gridcell'][aria-selected='true']:not([data-in-range]) {
455
+ color: HighlightText;
456
+ }
457
+ :scope [data-part='grid'] [role='gridcell'][aria-selected='true']:not([data-in-range])::before {
458
+ background-color: Highlight;
459
+ }
460
+
461
+ /* Range interior (data-in-range): the SAME Highlight/HighlightText pair as the endpoints —
462
+ one self-delimiting band (ADR-0093 clause 4), painted on the BAND (button) so interior stays
463
+ square while endpoints (painted on the POINT layer above) stay circular. Written explicitly
464
+ (rather than relying on the [aria-selected='true'] rule above alone) so the four-state
465
+ contract stays true independent of any future reordering of this stylesheet. */
466
+ :scope [data-part='grid'] [role='gridcell'][data-in-range] {
467
+ background-color: Highlight;
468
+ color: HighlightText;
469
+ }
470
+
471
+ /* Half-wash bridge: the same Highlight as the band — one contiguous run under WHCM too. */
472
+ :scope [data-part='grid'] [role='gridcell'][data-range-start]:not([data-range-end])::after,
473
+ :scope [data-part='grid'] [role='gridcell'][data-range-end]:not([data-range-start])::after {
474
+ background-color: Highlight;
475
+ }
476
+
477
+ /* Today ring: ButtonText INSET border on the fixed-diameter POINT layer — deliberately NOT
478
+ Highlight so focus/selected/today remain visually distinct: focus=Highlight-outside,
479
+ selected=Highlight-fill, today=ButtonText-inset. */
480
+ :scope [data-part='grid'] [role='gridcell'][data-today]::before {
481
+ box-shadow: inset 0 0 0 2px ButtonText;
482
+ }
483
+
484
+ /* Disabled cells: GrayText (the system muted role). */
485
+ :scope [data-part='grid'] [role='gridcell'][aria-disabled='true'] {
486
+ color: GrayText;
487
+ }
488
+
489
+ /* Nav buttons: ButtonText. */
490
+ :scope [data-part='prev'],
491
+ :scope [data-part='next'] {
492
+ color: ButtonText;
493
+ }
494
+ }
495
+ }