@codenhub/styles 0.5.0 → 0.6.0

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 (59) hide show
  1. package/README.md +7 -7
  2. package/dist/aesthetics/chunky-tile.css +1 -1
  3. package/dist/aesthetics/cyber.css +1 -1
  4. package/dist/aesthetics/glass.css +1 -1
  5. package/dist/aesthetics/index.css +1 -1
  6. package/dist/aesthetics/neobrutalism.css +1 -1
  7. package/dist/aesthetics/pixel.css +1 -1
  8. package/dist/aesthetics/sketch.css +1 -1
  9. package/dist/components.css +1 -1
  10. package/dist/index.css +1 -1
  11. package/dist/native.css +1 -1
  12. package/dist/palette.css +1 -1
  13. package/dist/theme.css +1 -1
  14. package/dist/tw/aesthetics/chunky-tile.css +39 -39
  15. package/dist/tw/aesthetics/cyber.css +16 -11
  16. package/dist/tw/aesthetics/glass.css +235 -10
  17. package/dist/tw/aesthetics/index.css +2 -1
  18. package/dist/tw/aesthetics/neobrutalism.css +23 -11
  19. package/dist/tw/aesthetics/pixel.css +18 -8
  20. package/dist/tw/aesthetics/sketch.css +14 -7
  21. package/dist/tw/box.css +49 -27
  22. package/dist/tw/components/button.css +35 -14
  23. package/dist/tw/components/feedback.css +59 -56
  24. package/dist/tw/components/form.css +200 -126
  25. package/dist/tw/components/index.css +3 -7
  26. package/dist/tw/components/loader.css +4 -5
  27. package/dist/tw/components/surface.css +71 -68
  28. package/dist/tw/components/tooltip.css +75 -41
  29. package/dist/tw/elevation.css +14 -22
  30. package/dist/tw/index.css +15 -19
  31. package/dist/tw/intent.css +31 -24
  32. package/dist/tw/native.css +15 -5
  33. package/dist/tw/palette.css +8 -5
  34. package/dist/tw/presentation.css +16 -30
  35. package/dist/tw/reset.css +21 -28
  36. package/dist/tw/surface.css +11 -12
  37. package/dist/tw/theme.css +58 -86
  38. package/dist/tw/utilities/content.css +268 -199
  39. package/dist/tw/utilities/layout.css +15 -25
  40. package/docs/accessibility.md +50 -33
  41. package/docs/changelog/0.6.0.md +52 -0
  42. package/docs/changelog/index.md +1 -0
  43. package/docs/concepts.md +6 -6
  44. package/docs/index.md +9 -0
  45. package/docs/integrating/nextjs.md +1 -1
  46. package/docs/integrating/tailwind.md +14 -6
  47. package/docs/setup.md +6 -6
  48. package/docs/usage/aesthetics.md +35 -8
  49. package/docs/usage/buttons.md +1 -1
  50. package/docs/usage/composing.md +1 -1
  51. package/docs/usage/content-and-layout.md +12 -11
  52. package/docs/usage/customizing.md +8 -9
  53. package/docs/usage/feedback.md +5 -3
  54. package/docs/usage/forms.md +51 -35
  55. package/docs/usage/theming.md +39 -40
  56. package/docs/usage/tooltips.md +37 -17
  57. package/llms-full.txt +331 -183
  58. package/llms.txt +6 -0
  59. package/package.json +5 -5
@@ -1,29 +1,53 @@
1
1
  @reference "../box.css";
2
2
 
3
+ /* A scroll box clips whatever paints outside it, and `overflow-x` takes the
4
+ vertical overflow with it, so an aesthetic's halo around the table -- cyber's
5
+ glow -- would be cut flush on every side. The wrapper makes room for the halo
6
+ inside itself and gives the same room back outside, so the table sits where
7
+ it does and only the scroll box grows. Zero wherever no halo is drawn. The
8
+ cost, under a halo: the wrapper reaches that far past its parent, which shows
9
+ only where the parent is flush with the viewport. Vertical margins still add
10
+ up to the same gap when they collapse with a sibling's, since the negative
11
+ one is paid back by the padding.
12
+
13
+ It reads the halo from its own scope, so an aesthetic class on the table
14
+ alone gives it nothing to make room for: the class belongs on the wrapper or
15
+ an ancestor, as `content-and-layout.md` says. A parent cannot read a child's
16
+ tokens, and reaching down with `:has()` would name an aesthetic here. */
3
17
  @utility table-wrap {
4
- @apply w-full overflow-x-auto;
18
+ @apply overflow-x-auto;
19
+
20
+ --_table-wrap-bleed: calc(var(--ui-halo-blur, 0px) + var(--ui-halo-spread, 0px));
21
+
22
+ box-sizing: border-box;
23
+ width: calc(100% + var(--_table-wrap-bleed) * 2);
24
+ margin: calc(var(--_table-wrap-bleed) * -1);
25
+ padding: var(--_table-wrap-bleed);
5
26
  }
