@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,120 @@
1
+ import { UIFormElement, type ReactiveProps } from '../../dom/index.js';
2
+ import type { FormValue } from '../../dom/index.js';
3
+ declare const rangeProps: {
4
+ min: {
5
+ reflect: true;
6
+ type: import("../../index.js").PropType<number | null>;
7
+ default: number | null;
8
+ attribute?: string | false;
9
+ };
10
+ max: {
11
+ reflect: true;
12
+ type: import("../../index.js").PropType<number | null>;
13
+ default: number | null;
14
+ attribute?: string | false;
15
+ };
16
+ step: {
17
+ reflect: true;
18
+ type: import("../../index.js").PropType<number | null>;
19
+ default: number | null;
20
+ attribute?: string | false;
21
+ };
22
+ value: {
23
+ reflect: true;
24
+ type: import("../../index.js").PropType<number | null>;
25
+ default: number | null;
26
+ attribute?: string | false;
27
+ };
28
+ size: {
29
+ reflect: true;
30
+ type: import("../../index.js").PropType<"sm" | "md" | "lg">;
31
+ default: "sm" | "md" | "lg";
32
+ attribute?: string | false;
33
+ };
34
+ name: {
35
+ reflect: true;
36
+ type: import("../../index.js").PropType<string>;
37
+ default: string;
38
+ attribute?: string | false;
39
+ };
40
+ disabled: {
41
+ reflect: true;
42
+ type: import("../../index.js").PropType<boolean>;
43
+ default: boolean;
44
+ attribute?: string | false;
45
+ };
46
+ required: {
47
+ reflect: true;
48
+ type: import("../../index.js").PropType<boolean>;
49
+ default: boolean;
50
+ attribute?: string | false;
51
+ };
52
+ };
53
+ export interface UIRangeElement extends ReactiveProps<typeof rangeProps> {
54
+ }
55
+ export declare class UIRangeElement extends UIFormElement {
56
+ #private;
57
+ static props: {
58
+ min: {
59
+ reflect: true;
60
+ type: import("../../index.js").PropType<number | null>;
61
+ default: number | null;
62
+ attribute?: string | false;
63
+ };
64
+ max: {
65
+ reflect: true;
66
+ type: import("../../index.js").PropType<number | null>;
67
+ default: number | null;
68
+ attribute?: string | false;
69
+ };
70
+ step: {
71
+ reflect: true;
72
+ type: import("../../index.js").PropType<number | null>;
73
+ default: number | null;
74
+ attribute?: string | false;
75
+ };
76
+ value: {
77
+ reflect: true;
78
+ type: import("../../index.js").PropType<number | null>;
79
+ default: number | null;
80
+ attribute?: string | false;
81
+ };
82
+ size: {
83
+ reflect: true;
84
+ type: import("../../index.js").PropType<"sm" | "md" | "lg">;
85
+ default: "sm" | "md" | "lg";
86
+ attribute?: string | false;
87
+ };
88
+ name: {
89
+ reflect: true;
90
+ type: import("../../index.js").PropType<string>;
91
+ default: string;
92
+ attribute?: string | false;
93
+ };
94
+ disabled: {
95
+ reflect: true;
96
+ type: import("../../index.js").PropType<boolean>;
97
+ default: boolean;
98
+ attribute?: string | false;
99
+ };
100
+ required: {
101
+ reflect: true;
102
+ type: import("../../index.js").PropType<boolean>;
103
+ default: boolean;
104
+ attribute?: string | false;
105
+ };
106
+ };
107
+ /**
108
+ * LLD-C1: the normalised value serialised as the form-submitted string.
109
+ * The base normalises before serialising so the form entry always reflects the clamped+snapped value.
110
+ */
111
+ protected formValue(): FormValue;
112
+ /**
113
+ * LLD-C2: hook for `internals.ariaValueText`. Override to return a locale-formatted value string
114
+ * (e.g. `"$12.00"`). The base returns `String(value)`. Named `valueText` (not `ariaValueText`) to
115
+ * avoid shadowing the `ARIAMixin.ariaValueText` DOM property.
116
+ */
117
+ protected valueText(value: number): string;
118
+ protected connected(): void;
119
+ }
120
+ export {};
@@ -0,0 +1,158 @@
1
+ // range-element.ts — UIRangeElement, the shared base for the Range class (ui-slider · ui-slider-multi).
2
+ // range-element.lld.md.
3
+ //
4
+ // Owns: the numeric value model with clamping/snapping (LLD-C1), the ARIA slider semantics
5
+ // (LLD-C2), keyboard step handling (LLD-C3), and the geometry seam — the `--value-pct` custom
6
+ // property (LLD-C5). The value-drag controller (LLD-C4) is wired by the leaf subclass from its
7
+ // own `connected()`.
8
+ //
9
+ // Layer: controls/_base/ — imports dom only.
10
+ // Inward-only ✓ (controls ← traits ← dom ← reactive).
11
+ import { UIFormElement, prop } from "../../dom/index.js";
12
+ const rangeProps = {
13
+ ...UIFormElement.formProps,
14
+ // LLD-C1: numeric range params. All reflected for attribute-driven construction.
15
+ // prop.number returns PropConfig<number | null>; defaults (0/100/1) mean values are never null in practice.
16
+ min: { ...prop.number(0), reflect: true },
17
+ max: { ...prop.number(100), reflect: true },
18
+ step: { ...prop.number(1), reflect: true },
19
+ value: { ...prop.number(0), reflect: true },
20
+ // ADR-0042 clause-2: the shared widget-box size axis (the track/thumb ride --md-sys-compact-{size}); all
21
+ // Range controls inherit it — typed here, not per-leaf (the same lesson as UIIndicatorElement's size).
22
+ size: { ...prop.enum(['sm', 'md', 'lg'], 'md'), reflect: true },
23
+ };
24
+ export class UIRangeElement extends UIFormElement {
25
+ static props = rangeProps;
26
+ // LLD-C3: the value at focus — committed baseline for `change` event emission on blur.
27
+ // Reset at connect so a reconnect without an intervening focus cannot fire a stale `change`.
28
+ #committed = null;
29
+ /**
30
+ * LLD-C1: normalise a raw value against the current min/max/step.
31
+ * - min > max → zero-length range; pin to min.
32
+ * - step ≤ 0 → continuous; clamp only, no snap.
33
+ * - step not dividing (max−min) → last reachable step is ≤ max (snap-down);
34
+ * max itself is always reachable exactly (the platform `<input type=range>` End rule).
35
+ */
36
+ #normalize(raw) {
37
+ const min = this.min ?? 0;
38
+ const max = this.max ?? 100;
39
+ const step = this.step ?? 1;
40
+ if (min > max)
41
+ return min;
42
+ const clamped = Math.max(min, Math.min(max, raw));
43
+ if (step <= 0)
44
+ return clamped;
45
+ // max is always reachable exactly — End → max, and clamping to max lands here too.
46
+ if (clamped === max)
47
+ return max;
48
+ const steps = Math.round((clamped - min) / step);
49
+ const snapped = min + steps * step;
50
+ // snap-down if snapped overshoots max
51
+ return snapped > max ? min + (steps - 1) * step : snapped;
52
+ }
53
+ /** LLD-C5: percentage position of value along [min, max] for the `--value-pct` geometry seam. */
54
+ #valuePct(value) {
55
+ const min = this.min ?? 0;
56
+ const max = this.max ?? 100;
57
+ if (min >= max)
58
+ return 0;
59
+ return ((value - min) / (max - min)) * 100;
60
+ }
61
+ /**
62
+ * LLD-C1: the normalised value serialised as the form-submitted string.
63
+ * The base normalises before serialising so the form entry always reflects the clamped+snapped value.
64
+ */
65
+ formValue() {
66
+ return String(this.#normalize(this.value ?? 0));
67
+ }
68
+ /**
69
+ * LLD-C2: hook for `internals.ariaValueText`. Override to return a locale-formatted value string
70
+ * (e.g. `"$12.00"`). The base returns `String(value)`. Named `valueText` (not `ariaValueText`) to
71
+ * avoid shadowing the `ARIAMixin.ariaValueText` DOM property.
72
+ */
73
+ valueText(value) {
74
+ return String(value);
75
+ }
76
+ connected() {
77
+ // Reset the committed baseline so a reconnect without a focus cannot fire a stale `change`.
78
+ this.#committed = null;
79
+ // LLD-C2: role is structural, not data-driven; set once here rather than inside an effect.
80
+ this.internals.role = 'slider';
81
+ // LLD-C1: normaliser effect — keeps value clamped + snapped on every change to value/min/max/step.
82
+ // The kernel's Object.is cutoff means no signal update + no re-run when the value is already normal.
83
+ this.effect(() => {
84
+ const raw = this.value ?? 0;
85
+ const normalized = this.#normalize(raw);
86
+ if (!Object.is(normalized, raw))
87
+ this.value = normalized;
88
+ });
89
+ // LLD-C2: ARIA effect — publishes ariaValueNow/Min/Max/Text reactively, tracking all four props.
90
+ this.effect(() => {
91
+ const value = this.#normalize(this.value ?? 0);
92
+ this.internals.ariaValueNow = String(value);
93
+ this.internals.ariaValueMin = String(this.min ?? 0);
94
+ this.internals.ariaValueMax = String(this.max ?? 100);
95
+ this.internals.ariaValueText = this.valueText(value);
96
+ });
97
+ // LLD-C5: geometry seam — `--value-pct` on the host style so the subclass CSS can paint the fill
98
+ // and position the thumb without needing to import any JS from this base.
99
+ this.effect(() => {
100
+ const value = this.#normalize(this.value ?? 0);
101
+ this.style.setProperty('--value-pct', String(this.#valuePct(value)));
102
+ });
103
+ // LLD-C3: keyboard step — Arrow ±step, PageUp/Down ±largeStep (10×step), Home/End → min/max.
104
+ // Disabled host is inert (effectiveDisabled covers own + fieldset/form disabled).
105
+ // Each live step emits `input`; `change` is emitted on blur when value moved (below).
106
+ this.listen(this, 'keydown', (event) => {
107
+ if (this.effectiveDisabled())
108
+ return;
109
+ const e = event;
110
+ const min = this.min ?? 0;
111
+ const max = this.max ?? 100;
112
+ const step = this.step ?? 1;
113
+ const largeStep = step * 10;
114
+ const current = this.#normalize(this.value ?? 0);
115
+ let next = null;
116
+ switch (e.key) {
117
+ case 'ArrowRight':
118
+ case 'ArrowUp':
119
+ next = this.#normalize(current + step);
120
+ break;
121
+ case 'ArrowLeft':
122
+ case 'ArrowDown':
123
+ next = this.#normalize(current - step);
124
+ break;
125
+ case 'PageUp':
126
+ next = this.#normalize(current + largeStep);
127
+ break;
128
+ case 'PageDown':
129
+ next = this.#normalize(current - largeStep);
130
+ break;
131
+ case 'Home':
132
+ next = min;
133
+ break;
134
+ case 'End':
135
+ next = max; // End reaches max exactly (platform rule, matches the normaliser's max fast-path)
136
+ break;
137
+ default:
138
+ return;
139
+ }
140
+ e.preventDefault();
141
+ if (!Object.is(next, current)) {
142
+ this.value = next;
143
+ this.emit('input');
144
+ }
145
+ });
146
+ // LLD-C3: change on commit — track value at focus; emit `change` on blur when value has moved.
147
+ this.listen(this, 'focus', () => {
148
+ this.#committed = this.#normalize(this.value ?? 0);
149
+ });
150
+ this.listen(this, 'blur', () => {
151
+ const current = this.#normalize(this.value ?? 0);
152
+ if (this.#committed !== null && !Object.is(current, this.#committed)) {
153
+ this.emit('change');
154
+ }
155
+ this.#committed = null;
156
+ });
157
+ }
158
+ }
@@ -0,0 +1,301 @@
1
+ /* container-box.css — the shared container BOX-MODEL (margin inset + the sticky-region pattern). The second
2
+ * shared surface layer (after `_surface/container.css` which owns the elevation/brightness PAINT); this one
3
+ * owns the SPACING + region rhythm every container/panel adopts by marking itself `[data-box]`. Imported by
4
+ * component-styles.css right after container.css, before any element sheet.
5
+ *
6
+ * THE MODEL (ruled with Kim, flow-root + margin-collapse — REVISED 2026-07-04, inset regions):
7
+ * • L1 `[data-box]` establishes a BFC (`display: flow-root`) and holds NO inner padding — the children's
8
+ * margins provide the inset. A BFC means adjacent children's block margins COLLAPSE to one inset (a
9
+ * uniform 6px between siblings) AND the edge margins are preserved (6px from the box edge) — not doubled.
10
+ * • L2 direct children get `margin: var(--ui-box-inset)` (6px) by default. A child OVERRIDES its own margin
11
+ * to go full-bleed (`margin: 0`) — `hr` and an explicit `[data-full-bleed]` opt-out still do.
12
+ * • `[padded]` = a full-bleed child (margin 0) whose CONTENT is inset (`padding: var(--ui-box-inset)`) — e.g.
13
+ * a header/row that spans the box edge-to-edge but keeps its text off the edge.
14
+ * • Region pattern (REVISED 2026-07-04): `header`/`footer`/content (`main`/`[data-region]`) are now INSET,
15
+ * not full-bleed — they carry the SAME `--ui-box-inset` margin as every other child (the BFC collapsing
16
+ * above keeps the gutter uniform at 6px, frame↔region and region↔region alike). `header`/`footer` stay
17
+ * `position: sticky` + `background: inherit` (the box surface) so scrolled content never shows through —
18
+ * trivially overridden (a child that sets its own background wins). `hr` is still a full-bleed divider.
19
+ * `main`/content is the scroll region between them (it may itself be another container, or a list).
20
+ * • Z-DEPTH SCOPE (ADR-0052): `[data-box]` is also its own stacking context (`isolation: isolate` — the one
21
+ * purpose-built property: no containing block, no paint side effects). Every descendant z-index (the sticky
22
+ * brackets' `z-index: 1` below, a panel's internal chrome) resolves INSIDE the box and can never fight a
23
+ * sibling container or the page — no global z ladder can exist. Overlays are unaffected: popover/dialog
24
+ * surfaces ride the native TOP LAYER (ADR-0043/0045), which is immune to stacking contexts by spec.
25
+ * • EDGE-AWARE SCROLL FADE (the gutter-exposure fix, same revision): an inset sticky bracket's background no
26
+ * longer reaches the frame edge, so scrolled content can peek through the thin gutter around it. The fix
27
+ * lives on the CONTENT, not the bracket — `traits/scroll-fade.ts` toggles `data-fade-top`/`data-fade-bottom`
28
+ * on the scroll viewport from its live scroll position, and the `[data-fade-top]`/`[data-fade-bottom]`
29
+ * rules near the foot of this file mask only the edge that genuinely hides content (never a flat guess).
30
+ *
31
+ * Everything is specificity-0 (`:where()`), so an element's own `@scope` block cleanly overrides any of it.
32
+ * Opt-in only: an element is unaffected until it (or its control) marks the surface `[data-box]`.
33
+ *
34
+ * `--ui-box-inset` defaults to 0.375rem (6px at the 16px root) and is themeable per box. */
35
+
36
+ :where([data-box]) {
37
+ --ui-box-inset: 0.375rem; /* 6px — the uniform inset/gap unit; a box may repoint it */
38
+ display: flow-root; /* BFC: child margins collapse to ONE inset between siblings + hold the edge inset */
39
+ isolation: isolate; /* the box is its own Z-DEPTH SCOPE (ADR-0052): descendant z-indexes (the sticky
40
+ brackets below, panel-internal chrome) resolve inside THIS stacking context and
41
+ cannot paint over a sibling container — layout BFC + paint scope, one opt-in */
42
+
43
+ /* ── Region padding system (header / footer / content wrappers) — ruled with Kim ──
44
+ * The named regions carry their OWN internal padding + gap, distinct from the inset margin above:
45
+ * inline 12px · block 6px · gap 8px.
46
+ * A NESTED content steps its INLINE padding IN by one inset (6px) per level (12 → 6 → 6…, floored), so a
47
+ * content-inside-content stays concentric with its parent's ink: parent inline-pad 12 = child inset margin 6
48
+ * + child inline-pad 6. Block padding + gap stay constant at every level. */
49
+ --ui-box-pad-inline: 0.75rem; /* 12px — region inline padding */
50
+ --ui-box-pad-block: 0.375rem; /* 6px — region block padding */
51
+ --ui-box-gap: 0.5rem; /* 8px — gap between a region's own children */
52
+
53
+ /* Edge-aware scroll fade (below) — the depth of the mask band at a faded edge. Density-linked (like the
54
+ old per-component --ui-card-fade it replaces), NOT the rem-based region padding above (a decorative
55
+ affordance depth, not a layout spacing law). Any consumer may repoint it locally. The literal 1rem
56
+ fallback keeps this self-contained even where only this sheet is loaded (e.g. container-box.browser.test.ts,
57
+ which never imports the dimensional --md-sys-space-* ramp). */
58
+ --ui-box-fade: var(--md-sys-space-lg, 1rem);
59
+ }
60
+
61
+ /* Popover-carrier re-assertion (the s13 e2e finding, 2026-07-01): the unconditional `display: flow-root`
62
+ * above is AUTHOR-origin CSS, and author beats user-agent regardless of specificity — so on a box that is
63
+ * ALSO a `[popover]` (select/menu/combo-box's panels, ADR-0046), it silently defeats the UA's own
64
+ * `[popover]:not(:popover-open) { display: none }` hidden-until-shown rule. The bug class: a CLOSED panel
65
+ * kept participating in layout AND focus (rovingFocus's `tabindex=0` current option stayed real), so Tab
66
+ * landed inside a closed listbox instead of skipping to the next control. This rule re-asserts the UA
67
+ * semantics at specificity 0 (`:where`, same as everything else here) — an OPEN popover, or any non-popover
68
+ * box, keeps `flow-root` from the block above untouched; a component may still deliberately override. */
69
+ :where([data-box][popover]:not(:popover-open)) {
70
+ display: none;
71
+ }
72
+
73
+ /* Dialog-carrier re-assertion (the SAME class of bug as the `[popover]` rule above, hit by ui-modal's
74
+ * `<dialog data-part="dialog" data-box>` part, 2026-07-07): the unconditional `display: flow-root` is
75
+ * author-origin CSS and defeats the UA's own `dialog:not([open]) { display: none }` rule (author always beats
76
+ * UA, at ANY specificity) — so a CLOSED dialog kept rendering. Worse than the popover case: the UA's default
77
+ * `dialog { position: absolute; inset: 0; margin: auto; … }` then CENTRED the still-visible box on the page,
78
+ * exactly as if `showModal()` had been called (no backdrop, no top layer, no focus trap — just an inert box
79
+ * sitting on top of the page content). Two such closed dialogs on one page render stacked directly on top of
80
+ * each other, dead centre. Re-assert the UA semantics at specificity 0; an OPEN dialog (native `[open]`
81
+ * reflects `showModal()`/`show()`) keeps flow-root from the block above untouched. */
82
+ :where(dialog[data-box]:not([open])) {
83
+ display: none;
84
+ }
85
+
86
+ /* header / footer — the region padding + gap. A row of controls sits comfortably: inline 12 / block 6 with an
87
+ 8px gap. `display: flex` makes the gap apply; the element's own @scope may override the direction/alignment.
88
+ REVISED 2026-07-04: header/footer are no longer full-bleed — they carry the SAME `--ui-box-inset` margin as
89
+ every other `[data-box]` child (the generic `> *` rule below), so a header/footer divider or background no
90
+ longer reaches the box edge; it floats inside the frame with a uniform 6px gutter, same as any other region. */
91
+ :where([data-box]) > :where(header, footer, [data-region='header'], [data-region='footer']) {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: var(--ui-box-gap);
95
+ padding-inline: var(--ui-box-pad-inline);
96
+ padding-block: var(--ui-box-pad-block);
97
+ }
98
+
99
+ /* content — the region between the brackets: a column of stacked children with the 8px region gap. The INLINE
100
+ padding steps IN one inset per NESTING level so a content-inside-content stays concentric with its parent's
101
+ ink (parent 12 = child inset-margin 6 + child pad 6). Expressed as explicit descendant levels rather than a
102
+ self-referencing custom property (which CSS treats as a cycle → invalid): L1 12 · L2 6 · L3+ 6 (the block-pad
103
+ floor — L2 and the floor now coincide, since the inset and the block-pad token are both 6px). Three levels
104
+ covers real nesting; deeper content stays at the 6px floor. */
105
+ :where([data-region='content'], main) {
106
+ display: block; /* NOT flex — overflow:auto scrolls a block, and a block child keeps its height (flex would
107
+ shrink it and break a scroll viewport). The region gap comes from an adjacent-sibling margin. */
108
+ padding-inline: var(--ui-box-pad-inline); /* L1 = 12px */
109
+ padding-block: var(--ui-box-pad-block);
110
+ }
111
+ /* The 8px region rhythm between content children — a gap-without-flex adjacent-sibling margin (scroll-safe);
112
+ the first child takes no top margin (the block padding provides the inset). */
113
+ :where([data-region='content'], main) > * + * {
114
+ margin-block-start: var(--ui-box-gap);
115
+ }
116
+ :where([data-region='content'], main) :where([data-region='content'], main) {
117
+ padding-inline: calc(var(--ui-box-pad-inline) - var(--ui-box-inset)); /* L2 = 6px (12 − 6) */
118
+ }
119
+ :where([data-region='content'], main) :where([data-region='content'], main) :where([data-region='content'], main) {
120
+ padding-inline: var(--ui-box-pad-block); /* L3+ floor = 6px */
121
+ }
122
+
123
+ /* Every direct child carries the uniform inset margin (→ 6px from the edges, 6px between siblings) — INCLUDING
124
+ the region wrappers below (REVISED 2026-07-04: they are no longer full-bleed). The BFC established on
125
+ `[data-box]` above collapses adjacent children's margins to ONE inset, so this single rule is what gives
126
+ every gutter (frame↔region, region↔region) the same uniform 6px, never doubled. */
127
+ :where([data-box]) > * {
128
+ margin: var(--ui-box-inset);
129
+ }
130
+
131
+ /* Full-bleed children — span the box edge-to-edge. Only a genuine divider (`hr`) or an explicit opt-out
132
+ (`[data-full-bleed]`) still overrides to full-bleed; the region wrappers (header/content/footer/main/
133
+ [data-region]) NO LONGER appear here (REVISED 2026-07-04) — they keep the generic inset margin above, so a
134
+ region floats inside the frame instead of spanning it edge-to-edge. */
135
+ :where([data-box]) > :where(hr, [data-full-bleed]) {
136
+ margin: 0;
137
+ }
138
+
139
+ /* `[padded]` — full-bleed box, content inset (margin 0 + inset padding). */
140
+ :where([data-box]) > :where([padded]) {
141
+ margin: 0;
142
+ padding: var(--ui-box-inset);
143
+ }
144
+
145
+ /* Sticky regions — pinned to the box's scroll edges, inheriting the box surface so scrolled content does not
146
+ bleed through. `background: inherit` copies the parent's resolved background value (a header itself easily
147
+ overrides by declaring its own background).
148
+ REVISED 2026-07-04 (a cross-engine finding, both Chromium + WebKit): once a sticky box is actually STUCK, its
149
+ own margin is NOT honored as extra offset — the resting position is governed purely by the inset value, the
150
+ margin collapses to nothing (verified in container-box.browser.test.ts; this is NOT a bug in that test, it
151
+ is real platform behaviour). Since a region now carries a 6px margin at REST (the generic `> *` rule above),
152
+ an `inset-block-start: 0` would show the correct 6px gutter before scrolling and then SNAP flush (0px) the
153
+ moment it sticks — a jarring visual jump. The fix: the inset offset itself is `--ui-box-inset` (6px), not 0,
154
+ so the STUCK resting gap matches the AT-REST margin gap exactly, in both states. */
155
+ :where([data-box]) > :where(header, [data-region='header']) {
156
+ position: sticky;
157
+ inset-block-start: var(--ui-box-inset);
158
+ background: inherit;
159
+ z-index: 1;
160
+ }
161
+ :where([data-box]) > :where(footer, [data-region='footer']) {
162
+ position: sticky;
163
+ inset-block-end: var(--ui-box-inset);
164
+ background: inherit;
165
+ z-index: 1;
166
+ }
167
+
168
+ /* Divider — a full-bleed hairline rule between regions. Zero block-size so it adds no row height beyond the
169
+ 1px rule; the surrounding children's margins provide the breathing room. */
170
+ :where([data-box]) > hr {
171
+ border: 0;
172
+ border-block-start: 1px solid var(--md-sys-color-neutral-outline-variant);
173
+ block-size: 0;
174
+ margin: 0;
175
+ }
176
+
177
+ /* forced-colors — the sticky regions must stay opaque as a system colour (a transparent sticky header would
178
+ let scrolled text show through). The divider maps to a system border. */
179
+ @media (forced-colors: active) {
180
+ :where([data-box]) > :where(header, footer, [data-region='header'], [data-region='footer']) {
181
+ background: Canvas;
182
+ }
183
+ :where([data-box]) > hr {
184
+ border-block-start-color: CanvasText;
185
+ }
186
+ }
187
+
188
+ /* ── Edge-aware scroll fade ───────────────────────────────────────────────────────────────────────────────
189
+ * The gutter-exposure fix (the 2026-07-04 z-index + mask revision): once header/footer regions became INSET
190
+ * (above) rather than full-bleed, a sticky bracket's own `background: inherit` no longer reaches the frame
191
+ * edge — the thin gutter AROUND the bracket can show scrolled content peeking through. The z-index:1 the
192
+ * sticky regions already carry (isolated per-box via ADR-0052) fully masks content DIRECTLY under a bracket;
193
+ * this closes the remaining gap by having the SCROLLING CONTENT fade its own edges before they ever reach
194
+ * that gutter, so nothing hard shows there regardless.
195
+ *
196
+ * `traits/scroll-fade.ts` toggles `data-fade-top`/`data-fade-bottom` on the actual scroll VIEWPORT from its
197
+ * live scroll position — a flag is present ONLY when content is genuinely hidden past that edge, so a short /
198
+ * non-scrolling viewport (neither flag) is never faded and an at-an-edge viewport fades only the OTHER edge.
199
+ * The three combinations below are the whole matrix; no flags at all ⇒ no rule matches ⇒ mask-image stays
200
+ * `none` (the element's un-masked default). This is a GENERIC, cross-family rule (not scoped to `[data-box]`
201
+ * — ui-card is the scroll viewport in the box-model but is not itself a `[data-box]`) consumed identically by
202
+ * ui-card, the modal dialog, and the select/menu/combo-box panels; each opts in by wiring the trait, nothing
203
+ * more. The gradient recipe itself is the one card.css shipped first (alpha-based — `mask-mode` defaults to
204
+ * `alpha` for an image source, so ANY opaque colour reads as "visible"; `#000`/`transparent` here are mask
205
+ * channel values, not a theme paint). The -webkit- prefix is still required (no unprefixed mask-image on
206
+ * WebKit/Safari).
207
+ *
208
+ * PRESENCE-AWARE OFFSET (2026-07-04): the fade must adapt to a sticky header/footer. These sticky brackets DO
209
+ * keep `background: inherit` (+ `z-index: 1`, above) to occlude content directly beneath them — but the fade
210
+ * itself is carried by the GRADIENT, not by demanding an opaque bracket edge-to-edge: per Kim (2026-07-04) it
211
+ * is fine for a bit of the gradient to show THROUGH the bracket zone. So the fade RAMP simply reaches PAST a
212
+ * present bracket: content emerging under it fades out, while the ramp's transparent end still sits at the true
213
+ * viewport edge (where the bracket floats). `traits/scroll-fade.ts` publishes each edge's
214
+ * bracket band as `--ui-box-head` / `--ui-box-foot` — its rendered block-size + inset gutter, measured live
215
+ * (ResizeObserver), or 0px when that edge has NO bracket. Each ramp's OPAQUE end lands at
216
+ * `bracket-band + --ui-box-fade`, so a bracketed edge stays faded until content has fully cleared the bracket,
217
+ * and a bracketless edge (offset 0px) collapses to `--ui-box-fade` — the EXACT pre-offset mask. Absent the
218
+ * trait, the `, 0px` var-fallbacks reduce every stop back to that original recipe, so the rule is
219
+ * self-contained. (The precise how-much-through-the-bracket look is a fine-tune of these two knobs.)
220
+ *
221
+ * THE HOLD (added for ui-card-content's overlaid, backgroundless brackets — 2026-07-07): a bare
222
+ * `transparent 0 → opaque at band+fade` ramp lets content RAMP UP continuously across the bracket's own band —
223
+ * by the time content reaches the band's own far edge it can already be >50% visible, so a crisp bracket TEXT
224
+ * sitting over it still shows a partially-opaque scrolled line layered underneath (measured, card.browser.test.ts).
225
+ * `--ui-box-head-hold`/`--ui-box-foot-hold` (both default `0px`) insert an EXTRA same-colour stop immediately
226
+ * after each edge's `transparent`/`#000` anchor, at the hold depth — two coincident stops (identical position,
227
+ * identical colour) are a geometric NO-OP (nothing renders between them), so every consumer that never sets
228
+ * these (modal/select/menu/combo-box — opaque `background: inherit` brackets, unaffected by this at all) gets a
229
+ * RENDER-IDENTICAL mask — not string-identical: the fallback `var(…, 0px)` still serializes two extra
230
+ * coincident stops into their computed gradient, it is only the PAINT that is unchanged — proven by this
231
+ * file's own tests staying green. A consumer that DOES set the hold to its
232
+ * own bracket band (ui-card-content, card.css) instead holds fully transparent/opaque THROUGH the whole band —
233
+ * content is genuinely invisible behind bracket text — and the soft running fade lives entirely in the
234
+ * `--ui-box-fade`-deep strip just past the band, never inside it. */
235
+ :where([data-fade-top][data-fade-bottom]) {
236
+ /* Both edges hidden: transparent at each viewport edge → opaque once content has cleared that edge's bracket
237
+ band + the fade depth. Both offsets 0px ⇒ the original symmetric transparent→#000→#000→transparent mask. */
238
+ -webkit-mask-image: linear-gradient(
239
+ to bottom,
240
+ transparent 0,
241
+ transparent var(--ui-box-head-hold, 0px),
242
+ #000 calc(var(--ui-box-head, 0px) + var(--ui-box-fade, 1rem)),
243
+ #000 calc(100% - var(--ui-box-foot, 0px) - var(--ui-box-fade, 1rem)),
244
+ transparent calc(100% - var(--ui-box-foot-hold, 0px)),
245
+ transparent 100%
246
+ );
247
+ mask-image: linear-gradient(
248
+ to bottom,
249
+ transparent 0,
250
+ transparent var(--ui-box-head-hold, 0px),
251
+ #000 calc(var(--ui-box-head, 0px) + var(--ui-box-fade, 1rem)),
252
+ #000 calc(100% - var(--ui-box-foot, 0px) - var(--ui-box-fade, 1rem)),
253
+ transparent calc(100% - var(--ui-box-foot-hold, 0px)),
254
+ transparent 100%
255
+ );
256
+ }
257
+ :where([data-fade-top]):not([data-fade-bottom]) {
258
+ /* Only the top hides content: fade past a present header, opaque below. Header offset 0px ⇒ the original
259
+ transparent 0 → #000 --ui-box-fade top fade. */
260
+ -webkit-mask-image: linear-gradient(
261
+ to bottom,
262
+ transparent 0,
263
+ transparent var(--ui-box-head-hold, 0px),
264
+ #000 calc(var(--ui-box-head, 0px) + var(--ui-box-fade, 1rem)),
265
+ #000 100%
266
+ );
267
+ mask-image: linear-gradient(
268
+ to bottom,
269
+ transparent 0,
270
+ transparent var(--ui-box-head-hold, 0px),
271
+ #000 calc(var(--ui-box-head, 0px) + var(--ui-box-fade, 1rem)),
272
+ #000 100%
273
+ );
274
+ }
275
+ :where([data-fade-bottom]):not([data-fade-top]) {
276
+ /* Only the bottom hides content: opaque above, fade past a present footer to the bottom edge. Footer offset
277
+ 0px ⇒ the original #000 → transparent 100% bottom fade. */
278
+ -webkit-mask-image: linear-gradient(
279
+ to bottom,
280
+ #000 0,
281
+ #000 calc(100% - var(--ui-box-foot, 0px) - var(--ui-box-fade, 1rem)),
282
+ transparent calc(100% - var(--ui-box-foot-hold, 0px)),
283
+ transparent 100%
284
+ );
285
+ mask-image: linear-gradient(
286
+ to bottom,
287
+ #000 0,
288
+ #000 calc(100% - var(--ui-box-foot, 0px) - var(--ui-box-fade, 1rem)),
289
+ transparent calc(100% - var(--ui-box-foot-hold, 0px)),
290
+ transparent 100%
291
+ );
292
+ }
293
+
294
+ /* forced-colors — a mask over system-rendered text harms legibility; drop it (the sticky-region Canvas
295
+ background above still keeps the bracket itself opaque). */
296
+ @media (forced-colors: active) {
297
+ :where([data-fade-top], [data-fade-bottom]) {
298
+ -webkit-mask-image: none;
299
+ mask-image: none;
300
+ }
301
+ }