@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,1482 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── The frame ──────────────────────────────────────────────────────────
4
+ *
5
+ * The root is the frame and the scroller is one element in. Both boxes are
6
+ * needed and each answers a different question: `position: sticky` resolves
7
+ * against the nearest scrolling ancestor, which is the scroller either way,
8
+ * while what the root holds is everything that must **not** move when the
9
+ * table is scrolled sideways — the border, the radius, the clip, and the
10
+ * footer where a paged listing puts its control. Read the other way round,
11
+ * with the root scrolling, a framed table and its pagination were two boxes
12
+ * with a seam between them.
13
+ *
14
+ * `prose` makes the same division for the same reason, and every token below
15
+ * is declared here so both boxes and every cell read one set.
16
+ */
17
+ [data-ui='table'][data-part='root'] {
18
+ /* Geometry. Every value is the control scale's, because a row is a
19
+ control-sized thing: one theme decision retunes the rows, the buttons and
20
+ the fields together, and `@media (pointer: coarse)` grows all three at
21
+ once without this family writing a rule. `list-item` took the same
22
+ decision for the same reason, and `docs/product/prd.md` §13.1 names
23
+ `table.row.height` as a density-derived value — which this is. */
24
+ --_row-min-block: var(
25
+ --slotted-table-row-min-block-size,
26
+ var(--slotted-control-height-md, 34px)
27
+ );
28
+ --_font-size: var(--slotted-table-font-size, var(--slotted-control-font-size-md, 13px));
29
+ --_icon-size: var(--slotted-table-icon-size, var(--slotted-control-icon-size-md, 16px));
30
+ --_gap: var(--slotted-table-gap, var(--slotted-control-gap-md, 8px));
31
+ --_pad-inline: var(
32
+ --slotted-table-cell-padding-inline,
33
+ var(--slotted-control-padding-inline-md, 13px)
34
+ );
35
+ /**
36
+ * What is left of the row's height around one line of text, so a one-line
37
+ * row is exactly the control height at every density and a row of two lines
38
+ * grows by its second line and nothing else. A floor of one space step keeps
39
+ * a row whose text is taller than the control from losing its inset.
40
+ */
41
+ --_pad-block: var(
42
+ --slotted-table-cell-padding-block,
43
+ max(
44
+ var(--slotted-space-2, 0.25rem),
45
+ calc((var(--_row-min-block) - var(--_font-size) * var(--slotted-line-height-md, 1.5)) / 2)
46
+ )
47
+ );
48
+ /* One line of the table's own text, which is what a mark beside a value is
49
+ aligned against — the row is taller than one line whenever a value wraps,
50
+ and a control centred in the cell would then float away from the line it
51
+ belongs to. */
52
+ --_line: calc(var(--_font-size) * var(--slotted-line-height-md, 1.5));
53
+ --_radius: var(--slotted-table-radius, var(--slotted-radius-md, 6px));
54
+ --_rule: var(--slotted-table-border-color, var(--slotted-border-subtle, ButtonBorder));
55
+ --_rule-strong: var(
56
+ --slotted-table-border-color-strong,
57
+ var(--slotted-border-strong, ButtonBorder)
58
+ );
59
+ --_rule-width: var(--slotted-table-border-width, var(--slotted-border-width-thin, 1px));
60
+
61
+ /* Ink and ground. The row's ground is stated rather than left transparent,
62
+ because a sticky cell inherits it to cover the rows sliding under it —
63
+ and a transparent sticky cell shows the table through itself. */
64
+ --_text: var(--slotted-table-color, var(--slotted-text, CanvasText));
65
+ --_muted: var(--slotted-table-color-muted, var(--slotted-text-muted, GrayText));
66
+ /* The ground a raised container publishes (a card, a section on a surface),
67
+ else the page's surface. */
68
+ --_surface: var(--slotted-table-surface, var(--slotted-ground, var(--slotted-surface, Canvas)));
69
+ /**
70
+ * The header band, mixed from the ink rather than taken from a tone. Read
71
+ * in light, `--slotted-tone-secondary-surface` was both too blue and too
72
+ * dark for a band that runs the width of the page: the tone is calibrated
73
+ * to enclose a chip, and a chip is a few characters wide.
74
+ *
75
+ * Mixing keeps the band neutral, keeps its direction right in every scheme —
76
+ * darker than the page in light, lighter in dark — and puts it one step
77
+ * above the stripe, which is mixed the same way at a smaller share. The
78
+ * stripe and the band are then one idea at two strengths instead of two
79
+ * values from two scales that happen to differ.
80
+ */
81
+ --_head-surface: var(
82
+ --slotted-table-header-surface,
83
+ color-mix(in oklab, var(--slotted-text, CanvasText) 8%, var(--_surface))
84
+ );
85
+ /**
86
+ * The stripe, and it is mixed rather than taken from a scale. Read at 420
87
+ * and at 720 in both schemes, `--slotted-surface-sunken` was invisible under
88
+ * a row of text — a zebra stripe exists to carry the eye across a wide row,
89
+ * and one that cannot be seen at a glance is a rule the family pays for and
90
+ * nobody reads. The foundation's own answer where no token holds a ground is
91
+ * to mix one from an ink at a small share, which is what
92
+ * `--slotted-highlight-tint-opacity` and the selection halo do.
93
+ *
94
+ * It is mixed into the surface rather than into transparency, because a
95
+ * sticky cell copies the row's own ground with `background-color: inherit`
96
+ * and a translucent one would show the scrolled rows through the column
97
+ * that exists to cover them.
98
+ */
99
+ --_stripe: var(
100
+ --slotted-table-row-surface-stripe,
101
+ color-mix(in oklab, var(--slotted-text, CanvasText) 5%, var(--_surface))
102
+ );
103
+ /* `docs/product/prd.md` §8 names `table.row.hover.background` as a decision an
104
+ application retunes on its own. It is one token, over the tone step every
105
+ neutral row in the library hovers with. */
106
+ --_hover: var(
107
+ --slotted-table-row-surface-hover,
108
+ var(--slotted-tone-secondary-subtle-hover, ButtonFace)
109
+ );
110
+ --_selected: var(
111
+ --slotted-table-row-surface-selected,
112
+ var(--slotted-tone-accent-surface, Canvas)
113
+ );
114
+ --_selected-hover: var(
115
+ --slotted-table-row-surface-selected-hover,
116
+ var(--slotted-tone-accent-subtle-hover, ButtonFace)
117
+ );
118
+ /* The ink of a selected row. A ground alone is a state `high-contrast`
119
+ cannot show — every tone ground is the page's own white there — and R1
120
+ forbids the rail two families reached for instead, so the pair that says
121
+ it is the ground and the ink, with the checkbox as the mark. */
122
+ --_selected-text: var(
123
+ --slotted-table-row-color-selected,
124
+ var(--slotted-tone-accent-text, CanvasText)
125
+ );
126
+
127
+ background-color: var(--_surface);
128
+ box-sizing: border-box;
129
+ color: var(--_text);
130
+ display: block;
131
+ /**
132
+ * Every step the sticky parts take is taken inside this box.
133
+ *
134
+ * A pinned column stands at `--slotted-z-index-sticky` and the header a
135
+ * step above it, and those steps are measured against each other — not
136
+ * against the page. Without a stacking context of its own the table hands
137
+ * them to whatever stacks outside it, and under `stickyTo="page"` a pinned
138
+ * column scrolling past an application's own bar painted straight over it.
139
+ * `isolation` contains the steps and creates no containing block, so
140
+ * nothing sticky changes what it resolves against.
141
+ */
142
+ isolation: isolate;
143
+ font-family: var(--slotted-table-font-family, var(--slotted-control-font-family));
144
+ font-size: var(--_font-size);
145
+ line-height: var(--slotted-line-height-md, 1.5);
146
+ max-inline-size: 100%;
147
+ }
148
+
149
+ [data-ui='table'][data-variant='framed'] {
150
+ border: var(--_rule-width) solid var(--_rule);
151
+ border-radius: var(--_radius);
152
+ /**
153
+ * A framed table is a box standing on the page, and it is separated from
154
+ * the page by the surface step, `--slotted-shadow-xs` — the same geometry
155
+ * a framed control carries at rest, at the ink the elevation scale gives a
156
+ * surface. `ruled` and `plain` draw no box and cast nothing.
157
+ */
158
+ box-shadow: var(--slotted-table-shadow, var(--slotted-shadow-xs, none));
159
+ /**
160
+ * `clip` and not `hidden`, and the difference is the whole of `stickyTo`.
161
+ *
162
+ * Both clip what the scroller paints into the corners — a row's ground is
163
+ * square and would otherwise fill the radius — but `hidden` makes the box a
164
+ * **scroll container**, and a sticky element resolves against the nearest
165
+ * one of those. A framed table asking its header to stick to the page got a
166
+ * header that stuck to the frame instead, and a frame with no height holds
167
+ * nothing: the header simply left with the rows. `clip` clips without
168
+ * scrolling, so the nearest scroll container stays whatever the consumer
169
+ * meant it to be.
170
+ */
171
+ overflow: clip;
172
+ }
173
+
174
+ /**
175
+ * ── The scroller ───────────────────────────────────────────────────────
176
+ *
177
+ * The region wiring — `role="region"`, `tabindex="0"` and a name — is written
178
+ * by the binding and only while the content overflows, measured by
179
+ * `@slotted/core/overflow`. A tab stop on a table that fits makes a reader
180
+ * press Tab for nothing, and a wide table with no tab stop is unreachable
181
+ * without a pointer.
182
+ */
183
+ [data-ui='table'] [data-part='scroll'] {
184
+ /* `overscroll-behavior-inline: contain` keeps a sideways flick inside the
185
+ table instead of walking the page back a step, which is the gesture a
186
+ wide table on a trackpad produces constantly. */
187
+ overflow: auto;
188
+ overscroll-behavior-inline: contain;
189
+ }
190
+
191
+ /* The ring is drawn inside, because a scroller flush against the frame would
192
+ otherwise draw half its ring outside it. */
193
+ [data-ui='table'] [data-part='scroll']:focus-visible {
194
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
195
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
196
+ }
197
+
198
+ /**
199
+ * ── The table ──────────────────────────────────────────────────────────
200
+ *
201
+ * `border-collapse: separate` rather than `collapse`, and the two reasons are
202
+ * the two features above: a collapsed border is drawn on the table's own box
203
+ * and a sticky cell leaves its borders behind, and a collapsed table refuses
204
+ * a border radius. The rules are therefore drawn as one edge on each cell,
205
+ * never as a border on the row.
206
+ */
207
+ [data-ui='table'] [data-part='table'] {
208
+ border-collapse: separate;
209
+ border-spacing: 0;
210
+ /* The table fills the scroller and grows past it when its columns need
211
+ more; `min-inline-size: 100%` is what makes both true at once. */
212
+ inline-size: max-content;
213
+ min-inline-size: 100%;
214
+ text-align: start;
215
+ }
216
+
217
+ [data-ui='table'][data-layout='fixed'] [data-part='table'] {
218
+ inline-size: 100%;
219
+ table-layout: fixed;
220
+ }
221
+
222
+ /**
223
+ * ── The caption ────────────────────────────────────────────────────────
224
+ *
225
+ * The table's name, and the element the platform already associates with it.
226
+ * A caption is the accessible name whether it is read or hidden, so a
227
+ * consumer who hides it with `visually-hidden` loses the picture and keeps
228
+ * the name.
229
+ */
230
+ [data-ui='table'] [data-part='caption'] {
231
+ caption-side: block-start;
232
+ color: var(--_muted);
233
+ font-size: var(--slotted-font-size-xs, 0.75rem);
234
+ padding-block: var(--_pad-block);
235
+ padding-inline: var(--_pad-inline);
236
+ text-align: start;
237
+ }
238
+
239
+ /**
240
+ * ── The header ─────────────────────────────────────────────────────────
241
+ *
242
+ * A column header is what a screen reader repeats for every cell under it, so
243
+ * it is set in the page's own ink rather than in the muted step: a header a
244
+ * reader has to look for twice is a header that has stopped naming anything.
245
+ */
246
+ /* The chooser's header is a `selection` cell and not a `column-header`, so
247
+ every rule naming only the second stopped at it: the band's own line under
248
+ the head broke exactly over the checkbox. */
249
+ /* Two selectors rather than one `:is()`: `:is()` takes the specificity of
250
+ its strongest argument, and the selection's two-part selector lifted the
251
+ column header above the alignment rule that comes after it — a header
252
+ carrying data-align="end" computed text-align: start, so a numeric
253
+ column's digits ended at one edge and their name at the other. */
254
+ [data-ui='table'] [data-part='column-header'],
255
+ [data-ui='table'] [data-part='head'] [data-part='selection'] {
256
+ background-color: var(--_head-surface);
257
+ border-block-end: var(--_rule-width) solid var(--_rule-strong);
258
+ color: var(--_text);
259
+ font-weight: var(--slotted-font-weight-semibold, 600);
260
+ padding-block: var(--_pad-block);
261
+ padding-inline: var(--_pad-inline);
262
+ /* Positioned here and not beside the slot that needs it. The actions slot is
263
+ absolute inside the header, so the header has to be a containing block —
264
+ and a `position` written later, next to that slot, would land after the
265
+ sticky rules and quietly turn a sticky header back into a static one.
266
+ That is exactly what happened to the one header carrying a filter. */
267
+ position: relative;
268
+ text-align: start;
269
+ /* A header wraps rather than truncating: it is two or three words, and the
270
+ one thing a reader cannot recover is the name of the column they are
271
+ reading. */
272
+ vertical-align: bottom;
273
+ }
274
+
275
+ [data-ui='table'][data-variant='plain'] [data-part='column-header'] {
276
+ background-color: transparent;
277
+ }
278
+
279
+ /**
280
+ * ── Rows and cells ─────────────────────────────────────────────────────
281
+ */
282
+ [data-ui='table'] [data-part='row'] {
283
+ background-color: var(--_surface);
284
+ }
285
+
286
+ /**
287
+ * ── Where a cell's content sits in its row ─────────────────────────────
288
+ *
289
+ * The middle, because a row is as tall as its tallest cell: a one-line value
290
+ * beside a three-line one reads as belonging to the same record when both are
291
+ * centred, and reads as hanging from the ceiling when both are not. It is the
292
+ * answer Carbon, AG Grid and Material each arrive at, and the answer a table
293
+ * of one-line rows cannot tell apart.
294
+ *
295
+ * The other two are real, so the axis names all three rather than a token
296
+ * naming one. `start` is what a table of paragraphs wants — a cell of prose
297
+ * beside a date wants the date on the first line, not floating beside the
298
+ * middle of the paragraph. A table states it once and a cell overrides it,
299
+ * because the cell that wants the other answer is usually one: the actions
300
+ * pinned at the end of a tall row.
301
+ */
302
+ [data-ui='table'] {
303
+ --_cell-align: middle;
304
+ }
305
+
306
+ [data-ui='table'][data-cell-align='start'] {
307
+ --_cell-align: top;
308
+ }
309
+
310
+ [data-ui='table'][data-cell-align='end'] {
311
+ --_cell-align: bottom;
312
+ }
313
+
314
+ [data-ui='table'] [data-part='cell'],
315
+ [data-ui='table'] [data-part='row-header'] {
316
+ padding-block: var(--_pad-block);
317
+ padding-inline: var(--_pad-inline);
318
+ text-align: start;
319
+ vertical-align: var(--_cell-align);
320
+ }
321
+
322
+ [data-ui='table'] [data-block-align='start'] {
323
+ vertical-align: top;
324
+ }
325
+
326
+ [data-ui='table'] [data-block-align='center'] {
327
+ vertical-align: middle;
328
+ }
329
+
330
+ [data-ui='table'] [data-block-align='end'] {
331
+ vertical-align: bottom;
332
+ }
333
+
334
+ /**
335
+ * ── A control in a cell ────────────────────────────────────────────────
336
+ *
337
+ * A cell holding a `plain` control gives its inset to the control, the way
338
+ * the header gives its inset to `sort`: `padding-inline: 0` here, and the
339
+ * control's own padding tokens set to the cell's inset, so the edited value
340
+ * stands where the read value stood, under the column's name. With both
341
+ * insets kept, the value sat one inset in from the header — the picture the
342
+ * `in-a-row` scene of `text-field` showed. `--_pad-inline` resolves on the
343
+ * root and is inherited resolved, so redefining the token it reads does not
344
+ * feed back into it.
345
+ *
346
+ * The block inset moves too, and it moves into the control's height: the
347
+ * control is exactly the row's minimum block size, whatever size step the
348
+ * consumer wrote, so a row with a control in it is as tall as a row without
349
+ * one. Read with the block padding kept, a `sm` control stood in a
350
+ * comfortable row ten pixels taller than the rows around it — the reflow
351
+ * the `plain` variant exists to avoid. The control's width stays the
352
+ * control's own, as in a form.
353
+ *
354
+ * The selector names the fifteen `plain` field families
355
+ * `plain-variant.test.mjs` holds, and no other: a `kbd`, a `listbox`, a
356
+ * `timeline` or a nested table has a `plain` variant of its own and is not a
357
+ * control. `table.styles.test.mjs` holds the two lists against each other.
358
+ */
359
+ [data-ui='table']
360
+ :is([data-part='cell'], [data-part='row-header']):has(
361
+ > [data-part='root'][data-variant='plain']:is(
362
+ [data-ui='select'],
363
+ [data-ui='combobox'],
364
+ [data-ui='multi-select'],
365
+ [data-ui='text-field'],
366
+ [data-ui='textarea'],
367
+ [data-ui='number-field'],
368
+ [data-ui='currency-field'],
369
+ [data-ui='date-field'],
370
+ [data-ui='time-field'],
371
+ [data-ui='datetime-field'],
372
+ [data-ui='tags-input'],
373
+ [data-ui='date-range-picker'],
374
+ [data-ui='time-range-picker'],
375
+ [data-ui='datetime-range-picker'],
376
+ [data-ui='entity-picker']
377
+ )
378
+ ) {
379
+ --slotted-control-padding-inline-sm: var(--_pad-inline);
380
+ --slotted-control-padding-inline-md: var(--_pad-inline);
381
+ --slotted-control-padding-inline-lg: var(--_pad-inline);
382
+ --slotted-control-height-sm: var(--_row-min-block);
383
+ --slotted-control-height-md: var(--_row-min-block);
384
+ --slotted-control-height-lg: var(--_row-min-block);
385
+
386
+ padding: 0;
387
+ }
388
+
389
+ /* A row header names the record, so it carries the weight one step above the
390
+ values beside it and nothing else. It states no colour of its own: read
391
+ against the row's ink, a selected record's name stayed the page's black
392
+ while every value beside it turned accent, and a disabled row's name stayed
393
+ at full strength — the two states the row paints and this cell refused. */
394
+ [data-ui='table'] [data-part='row-header'] {
395
+ font-weight: var(--slotted-font-weight-medium, 500);
396
+ }
397
+
398
+ /* The rule between two rows, drawn on the cells because the row cannot carry
399
+ it under `border-collapse: separate`. The last row of the body gives its
400
+ rule up unless a footer follows, so a `ruled` table does not end in a line
401
+ with nothing under it. */
402
+ [data-ui='table']:is([data-variant='ruled'], [data-variant='framed'])
403
+ [data-part='body']
404
+ > [data-part='row']
405
+ > * {
406
+ border-block-end: var(--_rule-width) solid var(--_rule);
407
+ }
408
+
409
+ [data-ui='table']:is([data-variant='ruled'], [data-variant='framed'])
410
+ [data-part='body']
411
+ > [data-part='row']:last-child
412
+ > * {
413
+ border-block-end-color: transparent;
414
+ }
415
+
416
+ /* The stripe is the weakest ground the family paints and is declared before
417
+ every state, so a selected or hovered stripe reads as selected or hovered.
418
+ `nth-child(even)` counts rows the family renders; a detail row is a row of
419
+ the same body, so the stripe follows the record it belongs to rather than
420
+ alternating around it — see the detail rule below. */
421
+ [data-ui='table'][data-striped] [data-part='body'] > [data-part='row']:nth-child(even) {
422
+ background-color: var(--_stripe);
423
+ }
424
+
425
+ /**
426
+ * ── The states of a row ────────────────────────────────────────────────
427
+ *
428
+ * In the order `STATE_PRECEDENCE` declares them, weakest last, so a row that
429
+ * is two things at once shows the stronger one. `disabled` is the guard on
430
+ * every pointer state rather than a rule of its own: a row a consumer has
431
+ * disabled does not answer the pointer at all.
432
+ */
433
+ [data-ui='table'] [data-part='body'] > [data-part='row']:not([data-disabled]):hover {
434
+ background-color: var(--_hover);
435
+ }
436
+
437
+ /**
438
+ * A selected row is the accent ground with the accent ink, and it carries no
439
+ * rail: R1 is a decision of this repository — a band of a few pixels on the
440
+ * start edge may not say `selected`, and the state is said by the ground, the
441
+ * ink and the checkbox that owns it. `list-item` and `listbox` each drew one
442
+ * and each gave it up.
443
+ *
444
+ * The ink is what carries the state where the ground cannot: every tone
445
+ * ground is the page's own white in `high-contrast`, and the checkbox's mark
446
+ * is what survives forced colours, which is where WCAG 2.2 SC 1.4.1 puts the
447
+ * requirement anyway.
448
+ */
449
+ [data-ui='table'] [data-part='body'] > [data-part='row'][data-selected] {
450
+ background-color: var(--_selected);
451
+ color: var(--_selected-text);
452
+ }
453
+
454
+ [data-ui='table']
455
+ [data-part='body']
456
+ > [data-part='row'][data-selected]:not([data-disabled]):hover {
457
+ background-color: var(--_selected-hover);
458
+ }
459
+
460
+ [data-ui='table'] [data-part='row'][data-disabled] {
461
+ color: var(--slotted-disabled-foreground, GrayText);
462
+ }
463
+
464
+ /**
465
+ * ── Alignment and numerals ─────────────────────────────────────────────
466
+ *
467
+ * Two facts rather than one axis: a number is set in tabular figures
468
+ * whichever way it is aligned, and a right-aligned label is not a number.
469
+ * `start` and `end` are the logical values, so a column aligned to the end is
470
+ * on the right in English and on the left in Arabic.
471
+ */
472
+ [data-ui='table'] [data-align='start'] {
473
+ text-align: start;
474
+ }
475
+
476
+ [data-ui='table'] [data-align='center'] {
477
+ text-align: center;
478
+ }
479
+
480
+ [data-ui='table'] [data-align='end'] {
481
+ text-align: end;
482
+ }
483
+
484
+ /* A ten under a nine is only a column if the digits are the same width. The
485
+ alignment is inferred once, and only where the consumer stated none. */
486
+ [data-ui='table'] [data-numeric] {
487
+ font-variant-numeric: tabular-nums;
488
+ }
489
+
490
+ [data-ui='table'] [data-numeric]:not([data-align]) {
491
+ text-align: end;
492
+ }
493
+
494
+ /**
495
+ * ── What a long value does ─────────────────────────────────────────────
496
+ *
497
+ * Three treatments, and none of them removes the value: the briefing's
498
+ * decision is that a truncated string stays readable to assistive technology
499
+ * and reachable in full.
500
+ */
501
+ [data-ui='table'] [data-text='wrap'] {
502
+ /* An identifier, a hash or a URL is one unbroken token, and every surveyed
503
+ table lets it set the column's width and push the rest of the table
504
+ sideways. */
505
+ overflow-wrap: anywhere;
506
+ }
507
+
508
+ [data-ui='table'] [data-text='nowrap'] {
509
+ white-space: nowrap;
510
+ }
511
+
512
+ [data-ui='table'] [data-text='truncate'] {
513
+ overflow: hidden;
514
+ text-overflow: ellipsis;
515
+ white-space: nowrap;
516
+ }
517
+
518
+ /**
519
+ * ── Sticky ─────────────────────────────────────────────────────────────
520
+ *
521
+ * The header sticks to the root, which is the scroller. Each sticky cell
522
+ * carries its own ground, because what slides under it is the rest of the
523
+ * table: a transparent sticky cell shows the rows through itself.
524
+ *
525
+ * The three stacking steps are one decision. A header cell that is also a
526
+ * sticky column is both at once, and with a single step the header slides
527
+ * under the pinned column or the pinned column over the header — the corner
528
+ * is where this always goes wrong.
529
+ */
530
+ /**
531
+ * Where the header stops, and what it stops inside.
532
+ *
533
+ * `scroll` is the default: the header sticks to the family's own scroller,
534
+ * which is what keeps it right inside a drawer, a dialog or a split pane —
535
+ * the box the reader scrolls is the box the header stays in.
536
+ *
537
+ * `page` gives that box up. A sticky element resolves against its nearest
538
+ * scrolling ancestor, so for the header to stay while the *page* scrolls, the
539
+ * family must not be a scrolling ancestor at all — and a box that scrolls one
540
+ * axis scrolls both, because CSS computes a visible axis to `auto` the moment
541
+ * the other is `auto`. So the scroller releases its overflow, and the cost is
542
+ * written where the choice is made: a table wider than the page pushes the
543
+ * page sideways instead of scrolling inside its frame.
544
+ *
545
+ * The offset is where the header stops. An application with a bar of its own
546
+ * sets `--slotted-table-sticky-offset` once, and every table under it stops
547
+ * below the bar rather than under it.
548
+ */
549
+ [data-ui='table'][data-sticky-header] [data-part='column-header'],
550
+ /* The chooser's own header is a `selection` cell and not a `column-header`,
551
+ and a rule naming only the second left the select-all scrolling away while
552
+ every column beside it stayed. */
553
+ [data-ui='table'][data-sticky-header] [data-part='head'] [data-part='selection'] {
554
+ inset-block-start: var(--slotted-table-sticky-offset, 0);
555
+ position: sticky;
556
+ z-index: calc(var(--slotted-z-index-sticky, 100) + 1);
557
+ }
558
+
559
+ [data-ui='table'][data-sticky-to='page'] [data-part='scroll'] {
560
+ overflow: visible;
561
+ }
562
+
563
+ /**
564
+ * The summary, pinned to the other end of the same box.
565
+ *
566
+ * A listing whose total scrolls out of view makes the reader scroll back for
567
+ * the number they came for, so the `tfoot` is held against the bottom the way
568
+ * the `thead` is held against the top — and it answers `stickyTo` the same
569
+ * way, through an offset of its own for a page with a bar across the bottom.
570
+ *
571
+ * It is the `tfoot` that is pinned and not the footer bar: the bar is a
572
+ * sibling of the scroller and never leaves it in the first place.
573
+ */
574
+ [data-ui='table'][data-sticky-footer] [data-part='summary'] > [data-part='row'] > * {
575
+ inset-block-end: var(--slotted-table-sticky-offset-end, 0);
576
+ position: sticky;
577
+ z-index: calc(var(--slotted-z-index-sticky, 100) + 1);
578
+ }
579
+
580
+ /* The corner again, at the bottom: a summary cell that is also a pinned
581
+ column takes both insets and the step above both. */
582
+ [data-ui='table'][data-sticky-footer] [data-part='summary'] [data-sticky] {
583
+ z-index: calc(var(--slotted-z-index-sticky, 100) + 2);
584
+ }
585
+
586
+ [data-ui='table'] [data-sticky='start'] {
587
+ /* The ground the row is painted with, whatever state it is in: `inherit`
588
+ takes the row's own computed value, so a hovered, striped or selected row
589
+ keeps one colour across the seam. */
590
+ background-color: inherit;
591
+ /**
592
+ * Where the column stops, and it is stated rather than measured. Two
593
+ * columns pinned to the same edge both resolve against the scroller, so
594
+ * without the offset the second is drawn over the first — a checkbox column
595
+ * and the identifier beside it landed on the same pixel. The consumer
596
+ * states how wide the first one is, because measuring it would mean reading
597
+ * the layout during render.
598
+ */
599
+ inset-inline-start: var(--slotted-table-sticky-inset, 0);
600
+ position: sticky;
601
+ z-index: var(--slotted-z-index-sticky, 100);
602
+ }
603
+
604
+ [data-ui='table'] [data-sticky='end'] {
605
+ background-color: inherit;
606
+ inset-inline-end: var(--slotted-table-sticky-inset, 0);
607
+ position: sticky;
608
+ z-index: var(--slotted-z-index-sticky, 100);
609
+ }
610
+
611
+ [data-ui='table'][data-sticky-header] [data-part='column-header'][data-sticky],
612
+ [data-ui='table'][data-sticky-header] [data-part='head'] [data-part='selection'][data-sticky] {
613
+ background-color: var(--_head-surface);
614
+ z-index: calc(var(--slotted-z-index-sticky, 100) + 2);
615
+ }
616
+
617
+ /**
618
+ * The edge that says a column is pinned rather than merely first, drawn as a
619
+ * line of its own rather than as a border on the cell.
620
+ *
621
+ * A border would be right if a cell had only one. This one has two — the
622
+ * pinned edge and, in the header, the rule under the band — and where two
623
+ * borders of a box meet the browser mitres them: the corner is split
624
+ * diagonally between the two, which left a visible notch in the header's
625
+ * horizontal rule exactly where the pinned column crossed it. Read at 420
626
+ * pixels on the scrolled scene.
627
+ *
628
+ * As a pseudo-element the line simply crosses the rule, which is what a
629
+ * spreadsheet does and what the eye reads as one continuous column boundary.
630
+ * It is stretched past the cell's own **top** border, so the line is unbroken
631
+ * down the body instead of stopping a pixel short at every row.
632
+ *
633
+ * Past the top and not past the bottom, and the difference is a pixel the
634
+ * reader can see. Overflow beyond the block-end edge is scrollable overflow,
635
+ * so a line hanging a pixel below the last row made the scroller one pixel
636
+ * taller than its table — and at the end of the scroll a pinned summary sat
637
+ * that pixel above the bottom, which reads as a row that shifts as it lands.
638
+ * Overflow beyond the block-start edge is not scrollable, so the same line
639
+ * drawn upwards bridges every row boundary and adds nothing to scroll.
640
+ */
641
+ /* No `position` here. A pinned cell is already `position: sticky`, which is a
642
+ positioned box, so the line below resolves against it — and a `relative`
643
+ written at this point in the file would land after the sticky rules and
644
+ unpin every pinned column. */
645
+ [data-ui='table'] [data-sticky]::after {
646
+ background-color: var(--slotted-table-border-color, var(--slotted-border-subtle));
647
+ content: '';
648
+ inline-size: var(--_rule-width);
649
+ inset-block: calc(var(--_rule-width) * -1) 0;
650
+ position: absolute;
651
+ z-index: 1;
652
+ }
653
+
654
+ [data-ui='table'] [data-sticky='start']::after {
655
+ inset-inline-end: 0;
656
+ }
657
+
658
+ [data-ui='table'] [data-sticky='end']::after {
659
+ inset-inline-start: 0;
660
+ }
661
+
662
+ /**
663
+ * The affordance, and it is the pinned column's own edge rather than a fade
664
+ * over the whole scroller. A gradient at the trailing edge would fade the
665
+ * pinned column with everything else, which is the one thing that must stay
666
+ * crisp; a shadow needs an ink no scale holds. The edge darkens exactly while
667
+ * something is hidden under it — `data-overflowing` and the two end
668
+ * attributes come from `@slotted/core/overflow`, which is the same
669
+ * measurement the region wiring is written from.
670
+ *
671
+ * A table with no pinned column keeps the three affordances the platform and
672
+ * the family already give it: a scrollbar, a column visibly cut mid-word at
673
+ * the edge, and a tab stop that scrolls with the arrow keys.
674
+ */
675
+ /**
676
+ * The pinned column draws **one ordinary rule** and no more.
677
+ *
678
+ * It carried a darker line while something was hidden under it, which is what
679
+ * a spreadsheet does — and read on a real listing it was wrong twice. A table
680
+ * that pins a chooser and the identifier beside it has two seams and only one
681
+ * of them is a boundary, so the reader was told twice; and the boundary line
682
+ * itself, at the weight the affordance needed, read as a wall through a table
683
+ * whose other columns are separated by a hairline.
684
+ *
685
+ * The affordances the reader keeps are the ones the platform and the family
686
+ * already give: a scrollbar, a column visibly cut mid-word at the edge, and a
687
+ * named scrolling region with a tab stop that the arrow keys move.
688
+ */
689
+
690
+ /**
691
+ * ── The sort control ───────────────────────────────────────────────────
692
+ *
693
+ * The APG's own sortable example wraps the header's text in a `button`, and
694
+ * so does this: the header cell gives up its inset and the control takes it,
695
+ * so the whole header is the target rather than the words inside it.
696
+ */
697
+ [data-ui='table'] [data-part='column-header']:has(> [data-part='sort']) {
698
+ padding: 0;
699
+ }
700
+
701
+ /**
702
+ * ── The controls a column carries ──────────────────────────────────────
703
+ *
704
+ * A listing screen puts a filter or a column menu in the header, and the
705
+ * control has to sit inside a cell this family lays out. With the sort
706
+ * control filling the header there was nowhere for a second target to go, so
707
+ * the header becomes a row: the sort control takes the space that is left and
708
+ * the slot holds its controls against the trailing edge. Two targets, never
709
+ * overlapping, and neither of them steals the other's press.
710
+ *
711
+ * The slot holds the control and none of its state. What a filter means and
712
+ * what it leaves is the consumer's query, and `filter-bar` owns the rules.
713
+ */
714
+ /**
715
+ * One rule rather than three. The reservation was written twice — once on the
716
+ * sort control and once on a cell selected by
717
+ * `:has(> actions):not(:has(> sort))` — and reserving on the cell says it once
718
+ * for both: the sort control fills the cell's content box, so room taken from
719
+ * the cell is room the control never had.
720
+ *
721
+ * The slot is positioned against the cell, and the cell stays a table cell.
722
+ * `display: flex` on a `th` takes it out of the table's own formatting context
723
+ * and the browser wraps it in an anonymous cell: read on an earlier capture,
724
+ * two headers laid out as flex boxes stacked into one column and took the
725
+ * table's geometry with them.
726
+ *
727
+ * The room is stated rather than measured. The controls in it are the
728
+ * consumer's own, so their width is not a fact this family holds, and
729
+ * measuring it would mean reading the layout during render.
730
+ */
731
+ [data-ui='table'] [data-part='column-header']:has(> [data-part='column-actions']) {
732
+ /**
733
+ * The room is what the slot actually occupies plus one gap: the slot stands
734
+ * `--_pad-inline` in from the cell's edge and its control is one target
735
+ * wide, so a reservation of the target alone is five pixels short of where
736
+ * the control starts. Read on a Status column whose name is nearly as wide
737
+ * as its values, the funnel sat three pixels from the last letter — the two
738
+ * numbers were describing different things and only the wide columns hid it.
739
+ */
740
+ --_actions-room: var(
741
+ --slotted-table-column-actions-size,
742
+ calc(var(--_pad-inline) + var(--slotted-target-min, 24px) + var(--_gap))
743
+ );
744
+
745
+ padding-inline-end: var(--_actions-room);
746
+ }
747
+
748
+ [data-ui='table'] [data-part='column-actions'] {
749
+ align-items: center;
750
+ display: inline-flex;
751
+ gap: var(--_gap);
752
+ inset-block: 0;
753
+ inset-inline-end: var(--_pad-inline);
754
+ position: absolute;
755
+ }
756
+
757
+ [data-ui='table'] [data-part='sort'] {
758
+ /* The mark is centred on the text rather than dropped at the top of the
759
+ line: with the default `stretch` resolving to `flex-start` under a stated
760
+ block size, the chevron sat two pixels above the capitals and read as a
761
+ mark nobody had placed. */
762
+ align-items: center;
763
+ background-color: transparent;
764
+ border: 0;
765
+ color: inherit;
766
+ /* The hand, because the header is a control. `select` restored it to its
767
+ own trigger and its options for the same reason: a target that does not
768
+ say it is one is a target a pointer reader finds by accident. */
769
+ cursor: pointer;
770
+ /* The control fills the header cell, so a reader aiming at a column name
771
+ hits the button and not the two pixels beside it — less the room the
772
+ controls slot reserves, which is the rule above. */
773
+ display: flex;
774
+ font: inherit;
775
+ gap: var(--_gap);
776
+ inline-size: 100%;
777
+ /* The name at the leading edge and the mark at the trailing one, so every
778
+ column's controls stand in one line down the header row. With the mark
779
+ set immediately after the name it landed at a different distance in every
780
+ column — beside a short name, halfway across a wide one — and beside a
781
+ filter trigger pinned to the edge the pair read as two unrelated marks. */
782
+ justify-content: space-between;
783
+ min-block-size: var(--slotted-target-min, 24px);
784
+ min-inline-size: 0;
785
+ padding-block: var(--_pad-block);
786
+ padding-inline: var(--_pad-inline);
787
+ text-align: inherit;
788
+ }
789
+
790
+ /* An aligned column keeps its name over its values, and the mark follows the
791
+ name rather than the cell: a centred column reads as a centred pair, and a
792
+ numeric column's name and mark both stand at the edge its digits end on. */
793
+ [data-ui='table'] [data-align='center'] > [data-part='sort'] {
794
+ justify-content: center;
795
+ }
796
+
797
+ [data-ui='table'] :is([data-align='end'], [data-numeric]:not([data-align])) > [data-part='sort'] {
798
+ justify-content: end;
799
+ }
800
+
801
+ /**
802
+ * Hover is painted on the **cell**, and only on a cell that sorts.
803
+ *
804
+ * Two things were wrong when the control painted it. The control's box stops
805
+ * where the controls slot begins — that is the room the cell reserves — so
806
+ * the fill ended in the middle of the header with the filter trigger sitting
807
+ * outside it, on the band. And a header that does not sort answered the
808
+ * pointer with a ground anyway, which offers an interaction it does not have.
809
+ *
810
+ * The ground goes **darker**, not lighter: the band is already a tinted
811
+ * ground, and the step every neutral row hovers with is lighter than it, so a
812
+ * hovered header was a pale hole in the band rather than a control answering
813
+ * the pointer. It is mixed from the ink for the reason the stripe is — no
814
+ * scale holds a ground one step under this one, and mixing keeps the
815
+ * direction right in every scheme.
816
+ */
817
+ [data-ui='table'] [data-part='column-header']:has(> [data-part='sort']:not(:disabled)):hover {
818
+ background-color: var(
819
+ --slotted-table-header-surface-hover,
820
+ color-mix(in oklab, var(--slotted-text, CanvasText) 7%, var(--_head-surface))
821
+ );
822
+ }
823
+
824
+ [data-ui='table'] [data-part='column-header'] {
825
+ transition: background-color
826
+ var(--slotted-control-transition-duration, var(--slotted-duration-short))
827
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
828
+ }
829
+
830
+ [data-ui='table'] [data-part='sort']:focus-visible {
831
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
832
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
833
+ }
834
+
835
+ /**
836
+ * The direction mark: the `Glyph` the binding draws after the name. One glyph
837
+ * rather than two: `chevron-down` turned a half turn is `chevron-up`, and a
838
+ * family that added an arrow pair to the foundation would be adding it for
839
+ * itself.
840
+ *
841
+ * An unsorted sortable column draws the mark muted and only under the
842
+ * pointer or under focus, so the column says it can be sorted without twelve
843
+ * marks competing with twelve names. The sorted column keeps its mark drawn.
844
+ */
845
+ [data-ui='table'] [data-part='sort'] > [data-ui='icon'] {
846
+ --slotted-icon-size: var(--_icon-size);
847
+ opacity: 0;
848
+ transition:
849
+ opacity var(--slotted-control-transition-duration, var(--slotted-duration-short))
850
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
851
+ rotate var(--slotted-control-transition-duration, var(--slotted-duration-short))
852
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
853
+ }
854
+
855
+ [data-ui='table'] [data-part='sort']:not(:disabled):hover > [data-ui='icon'],
856
+ [data-ui='table'] [data-part='sort']:focus-visible > [data-ui='icon'] {
857
+ opacity: 0.55;
858
+ }
859
+
860
+ [data-ui='table'] [aria-sort] > [data-part='sort'] > [data-ui='icon'] {
861
+ opacity: 1;
862
+ }
863
+
864
+ /* Ascending is the same mark turned over. The rotation is animated because it
865
+ is the one movement in this family a reader has to follow: the column did
866
+ not change, its direction did. */
867
+ [data-ui='table'] [aria-sort='ascending'] > [data-part='sort'] > [data-ui='icon'] {
868
+ rotate: 180deg;
869
+ }
870
+
871
+ /**
872
+ * ── The selection column ───────────────────────────────────────────────
873
+ *
874
+ * The cell is the family's and the control is not: `checkbox` owns the square,
875
+ * the check and the indeterminate bar, and a chooser drawn here would be a
876
+ * second answer to a question that family answers. The cell holds it to the
877
+ * width of the control and centres it on the row's first line.
878
+ *
879
+ * The width is **stated** rather than shrink-wrapped, and that is the whole
880
+ * reason it is a token. A listing pins the chooser and the identifier beside
881
+ * it, and the second one has to say where the first stops: shrink-wrapped, the
882
+ * answer was 42 pixels — a number nothing names and nobody can state — so a
883
+ * consumer stating the nearest round length pinned the identifier six pixels
884
+ * past the chooser and over the column behind it. Now one name answers both
885
+ * places: this width, and the `stickyOffset` beside it.
886
+ */
887
+ [data-ui='table'] [data-part='selection'] {
888
+ inline-size: var(--slotted-table-selection-size, 3rem);
889
+ padding-block: var(--_pad-block);
890
+ padding-inline: var(--_pad-inline);
891
+ text-align: center;
892
+ /* The chooser follows the table's own answer: a table that centres its
893
+ values and hangs its checkboxes from the top is two decisions where the
894
+ reader sees one row. */
895
+ vertical-align: var(--_cell-align);
896
+ white-space: nowrap;
897
+ }
898
+
899
+ /* The control is one line tall whatever the row is, so it sits on the line
900
+ the axis puts it on rather than stretching down a three-line cell. */
901
+ [data-ui='table'] [data-part='selection'] > [data-ui='checkbox'] {
902
+ block-size: var(--_line);
903
+ }
904
+
905
+ /**
906
+ * The chooser keeps its box and gives up its halo.
907
+ *
908
+ * A checkbox standing on its own answers the pointer with a circle behind the
909
+ * box, which is what tells a reader the target is larger than the square. In
910
+ * a table the row is already answering the pointer with a ground the width of
911
+ * the page, and a second circle inside it read as a mark on the row rather
912
+ * than as a state of the control.
913
+ *
914
+ * It is turned off by the token the halo is painted at rather than by a rule
915
+ * that reaches into `checkbox`: a custom property set on this family's own
916
+ * cell is composition, where a selector naming another family's parts would
917
+ * be this stylesheet deciding how that one looks. The target is unaffected —
918
+ * it is a separate pseudo-element, and it stays at the WCAG minimum.
919
+ */
920
+ [data-ui='table'] [data-part='selection'] {
921
+ --slotted-selection-halo-opacity: 0;
922
+ }
923
+
924
+ /**
925
+ * A cell holding nothing but the expander is a gutter rather than a column:
926
+ * it takes the control's width and every row keeps it, so a set where three
927
+ * records in twenty have a detail does not indent those three and leave the
928
+ * other seventeen a step to the start of them. Read on the first capture, the
929
+ * expander inside the row header moved one identifier and left the column
930
+ * ragged.
931
+ */
932
+ [data-ui='table'] [data-part='cell']:has(> [data-part='expander']) {
933
+ inline-size: 1%;
934
+ padding-inline-end: 0;
935
+ white-space: nowrap;
936
+ }
937
+
938
+ /**
939
+ * The element a binding wraps its trigger in is not a box.
940
+ *
941
+ * React renders the trigger straight into the slot; Angular reaches its
942
+ * templates through elements, so the filter is a `span` holding a `span`
943
+ * holding the button. Left alone, the flex item is the outer wrapper and the
944
+ * geometry below lands on a button that is no longer the thing being laid
945
+ * out. `display: contents` removes both wrappers from layout and leaves the
946
+ * button as the slot's own child, which is the box tree React already has.
947
+ */
948
+ [data-ui='table'] [data-part='column-actions'] *:has([data-part='filter']) {
949
+ display: contents;
950
+ }
951
+
952
+ /**
953
+ * ── The expander ───────────────────────────────────────────────────────
954
+ *
955
+ * The Disclosure pattern, borrowed whole: a `button` carrying `aria-expanded`
956
+ * and naming the row it opens. The mark is `chevron-right`, turned a quarter
957
+ * turn when the detail is open — and mirrored in a right-to-left document,
958
+ * where a closed disclosure points the other way.
959
+ */
960
+ [data-ui='table'] [data-part='expander'],
961
+ [data-ui='table'] [data-part='column-actions'] > button,
962
+ [data-ui='table'] button[data-part='column-actions'],
963
+ [data-ui='table'] [data-part='filter'] {
964
+ background-color: transparent;
965
+ border: 0;
966
+ border-radius: var(--slotted-table-control-radius, var(--slotted-radius-sm, 4px));
967
+ color: var(--_muted);
968
+ cursor: pointer;
969
+ display: inline-flex;
970
+ /* An icon-only control is smaller than the WCAG 2.2 minimum in both axes
971
+ unless the family says otherwise, which is the family's job rather than
972
+ the theme's. */
973
+ block-size: var(--slotted-target-min, 24px);
974
+ inline-size: var(--slotted-target-min, 24px);
975
+ /**
976
+ * The target is taller than the line it stands beside, so the box is pulled
977
+ * back onto that line: without this the row grew by the difference and the
978
+ * chevron sat two pixels under the identifier it opens. The margin is
979
+ * negative by exactly the overshoot, the cell's own inset absorbs it, and
980
+ * the row is still measured by its text.
981
+ */
982
+ margin-block: calc((var(--_line) - var(--slotted-target-min, 24px)) / 2);
983
+ padding: 0;
984
+ /* Both axes, stated separately, because `place-content` is not the property
985
+ that centres a single item: `align-content` aligns lines and a single-line
986
+ flex container has one, so the mark fell back to `align-items: stretch`,
987
+ which a stated block size resolves to `flex-start`. Read on the capture,
988
+ the chevron sat against the top edge of its own 24-pixel box. */
989
+ align-items: center;
990
+ justify-content: center;
991
+ transition: background-color
992
+ var(--slotted-control-transition-duration, var(--slotted-duration-short))
993
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
994
+ }
995
+
996
+ /**
997
+ * On a finger the target is the comfortable size in both axes and the box
998
+ * is the one a mouse sees: a transparent `::after` centred on the control
999
+ * reaches 44 pixels, and the ground and the ring stay square. A header's
1000
+ * trigger stands after the sort button and its mark, so it does not reach
1001
+ * back across them.
1002
+ */
1003
+ @media (pointer: coarse) {
1004
+ [data-ui='table'] [data-part='expander'],
1005
+ [data-ui='table'] [data-part='column-actions'] > button,
1006
+ [data-ui='table'] button[data-part='column-actions'],
1007
+ [data-ui='table'] [data-part='filter'] {
1008
+ position: relative;
1009
+ }
1010
+
1011
+ [data-ui='table'] [data-part='expander']::after,
1012
+ [data-ui='table'] [data-part='column-actions'] > button::after,
1013
+ [data-ui='table'] button[data-part='column-actions']::after,
1014
+ [data-ui='table'] [data-part='filter']::after {
1015
+ content: '';
1016
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
1017
+ position: absolute;
1018
+ }
1019
+
1020
+ [data-ui='table'] [data-part='column-actions'] > button::after,
1021
+ [data-ui='table'] button[data-part='column-actions']::after,
1022
+ [data-ui='table'] [data-part='filter']::after {
1023
+ inset-inline-start: 0;
1024
+ }
1025
+ }
1026
+
1027
+ [data-ui='table'] [data-part='expander']:not(:disabled):hover {
1028
+ background-color: var(--_hover);
1029
+ color: var(--_text);
1030
+ }
1031
+
1032
+ /* On the band the hover darkens, for the reason the sort control's does: the
1033
+ step a row hovers with is lighter than the band and reads as a hole in it. */
1034
+ [data-ui='table'] [data-part='column-actions'] > button:not(:disabled):hover,
1035
+ [data-ui='table'] button[data-part='column-actions']:not(:disabled):hover,
1036
+ [data-ui='table'] [data-part='filter']:not(:disabled):hover {
1037
+ background-color: var(
1038
+ --slotted-table-header-surface-hover,
1039
+ color-mix(in oklab, var(--slotted-text, CanvasText) 7%, var(--_head-surface))
1040
+ );
1041
+ color: var(--_text);
1042
+ }
1043
+
1044
+ /* The ring is flush with the control rather than drawn inside it. The two
1045
+ larger targets — the region and the sort button — draw theirs inward,
1046
+ because both stand against an edge the ring would otherwise cross; these
1047
+ are 24-pixel boxes inside a cell's own inset, and read at that size an
1048
+ inward ring closed on the chevron. */
1049
+ [data-ui='table'] [data-part='expander']:focus-visible,
1050
+ [data-ui='table'] [data-part='column-actions'] > button:focus-visible,
1051
+ [data-ui='table'] button[data-part='column-actions']:focus-visible,
1052
+ [data-ui='table'] [data-part='filter']:focus-visible {
1053
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
1054
+ outline-offset: 0;
1055
+ }
1056
+
1057
+ /**
1058
+ * The artwork a consumer puts in the header trigger is drawn at the family's
1059
+ * own glyph size, so a filter mark and the sort mark beside it read as one
1060
+ * pair rather than as a control and a mark. Read at the gate, a `button` from
1061
+ * the library at its smallest step stood thirty pixels tall against a line of
1062
+ * nineteen and a half — the control scale is right for a control in a form
1063
+ * and wrong for a mark in a header.
1064
+ */
1065
+ [data-ui='table'] :is([data-part='column-actions'], [data-part='filter']) :is(svg, img) {
1066
+ block-size: var(--_icon-size);
1067
+ inline-size: var(--_icon-size);
1068
+ }
1069
+
1070
+ /**
1071
+ * ── The filter ─────────────────────────────────────────────────────────
1072
+ *
1073
+ * The trigger takes the slot's geometry above; what is left is the one thing
1074
+ * it must say on its own — that the column is filtered. The mark turns to the
1075
+ * accent ink and holds it whether the pointer is on it or not, because a
1076
+ * filter a reader forgot they set is the defect this state exists to prevent.
1077
+ */
1078
+ [data-ui='table'] [data-part='filter'][data-filtered],
1079
+ [data-ui='table'] [data-part='filter'][data-filtered]:not(:disabled):hover {
1080
+ color: var(--slotted-tone-accent-text, LinkText);
1081
+ }
1082
+
1083
+ /**
1084
+ * The panel, which is drawn wherever `popover` puts it — inside the header or
1085
+ * portalled to the end of the document. So it carries the family's identity
1086
+ * on itself rather than reading it from an ancestor: a rule that reached for
1087
+ * `[data-ui='table']` above would style the panel in one of the two.
1088
+ *
1089
+ * And it declares the values it uses rather than reading them from the root,
1090
+ * for the same reason. Every `--_` above is declared on `[data-part='root']`;
1091
+ * portalled to the end of the document the panel is not inside it, and a
1092
+ * custom property that resolves to nothing takes its whole declaration with
1093
+ * it — `gap: var(--_gap)` became no gap at all, silently, in the one
1094
+ * arrangement `popover` uses by default. The eight below are the panel's own,
1095
+ * from the same theme tokens the root reads.
1096
+ */
1097
+ [data-ui='table'][data-part='filter-panel'] {
1098
+ --_gap: var(--slotted-table-gap, var(--slotted-control-gap-md, 8px));
1099
+ --_font-size: var(--slotted-table-font-size, var(--slotted-control-font-size-md, 13px));
1100
+ --_line: calc(var(--_font-size) * var(--slotted-line-height-md, 1.5));
1101
+ --_pad-block: var(--slotted-space-2, 4px);
1102
+ --_text: var(--slotted-table-color, var(--slotted-text, CanvasText));
1103
+ --_muted: var(--slotted-table-color-muted, var(--slotted-text-muted, GrayText));
1104
+ --_rule: var(--slotted-table-border-color, var(--slotted-border-subtle, ButtonBorder));
1105
+ --_rule-width: var(--slotted-table-border-width, var(--slotted-border-width-thin, 1px));
1106
+ --_hover: var(
1107
+ --slotted-table-row-surface-hover,
1108
+ var(--slotted-tone-secondary-subtle-hover, ButtonFace)
1109
+ );
1110
+
1111
+ /**
1112
+ * The inset counts inside the width, and that is not a preference.
1113
+ * `min-inline-size` beats `max-inline-size`, and under the default
1114
+ * `content-box` both resolve against the content: `min(15rem, 100%)` gave
1115
+ * the panel the whole of the layer's width and then added twenty-four
1116
+ * pixels of padding to it, so a panel measured 229 pixels inside a
1117
+ * 205-pixel surface — the audit's number, on the first scene it read.
1118
+ */
1119
+ box-sizing: border-box;
1120
+ display: grid;
1121
+ gap: var(--_gap);
1122
+ /* The panel is a surface of its own, so it states its type rather than
1123
+ inheriting it. Unportalled it is drawn inside the header cell, and a
1124
+ header is set in semibold: every value in the list came out bold, which
1125
+ reads as a set of headings rather than a set of values. */
1126
+ color: var(--_text);
1127
+ font-size: var(--_font-size);
1128
+ font-weight: var(--slotted-font-weight-regular, 400);
1129
+ line-height: var(--slotted-line-height-md, 1.5);
1130
+ /* The panel brings its own inset. `popover` puts the inset on its body and
1131
+ its footer, and this family draws neither: the whole surface is one part
1132
+ of its own, which is what keeps the condition, the values and the two
1133
+ actions on one rhythm. */
1134
+ padding: var(--slotted-space-5, 12px);
1135
+ /**
1136
+ * A floor rather than a fixed width. A set filter needs room for a value and
1137
+ * its count; a typed one needs room for an operator name, and "Does not
1138
+ * contain" and "Ist nicht leer" are wider than the panel a checkbox list
1139
+ * fits in. The chooser truncates its own button, so the floor holds the
1140
+ * common case and a wider panel is the consumer's to ask for.
1141
+ *
1142
+ * The floor gives way to the room there is, and the room is the positioner's
1143
+ * own number rather than a percentage. Two readings got this wrong before
1144
+ * it was measured: a bare `15rem` won against `max-inline-size: 100%` — a
1145
+ * minimum always does — and stood eight pixels outside the layer at a
1146
+ * 240-pixel viewport; `min(15rem, 100%)` then resolved its percentage
1147
+ * against a containing block that is itself shrink-to-fit, and the panel
1148
+ * came out at 217 pixels, twenty-three under the floor it was supposed to
1149
+ * hold. `--slotted-positioner-available-width` is the space the engine
1150
+ * measured, which is what "the room there is" actually means.
1151
+ *
1152
+ * Seventeen rem rather than fifteen, and the number is measured too: the
1153
+ * widest operator name is 146 pixels of text, the chooser adds its own
1154
+ * inset and chevron, and a range wants two seven-rem fields on one row.
1155
+ * Fifteen left 193 pixels of content and both a squeezed chooser and a
1156
+ * stacked range; seventeen leaves 248 and neither.
1157
+ */
1158
+ min-inline-size: min(17rem, var(--slotted-positioner-available-width, 100%));
1159
+ max-inline-size: 100%;
1160
+ }
1161
+
1162
+ /**
1163
+ * One condition: the operator above the operands it takes. They are stacked
1164
+ * rather than set on a row, because an operator name is the widest thing in
1165
+ * the panel and a row would give the value what was left of 15rem.
1166
+ */
1167
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-condition'] {
1168
+ display: grid;
1169
+ gap: calc(var(--_gap) / 2);
1170
+ }
1171
+
1172
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-operator'],
1173
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-search'] {
1174
+ display: block;
1175
+ }
1176
+
1177
+ /**
1178
+ * The operands, and the only place in the family where the number of controls
1179
+ * is not fixed: none for `is empty`, one for a comparison, two for a range.
1180
+ *
1181
+ * `auto-fit` answers all three without a query and without a second element.
1182
+ * Two fields need fourteen and a half rem between them and the panel's floor
1183
+ * leaves thirteen and a half, so a range stacks at the default width and sits
1184
+ * on one row the moment a consumer gives the panel more — which is the right
1185
+ * way round: two number fields side by side in a 15rem panel are two fields
1186
+ * nobody can read.
1187
+ */
1188
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-operand'] {
1189
+ display: grid;
1190
+ gap: calc(var(--_gap) / 2);
1191
+ grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
1192
+ }
1193
+
1194
+ /* A set's operand is a search and a list, which own their own stacking. */
1195
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-operand'][data-kind='set'] {
1196
+ grid-template-columns: 1fr;
1197
+ gap: var(--_gap);
1198
+ }
1199
+
1200
+ /**
1201
+ * Three operands that are one control wide, whatever the operator's count.
1202
+ *
1203
+ * `data-range` is a `between` drawn as one range picker — `date-range-picker`
1204
+ * on a date, `time-range-picker` on a time, `datetime-range-picker` on a
1205
+ * date-time: the grid above would put the single child in the first of its
1206
+ * two columns and leave the second empty. A date-time field is seven
1207
+ * segments wide, so two of them side by side at the panel's floor are two
1208
+ * fields nobody can read; a range of date-times stacks, as a range of
1209
+ * numbers did at fifteen rem. The chips of a set typed as tags wrap inside
1210
+ * one field, and the field takes the panel's width so the wrapping has
1211
+ * somewhere to go.
1212
+ */
1213
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-operand'][data-range],
1214
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-operand'][data-kind='datetime'],
1215
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-operand'][data-operand='tags'] {
1216
+ grid-template-columns: 1fr;
1217
+ }
1218
+
1219
+ /**
1220
+ * A time field is narrower than a date field — `09:00` against
1221
+ * `14/09/2026` — but the trigger beside it is the same width, so two of them
1222
+ * on one row want eight rem each rather than seven. At the floor the two
1223
+ * stack; a panel given two rem more puts them side by side. The rule is for
1224
+ * a consumer's own `renderOperand` drawing two time pickers: the family's
1225
+ * `between` is one range picker, marked `data-range`, and this rule stands
1226
+ * after the one above, so it steps aside for that mark.
1227
+ */
1228
+ [data-ui='table'][data-part='filter-panel']
1229
+ [data-part='filter-operand'][data-kind='time']:not([data-range]) {
1230
+ grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
1231
+ }
1232
+
1233
+ /**
1234
+ * The join between two conditions. It is a rule with a word on it: the two
1235
+ * conditions are separate boxes and the reader has to see where the first ends
1236
+ * before being asked how the second joins it.
1237
+ */
1238
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-join'] {
1239
+ align-items: center;
1240
+ border-block-start: var(--_rule-width) solid var(--_rule);
1241
+ display: flex;
1242
+ flex-wrap: wrap;
1243
+ gap: var(--_gap);
1244
+ padding-block-start: var(--_gap);
1245
+ }
1246
+
1247
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-join'] > [data-label] {
1248
+ color: var(--_muted);
1249
+ }
1250
+
1251
+ /**
1252
+ * The values, scrolling inside the panel rather than making the panel taller
1253
+ * than the screen. Eight rows is the height a set filter opens at in AG Grid
1254
+ * and about the height Excel's opens at; past that the reader searches.
1255
+ */
1256
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-list'] {
1257
+ display: grid;
1258
+ gap: var(--_gap);
1259
+ max-block-size: calc(var(--_line) * 9);
1260
+ overflow-y: auto;
1261
+ /* The scrollbar sits outside the rows rather than over them. */
1262
+ padding-inline-end: var(--_gap);
1263
+ }
1264
+
1265
+ /**
1266
+ * One value, and the whole row is its target.
1267
+ *
1268
+ * The chooser gives up its halo here for the reason the selection column
1269
+ * gives up its own, and for a second: the list scrolls, so the circle drawn
1270
+ * outside the square was clipped against the panel's own edge — a mark that
1271
+ * only appears cut. The row answers the pointer with a ground instead, which
1272
+ * is what a value list in Ant, AG Grid and Excel does, and the ground is
1273
+ * pulled out to the panel's inset so it reads as a row rather than as a box
1274
+ * around the words.
1275
+ */
1276
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-option'] {
1277
+ --slotted-selection-halo-opacity: 0;
1278
+
1279
+ align-items: center;
1280
+ border-radius: var(--slotted-table-control-radius, var(--slotted-radius-sm, 4px));
1281
+ display: flex;
1282
+ gap: var(--_gap);
1283
+ justify-content: space-between;
1284
+ margin-inline: calc(var(--_gap) * -1);
1285
+ padding-inline: var(--_gap);
1286
+ }
1287
+
1288
+ [data-ui='table'][data-part='filter-panel']
1289
+ [data-part='filter-option']:not([data-disabled]):hover {
1290
+ background-color: var(--_hover);
1291
+ }
1292
+
1293
+ /* The count is the consumer's fact about their own query, so it is set apart
1294
+ from the value rather than beside it as a second label. */
1295
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-option'] > [data-count] {
1296
+ color: var(--_muted);
1297
+ font-variant-numeric: tabular-nums;
1298
+ }
1299
+
1300
+ /* The covering checkbox is a row of the same list and a boundary at once: it
1301
+ answers for everything under it, so the rule says where "everything" ends. */
1302
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-option'][data-covering] {
1303
+ border-block-end: var(--_rule-width) solid var(--_rule);
1304
+ padding-block-end: var(--_gap);
1305
+ }
1306
+
1307
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-empty'] {
1308
+ color: var(--_muted);
1309
+ padding-block: var(--_pad-block);
1310
+ text-align: center;
1311
+ }
1312
+
1313
+ /* Clear at the leading edge and Apply at the trailing one: the destructive
1314
+ answer is farthest from the one a reader presses without reading. */
1315
+ [data-ui='table'][data-part='filter-panel'] [data-part='filter-actions'] {
1316
+ border-block-start: var(--_rule-width) solid var(--_rule);
1317
+ display: flex;
1318
+ gap: var(--_gap);
1319
+ justify-content: space-between;
1320
+ padding-block-start: var(--_gap);
1321
+ }
1322
+
1323
+ /* The chevron is the `Glyph` the binding draws inside the button. */
1324
+ [data-ui='table'] [data-part='expander'] > [data-ui='icon'] {
1325
+ --slotted-icon-size: var(--_icon-size);
1326
+ rotate: 0deg;
1327
+ transition: rotate var(--slotted-control-transition-duration, var(--slotted-duration-short))
1328
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
1329
+ }
1330
+
1331
+ [data-ui='table'] [data-part='expander'][aria-expanded='true'] > [data-ui='icon'] {
1332
+ rotate: 90deg;
1333
+ }
1334
+
1335
+ /* A chevron is direction-sensitive artwork, so it is mirrored rather than
1336
+ reused: a closed row in Arabic points to the start of the line, which is
1337
+ the right-hand side. */
1338
+ [data-ui='table']:dir(rtl) [data-part='expander'] > [data-ui='icon'] {
1339
+ scale: -1 1;
1340
+ }
1341
+
1342
+ [data-ui='table']:dir(rtl) [data-part='expander'][aria-expanded='true'] > [data-ui='icon'] {
1343
+ rotate: -90deg;
1344
+ }
1345
+
1346
+ /**
1347
+ * ── The detail ─────────────────────────────────────────────────────────
1348
+ *
1349
+ * One row holding one cell that spans the table. It is not rendered while it
1350
+ * is closed, so nothing is in the DOM that a reader is told about and cannot
1351
+ * reach, and it takes the sunken ground so the record it belongs to still
1352
+ * reads as one thing.
1353
+ */
1354
+ [data-ui='table'] [data-part='detail'] {
1355
+ background-color: var(--_stripe);
1356
+ }
1357
+
1358
+ [data-ui='table'] [data-part='detail'] > [data-part='cell'] {
1359
+ padding-block: var(--_pad-block);
1360
+ padding-inline: var(--_pad-inline);
1361
+ }
1362
+
1363
+ /* A detail row is not a record, so it takes no stripe of its own and never
1364
+ shifts the alternation of the rows around it. */
1365
+ [data-ui='table'][data-striped] [data-part='body'] > [data-part='detail'] {
1366
+ background-color: var(--_stripe);
1367
+ }
1368
+
1369
+ /**
1370
+ * ── Empty and loading ──────────────────────────────────────────────────
1371
+ */
1372
+ [data-ui='table'] [data-part='empty'] {
1373
+ color: var(--_muted);
1374
+ padding-block: var(--slotted-space-9, 2rem);
1375
+ padding-inline: var(--_pad-inline);
1376
+ text-align: center;
1377
+ }
1378
+
1379
+ /* The skeleton rows of a first load. The shapes are `skeleton`'s own; what
1380
+ this family supplies is the row that holds them, so the table's shape is
1381
+ visible before its content arrives. */
1382
+ [data-ui='table'] [data-part='loader'] > [data-part='row'] > * {
1383
+ padding-block: var(--_pad-block);
1384
+ padding-inline: var(--_pad-inline);
1385
+ }
1386
+
1387
+ /**
1388
+ * A refresh over rows a reader is already looking at is not a first load. The
1389
+ * body dims and nothing moves: the column widths are the table's, the scroll
1390
+ * position is the reader's, and swapping rows for skeletons would take both.
1391
+ */
1392
+ [data-ui='table'][data-loading] [data-part='body'] {
1393
+ opacity: 0.55;
1394
+ transition: opacity var(--slotted-control-transition-duration, var(--slotted-duration-short))
1395
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
1396
+ }
1397
+
1398
+ /**
1399
+ * ── The summary rows ───────────────────────────────────────────────────
1400
+ *
1401
+ * The `tfoot`, which the HTML Standard calls the column summaries of the
1402
+ * table: a total, a count, a unit. It carries the header's rule on its
1403
+ * leading edge rather than the body's, because what it separates is the same
1404
+ * kind of boundary — and it is not the `footer`, which is a bar outside the
1405
+ * table holding the consumer's own controls.
1406
+ */
1407
+ [data-ui='table'] [data-part='summary'] > [data-part='row'] > * {
1408
+ background-color: var(--_head-surface);
1409
+ border-block-start: var(--_rule-width) solid var(--_rule-strong);
1410
+ font-weight: var(--slotted-font-weight-medium, 500);
1411
+ padding-block: var(--_pad-block);
1412
+ padding-inline: var(--_pad-inline);
1413
+ }
1414
+
1415
+ [data-ui='table'][data-variant='plain'] [data-part='summary'] > [data-part='row'] > * {
1416
+ background-color: transparent;
1417
+ }
1418
+
1419
+ /**
1420
+ * ── The footer bar ─────────────────────────────────────────────────────
1421
+ *
1422
+ * Under the table and inside the frame, where a paged listing puts its
1423
+ * control. It is a sibling of the scroller rather than a child, so a table
1424
+ * scrolled sideways does not take its pagination with it — which is the whole
1425
+ * reason the frame and the scroller are two boxes.
1426
+ *
1427
+ * What stands in it is the consumer's own: `pagination`, a page-size select,
1428
+ * a count, a bulk-action bar. The family draws the rule above it and the
1429
+ * inset around it, and nothing else.
1430
+ */
1431
+ [data-ui='table'] [data-part='footer'] {
1432
+ align-items: center;
1433
+ border-block-start: var(--_rule-width) solid var(--_rule);
1434
+ display: flex;
1435
+ flex-wrap: wrap;
1436
+ gap: var(--_gap);
1437
+ padding-block: var(--_pad-block);
1438
+ padding-inline: var(--_pad-inline);
1439
+ }
1440
+
1441
+ /**
1442
+ * The last thing in the bar takes the room that is left, and that is not a
1443
+ * preference: `pagination` answers its own inline size, and an element with
1444
+ * `container-type: inline-size` contributes no intrinsic width — dropped into
1445
+ * a bar that shrink-wrapped its children it resolved to nothing, hid its own
1446
+ * page list under its 22rem query and left two chevrons in an empty row. The
1447
+ * same is true of every family that answers its own width, which
1448
+ * `references/stories.md` records for the story stages.
1449
+ *
1450
+ * So the bar states the room instead of asking for it, and a control that
1451
+ * wants to sit at the trailing edge justifies itself there.
1452
+ */
1453
+ [data-ui='table'] [data-part='footer'] > :last-child {
1454
+ flex: 1 1 auto;
1455
+ /* And it aligns its own content to the trailing edge, which is where a
1456
+ listing screen puts its paging control. The rule is structural rather
1457
+ than a reach into another family: it names the last thing in the bar, not
1458
+ what that thing is. */
1459
+ justify-content: end;
1460
+ }
1461
+
1462
+ /**
1463
+ * ── Print ──────────────────────────────────────────────────────────────
1464
+ *
1465
+ * A scroller, a fixed height and a sticky offset are all wrong on paper. Once
1466
+ * the family stops holding them, the platform's own rule applies: a `thead`
1467
+ * repeats at the top of every printed page.
1468
+ */
1469
+ @media print {
1470
+ [data-ui='table'] [data-part='scroll'] {
1471
+ max-block-size: none;
1472
+ overflow: visible;
1473
+ }
1474
+
1475
+ [data-ui='table'][data-sticky-header] [data-part='column-header'],
1476
+ [data-ui='table'][data-sticky-header] [data-part='head'] [data-part='selection'],
1477
+ [data-ui='table'][data-sticky-footer] [data-part='summary'] > [data-part='row'] > *,
1478
+ [data-ui='table'] [data-sticky] {
1479
+ position: static;
1480
+ }
1481
+ }
1482
+ }