6
27
 
7
28
  /* The one component that takes the frame of `box` and not the whole of it. The
8
- border, the radius and the clip come from the box; the head tone, the cell
9
- rules and the row hover are private tokens, because none of the three axes has
10
- a vocabulary for the inside of a table.
11
-
12
- Its `overflow: hidden` is what keeps `border-separate` corners inside the
13
- radius, and it does not compose with an aesthetic's `clip-path`: where both
14
- apply, the clip wins and the corners square off. Recorded in `registry.json`
15
- beside the decision. */
29
+ border and the plate come from the box, and the corner is a surface's,
30
+ because a table is a pane of content rather than a control; the head tone,
31
+ the cell rules and the row hover are private tokens, because none of the
32
+ three axes has a vocabulary for the inside of a table.
33
+
34
+ Nothing here clips the table. Chromium and WebKit lay a `<caption>` out
35
+ inside the table's own box while painting the plate and the frame around the
36
+ rows alone, so anything that acts on the whole box -- an `overflow` clip, a
37
+ `clip-path`, a backdrop filter, a positioned layer -- spans the caption: an
38
+ `overflow: hidden` would round the caption's corners instead of the head's
39
+ and cut the caption's first letter. The corner cells take the table's corners
40
+ instead (below), which every engine draws on the rows. Recorded in
41
+ `registry.json` beside the decision. */
16
42
  @utility data-table {
17
- @apply box text-body w-full border-separate text-left;
43
+ @apply box text-body w-full border-separate text-start;
18
44
 
19
45
  --_d-fill: 12%;
20
46
  --_d-fg-on-fill: 0%;
21
47
  /* Edgeless, so the frame is asked for rather than inherited from a default
22
- nobody can see in their own markup. Published `edged`, `.edged` on a table
23
- computed exactly what no class computed -- a switch that could not move
24
- anything -- and the frame arrived because of a line in `registry.json`.
25
- `.edged` draws it now, and the two boundaries a table keeps whatever else it
26
- draws mean a frameless one still reads as a table. */
48
+ nobody can see in their own markup. `.edged` draws it, and the two
49
+ boundaries a table keeps whatever else it draws mean a frameless one still
50
+ reads as a table. */
27
51
  --_d-border: 0%;
28
52
  --_d-elevation: 0;
29
53
 
@@ -31,30 +55,25 @@
31
55
  other and a neutral table head is otherwise a slab of the page's ink with
32
56
  ink printed on it. */
33
57
  --table-fill: calc(min(var(--ui-fill, var(--_d-fill)), var(--intent-fill-max, 100%)) * var(--ui-bg-alpha, 1));
34
- /* The head's plate is the tint, so a table carrying no fill has no head plate.
35
- `.ghost` means no tint here the way it means no fill everywhere else, and a
36
- ghost table is boundaries and type and nothing else -- which is what `.ghost`
37
- is for on this component, since the published resting fill is `soft`.
38
-
39
- Zero stays zero, and nothing past it saturates the same way any more. A
40
- `* 100` multiplier used to put every non-zero fill over the `min(100%, ...)`
41
- ceiling in one step -- `.soft`'s 12% and `.solid`'s 100% both landed on a
42
- fully opaque plate, so the only thing left to tell them apart was how much
43
- `--intent-color` got mixed onto it after (`--table-fill`, 12% vs 20% capped),
44
- a difference too small to read as the volume-level step presentation
45
- promises everywhere else in the model. `* 5` keeps zero at zero and a
46
- `.solid`'s 100% raw fill still clears the ceiling -- the same fully opaque
47
- head it always drew -- but stops `.soft`'s 12% short, at 60%: a visibly
48
- lighter, partly-page-showing-through plate instead of a second opaque one,
49
- which is what makes the two presentations separate again.
50
-
51
- It reads `--ui-fill` rather than `--table-fill` because that is the question
52
- being asked: what the presentation wanted, not what the intent cap and an
53
- aesthetic's alpha left of it. A glass `.soft` table still wants its head
54
- plate, and neutral's 20% cap has no bearing on whether there is one. Reading
55
- the raw class value also keeps the expression a `calc()` over one `var()`
56
- rather than over `--table-fill`'s own `calc()`/`min()` chain, which matters
57
- for the reason `--table-rule-plated` below records. */
58
+ /* The head's plate is the tint, so a table carrying no fill has no head
59
+ plate. `.ghost` means no tint here the way it means no fill everywhere
60
+ else, and a ghost table is boundaries and type and nothing else -- which is
61
+ what `.ghost` is for on this component, since the published resting fill is
62
+ `soft`.
63
+
64
+ Zero stays zero, and nothing past it saturates: `* 5` keeps zero at zero,
65
+ lets `.solid`'s 100% clear the `min(100%, ...)` ceiling to a fully opaque
66
+ head, and stops `.soft`'s 12% short at 60%, a visibly lighter plate, so the
67
+ two presentations separate. A larger multiplier lands both on a fully
68
+ opaque plate, leaving only the small intent mix to tell them apart.
69
+
70
+ It reads `--ui-fill` rather than `--table-fill` because the question is
71
+ what the presentation wanted, not what the intent cap and an aesthetic's
72
+ alpha left of it: a glass `.soft` table still wants its head plate, and
73
+ neutral's 20% cap has no bearing on whether there is one. Reading the raw
74
+ class value also keeps the expression a `calc()` over one `var()` rather
75
+ than over `--table-fill`'s own `calc()`/`min()` chain, which matters for
76
+ the reason `--table-rule-plated` below records. */
58
77
  --table-ground: min(100%, calc(var(--ui-fill, var(--_d-fill)) * 5));
59
78
  --table-head-plate: color-mix(in oklab, var(--intent-subtle) var(--table-ground), transparent);
60
79
  --table-head-bg: color-mix(in oklab, var(--intent-color) var(--table-fill), var(--table-head-plate));
@@ -62,147 +81,201 @@
62
81
 
63
82
  /* Untinted rather than mixed, the same call `.card.soft` makes: 12% ink onto
64
83
  `--intent-subtle` still reads as `.solid` on neutral, since neutral's ink
65
- and ground are both on the same greyscale axis and no ramp over that ground
66
- changes it much. A head has no hover or interaction state to reveal a tint
67
- into the way a card does on `.interactive`/`.hoverable`, so there is no
68
- second state to move a mechanism to -- `.soft` is `--color-foreground`,
69
- flat, always, for every intent. That mutes a colored `.success`/`.destructive`
70
- head at rest the way it mutes a non-interactive `.card.soft`; accepted for
71
- the same reason, not solved a different way.
72
-
73
- The table's own base plate rests the same way, for a reason the head fix
74
- alone did not cover: a `.soft` head landing at `--color-foreground` (five
75
- units off the page) sitting directly above a body still carrying the
76
- ordinary 12% tint (twenty-four units past the head) read backwards -- the
77
- seam that should read as one table read as the head belonging to the page
78
- and the body being the odd one out. `--_fill-cap` is `box.css`'s override
79
- slot (the same one `.card.soft` uses, for the reasons the comment there
80
- gives), and `--_d-ground` is box's own private ground
81
- slot, unclaimed here until now -- both land on the table's own element,
82
- same as `--table-fill`/`--table-head-plate` do, so nothing here fights
83
- `.soft` for `--ui-fill` either. `.ghost` is left at box's default
84
- (`--_d-ground: transparent`), so it stays the fully bare boundaries-only
85
- table it already was -- `.soft` now differs from it by keeping an opaque,
86
- untinted plate and a head label strip, not by how much ink is in either.
84
+ and ground are both on the same greyscale axis. A head has no hover or
85
+ interaction state to reveal a tint into the way a card does, so `.soft` is
86
+ `--color-foreground`, flat, always, for every intent. That mutes a coloured
87
+ `.success`/`.destructive` head at rest the way it mutes a non-interactive
88
+ `.card.soft`; accepted for the same reason.
89
+
90
+ The table's own base plate rests the same way: a `.soft` head at
91
+ `--color-foreground` (five units off the page) above a body carrying the
92
+ ordinary 12% tint (twenty-four units past the head) reads backwards, as
93
+ though the head belonged to the page and the body were the odd one out.
94
+ `--_fill-cap` is `box.css`'s override slot (the one `.card.soft` uses) and
95
+ `--_d-ground` is box's private ground slot; both land on the table's own
96
+ element, so nothing here fights `.soft` for `--ui-fill`. `.ghost` is left
97
+ at box's default (`--_d-ground: transparent`), so it stays the fully bare
98
+ boundaries-only table; `.soft` differs from it by keeping an opaque,
99
+ untinted plate and a head label strip.
87
100
 
88
101
  `:not(.solid, .ghost)` rather than `.soft`: the published resting fill is
89
- `soft`, so a bare `<table class="data-table">` -- the members table in
90
- `playground/app-shell/`, with no fill class at all -- is the common case
91
- this has to reach, not the edge case. `.soft` alone would miss every table
92
- that gets soft by doing nothing. */
102
+ `soft`, so a bare `<table class="data-table">` with no fill class at all is
103
+ the common case this has to reach. */
93
104
  &:not(.solid, .ghost) {
94
105
  --table-fill: 0%;
95
- --table-head-plate: var(--color-foreground);
106
+ --table-head-plate: var(--table-ground-plate);
96
107
  --_fill-cap: 0%;
97
- --_d-ground: var(--color-foreground);
108
+ --_d-ground: var(--table-ground-plate);
109
+
110
+ /* A table is a pane of content, so under an aesthetic whose surfaces are
111
+ glass it is glass too: the plate is the aesthetic's surface ground when
112
+ one is named, and the blur behind it is the one surfaces take. Head and
113
+ body share the plate, as they do on the page. Only this arm reads them:
114
+ a `.solid` table is opaque, like every `.solid`, and a `.ghost` one is
115
+ boundaries and type with nothing behind them to blur. */
116
+ --table-ground-plate: var(--ui-surface-ground, var(--color-foreground));
117
+
118
+ -webkit-backdrop-filter: var(--ui-backdrop, none);
119
+ backdrop-filter: var(--ui-backdrop, none);
98
120
  }
99
121
 
100
122
  /* How much rule there is between body rows, and none unless asked for. Ruling
101
- every row is a style choice rather than part of what a table is, and tying it
102
- to the edge axis made it arrive by implication instead: a table drew rules
103
- because its published edge default happened to be `edged`, which is a fact
104
- about the registry rather than anything the markup says. A consumer reading
105
- their own HTML could not tell whether they had asked for rules.
106
-
107
- `.ruled` is that ask and `.ruleless` is its opposite, with `--ui-rule` behind
108
- both -- so a consumer who wants a whole region ruled, or a whole region bare,
109
- sets the token on a container instead of classing every table.
123
+ every row is a style choice rather than part of what a table is, so it is
124
+ not tied to the edge axis: `.ruled` asks for it, `.ruleless` is its
125
+ opposite, and `--ui-rule` is behind both, so a consumer who wants a whole
126
+ region ruled, or a whole region bare, sets the token on a container instead
127
+ of classing every table.
110
128
 
111
129
  One token covers three positions because the two places that read it take
112
130
  different fallbacks. Unset, each site answers for itself: no rules between
113
131
  rows, but the boundaries below still drawn. Set, the answer is the same at
114
- both. So `.ruled` writes 100% and gets rules everywhere, `.ruleless` writes 0%
115
- and gets a table with no lines in it at all, and a table nobody classed sits
116
- between them. A second token would have said the same thing with more parts,
117
- and would have let the two drift. */
132
+ both. So `.ruled` writes 100% and gets rules everywhere, `.ruleless` writes
133
+ 0% and gets a table with no lines in it at all, and a table nobody classed
134
+ sits between them. */
118
135
  --table-rule-amount: var(--ui-rule, 0%);
119
136
  --table-boundary-amount: var(--ui-rule, 100%);
120
137
  /* What the rule is drawn in, which is not the same tone at every fill.
121
- `.ghost` and `.soft` keep `--intent-border` -- the slot an aesthetic restates
122
- through `--ui-ink` -- because at those fills the plate has not swallowed it.
123
- `.solid` is where it has: at a full fill `--intent-border` *is* the plate for
124
- every intent, and at neutral's capped fill it is a near shade of the same
125
- grey, so the rule the rows are separated by disappears exactly where the
126
- plate is busiest.
127
-
128
- The filled case draws in `currentColor` instead, at a fifth. That is the ink
129
- the cell is already printing -- `--table-head-fg` on a head, `box`'s own
130
- `--_fg` in the body -- so it reads on a light plate and a dark one without a
131
- per-intent branch, and it lands *over* the plate because a translucent border
132
- composites onto the background the element already painted.
133
-
134
- Naming the ink rather than composing it is also what keeps this expression
135
- cheap, and cheap is a requirement here rather than a preference. Written as
136
- `color-mix(--table-head-fg 20%, --_bg)` the rule expanded, through four levels
137
- of substitution, into a `color-mix()` over two more `color-mix()`es over a
138
- `min()`/`max()`/`calc()` chain -- and every `th` and `td` on a page resolves it.
139
- WebKit's renderer crashed outright on the two routes that draw tables, taking
140
- 22 specs across four files with it, while Chromium and Firefox rendered them
141
- correctly. Neither the depth nor the table alone did it: bisected, the new
142
- tokens were fine until the cells consumed them.
143
-
144
- `currentColor` and `transparent` are leaves the engine has resolved already,
145
- which is what takes the expression back down to something every engine can
146
- carry. `--table-row-hover` below is written the same way, for the same
147
- reason. */
138
+ `.ghost` and `.soft` keep `--intent-border` -- the slot an aesthetic
139
+ restates through `--ui-ink` -- because at those fills the plate has not
140
+ swallowed it. `.solid` is where it has: at a full fill `--intent-border`
141
+ *is* the plate for every intent, and at neutral's capped fill it is a near
142
+ shade of the same grey, so the row rule disappears exactly where the plate
143
+ is busiest.
144
+
145
+ The filled case draws in `currentColor` at a fifth instead: the ink the
146
+ cell is already printing (`--table-head-fg` on a head, `box`'s own `--_fg`
147
+ in the body), so it reads on a light plate and a dark one without a
148
+ per-intent branch, and it lands *over* the plate because a translucent
149
+ border composites onto the background the element already painted.
150
+
151
+ Naming the ink rather than composing it also keeps this expression cheap,
152
+ which is a requirement: every `th` and `td` on a page resolves it, and
153
+ written as `color-mix(--table-head-fg 20%, --_bg)` it expands, through four
154
+ levels of substitution, into a `color-mix()` over two more `color-mix()`es
155
+ over a `min()`/`max()`/`calc()` chain, which crashes WebKit's renderer on
156
+ the routes that draw tables. `currentColor` and `transparent` are leaves
157
+ the engine has already resolved, which takes the expression back down to
158
+ something every engine can carry. `--table-row-hover` below is written the
159
+ same way, for the same reason. */
148
160
  --table-rule-plated: color-mix(in oklab, currentColor 20%, transparent);
149
161
  --table-rule-tone: color-mix(in oklab, var(--table-rule-plated) var(--ui-fill, var(--_d-fill)), var(--intent-border));
150
162
  --table-rule: color-mix(in oklab, var(--table-rule-tone) var(--table-rule-amount), transparent);
151
163
  --table-boundary: color-mix(in oklab, var(--table-rule-tone) var(--table-boundary-amount), transparent);
152
164
  /* A hovered row is a step from the table it is in, not from a tone the page
153
- happens to own. Mixed into `--color-foreground` it was neither: a solid
154
- table paints its body with the intent, so a primary table in dark theme is
155
- white and its hovered row came out near-black -- a dark band across a light
156
- table. Painting a translucent step instead makes it relative at every fill,
157
- the way `box-hover` is: the row composites onto whatever plate the table
158
- drew, so a filled table shifts within its own colour, a soft one deepens its
159
- tint, and a ghost one picks up a faint wash of the page.
160
-
161
- Which tone it steps toward has to move with the fill as well. On a bare or
162
- tinted plate that is `--intent-hover`, the deepening every other component
163
- gets. On a filled one it cannot be: `--intent-hover` is
164
- `mix(--intent-color 82%, --intent-strong)`, which is very nearly the colour
165
- the plate is already painted, so a step toward it is no step. Measured, a
166
- solid success table moved 1.00:1 in light and a solid destructive one
167
- 1.008:1 -- the feedback was declared and invisible. A filled plate steps
168
- toward `currentColor` instead, which is the ink that plate already carries
169
- and therefore always contrasts with it.
165
+ happens to own. A translucent step is relative at every fill, the way
166
+ `box-hover` is: the row composites onto whatever plate the table drew, so a
167
+ filled table shifts within its own colour, a soft one deepens its tint, and
168
+ a ghost one picks up a faint wash of the page. (Mixed into
169
+ `--color-foreground`, a solid primary table in dark theme would hover to
170
+ near-black, a dark band across a light table.)
171
+
172
+ Which tone it steps toward moves with the fill. On a bare or tinted plate
173
+ that is `--intent-hover`, the deepening every other component gets. On a
174
+ filled one it cannot be: `--intent-hover` is `mix(--intent-color 82%,
175
+ --intent-strong)`, very nearly the colour the plate is already painted, so
176
+ a step toward it is no step (a solid success table moves 1.00:1 in light).
177
+ A filled plate steps toward `currentColor` instead, the ink that plate
178
+ already carries and therefore always contrasts with it.
170
179
 
171
180
  Both ends are leaves, and the step lands on `transparent` rather than on
172
- `--_bg`, for the reason `--table-rule-plated` above gives: compositing is
173
- what the browser does with a translucent background anyway, so naming `--_bg`
174
- buys the same colour for two more levels of substitution. */
181
+ `--_bg`, for the reason `--table-rule-plated` above gives. */
175
182
  --table-hover-tone: color-mix(in oklab, currentColor var(--ui-fill, var(--_d-fill)), var(--intent-hover));
176
183
  --table-row-hover: color-mix(in oklab, var(--table-hover-tone) 12%, transparent);
177
184
 
178
185
  border-spacing: 0;
179
- overflow: hidden;
186
+
187
+ /* A surface's corner, as `surface` computes it: a table sits beside cards and
188
+ panels as a pane, and under `.cyber` it took the controls' diagonal while
189
+ every pane around it cut the other one. */
190
+ --_corner-surface: calc(var(--ui-corner-surface, var(--radius-surface)) * var(--_scale));
191
+ border-radius: var(
192
+ --ui-radius-surface,
193
+ calc(var(--_corner-surface) * var(--ui-corner-tl, 1)) calc(var(--_corner-surface) * var(--ui-corner-tr, 1))
194
+ calc(var(--_corner-surface) * var(--ui-corner-br, 1)) calc(var(--_corner-surface) * var(--ui-corner-bl, 1))
195
+ );
196
+ /* No silhouette clip either, for the caption's sake: `.pixel`'s stepped clip
197
+ cut the caption's corners and left the head's square. A pixel table is
198
+ square, which is the chip answer that aesthetic already gives. */
199
+ clip-path: none;
200
+
201
+ /* The corners pass down to the cells that sit in them. Each section and row
202
+ inherits the table's four corners -- a radius does not apply to either, but
203
+ it still computes and inherits -- and each corner cell takes only its own
204
+ corner, as a longhand, so an elliptical or bevelled corner arrives whole.
205
+ The first row is the head's when there is one and the first body's when
206
+ not, and the last row likewise the foot's or the last body's; a caption
207
+ and a `colgroup` are skipped. A cell spanning rows into a corner is not
208
+ reached, and a percentage corner resolves against the cell. */
209
+ & > :is(thead, tbody, tfoot),
210
+ & > :is(thead, tbody, tfoot) > tr {
211
+ border-radius: inherit;
212
+ corner-shape: inherit;
213
+ }
214
+
215
+ & > :is(thead, tbody, tfoot) > tr > * {
216
+ corner-shape: inherit;
217
+ }
218
+
219
+ & > thead > tr:first-child,
220
+ &:not(:has(> thead)) > tbody:first-of-type > tr:first-child {
221
+ & > :first-child {
222
+ border-start-start-radius: inherit;
223
+ }
224
+
225
+ & > :last-child {
226
+ border-start-end-radius: inherit;
227
+ }
228
+ }
229
+
230
+ & > tfoot > tr:last-child,
231
+ &:not(:has(> tfoot)) > tbody:last-of-type > tr:last-child {
232
+ & > :first-child {
233
+ border-end-start-radius: inherit;
234
+ }
235
+
236
+ & > :last-child {
237
+ border-end-end-radius: inherit;
238
+ }
239
+ }
180
240
 
181
241
  & caption {
182
- @apply text-label text-text-secondary pb-2 text-left;
242
+ @apply text-label text-text-secondary pb-2 text-start;
183
243
  }
184
244
 
245
+ /* The head's plate is painted on its cells rather than on the section, so the
246
+ corner cells round it: a section's background ignores the cells' corners. */
185
247
  & thead,
186
248
  & tfoot {
187
- background-color: var(--table-head-bg);
188
249
  color: var(--table-head-fg);
189
250
  }
190
251
 
252
+ & :is(thead, tfoot) > tr > * {
253
+ background-color: var(--table-head-bg);
254
+ }
255
+
191
256
  /* Every cell carries the hairline; what varies is whether it is painted. The
192
- width tracks the aesthetic's own line weight (`--_width`, the same token
193
- `box` uses for the table's outer frame) rather than a fixed `1px`, so a
194
- row rule reads as the same material as the boundary around it instead of
195
- as a leftover default under an aesthetic whose whole point is thickness --
196
- `.chunky-tile`'s 2px frame used to sit over a 1px row rule, which read as
197
- unstyled rather than as a deliberate, quieter line. It still survives a
198
- fill that would otherwise paint over it, because color and width are
199
- independent here: `.ruleless` zeroes the color, not this. */
257
+ width tracks the aesthetic's own line weight rather than a fixed `1px`, so
258
+ a row rule reads as the same material as the boundary around it. It
259
+ survives a fill that would otherwise paint over it, because color and width
260
+ are independent here: `.ruleless` zeroes the color, not this.
261
+
262
+ The width is the aesthetic's, not `box`'s `--_width`: that one is capped by
263
+ `--ui-border-max`, which `.pixel` sets to zero because its frame is an
264
+ inset ring a clip cannot cut. A rule is not a frame and nothing clips a
265
+ table, so under pixel the capped width would draw no rule at all -- no line
266
+ under the head or above the foot, and `.ruled` would change nothing. A
267
+ divider reads the same uncapped width for the same reason.
268
+
269
+ Alignment is logical and stated on the cells, not only on the table: a
270
+ `th` centers unless its parent names an alignment, and `start` is the
271
+ initial value, which counts as naming none. */
272
+ --_table-rule-width: var(--ui-border-width, var(--border-width));
200
273
  & th,
201
274
  & td {
202
- @apply px-4 py-3 align-top;
275
+ @apply px-4 py-3 text-start align-top;
203
276
 
204
277
  border-bottom-style: var(--ui-rule-style, var(--ui-line-style, solid));
205
- border-bottom-width: var(--_width);
278
+ border-bottom-width: var(--_table-rule-width);
206
279
  border-color: var(--table-rule);
207
280
  }
208
281
 
@@ -221,7 +294,7 @@
221
294
 
222
295
  & tfoot tr:first-child > * {
223
296
  border-top-style: var(--ui-rule-style, var(--ui-line-style, solid));
224
- border-top-width: var(--_width);
297
+ border-top-width: var(--_table-rule-width);
225
298
  border-top-color: var(--table-boundary);
226
299
  }
227
300
 
@@ -243,57 +316,49 @@
243
316
  /* Rows deliberately inherit the table's intent instead of resetting it, so a
244
317
  `.data-table.success` tints its own rows. A `<tr class="warning">` still wins,
245
318
  because an intent class on the element beats an inherited value. */
246
- & tbody tr {
319
+ & tbody tr > * {
247
320
  transition: background-color var(--motion-duration-fast) var(--motion-ease);
248
321
  }
249
322
 
250
- & tbody tr:hover {
323
+ /* On the cells, for the reason the head's plate is: a hovered last row keeps
324
+ the table's bottom corners. */
325
+ & tbody tr:hover > * {
251
326
  background-color: var(--table-row-hover);
252
327
  }
253
328
 
254
329
  /* The rule comes off the row that is visually last, which is not always the
255
- last row of the body. Written as `tbody tr:last-child` it came off the body's
256
- last row unconditionally, so a table with a footer lost the one rule that
257
- separates the two -- the summary ran straight on from the rows it summarises --
258
- while the footer kept a bottom rule of its own, drawing a hairline immediately
259
- inside the frame. A missing line where one belongs and a doubled one where it
260
- does not, from the same declaration.
330
+ last row of the body. `tbody tr:last-child` would remove the body's last
331
+ row rule unconditionally, so a table with a footer would lose the one rule
332
+ that separates the two while the footer kept a bottom rule of its own,
333
+ drawing a hairline immediately inside the frame.
261
334
 
262
335
  Both selectors are `:last-child` on the section, so whichever section the
263
- markup puts last is the one that loses its rule. That covers a footer written
264
- before the body, which HTML still allows, and multiple bodies, where only the
265
- last one ends the table.
266
-
267
- It went unseen because it was invisible until this release: `thead` and
268
- `tfoot` shared an unconditional `--intent-subtle` plate, so the footer was a
269
- band and the band did the separating. `.ghost` dropping that plate is what
270
- left the rule doing the job alone. */
336
+ markup puts last is the one that loses its rule. That covers a footer
337
+ written before the body, which HTML allows, and multiple bodies, where only
338
+ the last one ends the table. `.ghost` drops the head and foot plate, which
339
+ leaves this rule doing the separating alone. */
271
340
  & tbody:last-child tr:last-child > *,
272
341
  & tfoot:last-child tr:last-child > * {
273
342
  @apply border-b-0;
274
343
  }
275
344
  }
276
345
 
277
- /* Three components rest on a ground rather than on nothing: a key cap, an inline
278
- code chip and a code block are each a quiet tinted plate at rest, and that tone
279
- is `--intent-subtle` rather than a fill of the component's own intent.
280
-
281
- All three rest at `soft`, and `.ghost` is published as unsupported on them.
282
- The ground is what makes a chip a chip, and `.ghost` asks for no fill while
283
- the ground stays -- so on these three it drew the plate anyway and meant
284
- nothing, which is the opposite of what it means everywhere else. Worse, at
285
- zero fill the plate is `--intent-subtle` alone, and that slot is a near-page
286
- tint: measured on the light page, a ghost `code` chip was `#e5e5e5` for both
287
- neutral and primary, and `#f5f5f5` for secondary at 1.04:1 against a `#fafafa`
288
- page. A class that renders four intents as the same invisible chip is not a
289
- variation. Resting at `soft` puts a real 12% of the intent over the ground, so
290
- the plate is a step past the page and the intents separate.
291
-
292
- Each used to restate its foreground too, because `box` mixed toward
293
- `--intent-color` -- the tone meant to sit *on* a page rather than on a tint of
294
- itself, and amber-600 on amber-100 is a smudge. `box` now mixes toward
295
- `--intent-strong`, which is what these three were reaching for, so the three
296
- copies are gone and the ground is the only thing left to declare. */
346
+ /* Three components rest on a ground rather than on nothing: a key cap, an
347
+ inline code chip and a code block are each a quiet tinted plate at rest, and
348
+ that tone is `--intent-subtle` rather than a fill of the component's own
349
+ intent.
350
+
351
+ All three rest at `soft`, and `.ghost` is published as unsupported on them:
352
+ the ground draws the plate whatever the fill says, so `.ghost` would draw it
353
+ anyway and mean nothing, and at zero fill the plate is `--intent-subtle`
354
+ alone, a near-page tint that renders four intents as the same invisible chip
355
+ (`#e5e5e5` for neutral and primary on the light page). Resting at `soft` puts
356
+ a real 12% of the intent over the ground, so the plate is a step past the
357
+ page and the intents separate.
358
+
359
+ The foreground is `box`'s, which mixes toward `--intent-strong`, the tone
360
+ meant to sit on a page rather than on a tint of itself, so the ground is the
361
+ only thing left to declare. */
297
362
  /* Neutral, for the same reason `.panel`'s own carve-out is: 12% of
298
363
  `--color-text` mixed onto `--intent-subtle` (`--color-surface` for
299
364
  neutral) reads as an assertive slab rather than a quiet chip -- measured,
@@ -376,7 +441,7 @@
376
441
 
377
442
  The bar is twice the line width the aesthetic asks for, plus a pixel, which is
378
443
  this component's own choice about what a quotation looks like rather than a
379
- scale applied to an edge -- there is no such scale any more. The constant is
444
+ scale applied to an edge. The constant is
380
445
  what makes the bar read as a bar at the default hairline: doubling 1px gives
381
446
  2px, and a quotation mark needs more presence than a table rule. */
382
447
  @utility quote {
@@ -389,22 +454,27 @@
389
454
  `--ui-bg-alpha` thins it. */
390
455
  --quote-capped: min(var(--ui-fill, var(--_d-fill)), var(--intent-fill-max, 100%));
391
456
  --quote-fill: calc(var(--quote-capped) * var(--ui-bg-alpha, 1));
392
- --quote-bg: color-mix(in oklab, var(--intent-color) var(--quote-fill), transparent);
457
+ /* `box`'s `--_plate`: a fill the intent's cap stopped short rests on the page
458
+ background, so a neutral `.solid` quotation is a plate rather than a tint
459
+ of whatever is behind it. */
460
+ --quote-plate: clamp(0%, calc((var(--ui-fill, var(--_d-fill)) - var(--quote-capped)) * 1000), 100%);
461
+ --quote-bg: color-mix(
462
+ in oklab,
463
+ var(--intent-color) var(--quote-fill),
464
+ color-mix(in oklab, var(--color-background) var(--quote-plate), transparent)
465
+ );
393
466
  /* The same edge blend `box` draws, in the same order and for the same reason:
394
467
  the blend is the line's colour, and how much line there is comes after it.
395
- Written the other way round -- how much line, then blend the fill into it --
396
- `.edgeless` lands on the fill colour rather than on nothing, and the border
397
- box paints that over the fill it already drew, so a quotation that asked for
398
- no bar got a bar of two coats of its own tint.
399
-
400
- It also fades toward `transparent` rather than toward the plate, as `box`'s
401
- does: the plate runs under the bar, so a bar blended toward it painted a
402
- translucent plate twice and drew a darker stripe on a neutral `.solid`
403
- quotation.
404
-
405
- That this had to be fixed three times is the cost of a quotation drawing
406
- its own edge instead of taking `box`'s. Recorded in `registry.json` beside
407
- the decision to compose none of `box`. */
468
+ Written the other way round, `.edgeless` lands on the fill colour rather
469
+ than on nothing, and the border box paints that over the fill it already
470
+ drew. It also fades toward `transparent` rather than toward the plate, as
471
+ `box`'s does: the plate runs under the bar, so a bar blended toward it
472
+ would paint a translucent plate twice and draw a darker stripe on a neutral
473
+ `.solid` quotation.
474
+
475
+ A quotation draws its own edge instead of taking `box`'s, so an edge change
476
+ is made in `box`, `.quote`, and `.progress`. Recorded in `registry.json`
477
+ beside the decision to compose none of `box`. */
408
478
  --quote-line: color-mix(in oklab, transparent var(--ui-fill, var(--_d-fill)), var(--intent-border));
409
479
  --quote-edge: color-mix(in oklab, var(--quote-line) var(--ui-border, var(--_d-border)), transparent);
410
480
  --quote-width: calc(var(--ui-border-width, var(--border-width)) * 2 + 1px);
@@ -414,13 +484,12 @@
414
484
  background-color: var(--quote-bg);
415
485
  border-inline-start-color: var(--quote-edge);
416
486
  border-inline-start-width: var(--quote-width);
417
- /* The ink is gated by the plate, not tied to the fill: `--ui-fg-on-fill` is the
418
- ceiling, and the contrast tone only ramps in past a half fill, reaching full
419
- at a full fill. Below that the ink stays `--intent-strong`, the tone chosen
420
- to read on the page. This is `box`'s `--_on-fill` exactly -- `min(asked,
421
- --intent-fill-max)` read plausibly and was wrong at the bottom of the range,
422
- printing a capped-neutral `.solid` quote at ~6.6:1 where the ramp gives
423
- ~11:1, and this renderer has to agree with `box`. */
487
+ /* The ink is gated by the plate, not tied to the fill: `--ui-fg-on-fill` is
488
+ the ceiling, and the contrast tone only ramps in past a half fill, reaching
489
+ full at a full fill. Below that the ink stays `--intent-strong`, the tone
490
+ chosen to read on the page. This is `box`'s `--_on-fill` exactly, and this
491
+ renderer has to agree with it: a fill-proportional ink prints a
492
+ capped-neutral `.solid` quote at ~6.6:1 where the ramp gives ~11:1. */
424
493
  color: color-mix(
425
494
  in oklab,
426
495
  var(--intent-contrast)