@slotted/styles 0.0.0-stage → 0.1.0-beta.1

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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/package.json +276 -3
  4. package/src/accordion/accordion.css +96 -0
  5. package/src/accordion/accordion.tokens.json +14 -0
  6. package/src/action-bar/action-bar.css +455 -0
  7. package/src/action-bar/action-bar.tokens.json +50 -0
  8. package/src/action-sheet/action-sheet.css +462 -0
  9. package/src/action-sheet/action-sheet.tokens.json +59 -0
  10. package/src/alert/alert.css +764 -0
  11. package/src/alert/alert.tokens.json +77 -0
  12. package/src/app-shell/app-shell.css +1899 -0
  13. package/src/app-shell/app-shell.tokens.json +88 -0
  14. package/src/aspect-ratio/aspect-ratio.css +131 -0
  15. package/src/aspect-ratio/aspect-ratio.tokens.json +1 -0
  16. package/src/attachment/attachment.css +906 -0
  17. package/src/attachment/attachment.tokens.json +85 -0
  18. package/src/avatar/avatar.css +598 -0
  19. package/src/avatar/avatar.tokens.json +64 -0
  20. package/src/avatar-group/avatar-group.css +268 -0
  21. package/src/avatar-group/avatar-group.tokens.json +16 -0
  22. package/src/badge/badge.css +462 -0
  23. package/src/badge/badge.tokens.json +58 -0
  24. package/src/breadcrumb/breadcrumb.css +441 -0
  25. package/src/breadcrumb/breadcrumb.tokens.json +50 -0
  26. package/src/button/button.css +754 -0
  27. package/src/button/button.tokens.json +72 -0
  28. package/src/button-group/button-group.css +455 -0
  29. package/src/button-group/button-group.tokens.json +11 -0
  30. package/src/calendar/calendar.css +846 -0
  31. package/src/calendar/calendar.tokens.json +67 -0
  32. package/src/card/card.css +602 -0
  33. package/src/card/card.tokens.json +68 -0
  34. package/src/carousel/carousel.css +860 -0
  35. package/src/carousel/carousel.tokens.json +62 -0
  36. package/src/cascader/cascader.css +1320 -0
  37. package/src/cascader/cascader.tokens.json +103 -0
  38. package/src/chart/chart.css +1473 -0
  39. package/src/chart/chart.tokens.json +107 -0
  40. package/src/checkbox/checkbox.css +1011 -0
  41. package/src/checkbox/checkbox.tokens.json +102 -0
  42. package/src/checkbox-group/checkbox-group.css +70 -0
  43. package/src/checkbox-group/checkbox-group.tokens.json +6 -0
  44. package/src/code-block/code-block.css +555 -0
  45. package/src/code-block/code-block.tokens.json +69 -0
  46. package/src/collapsible/collapsible.css +546 -0
  47. package/src/collapsible/collapsible.tokens.json +55 -0
  48. package/src/color-picker/color-picker.css +607 -0
  49. package/src/color-picker/color-picker.tokens.json +58 -0
  50. package/src/column-manager/column-manager.css +452 -0
  51. package/src/column-manager/column-manager.tokens.json +43 -0
  52. package/src/combobox/combobox.css +1048 -0
  53. package/src/combobox/combobox.tokens.json +112 -0
  54. package/src/command-palette/command-palette.css +1000 -0
  55. package/src/command-palette/command-palette.tokens.json +75 -0
  56. package/src/container/container.css +198 -0
  57. package/src/container/container.tokens.json +18 -0
  58. package/src/context-menu/context-menu.css +104 -0
  59. package/src/context-menu/context-menu.tokens.json +11 -0
  60. package/src/currency-field/currency-field.css +787 -0
  61. package/src/currency-field/currency-field.tokens.json +80 -0
  62. package/src/dashboard-grid/dashboard-grid.css +748 -0
  63. package/src/dashboard-grid/dashboard-grid.tokens.json +67 -0
  64. package/src/data-grid/data-grid.css +2484 -0
  65. package/src/data-grid/data-grid.tokens.json +100 -0
  66. package/src/data-list/data-list.css +455 -0
  67. package/src/data-list/data-list.tokens.json +35 -0
  68. package/src/date-field/date-field.css +560 -0
  69. package/src/date-field/date-field.tokens.json +80 -0
  70. package/src/date-picker/date-picker.css +420 -0
  71. package/src/date-picker/date-picker.tokens.json +49 -0
  72. package/src/date-range-picker/date-range-picker.css +983 -0
  73. package/src/date-range-picker/date-range-picker.tokens.json +109 -0
  74. package/src/datetime-field/datetime-field.css +850 -0
  75. package/src/datetime-field/datetime-field.tokens.json +91 -0
  76. package/src/datetime-picker/datetime-picker.css +843 -0
  77. package/src/datetime-picker/datetime-picker.tokens.json +78 -0
  78. package/src/datetime-range-picker/datetime-range-picker.css +1375 -0
  79. package/src/datetime-range-picker/datetime-range-picker.tokens.json +135 -0
  80. package/src/description-list/description-list.css +675 -0
  81. package/src/description-list/description-list.tokens.json +66 -0
  82. package/src/detail-panel/detail-panel.css +329 -0
  83. package/src/detail-panel/detail-panel.tokens.json +33 -0
  84. package/src/dialog/dialog.css +968 -0
  85. package/src/dialog/dialog.tokens.json +66 -0
  86. package/src/divider/divider.css +131 -0
  87. package/src/divider/divider.tokens.json +18 -0
  88. package/src/drawer/drawer.css +1303 -0
  89. package/src/drawer/drawer.tokens.json +75 -0
  90. package/src/entity-picker/entity-picker.css +1367 -0
  91. package/src/entity-picker/entity-picker.tokens.json +118 -0
  92. package/src/event-calendar/event-calendar.css +2148 -0
  93. package/src/event-calendar/event-calendar.tokens.json +87 -0
  94. package/src/field/field.css +707 -0
  95. package/src/field/field.tokens.json +58 -0
  96. package/src/field-group/field-group.css +226 -0
  97. package/src/field-group/field-group.tokens.json +33 -0
  98. package/src/filter-bar/filter-bar.css +1306 -0
  99. package/src/filter-bar/filter-bar.tokens.json +78 -0
  100. package/src/form/form.css +218 -0
  101. package/src/form/form.tokens.json +32 -0
  102. package/src/fullscreen/fullscreen.css +376 -0
  103. package/src/fullscreen/fullscreen.tokens.json +50 -0
  104. package/src/grid/grid.css +173 -0
  105. package/src/grid/grid.tokens.json +20 -0
  106. package/src/highlight/highlight.css +135 -0
  107. package/src/highlight/highlight.tokens.json +16 -0
  108. package/src/hover-card/hover-card.css +93 -0
  109. package/src/hover-card/hover-card.tokens.json +17 -0
  110. package/src/icon/icon.css +136 -0
  111. package/src/icon/icon.tokens.json +14 -0
  112. package/src/image/image.css +449 -0
  113. package/src/image/image.tokens.json +27 -0
  114. package/src/import-export-center/import-export-center.css +1175 -0
  115. package/src/import-export-center/import-export-center.tokens.json +84 -0
  116. package/src/inline/inline.css +149 -0
  117. package/src/inline/inline.tokens.json +14 -0
  118. package/src/inline-message/inline-message.css +396 -0
  119. package/src/inline-message/inline-message.tokens.json +19 -0
  120. package/src/kbd/kbd.css +490 -0
  121. package/src/kbd/kbd.tokens.json +40 -0
  122. package/src/link/link.css +237 -0
  123. package/src/link/link.tokens.json +25 -0
  124. package/src/list/list.css +240 -0
  125. package/src/list/list.tokens.json +42 -0
  126. package/src/list-item/list-item.css +937 -0
  127. package/src/list-item/list-item.tokens.json +114 -0
  128. package/src/listbox/listbox.css +674 -0
  129. package/src/listbox/listbox.tokens.json +72 -0
  130. package/src/loading-overlay/loading-overlay.css +730 -0
  131. package/src/loading-overlay/loading-overlay.tokens.json +48 -0
  132. package/src/markdown/markdown.css +168 -0
  133. package/src/markdown/markdown.tokens.json +18 -0
  134. package/src/menu/menu.css +817 -0
  135. package/src/menu/menu.tokens.json +78 -0
  136. package/src/menubar/menubar.css +170 -0
  137. package/src/menubar/menubar.tokens.json +32 -0
  138. package/src/message/message.css +1453 -0
  139. package/src/message/message.tokens.json +85 -0
  140. package/src/message-scroller/message-scroller.css +371 -0
  141. package/src/message-scroller/message-scroller.tokens.json +47 -0
  142. package/src/multi-select/multi-select.css +1427 -0
  143. package/src/multi-select/multi-select.tokens.json +114 -0
  144. package/src/navigation-menu/navigation-menu.css +1029 -0
  145. package/src/navigation-menu/navigation-menu.tokens.json +70 -0
  146. package/src/notification-center/notification-center.css +856 -0
  147. package/src/notification-center/notification-center.tokens.json +84 -0
  148. package/src/number-field/number-field.css +794 -0
  149. package/src/number-field/number-field.tokens.json +80 -0
  150. package/src/otp-input/otp-input.css +525 -0
  151. package/src/otp-input/otp-input.tokens.json +69 -0
  152. package/src/page/page.css +179 -0
  153. package/src/page/page.tokens.json +20 -0
  154. package/src/page-header/page-header.css +1181 -0
  155. package/src/page-header/page-header.tokens.json +71 -0
  156. package/src/pagination/pagination.css +500 -0
  157. package/src/pagination/pagination.tokens.json +56 -0
  158. package/src/popover/popover.css +526 -0
  159. package/src/popover/popover.tokens.json +60 -0
  160. package/src/progress-indicator/progress-indicator.css +1584 -0
  161. package/src/progress-indicator/progress-indicator.tokens.json +50 -0
  162. package/src/prompt-composer/prompt-composer.css +680 -0
  163. package/src/prompt-composer/prompt-composer.tokens.json +99 -0
  164. package/src/prose/prose.css +733 -0
  165. package/src/prose/prose.tokens.json +79 -0
  166. package/src/radio-group/radio-group.css +778 -0
  167. package/src/radio-group/radio-group.tokens.json +103 -0
  168. package/src/rating/rating.css +1071 -0
  169. package/src/rating/rating.tokens.json +88 -0
  170. package/src/result-state/result-state.css +980 -0
  171. package/src/result-state/result-state.tokens.json +63 -0
  172. package/src/runtime-properties.mjs +321 -0
  173. package/src/saved-views/saved-views.css +394 -0
  174. package/src/saved-views/saved-views.tokens.json +41 -0
  175. package/src/scroll-area/scroll-area.css +481 -0
  176. package/src/scroll-area/scroll-area.tokens.json +15 -0
  177. package/src/section/section.css +177 -0
  178. package/src/section/section.tokens.json +39 -0
  179. package/src/segmented-control/segmented-control.css +564 -0
  180. package/src/segmented-control/segmented-control.tokens.json +67 -0
  181. package/src/select/select.css +1593 -0
  182. package/src/select/select.tokens.json +110 -0
  183. package/src/side-navigation/side-navigation.css +1096 -0
  184. package/src/side-navigation/side-navigation.tokens.json +87 -0
  185. package/src/skeleton/skeleton.css +431 -0
  186. package/src/skeleton/skeleton.tokens.json +29 -0
  187. package/src/slider/slider.css +792 -0
  188. package/src/slider/slider.tokens.json +75 -0
  189. package/src/sparkline/sparkline.css +282 -0
  190. package/src/sparkline/sparkline.tokens.json +45 -0
  191. package/src/spinner/spinner.css +224 -0
  192. package/src/spinner/spinner.tokens.json +8 -0
  193. package/src/split-button/split-button.css +253 -0
  194. package/src/split-button/split-button.tokens.json +12 -0
  195. package/src/splitter/splitter.css +336 -0
  196. package/src/splitter/splitter.tokens.json +24 -0
  197. package/src/stack/stack.css +128 -0
  198. package/src/stack/stack.tokens.json +14 -0
  199. package/src/stat-card/stat-card.css +641 -0
  200. package/src/stat-card/stat-card.tokens.json +95 -0
  201. package/src/status-indicator/status-indicator.css +481 -0
  202. package/src/status-indicator/status-indicator.tokens.json +38 -0
  203. package/src/stepper/stepper.css +629 -0
  204. package/src/stepper/stepper.tokens.json +72 -0
  205. package/src/switch/switch.css +703 -0
  206. package/src/switch/switch.tokens.json +75 -0
  207. package/src/table/table.css +1482 -0
  208. package/src/table/table.tokens.json +69 -0
  209. package/src/table-of-contents/table-of-contents.css +533 -0
  210. package/src/table-of-contents/table-of-contents.tokens.json +53 -0
  211. package/src/tabs/tabs.css +998 -0
  212. package/src/tabs/tabs.tokens.json +82 -0
  213. package/src/tag/tag.css +768 -0
  214. package/src/tag/tag.tokens.json +85 -0
  215. package/src/tags-input/tags-input.css +757 -0
  216. package/src/tags-input/tags-input.tokens.json +84 -0
  217. package/src/text-field/text-field.css +900 -0
  218. package/src/text-field/text-field.tokens.json +87 -0
  219. package/src/textarea/textarea.css +365 -0
  220. package/src/textarea/textarea.tokens.json +59 -0
  221. package/src/time-field/time-field.css +719 -0
  222. package/src/time-field/time-field.tokens.json +86 -0
  223. package/src/time-picker/time-picker.css +665 -0
  224. package/src/time-picker/time-picker.tokens.json +66 -0
  225. package/src/time-range-picker/time-range-picker.css +1287 -0
  226. package/src/time-range-picker/time-range-picker.tokens.json +134 -0
  227. package/src/timeline/timeline.css +1596 -0
  228. package/src/timeline/timeline.tokens.json +96 -0
  229. package/src/toast/toast.css +943 -0
  230. package/src/toast/toast.tokens.json +90 -0
  231. package/src/toggle/toggle.css +290 -0
  232. package/src/toggle/toggle.tokens.json +54 -0
  233. package/src/toggle-group/toggle-group.css +463 -0
  234. package/src/toggle-group/toggle-group.tokens.json +54 -0
  235. package/src/toolbar/toolbar.css +434 -0
  236. package/src/toolbar/toolbar.tokens.json +49 -0
  237. package/src/tooltip/tooltip.css +368 -0
  238. package/src/tooltip/tooltip.tokens.json +39 -0
  239. package/src/top-navigation/top-navigation.css +1287 -0
  240. package/src/top-navigation/top-navigation.tokens.json +91 -0
  241. package/src/tour/tour.css +680 -0
  242. package/src/tour/tour.tokens.json +67 -0
  243. package/src/transfer/transfer.css +571 -0
  244. package/src/transfer/transfer.tokens.json +49 -0
  245. package/src/tree/tree.css +1000 -0
  246. package/src/tree/tree.tokens.json +80 -0
  247. package/src/typography/typography.css +211 -0
  248. package/src/typography/typography.tokens.json +31 -0
  249. package/src/upload/upload.css +1706 -0
  250. package/src/upload/upload.tokens.json +123 -0
  251. package/src/visually-hidden/visually-hidden.css +52 -0
  252. package/src/visually-hidden/visually-hidden.tokens.json +1 -0
  253. package/src/wizard/wizard.css +740 -0
  254. package/src/wizard/wizard.tokens.json +63 -0
@@ -0,0 +1,900 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── The single-line text control ───────────────────────────────────────
4
+ *
5
+ * A frame around an `input`, rather than the `input` itself. The frame is
6
+ * what holds an adornment, a clear button and a reveal button inside one
7
+ * border, and no void element can hold a child. WAI-ARIA settles the rest:
8
+ * a `textbox` may hold only non-interactive, presentational children, so the
9
+ * two buttons are siblings of the control inside a frame with no role.
10
+ *
11
+ * Every rule keys on `data-ui` and `data-part`. The field around the control
12
+ * mirrors its state onto the frame rather than styling through, so a control
13
+ * used outside a field looks the same as one inside it and the field's DOM
14
+ * never becomes part of this stylesheet's contract.
15
+ */
16
+ [data-ui='text-field'][data-part='root'] {
17
+ /**
18
+ * Type, height, inline padding, gap and glyph size all come from the
19
+ * control scales — the only ones the theme restates per density — so a
20
+ * field in a dense table shrinks whole rather than in parts.
21
+ */
22
+ --_font-size: var(--slotted-control-font-size-md, 13px);
23
+ --_height: var(--slotted-control-height-md, 34px);
24
+ --_padding-inline: var(--slotted-control-padding-inline-md, 13px);
25
+ --_gap: var(--slotted-control-gap-md, 8px);
26
+ /**
27
+ * The glyph scale falls to 13px and 12px at the dense step, and a clear
28
+ * mark that small is a speck beside the text (SL3). The floor is the
29
+ * family's own token so a theme can retune it; 14px is the size the
30
+ * comfortable `sm` step draws, and the smallest a two-stroke mark reads at.
31
+ */
32
+ --_icon-floor: var(--slotted-text-field-icon-size-min, 14px);
33
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
34
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
35
+
36
+ /**
37
+ * The drawn button, and only the drawn button. It is sized by its glyph —
38
+ * a step of air on each side, the same box every inner control in the
39
+ * library draws (R2) — and capped at the inner size of a control at this
40
+ * step, `--slotted-control-inner-size-*`, the height a key or a chip
41
+ * already takes inside the same frame. Before the cap it was bounded by
42
+ * the frame's inner box less a step of air, which let it measure 22px in a
43
+ * 26px field at the compact step, where the inside of that field is 20px.
44
+ *
45
+ * The WCAG 2.2 minimum is not this measurement. At `sm` and `dense` the
46
+ * whole control is 24 pixels tall, so a button inset inside it cannot also
47
+ * be 24; the target is recovered outside the paint, on the pseudo-element
48
+ * further down.
49
+ */
50
+ --_action-size: var(
51
+ --slotted-text-field-action-size,
52
+ min(calc(var(--_icon-size) + 2 * var(--slotted-space-2, 0.25rem)), var(--_inner-size))
53
+ );
54
+
55
+ /**
56
+ * The air between a button's ground and the frame's inner edge, the same
57
+ * in both axes, and the corner that makes the two shapes concentric. The
58
+ * corner never falls below the smallest step of the radius scale: a
59
+ * square ground inside a rounded frame reads as a hole cut in it.
60
+ */
61
+ --_inset: max(
62
+ 0px,
63
+ calc(
64
+ (var(--_height) - 2 * var(--slotted-control-border-width, 1px) - var(--_action-size)) / 2
65
+ )
66
+ );
67
+ --_action-radius: max(
68
+ var(--slotted-radius-xs, 2px),
69
+ calc(var(--slotted-text-field-radius, var(--slotted-control-radius, 4px)) - var(--_inset))
70
+ );
71
+ /**
72
+ * What a button inside the frame animates, written once for the three
73
+ * built-in buttons and the consumer's `action`: its ground for hover and
74
+ * press, its ink for a copy that finished.
75
+ */
76
+ --_action-transition:
77
+ background-color var(--slotted-control-transition-duration)
78
+ var(--slotted-control-transition-easing),
79
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
80
+
81
+ align-items: center;
82
+ background-color: var(--slotted-text-field-background, var(--slotted-control-surface, Field));
83
+ /**
84
+ * The control step of the border scale rather than the strong one. A
85
+ * separator between two surfaces and the edge of a control are the same
86
+ * ratio and not the same weight, and reading `--slotted-border-strong`
87
+ * here drew a frame that was the darkest line on the form.
88
+ */
89
+ border: var(--slotted-control-border-width, 1px) solid
90
+ var(--slotted-text-field-border-color, var(--slotted-control-border-color, ButtonBorder));
91
+ border-radius: var(--slotted-text-field-radius, var(--slotted-control-radius, 4px));
92
+ /**
93
+ * The shadow every framed control carries. It is barely visible on purpose:
94
+ * it separates the control from the ground it sits on without adding a
95
+ * second edge, which is what lets the border soften.
96
+ */
97
+ box-shadow: var(--slotted-text-field-shadow, var(--slotted-control-shadow, none));
98
+ box-sizing: border-box;
99
+ color: var(--slotted-text-field-color, var(--slotted-text, CanvasText));
100
+ display: flex;
101
+ font-family: var(--slotted-control-font-family, system-ui, sans-serif);
102
+ font-size: var(--_font-size);
103
+ gap: var(--_gap);
104
+ inline-size: 100%;
105
+ /* A control in a grid track narrower than its own intrinsic width would
106
+ otherwise refuse to shrink and take the drawer's edge with it. */
107
+ min-inline-size: 0;
108
+ /* A minimum rather than a height: the frame is the taller of the control
109
+ scale and whatever a consumer put in an adornment. */
110
+ min-block-size: var(--_height);
111
+ padding-inline: var(--_padding-inline);
112
+ transition:
113
+ background-color var(--slotted-control-transition-duration)
114
+ var(--slotted-control-transition-easing),
115
+ border-color var(--slotted-control-transition-duration)
116
+ var(--slotted-control-transition-easing),
117
+ box-shadow var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
118
+ }
119
+
120
+ /**
121
+ * A button at either end of the frame is inset concentrically: the air
122
+ * beside it equals the air above and below it, and its corner is the
123
+ * frame's corner less that air, so the ground a hover paints is a smaller
124
+ * copy of the frame rather than a box that drifted toward the value. The
125
+ * earlier rule aligned the glyph to the text's margin and left the hover
126
+ * ground nine pixels from the end and four from the top at `md`; ReUI's
127
+ * `c-input-group-11` measures four and four with a radius of seven inside
128
+ * ten (docs/history/designs/2026-09-30-text-field-reui-dossier.md § 3).
129
+ *
130
+ * The cost is written down: at `md` the glyph now ends four pixels nearer
131
+ * the border than the text begins from it.
132
+ */
133
+ [data-ui='text-field'][data-part='root']:has(
134
+ > :is([data-part='clear'], [data-part='copy'], [data-part='reveal']):not([hidden]):last-child,
135
+ > [data-part='end']:last-child > [data-part='action']:last-child
136
+ ) {
137
+ padding-inline-end: var(--_inset);
138
+ }
139
+
140
+ [data-ui='text-field'][data-part='root']:has(
141
+ > [data-part='start']:first-child > [data-part='action']:first-child
142
+ ) {
143
+ padding-inline-start: var(--_inset);
144
+ }
145
+
146
+ /**
147
+ * The control. It gives up its own frame entirely — border, ground, ring and
148
+ * padding all belong to the box around it — and keeps everything that makes
149
+ * it a text control: the value, the caret, the selection and every native
150
+ * attribute a consumer wrote.
151
+ */
152
+ [data-ui='text-field'] > [data-part='input'] {
153
+ background-color: transparent;
154
+ /* The frame's border box, not its content box: on a coarse pointer the frame
155
+ is the touch target's height and the control it holds is too. Stretched
156
+ rather than a percentage of the frame, which is indefinite wherever the
157
+ frame is not stretched itself — a search field in a bar measured 17
158
+ pixels. The control paints no ground, so the frame's edge still shows. */
159
+ align-self: stretch;
160
+ margin-block: calc(var(--slotted-control-border-width, 1px) * -1);
161
+ border: 0;
162
+ color: inherit;
163
+ flex: 1 1 auto;
164
+ font: inherit;
165
+ /* The frame draws the ring for it, so two rings never nest. */
166
+ outline: none;
167
+ padding: 0;
168
+ /* The intrinsic width of an `input` is about twenty characters, and a flex
169
+ item refuses to shrink below it without this. */
170
+ min-inline-size: 0;
171
+ /* The user agent pins a text control to `start`. Read from the box around
172
+ it instead, so a `plain` control in a numeric cell sits where the
173
+ cell's numbers do. */
174
+ text-align: inherit;
175
+ text-overflow: ellipsis;
176
+ }
177
+
178
+ [data-ui='text-field'] > [data-part='input']::placeholder {
179
+ color: var(--slotted-text-field-placeholder-color, var(--slotted-text-muted, GrayText));
180
+ /* A placeholder longer than the control is clipped at a word rather than
181
+ mid-glyph, which is the difference between a hint and a smear. */
182
+ text-overflow: ellipsis;
183
+ }
184
+
185
+ /**
186
+ * The family draws its own clear button, so the one WebKit adds to a
187
+ * `type="search"` control would be the second. The native decorations are
188
+ * withdrawn rather than left to overlap.
189
+ */
190
+ [data-ui='text-field'] > [data-part='input']::-webkit-search-cancel-button,
191
+ [data-ui='text-field'] > [data-part='input']::-webkit-search-decoration {
192
+ appearance: none;
193
+ }
194
+
195
+ /**
196
+ * Autofill. Chrome paints its own ground over the control's, and the classic
197
+ * repair — a hundred-pixel inset shadow — is the only way to reach it. The
198
+ * signal is kept rather than erased: an autofilled value is a fact worth
199
+ * seeing, so it is repainted in the accent surface the theme owns instead of
200
+ * in the browser's yellow.
201
+ */
202
+ [data-ui='text-field'] > [data-part='input']:autofill {
203
+ box-shadow: inset 0 0 0 100px
204
+ var(--slotted-text-field-autofill-background, var(--slotted-tone-accent-surface));
205
+ -webkit-text-fill-color: var(--slotted-text-field-color, var(--slotted-text, CanvasText));
206
+ }
207
+
208
+ /* ── Size ─────────────────────────────────────────────────────────────
209
+ *
210
+ * Three steps, matching the field the control sits in. The control reads the
211
+ * field's step rather than its own default when there is a field, because a
212
+ * `size="sm"` field holding an `md` control is a field and a control
213
+ * disagreeing about one row.
214
+ */
215
+
216
+ [data-ui='text-field'][data-part='root'][data-size='sm'] {
217
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
218
+ --_height: var(--slotted-control-height-sm, 30px);
219
+ --_padding-inline: var(--slotted-control-padding-inline-sm, 10px);
220
+ --_gap: var(--slotted-control-gap-sm, 6px);
221
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-sm, 14px));
222
+ --_inner-size: var(--slotted-control-inner-size-sm, 22px);
223
+ }
224
+
225
+ [data-ui='text-field'][data-part='root'][data-size='md'] {
226
+ --_font-size: var(--slotted-control-font-size-md, 13px);
227
+ --_height: var(--slotted-control-height-md, 34px);
228
+ --_padding-inline: var(--slotted-control-padding-inline-md, 13px);
229
+ --_gap: var(--slotted-control-gap-md, 8px);
230
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
231
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
232
+ }
233
+
234
+ [data-ui='text-field'][data-part='root'][data-size='lg'] {
235
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
236
+ --_height: var(--slotted-control-height-lg, 38px);
237
+ --_padding-inline: var(--slotted-control-padding-inline-lg, 16px);
238
+ --_gap: var(--slotted-control-gap-lg, 8px);
239
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-lg, 18px));
240
+ --_inner-size: var(--slotted-control-inner-size-lg, 28px);
241
+ }
242
+
243
+ /**
244
+ * Joined into a `button-group` beside a button — "https://" and a check, a
245
+ * query and its submit — the control is the part of the bar that gives way:
246
+ * it takes the width the buttons leave rather than the whole row, so the
247
+ * button's label never wraps to make room for it.
248
+ */
249
+ [data-ui='button-group'] > [data-ui='text-field'][data-part='root'] {
250
+ flex: 1 1 0;
251
+ inline-size: auto;
252
+ }
253
+
254
+ /* ── Variant: how much frame the control shows ────────────────────────
255
+ *
256
+ * `outline` carries `--slotted-control-surface`: the field is a plane of its
257
+ * own, a step off the ground in each scheme — white on the application's
258
+ * canvas in `light`, a step above the surface in `dark`. Transparent was
259
+ * tried first, on the rule that a form inside a card and the same form on
260
+ * the page both look right without a second token; what it produced was a
261
+ * field with no plane at all, found only by its edge.
262
+ * `subtle` sinks into the page. `plain` shows nothing at rest and is what a
263
+ * dense table needs: a cell that is only a control while it is being used,
264
+ * and that keeps exactly the same box so the row does not reflow when the
265
+ * pointer crosses it.
266
+ */
267
+
268
+ [data-ui='text-field'][data-part='root'][data-variant='subtle'] {
269
+ background-color: var(
270
+ --slotted-text-field-subtle-background,
271
+ var(--slotted-surface-sunken, Canvas)
272
+ );
273
+ border-color: var(
274
+ --slotted-text-field-subtle-border-color,
275
+ var(--slotted-border-subtle, ButtonBorder)
276
+ );
277
+ }
278
+
279
+ /* No edge, no lift, no ground and no corner of its own: the cell it sits in
280
+ is the box, the row is the ground, a cell has no corner, and the resting
281
+ shadow would draw the cell where there is none. */
282
+ [data-ui='text-field'][data-part='root'][data-variant='plain'] {
283
+ background-color: transparent;
284
+ border-color: transparent;
285
+ border-radius: var(--slotted-radius-none, 0px);
286
+ box-shadow: none;
287
+ }
288
+
289
+ /**
290
+ * The one rule the theme cannot write for the family. A transparent border
291
+ * has no system colour to be remapped to, so the `plain` control would lose
292
+ * its only edge exactly where an edge matters most.
293
+ */
294
+ @media (forced-colors: active) {
295
+ [data-ui='text-field'][data-part='root'][data-variant='plain'] {
296
+ border-color: ButtonBorder;
297
+ }
298
+ }
299
+
300
+ /* ── Monospace ────────────────────────────────────────────────────────
301
+ *
302
+ * A back-office form full of identifiers, hashes and keys, which is the
303
+ * audience `docs/product/prd.md` names first. Primer and Polaris both ship it.
304
+ */
305
+ [data-ui='text-field'][data-part='root'][data-mono] {
306
+ font-family: var(--slotted-font-family-mono, monospace);
307
+ /* Digits that do not shift as a value is typed over. */
308
+ font-variant-numeric: tabular-nums;
309
+ }
310
+
311
+ /* ── Adornments ───────────────────────────────────────────────────────
312
+ *
313
+ * Named for the reading direction rather than for a side, so the same markup
314
+ * puts the currency before the amount in both directions. They are quieter
315
+ * than the value: an adornment annotates, and a reader looking for what they
316
+ * typed should not have to step over it.
317
+ */
318
+ [data-ui='text-field'] > [data-part='start'],
319
+ [data-ui='text-field'] > [data-part='end'] {
320
+ align-items: center;
321
+ color: var(--slotted-text-field-adornment-color, var(--slotted-text-muted, GrayText));
322
+ display: inline-flex;
323
+ /* Never squeezed: the value gives up width first, because the value is the
324
+ part that can scroll. */
325
+ flex: 0 0 auto;
326
+ gap: var(--slotted-space-2, 0.25rem);
327
+ line-height: 1;
328
+ /**
329
+ * Its own bidi run, ordered by its own content. `R$` is a Latin letter and
330
+ * a currency symbol, and in a right-to-left document the symbol resolved to
331
+ * the paragraph's direction and printed `$R` — first as part of the value's
332
+ * run, then, with `isolate`, as an isolated run that was still ordered
333
+ * right-to-left. `plaintext` takes the direction from the adornment's own
334
+ * first strong character, so `R$` reads as itself and an Arabic unit reads
335
+ * as itself, in either document direction.
336
+ */
337
+ unicode-bidi: plaintext;
338
+ }
339
+
340
+ /**
341
+ * An `icon` placed in either adornment is sized by the control rather than
342
+ * by the page. The icon family reads `--slotted-icon-size` from whatever
343
+ * contains it, which is how a container sizes every glyph inside it without
344
+ * knowing anything else about that family — so a `sm` control in a dense
345
+ * table draws a 12-pixel glyph and nothing has to be written per instance.
346
+ * A bare `svg` a consumer passed instead is held to the same box.
347
+ */
348
+ [data-ui='text-field'] > [data-part='start'],
349
+ [data-ui='text-field'] > [data-part='end'] {
350
+ --slotted-icon-size: var(--_icon-size);
351
+ /* And a key or a chip placed there is the height the inside of this
352
+ control has, from the foundation's scale for it. */
353
+ --slotted-control-inner-size: var(--_inner-size);
354
+ }
355
+
356
+ [data-ui='text-field'] > [data-part='start'] > svg,
357
+ [data-ui='text-field'] > [data-part='end'] > svg {
358
+ block-size: var(--_icon-size);
359
+ inline-size: var(--_icon-size);
360
+ }
361
+
362
+ /**
363
+ * Optical alignment between the two kinds of adornment. A glyph fills the
364
+ * whole `--_icon-size` box, but its artwork is drawn inside a 24-unit grid
365
+ * that leaves two to three units of margin on each side; a character fills
366
+ * only its own advance width. Placed at the same padding edge the two boxes
367
+ * start together and the two *marks* do not, so `@` and `R$` hung two pixels
368
+ * outside the magnifier stacked above them.
369
+ *
370
+ * An adornment that draws no glyph therefore takes the margin the glyph box
371
+ * already has — an eighth of that box, the middle of the range the surveyed
372
+ * icon sets leave — on both sides. Both sides, because the same inset then
373
+ * keeps the gap to the value equal as well, and because a symmetric padding
374
+ * cannot disagree with `unicode-bidi: plaintext` about which side is which.
375
+ */
376
+ [data-ui='text-field']
377
+ > [data-part='start']:not(:has(svg, img, [data-ui='icon'], [data-part='action'])),
378
+ [data-ui='text-field']
379
+ > [data-part='end']:not(:has(svg, img, [data-ui='icon'], [data-part='action'])) {
380
+ padding-inline: calc(var(--_icon-size) / 8);
381
+ }
382
+
383
+ /* ── The buttons inside the frame ─────────────────────────────────────
384
+ *
385
+ * Four the family draws — clear, copy, reveal, and nothing else — and the
386
+ * consumer's own `action`, placed in either adornment: a search submit, a
387
+ * menu trigger naming a country, a tooltip on an information mark. All five
388
+ * are real buttons, share one box, one ink, one hover and one press, and
389
+ * are held to the target minimum by the family rather than by the page.
390
+ * Their glyphs are the `Glyph` element the binding renders: a character is
391
+ * never an icon.
392
+ */
393
+ [data-ui='text-field'] > :is([data-part='clear'], [data-part='copy'], [data-part='reveal']),
394
+ [data-ui='text-field'] > :is([data-part='start'], [data-part='end']) > [data-part='action'] {
395
+ --slotted-icon-size: var(--_icon-size);
396
+
397
+ align-items: center;
398
+ appearance: none;
399
+ background-color: transparent;
400
+ block-size: var(--_action-size);
401
+ border: 0;
402
+ border-radius: var(--_action-radius);
403
+ box-sizing: border-box;
404
+ color: var(--slotted-text-field-action-color, var(--slotted-text-muted, GrayText));
405
+ cursor: pointer;
406
+ display: inline-flex;
407
+ flex: 0 0 auto;
408
+ font: inherit;
409
+ gap: var(--slotted-space-2, 0.25rem);
410
+ justify-content: center;
411
+ line-height: 1;
412
+ min-inline-size: var(--_action-size);
413
+ /* The user agent pads a button, and inside a box sized to its glyph that
414
+ padding squeezed the glyph to twelve pixels. */
415
+ padding: 0;
416
+ /* The containing block of the hit area below. */
417
+ position: relative;
418
+ /* No tap delay waiting for a second tap the button cannot use. */
419
+ touch-action: manipulation;
420
+ transition: var(--_action-transition);
421
+ /* The adornments' rule, for the same reason: `+55` is its own run and
422
+ reads as itself in a right-to-left document. */
423
+ unicode-bidi: plaintext;
424
+ white-space: nowrap;
425
+ -webkit-tap-highlight-color: transparent;
426
+ }
427
+
428
+ /**
429
+ * The consumer's button named by its own text — "Search", a country code
430
+ * over a chevron — takes a step of the space scale on each side, the six
431
+ * pixels ReUI's trigger measures, and the weight a control label carries.
432
+ * One named by `aria-label` or `aria-labelledby` is an icon, and stays the
433
+ * square every built-in button is: a button with no text has to carry one of
434
+ * the two, so the attribute that makes it accessible is the one that shapes
435
+ * it.
436
+ */
437
+ [data-ui='text-field']
438
+ > :is([data-part='start'], [data-part='end'])
439
+ > [data-part='action']:not([aria-label], [aria-labelledby]) {
440
+ /* Words in the frame's quiet ink read as a suffix, not as something to
441
+ press: "Search" beside the value looked like a unit. A named command
442
+ or a chosen country code carries the value's ink; a lone mark stays
443
+ quiet, the way the built-in buttons are. */
444
+ color: var(--slotted-text-field-action-label-color, var(--slotted-text, CanvasText));
445
+ font-weight: var(--slotted-font-weight-medium, 500);
446
+ padding-inline: var(--slotted-space-3, 0.375rem);
447
+ }
448
+
449
+ /**
450
+ * Words and a mark in one button — a country code over its chevron, a model
451
+ * name, a notation. The mark is centred on the band the letters occupy, not
452
+ * on the line box: the line box keeps room under the baseline for
453
+ * descenders, so a box centred in it sits low beside the letters — every
454
+ * chevron in these buttons did. The rule is the briefing's (_A mark beside a
455
+ * line of text is aligned to the text_), here in a grid: items share the
456
+ * baseline, the mark's synthesized baseline is its bottom edge, a negative
457
+ * start margin takes back the height it would add above the letters, and a
458
+ * translate puts its centre at `(1cap + 1ex) / 4` above the baseline — the
459
+ * midpoint of the capitals' and the lowercase letters' own centres.
460
+ */
461
+ [data-ui='text-field']
462
+ > :is([data-part='start'], [data-part='end'])
463
+ > [data-part='action']:not([aria-label], [aria-labelledby]) {
464
+ align-content: center;
465
+ align-items: baseline;
466
+ display: inline-grid;
467
+ grid-auto-flow: column;
468
+ justify-content: center;
469
+ }
470
+
471
+ [data-ui='text-field']
472
+ > :is([data-part='start'], [data-part='end'])
473
+ > [data-part='action']:not([aria-label], [aria-labelledby])
474
+ > [data-ui='icon'] {
475
+ margin-block-start: calc((1cap + 1ex) / 4 - var(--_icon-size) / 2);
476
+ translate: 0 calc(var(--_icon-size) / 2 - (1cap + 1ex) / 4);
477
+ }
478
+
479
+ /**
480
+ * An adornment holding a button is not quiet text any more: its gap is the
481
+ * one between two buttons, and it does not pad a glyph that is not there.
482
+ */
483
+ [data-ui='text-field'] > :is([data-part='start'], [data-part='end']):has(> [data-part='action']) {
484
+ gap: 0;
485
+ }
486
+
487
+ /**
488
+ * The hit area, recovered outside the paint. The drawn button is inset
489
+ * inside the frame so the frame keeps its vertical air, and at `sm` with
490
+ * `dense` the whole control is 24 pixels tall — the WCAG 2.2 minimum — so
491
+ * nothing drawn inside it can also be 24 pixels. This grows the target back
492
+ * to that minimum in both axes and leaves a larger one alone, which is the
493
+ * correction `docs/token-foundation.md` leaves to the family rather than to
494
+ * the theme.
495
+ */
496
+ [data-ui='text-field']
497
+ > :is([data-part='clear'], [data-part='copy'], [data-part='reveal'])::after,
498
+ [data-ui='text-field']
499
+ > :is([data-part='start'], [data-part='end'])
500
+ > [data-part='action']::after {
501
+ content: '';
502
+ inset: min(0px, calc((100% - var(--slotted-target-min, 24px)) / 2));
503
+ position: absolute;
504
+ }
505
+
506
+ /* On a finger the target is the comfortable size in both axes and the box
507
+ is the one a mouse sees: the ground and the focus ring stay square, and
508
+ the hit area grows to 44 pixels around them. Side by side, no button
509
+ reaches across the edge it shares with the next. */
510
+ @media (pointer: coarse) {
511
+ [data-ui='text-field']
512
+ > :is([data-part='start'], [data-part='end'])
513
+ > [data-part='action']::after,
514
+ [data-ui='text-field']
515
+ > :is([data-part='clear'], [data-part='copy'], [data-part='reveal'])::after {
516
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
517
+ }
518
+
519
+ [data-ui='text-field']
520
+ > :is([data-part='clear'], [data-part='copy']):has(
521
+ ~ :is([data-part='copy'], [data-part='reveal']):not([hidden])
522
+ )::after,
523
+ [data-ui='text-field']
524
+ > :is([data-part='start'], [data-part='end'])
525
+ > [data-part='action']:has(+ [data-part='action'])::after {
526
+ inset-inline-end: 0;
527
+ }
528
+
529
+ [data-ui='text-field']
530
+ > :is([data-part='clear'], [data-part='copy']):not([hidden])
531
+ ~ :is([data-part='copy'], [data-part='reveal'])::after,
532
+ [data-ui='text-field']
533
+ > :is([data-part='start'], [data-part='end'])
534
+ > [data-part='action']
535
+ + [data-part='action']::after {
536
+ inset-inline-start: 0;
537
+ }
538
+ }
539
+
540
+ /* An open menu keeps its button pressed under the pointer as well, and a
541
+ finished copy keeps its success ink until the check goes. */
542
+ [data-ui='text-field']
543
+ > :is([data-part='clear'], [data-part='copy'], [data-part='reveal']):hover:not(
544
+ :disabled,
545
+ [data-complete]
546
+ ),
547
+ [data-ui='text-field']
548
+ > :is([data-part='start'], [data-part='end'])
549
+ > [data-part='action']:hover:not(:disabled, [aria-expanded='true']) {
550
+ background-color: var(
551
+ --slotted-text-field-action-hover-background,
552
+ var(--slotted-tone-secondary-subtle-hover)
553
+ );
554
+ }
555
+
556
+ /**
557
+ * Hover and press move the ground and nothing else, one step each (R2,
558
+ * `inner-control.test.mjs`): ReUI's inner button also brightens its ink and
559
+ * settles a pixel, and inside a 24-pixel box beside the value that reads as
560
+ * the value moving (dossier § 4, `recusar`).
561
+ */
562
+ [data-ui='text-field']
563
+ > :is([data-part='clear'], [data-part='copy'], [data-part='reveal']):active:not(:disabled),
564
+ [data-ui='text-field']
565
+ > :is([data-part='start'], [data-part='end'])
566
+ > [data-part='action']:active:not(:disabled) {
567
+ background-color: var(
568
+ --slotted-text-field-action-active-background,
569
+ var(--slotted-tone-secondary-subtle-active)
570
+ );
571
+ }
572
+
573
+ /* A menu or a popover the button opened keeps it pressed while it is open. */
574
+ [data-ui='text-field']
575
+ > :is([data-part='start'], [data-part='end'])
576
+ > [data-part='action'][aria-expanded='true'] {
577
+ background-color: var(
578
+ --slotted-text-field-action-active-background,
579
+ var(--slotted-tone-secondary-subtle-active)
580
+ );
581
+ }
582
+
583
+ [data-ui='text-field']
584
+ > :is([data-part='start'], [data-part='end'])
585
+ > [data-part='action']:disabled,
586
+ [data-ui='text-field'][data-part='root'][data-disabled]
587
+ > :is([data-part='start'], [data-part='end'])
588
+ > [data-part='action'] {
589
+ color: var(--slotted-disabled-foreground);
590
+ cursor: not-allowed;
591
+ }
592
+
593
+ /**
594
+ * A button carries its own ring rather than borrowing the frame's. Tabbing
595
+ * from the value to a button has to move something visible, and a ring
596
+ * around the whole frame would sit still while focus moved inside it.
597
+ */
598
+ [data-ui='text-field']
599
+ > :is([data-part='clear'], [data-part='copy'], [data-part='reveal']):focus-visible,
600
+ [data-ui='text-field']
601
+ > :is([data-part='start'], [data-part='end'])
602
+ > [data-part='action']:focus-visible {
603
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
604
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
605
+ }
606
+
607
+ /** A copy that finished: the mark changes as well as the ink, and returns on its own. */
608
+ [data-ui='text-field'] > [data-part='copy'][data-complete] {
609
+ color: var(
610
+ --slotted-text-field-copy-complete-color,
611
+ var(--slotted-tone-success-text, CanvasText)
612
+ );
613
+ }
614
+
615
+ /**
616
+ * The mark a button swaps — copy for a check, an eye for a crossed eye — is
617
+ * a new element each time, and it arrives rather than replaces: from four
618
+ * fifths of its size and transparent, on the entering curve. Nothing leaves:
619
+ * the old mark is gone in the same frame, so there is no exit to time.
620
+ */
621
+ [data-ui='text-field'] > :is([data-part='copy'], [data-part='reveal']) > [data-ui='icon'] {
622
+ transition:
623
+ opacity var(--slotted-duration-short) var(--slotted-easing-enter),
624
+ scale var(--slotted-duration-short) var(--slotted-easing-enter);
625
+ }
626
+
627
+ @starting-style {
628
+ [data-ui='text-field'] > :is([data-part='copy'], [data-part='reveal']) > [data-ui='icon'] {
629
+ opacity: 0;
630
+ scale: 0.8;
631
+ }
632
+ }
633
+
634
+ /* ── What comes and goes inside the frame ───────────────────────────────
635
+ *
636
+ * The ring while a check runs, the Caps Lock mark, the clear button once
637
+ * there is something to clear, the copy button once there is something to
638
+ * copy. Each is rendered for as long as the capability is on and hidden
639
+ * with the `hidden` attribute rather than removed, so it can leave as well
640
+ * as arrive: it grows in from four fifths on the entering curve and shrinks
641
+ * out on the leaving one, in the same short step, and `display` is held
642
+ * until it has gone.
643
+ */
644
+ [data-ui='text-field']
645
+ > :is([data-part='busy'], [data-part='caps-lock'], [data-part='clear'], [data-part='copy']) {
646
+ transition:
647
+ var(--_action-transition),
648
+ opacity var(--slotted-duration-short) var(--slotted-easing-enter),
649
+ scale var(--slotted-duration-short) var(--slotted-easing-enter),
650
+ display var(--slotted-duration-short) allow-discrete;
651
+ }
652
+
653
+ [data-ui='text-field']
654
+ > :is(
655
+ [data-part='busy'],
656
+ [data-part='caps-lock'],
657
+ [data-part='clear'],
658
+ [data-part='copy']
659
+ )[hidden] {
660
+ display: none;
661
+ opacity: 0;
662
+ scale: 0.8;
663
+ transition:
664
+ opacity var(--slotted-duration-short) var(--slotted-easing-exit),
665
+ scale var(--slotted-duration-short) var(--slotted-easing-exit),
666
+ display var(--slotted-duration-short) allow-discrete;
667
+ }
668
+
669
+ @starting-style {
670
+ [data-ui='text-field']
671
+ > :is([data-part='busy'], [data-part='caps-lock'], [data-part='clear'], [data-part='copy']) {
672
+ opacity: 0;
673
+ scale: 0.8;
674
+ }
675
+ }
676
+
677
+ /**
678
+ * The ring is the one `spinner` draws for every family, at this control's
679
+ * glyph size, so it occupies the box a mark beside it would.
680
+ */
681
+ [data-ui='text-field'] > [data-part='busy'] {
682
+ --slotted-spinner-size: var(--_icon-size);
683
+
684
+ color: var(--slotted-text-field-adornment-color, var(--slotted-text-muted, GrayText));
685
+ flex: 0 0 auto;
686
+ }
687
+
688
+ /**
689
+ * Caps Lock in a password: a warning about the next thing typed, not about
690
+ * the value, so it is the warning ink on the glyph alone and never the
691
+ * frame's edge — the frame's one alarming state is `invalid`.
692
+ */
693
+ [data-ui='text-field'] > [data-part='caps-lock'] {
694
+ --slotted-icon-size: var(--_icon-size);
695
+
696
+ align-items: center;
697
+ color: var(--slotted-text-field-caps-lock-color, var(--slotted-tone-warning-text, CanvasText));
698
+ display: inline-flex;
699
+ flex: 0 0 auto;
700
+ }
701
+
702
+ /* ── States ───────────────────────────────────────────────────────────
703
+ *
704
+ * Written strongest last, and each weaker one guarded against every stronger
705
+ * one it can meet, which is the order `STATE_PRECEDENCE` sets.
706
+ */
707
+
708
+ [data-ui='text-field'][data-part='root']:hover:not([data-disabled]):not([data-readonly]):not(
709
+ [data-invalid]
710
+ ) {
711
+ border-color: var(--slotted-text-field-hover-border-color, var(--slotted-tone-accent-border));
712
+ }
713
+
714
+ [data-ui='text-field'][data-part='root'][data-variant='subtle']:hover:not([data-disabled]):not(
715
+ [data-readonly]
716
+ ):not([data-invalid]) {
717
+ background-color: var(
718
+ --slotted-text-field-subtle-hover-background,
719
+ var(--slotted-tone-secondary-subtle-hover)
720
+ );
721
+ }
722
+
723
+ [data-ui='text-field'][data-part='root'][data-variant='plain']:hover:not([data-disabled]):not(
724
+ [data-readonly]
725
+ ):not([data-invalid]) {
726
+ background-color: var(
727
+ --slotted-text-field-plain-hover-background,
728
+ var(--slotted-tone-secondary-subtle-hover)
729
+ );
730
+ /* The accent edge the other variants take would draw the cell's outline. */
731
+ border-color: transparent;
732
+ }
733
+
734
+ /**
735
+ * The ring belongs to the frame, and it answers the control's focus rather
736
+ * than the frame's: a browser matches `:focus-visible` on a text input for a
737
+ * pointer press as well as for a Tab, which is exactly right for a control
738
+ * whose caret is the thing that moved.
739
+ */
740
+ [data-ui='text-field'][data-part='root']:has(> [data-part='input']:focus-visible) {
741
+ /**
742
+ * The resting shadow, recoloured. The geometry is the foundation's and the
743
+ * ink is the focus ring's, so the lift under the control and the ring
744
+ * around it are one colour and the state decides which colour that is.
745
+ */
746
+ box-shadow: var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
747
+ var(--slotted-focus-ring-color, Highlight);
748
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
749
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
750
+ }
751
+
752
+ /**
753
+ * The ring of a control whose box is not its own is drawn inside that box,
754
+ * the way the clear button draws its ring inside the frame: two pixels past
755
+ * the control is two pixels over the next row's rule. No lift either — a
756
+ * shadow that appears only under focus is a cell that changes size when it
757
+ * is used.
758
+ */
759
+ [data-ui='text-field'][data-part='root'][data-variant='plain']:has(
760
+ > [data-part='input']:focus-visible
761
+ ) {
762
+ box-shadow: none;
763
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
764
+ }
765
+
766
+ /**
767
+ * Invalid outranks hover and is written after it for that reason: a control
768
+ * reporting an error should not stop looking wrong because the pointer is
769
+ * over it.
770
+ *
771
+ * The ring is retargeted rather than repainted. Every rule that draws focus —
772
+ * the frame's outline, the shadow under it, the ring on either button — reads
773
+ * `--slotted-focus-ring-color`, so pointing that one name at the invalid
774
+ * colour on the element moves all of them at once. That is the whole of the
775
+ * rule for a family, and it is the rule `checkbox`, `radio-group` and
776
+ * `switch` inherit rather than restate when they arrive.
777
+ */
778
+ [data-ui='text-field'][data-part='root'][data-invalid] {
779
+ --slotted-focus-ring-color: var(
780
+ --slotted-text-field-invalid-focus-ring-color,
781
+ var(--slotted-focus-ring-color-invalid, Highlight)
782
+ );
783
+ --_invalid-edge-width: var(
784
+ --slotted-text-field-invalid-edge-width,
785
+ var(--slotted-control-invalid-edge-width, 1px)
786
+ );
787
+ --_invalid-edge-color: var(
788
+ --slotted-text-field-invalid-border-color,
789
+ var(--slotted-tone-danger-border)
790
+ );
791
+ --_invalid-edge: inset 0 0 0 var(--_invalid-edge-width) var(--_invalid-edge-color);
792
+
793
+ border-color: var(--slotted-text-field-invalid-border-color, var(--slotted-tone-danger-border));
794
+ /*
795
+ * Colour alone does not say the field is wrong (WCAG 1.4.1): in
796
+ * `high-contrast` the edge at rest, the danger border and both rings are
797
+ * one black. An edge inside the border makes the invalid line thicker in
798
+ * every scheme. It is a shadow rather than a wider border, so the box and
799
+ * the text do not move; it replaces the resting lift, which may be `none`
800
+ * and so cannot be listed beside it.
801
+ */
802
+ box-shadow: var(--_invalid-edge);
803
+ }
804
+
805
+ [data-ui='text-field'][data-part='root'][data-invalid]:has(> [data-part='input']:focus-visible) {
806
+ box-shadow:
807
+ var(--_invalid-edge),
808
+ var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
809
+ var(--slotted-focus-ring-color, Highlight);
810
+ }
811
+
812
+ [data-ui='text-field'][data-part='root'][data-variant='plain'][data-invalid]:has(
813
+ > [data-part='input']:focus-visible
814
+ ) {
815
+ /* The ring lies inside the box here, over the border and an edge as wide as
816
+ it; the edge reaches past the ring. */
817
+ box-shadow: inset 0 0 0
818
+ calc(
819
+ var(--slotted-focus-ring-offset, 2px) - var(--slotted-control-border-width, 1px) +
820
+ var(--_invalid-edge-width)
821
+ )
822
+ var(--_invalid-edge-color);
823
+ }
824
+
825
+ /*
826
+ * Forced colours remove every box shadow, and the edge with them. The
827
+ * width comes back as an outline inside the border, and a focused invalid
828
+ * control draws its ring double, which a valid ring is not.
829
+ */
830
+ @media (forced-colors: active) {
831
+ [data-ui='text-field'][data-part='root'][data-invalid] {
832
+ outline: var(--_invalid-edge-width) solid CanvasText;
833
+ outline-offset: calc(
834
+ -1 * (var(--slotted-control-border-width, 1px) + var(--_invalid-edge-width))
835
+ );
836
+ }
837
+
838
+ [data-ui='text-field'][data-part='root'][data-invalid]:has(
839
+ > [data-part='input']:focus-visible
840
+ ) {
841
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
842
+ var(--slotted-focus-ring-color, Highlight);
843
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
844
+ }
845
+
846
+ [data-ui='text-field'][data-part='root'][data-variant='plain'][data-invalid]:has(
847
+ > [data-part='input']:focus-visible
848
+ ) {
849
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
850
+ var(--slotted-focus-ring-color, Highlight);
851
+ outline-offset: calc(
852
+ -1 * (var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width))
853
+ );
854
+ }
855
+ }
856
+
857
+ /**
858
+ * Read-only stays focusable, selectable and legible, and it must not be
859
+ * mistakable for disabled: the value is still real and still submitted. The
860
+ * ground says "not yours to change" and the ink says nothing at all.
861
+ */
862
+ [data-ui='text-field'][data-part='root'][data-readonly] {
863
+ background-color: var(
864
+ --slotted-text-field-readonly-background,
865
+ var(--slotted-tone-secondary-subtle-hover)
866
+ );
867
+ cursor: default;
868
+ }
869
+
870
+ /**
871
+ * `:disabled` on the control is matched beside `data-disabled` on the frame,
872
+ * because a control can be disabled by something that never told it so: a
873
+ * disabled `fieldset` disables every control inside it, and the control has
874
+ * no way to know. The attribute alone left such a control looking fully
875
+ * operable and answering nothing — a defect the deleted `input` family
876
+ * recorded and fixed, and one no rebuild is allowed to reintroduce.
877
+ */
878
+ [data-ui='text-field'][data-part='root'][data-disabled],
879
+ [data-ui='text-field'][data-part='root']:has(> [data-part='input']:disabled) {
880
+ background-color: var(
881
+ --slotted-text-field-disabled-background,
882
+ var(--slotted-disabled-background)
883
+ );
884
+ border-color: var(--slotted-text-field-disabled-border-color, var(--slotted-disabled-border));
885
+ /* Nothing a person cannot reach is lifted off the page. */
886
+ box-shadow: none;
887
+ color: var(--slotted-text-field-disabled-color, var(--slotted-disabled-foreground));
888
+ cursor: not-allowed;
889
+ }
890
+
891
+ /**
892
+ * Muted to the colour the theme measures rather than faded by opacity: a
893
+ * value nobody can read is a field nobody can check, and a disabled form
894
+ * still has to say what it holds.
895
+ */
896
+ [data-ui='text-field'][data-part='root'][data-disabled] > [data-part='start'],
897
+ [data-ui='text-field'][data-part='root'][data-disabled] > [data-part='end'] {
898
+ color: var(--slotted-disabled-foreground);
899
+ }
900
+ }