@codenhub/styles 0.1.0 → 0.2.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.
- package/README.md +8 -69
- package/dist/aesthetics/chunky-tile.css +1 -1
- package/dist/aesthetics/index.css +1 -1
- package/dist/components.css +1 -1
- package/dist/index.css +1 -1
- package/dist/native.css +1 -1
- package/dist/theme.css +1 -1
- package/dist/tw/aesthetics/chunky-tile.css +20 -2
- package/dist/tw/box.css +45 -2
- package/dist/tw/components/button.css +38 -29
- package/dist/tw/components/feedback.css +58 -41
- package/dist/tw/components/form.css +139 -47
- package/dist/tw/components/index.css +16 -6
- package/dist/tw/components/loader.css +2 -16
- package/dist/tw/components/surface.css +122 -5
- package/dist/tw/components/tooltip.css +102 -91
- package/dist/tw/elevation.css +57 -14
- package/dist/tw/index.css +23 -5
- package/dist/tw/intent.css +36 -12
- package/dist/tw/native.css +4 -20
- package/dist/tw/reset.css +40 -32
- package/dist/tw/theme.css +46 -8
- package/dist/tw/utilities/content.css +104 -18
- package/dist/tw/utilities/index.css +1 -1
- package/dist/tw/utilities/layout.css +33 -0
- package/docs/accessibility.md +68 -48
- package/docs/changelog/0.1.0.md +37 -0
- package/docs/changelog/0.1.1.md +10 -0
- package/docs/changelog/0.2.0.md +41 -0
- package/docs/changelog/index.md +11 -0
- package/docs/concepts.md +21 -46
- package/docs/index.md +18 -45
- package/docs/integrating/astro.md +13 -7
- package/docs/integrating/index.md +25 -16
- package/docs/integrating/nextjs.md +31 -8
- package/docs/integrating/svelte.md +14 -8
- package/docs/integrating/tailwind.md +31 -15
- package/docs/integrating/vue.md +13 -7
- package/docs/setup.md +31 -32
- package/docs/usage/aesthetics.md +31 -107
- package/docs/usage/buttons.md +24 -36
- package/docs/usage/composing.md +31 -102
- package/docs/usage/content-and-layout.md +65 -83
- package/docs/usage/customizing.md +25 -90
- package/docs/usage/feedback.md +25 -56
- package/docs/usage/forms.md +48 -80
- package/docs/usage/index.md +9 -17
- package/docs/usage/theming.md +25 -81
- package/docs/usage/tooltips.md +55 -35
- package/llms-full.txt +1193 -1510
- package/package.json +121 -44
|
@@ -58,8 +58,21 @@
|
|
|
58
58
|
tick on light grey.
|
|
59
59
|
|
|
60
60
|
Ramping against `--_fill` keeps the two in step at every bound, including
|
|
61
|
-
the checked state that lifts them.
|
|
62
|
-
|
|
61
|
+
the checked state that lifts them.
|
|
62
|
+
|
|
63
|
+
`--_fg-on-fill-floor` is the seam `:checked` pins instead of touching
|
|
64
|
+
`--ui-fg-on-fill` directly. `.ghost`/`.soft` are unlayered CSS (P3, and see
|
|
65
|
+
model.md's `--_fill-cap` history) and `text-control` compiles into
|
|
66
|
+
Tailwind's `utilities` layer, so a layered `:checked` writing
|
|
67
|
+
`--ui-fg-on-fill` the way it writes `--_fill-cap`/`--_fill-floor` would
|
|
68
|
+
lose to `.ghost`'s unlayered `--ui-fg-on-fill: 0%` regardless of
|
|
69
|
+
specificity -- the same trap `--_fill-cap` exists to avoid on the fill
|
|
70
|
+
itself. A private floor `.ghost`/`.soft` never touch has nothing
|
|
71
|
+
unlayered contesting it. */
|
|
72
|
+
--_on-fill: max(
|
|
73
|
+
var(--_fg-on-fill-floor, 0%),
|
|
74
|
+
min(var(--ui-fg-on-fill, var(--_d-fg-on-fill)), max(0%, calc(var(--_fill) * 2 - 100%)))
|
|
75
|
+
);
|
|
63
76
|
/* The resting line is a fraction of the intent rather than all of it, and the
|
|
64
77
|
fraction is what makes the pointer visible. Drawn at full strength a control
|
|
65
78
|
already sits at its intent's darkest, and hover moves it to a neighbouring
|
|
@@ -104,8 +117,6 @@
|
|
|
104
117
|
|
|
105
118
|
@apply w-full appearance-none;
|
|
106
119
|
|
|
107
|
-
box-sizing: border-box;
|
|
108
|
-
|
|
109
120
|
&::placeholder {
|
|
110
121
|
color: var(--color-text-secondary);
|
|
111
122
|
}
|
|
@@ -122,14 +133,41 @@
|
|
|
122
133
|
--_line-tone: var(--intent-color);
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
/* R8, P6: `:checked`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
/* R8, P6: `:checked` pins the fill by making the floor and the cap the same
|
|
137
|
+
value, so `clamp()` returns exactly that value whatever `--ui-fill` asks --
|
|
138
|
+
still a bound on the input, still lifted rather than a rewrite of the
|
|
139
|
+
result (the failure mode P6 exists for: an early version pinned `--_fill`
|
|
140
|
+
and `--_fg` directly, which made a checked toggle ignore its own aesthetic
|
|
141
|
+
and elevation along with presentation).
|
|
142
|
+
|
|
143
|
+
The floor used to sit at `12%` rather than `100%`, which kept presentation
|
|
144
|
+
live on the checked mark too -- a checked `.soft` toggle rendered a pale
|
|
145
|
+
plate with just the mark at full strength, the classic ring-and-dot radio.
|
|
146
|
+
The stress-test pass found that cost more than it bought on a real screen:
|
|
147
|
+
three presentations gave a checkbox, a radio, and a switch three different
|
|
148
|
+
"on" identities to learn, while every *unchecked* toggle read the same
|
|
149
|
+
washed-out plate regardless of which class asked for it -- close enough to
|
|
150
|
+
the disabled look that a form full of ordinary, unchecked, un-intended
|
|
151
|
+
controls read as half-disabled. Pinning checked to one plate and freeing
|
|
152
|
+
presentation to separate the *unchecked* one instead (see
|
|
153
|
+
`checkbox`/`radio`/`switch` below) moves the axis to where it was doing
|
|
154
|
+
something. Lives here, not on the toggles, because a text input is never
|
|
155
|
+
`:checked`.
|
|
156
|
+
|
|
157
|
+
The ink is pinned alongside the fill, through `--_fg-on-fill-floor` rather
|
|
158
|
+
than `--ui-fg-on-fill` directly (see that seam, above): the other half of
|
|
159
|
+
what a fill class asks for is how much contrast ink it wants
|
|
160
|
+
(`--ui-fg-on-fill: 0%` is `.ghost`/`.soft` declaring "none, my plate stays
|
|
161
|
+
quiet"), and pinning the fill without pinning this left a checked
|
|
162
|
+
`.ghost`/`.soft` toggle with an opaque plate and a mark that still read as
|
|
163
|
+
though the plate were quiet -- both derived from the same intent tone,
|
|
164
|
+
close enough to disappear into each other. `--_on-fill` ramps against the
|
|
165
|
+
*composed* `--_fill`, so once that is pinned to 100% the ink has to reach
|
|
166
|
+
full contrast too, or the ramp's own ceiling has nothing to reach. */
|
|
130
167
|
&:checked {
|
|
131
168
|
--_fill-cap: 100%;
|
|
132
|
-
--_fill-floor:
|
|
169
|
+
--_fill-floor: 100%;
|
|
170
|
+
--_fg-on-fill-floor: 100%;
|
|
133
171
|
}
|
|
134
172
|
|
|
135
173
|
/* Native decorations a field replaces with its own artwork. */
|
|
@@ -166,11 +204,25 @@
|
|
|
166
204
|
--_fill-cap: 20%;
|
|
167
205
|
}
|
|
168
206
|
|
|
169
|
-
/* A checked
|
|
170
|
-
cap here.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
:
|
|
207
|
+
/* A checked text-control fills with its own intent, so neutral has to drop its
|
|
208
|
+
20% cap here. No toggle needs this arm any more (see below), but a text
|
|
209
|
+
input is never `:checked` in practice either -- kept for whatever custom
|
|
210
|
+
`.text-control` composition does check itself. Declared where the intents
|
|
211
|
+
are (0-2-0, unlayered): inside `text-control` `.neutral` would overrule it. */
|
|
212
|
+
:is(.text-control, input):checked {
|
|
213
|
+
--intent-fill-max: 100%;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* The three toggles read their own component cap (`--_fill-cap`, below) as the
|
|
217
|
+
ceiling regardless of intent, checked or not -- not just at `:checked`, the
|
|
218
|
+
way the arm above still works for a hypothetical checked text-control. A
|
|
219
|
+
neutral fill stops at 20% everywhere else because it is the page's own ink,
|
|
220
|
+
and a full one reads as a slab on a badge or a card. That reasoning does not
|
|
221
|
+
hold for a toggle at rest: a neutral checkbox is the common case, most
|
|
222
|
+
controls carry no intent class at all, and it has to look like a present,
|
|
223
|
+
interactive box rather than a washed-out one just because nothing colored
|
|
224
|
+
it. Declared where the intents are, for the reason given above. */
|
|
225
|
+
:is(.checkbox, .radio, .switch) {
|
|
174
226
|
--intent-fill-max: 100%;
|
|
175
227
|
}
|
|
176
228
|
|
|
@@ -184,6 +236,21 @@
|
|
|
184
236
|
--_edge-floor: 0%;
|
|
185
237
|
}
|
|
186
238
|
|
|
239
|
+
/* `.switch`'s one exception to the opt-out above: `.ghost` already asks for
|
|
240
|
+
zero fill, so a `.switch.ghost.edgeless` track has neither a line nor a
|
|
241
|
+
plate and disappears entirely -- nothing marks its bounds but the floating
|
|
242
|
+
knob. Every other fill on this component still has something to paint
|
|
243
|
+
(`.soft`/`.solid`/default all rest above 0%), so only this one pairing
|
|
244
|
+
needs the floor held up. `.switch.ghost.edgeless` renders identically to
|
|
245
|
+
`.switch.ghost.edged` now, published unsupported as a distinct look --
|
|
246
|
+
the same "produces a row indistinguishable from its neighbour" reason
|
|
247
|
+
`.solid.edgeless`/`.solid.edged` already collapse to one row for. Higher
|
|
248
|
+
specificity than the rule above (three classes, not an `:is()` of one)
|
|
249
|
+
wins regardless of source order. */
|
|
250
|
+
.switch.ghost.edgeless {
|
|
251
|
+
--_edge-floor: 100%;
|
|
252
|
+
}
|
|
253
|
+
|
|
187
254
|
/* An invalid control is destructive whatever else it was asked to be. A plain
|
|
188
255
|
unlayered rule, not `&[aria-invalid]` inside `@utility`: the intent reset is
|
|
189
256
|
unlayered too and would overrule a layered version (leaving a grey line and
|
|
@@ -309,9 +376,17 @@
|
|
|
309
376
|
No icons ship. Any element works in a slot -- an inline `<svg>`, an `<img>`,
|
|
310
377
|
or a class from an icon set such as `@codenhub/icons` -- placed before the
|
|
311
378
|
control for a leading adornment and after it for a trailing one. See
|
|
312
|
-
docs/usage/forms.md#icons.
|
|
379
|
+
docs/usage/forms.md#icons.
|
|
380
|
+
|
|
381
|
+
The gap between the control and its adornment is a token, `--input-group-gap`,
|
|
382
|
+
rather than a bare Tailwind `gap-2`: an icon reads that 0.5rem as the normal
|
|
383
|
+
breathing room a distinct glyph needs, but a text affix meant to glue onto the
|
|
384
|
+
value -- a URL host, a currency symbol, a unit -- reads the same gap as a
|
|
385
|
+
stray space breaking the two apart. A consumer sets `--input-group-gap: 0` on
|
|
386
|
+
their own `.input-group` for a flush affix; the default stays the icon-tuned
|
|
387
|
+
0.5rem so every existing icon composition is unchanged. */
|
|
313
388
|
@utility input-group {
|
|
314
|
-
@apply text-control flex w-full items-center
|
|
389
|
+
@apply text-control flex w-full items-center;
|
|
315
390
|
|
|
316
391
|
/* After the `@apply`, for the reason `ipt` states it: `text-control` inlines
|
|
317
392
|
its own resting geometry there, so a default written above it loses to the
|
|
@@ -322,9 +397,11 @@
|
|
|
322
397
|
--_d-border: 100%;
|
|
323
398
|
--_d-elevation: 0;
|
|
324
399
|
|
|
400
|
+
--input-group-gap: 0.5rem;
|
|
325
401
|
--input-group-pad: 0.75rem;
|
|
326
402
|
|
|
327
403
|
min-height: var(--control-height);
|
|
404
|
+
gap: var(--input-group-gap);
|
|
328
405
|
padding-inline: var(--input-group-pad);
|
|
329
406
|
|
|
330
407
|
/* The focus ring the wrapper cannot get from `:focus-visible` -- a `<div>` is
|
|
@@ -384,18 +461,26 @@
|
|
|
384
461
|
|
|
385
462
|
/* The three toggles compose `text-control` for its bounds, not its chrome: the
|
|
386
463
|
edge floor keeps a container's `.edgeless` from erasing the closed silhouette
|
|
387
|
-
(WCAG 1.4.11), and the checked state that
|
|
388
|
-
They raise the fill cap to
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
464
|
+
(WCAG 1.4.11), and the checked state that pins the fill (see `text-control`'s
|
|
465
|
+
`:checked` above) lives there. They raise the fill cap to 40% -- the 6% one
|
|
466
|
+
keeps typed text legible and a toggle has none, and 40% is where a resting
|
|
467
|
+
`.ghost` (0%), `.soft`/default (12%), and `.solid` (the cap itself) read as
|
|
468
|
+
three distinct plates rather than three shades of the same wash, the same
|
|
469
|
+
separation `.switch` was already measured against. Unclassed rests at
|
|
470
|
+
`.soft` rather than at the cap: `--_d-fill`/`--_d-fg-on-fill` below name the
|
|
471
|
+
same pair `.soft` does, so a bare `<input class="checkbox">` reads as the
|
|
472
|
+
quiet tint the stress-test pass preferred over the louder plate `.solid`
|
|
473
|
+
asks for -- a consumer still reaches that plate by writing `.solid` itself.
|
|
474
|
+
`.ghost` used to be unsupported here -- a checked one used to be a mark on
|
|
475
|
+
nothing -- and is not any more, now that checked reads the same plate
|
|
476
|
+
whatever the presentation.
|
|
477
|
+
See model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
|
|
393
478
|
@utility checkbox {
|
|
394
479
|
@apply text-control inline-grid size-4 cursor-pointer place-content-center;
|
|
395
480
|
|
|
396
|
-
--_d-fill:
|
|
397
|
-
--_d-fg-on-fill:
|
|
398
|
-
--_fill-cap:
|
|
481
|
+
--_d-fill: 12%;
|
|
482
|
+
--_d-fg-on-fill: 0%;
|
|
483
|
+
--_fill-cap: 40%;
|
|
399
484
|
--_line-rest: 100%;
|
|
400
485
|
--_d-border: 100%;
|
|
401
486
|
--_d-elevation: 0;
|
|
@@ -441,13 +526,14 @@
|
|
|
441
526
|
}
|
|
442
527
|
}
|
|
443
528
|
|
|
444
|
-
/* The same control with a round silhouette and a dot for its mark
|
|
529
|
+
/* The same control with a round silhouette and a dot for its mark, and the
|
|
530
|
+
same fill cap raise given on `checkbox`. */
|
|
445
531
|
@utility radio {
|
|
446
532
|
@apply text-control inline-grid size-4 cursor-pointer place-content-center rounded-full;
|
|
447
533
|
|
|
448
|
-
--_d-fill:
|
|
449
|
-
--_d-fg-on-fill:
|
|
450
|
-
--_fill-cap:
|
|
534
|
+
--_d-fill: 12%;
|
|
535
|
+
--_d-fg-on-fill: 0%;
|
|
536
|
+
--_fill-cap: 40%;
|
|
451
537
|
--_line-rest: 100%;
|
|
452
538
|
--_d-border: 100%;
|
|
453
539
|
--_d-elevation: 0;
|
|
@@ -472,12 +558,14 @@
|
|
|
472
558
|
content: "";
|
|
473
559
|
}
|
|
474
560
|
|
|
475
|
-
/* A checked radio thickens its ring and takes the intent whole on the line
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
561
|
+
/* A checked radio thickens its ring and takes the intent whole on the line.
|
|
562
|
+
The fill is pinned by `text-control`'s `:checked` now, not left to the
|
|
563
|
+
presentation -- a filled circle at every checked radio, whatever fill
|
|
564
|
+
class it carries, rather than the classic ring-and-dot a checked `.soft`
|
|
565
|
+
used to draw. The dot still takes `box`'s composed foreground, which walks
|
|
566
|
+
to the contrast tone as the (now always full) plate fills, so it stays
|
|
567
|
+
readable. `--_line-tone` rather than `--_edge`, so `text-control`'s edge
|
|
568
|
+
floor still draws the ring under a container's `.edgeless`. */
|
|
481
569
|
&:checked {
|
|
482
570
|
--_line-tone: var(--intent-color);
|
|
483
571
|
|
|
@@ -493,24 +581,28 @@
|
|
|
493
581
|
}
|
|
494
582
|
|
|
495
583
|
/* A2: every dimension derives from the border width `box` resolved, so the knob
|
|
496
|
-
cannot drift out of a track a thick aesthetic widened.
|
|
497
|
-
higher than the other toggles because its track is the whole component. */
|
|
584
|
+
cannot drift out of a track a thick aesthetic widened. */
|
|
498
585
|
@utility switch {
|
|
499
586
|
@apply text-control relative cursor-pointer;
|
|
500
587
|
|
|
501
|
-
--_d-fill:
|
|
502
|
-
--_d-fg-on-fill:
|
|
588
|
+
--_d-fill: 12%;
|
|
589
|
+
--_d-fg-on-fill: 0%;
|
|
503
590
|
--_line-rest: 100%;
|
|
504
591
|
--_d-border: 100%;
|
|
505
592
|
--_d-elevation: 0;
|
|
506
593
|
|
|
507
|
-
/* 40%, measured: at `text-control`'s 6% the three fills collapsed to
|
|
508
|
-
and the switch had to vary its *line* per fill class --
|
|
509
|
-
class decided an edge. 40% separates all three
|
|
510
|
-
light page, against ~20 for a visible one)
|
|
511
|
-
track's 100%, so the knob's position
|
|
512
|
-
|
|
513
|
-
|
|
594
|
+
/* 40%, measured: at `text-control`'s 6% the three resting fills collapsed to
|
|
595
|
+
two looks and the switch had to vary its *line* per fill class instead --
|
|
596
|
+
the one place a fill class decided an edge. 40% separates all three
|
|
597
|
+
(29/67/144 sRGB steps on the light page, against ~20 for a visible one)
|
|
598
|
+
without reaching a checked track's pinned 100%, so the knob's position
|
|
599
|
+
still says which. `axes.spec.ts` asserts all three. `checkbox`/`radio`
|
|
600
|
+
raise their own cap to the same value for the same reason -- a switch does
|
|
601
|
+
not need a higher one, a track is just the component every toggle's box
|
|
602
|
+
already was. A cap rather than a resting value, because it answers the
|
|
603
|
+
cascade case; `--intent-fill-max` (declared where the intents are, above)
|
|
604
|
+
lifts to 100% for all three toggles so a neutral one reads this cap too,
|
|
605
|
+
not the 20% ceiling a badge or a card stays under. */
|
|
514
606
|
--_fill-cap: 40%;
|
|
515
607
|
|
|
516
608
|
--switch-height: 1.25rem;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
@reference "tailwindcss";
|
|
2
2
|
@import "tailwindcss/utilities.css";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@source not "../../dev";
|
|
6
|
-
@source not "../../debug";
|
|
7
|
-
@source not "../../playground";
|
|
3
|
+
/* Everything that is not `src/` -- see the note in `index.css`. Keeps a class
|
|
4
|
+
name in a docs code fence from shipping as a real rule in `dist/components.css`. */
|
|
8
5
|
@import "../theme.css";
|
|
9
6
|
@import "../typography.css";
|
|
10
7
|
@import "../box.css";
|
|
@@ -15,4 +12,17 @@
|
|
|
15
12
|
@import "./surface.css";
|
|
16
13
|
@import "./tooltip.css";
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
/* The composition utilities -- `box`, `box-hover`, `box-active`, `loader-mask`,
|
|
16
|
+
`text-control`, `surface`, `input-group` -- are `@utility` rules whose bodies
|
|
17
|
+
the content scan will not emit on its own. `box` and friends appear only as
|
|
18
|
+
`@utility` blocks and `@apply` targets; `text-control`, `surface` and
|
|
19
|
+
`input-group` appear only inside `:is()`/`:where()` argument lists in
|
|
20
|
+
`intent.css` and `form.css`, never as a selector of their own. Their standalone
|
|
21
|
+
rules shipped only because docs prose happened to name them, and excluding
|
|
22
|
+
`docs/` from the scan removed that -- `.input-group` (and, with it, its focus
|
|
23
|
+
ring, its flush inner control, and `:has()`-propagated invalid state) went
|
|
24
|
+
silently dark, caught only by the browser suite. Safelisting all seven here
|
|
25
|
+
makes the emission deliberate; the composition-body check in
|
|
26
|
+
`exports.test.ts` fails the build if one is dropped. `text-control` and
|
|
27
|
+
`surface` are also documented as consumer-usable on their own. */
|
|
28
|
+
@source inline("box box-hover box-active loader-mask text-control surface input-group text-display text-title-lg text-title text-title-sm text-label-lg text-label text-body btn field label hint ipt email password url tel search date datetime month week time icon left right textarea select checkbox radio switch alert alert-icon badge skeleton progress tooltip tooltip-bubble tooltip-icon card panel interactive flush compact spacious neutral solid soft ghost edged edgeless flat raised floating loader");
|
|
@@ -28,19 +28,6 @@
|
|
|
28
28
|
.bars-wave,
|
|
29
29
|
.pulse-ring
|
|
30
30
|
)
|
|
31
|
-
),
|
|
32
|
-
.loading:not(
|
|
33
|
-
:where(
|
|
34
|
-
.dots-wave,
|
|
35
|
-
.dots-fade,
|
|
36
|
-
.dots-queue,
|
|
37
|
-
.dots-rotate,
|
|
38
|
-
.dots-grow,
|
|
39
|
-
.dots-grow-alternate,
|
|
40
|
-
.dot-bounce,
|
|
41
|
-
.bars-wave,
|
|
42
|
-
.pulse-ring
|
|
43
|
-
)
|
|
44
31
|
) {
|
|
45
32
|
--loader-art: url("data:image/svg+xml;charset=utf-8,<svg stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.spinner_V8m1{transform-origin:center;animation:spinner_zKoa 2s linear infinite}.spinner_V8m1 circle{stroke-linecap:round;animation:spinner_YpZS 1.5s ease-in-out infinite}@keyframes spinner_zKoa{100%{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%{stroke-dasharray:42 150;stroke-dashoffset:-16}95%,100%{stroke-dasharray:42 150;stroke-dashoffset:-59}}</style><g class='spinner_V8m1'><circle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'></circle></g></svg>");
|
|
46
33
|
}
|
|
@@ -87,15 +74,14 @@
|
|
|
87
74
|
a loader inside filled content matches the text around it and
|
|
88
75
|
`.loader.success` is green. There is nothing else to know. */
|
|
89
76
|
@utility loader {
|
|
90
|
-
@apply loader-mask inline-block size-
|
|
77
|
+
@apply loader-mask inline-block size-7 [&.lg]:size-9 [&.sm]:size-5;
|
|
91
78
|
|
|
92
79
|
color: var(--intent-color);
|
|
93
80
|
background-color: currentColor;
|
|
94
81
|
}
|
|
95
82
|
|
|
96
83
|
@media (prefers-reduced-motion: reduce) {
|
|
97
|
-
.loader
|
|
98
|
-
.loading {
|
|
84
|
+
.loader {
|
|
99
85
|
--loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='9.5' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-dasharray='42 18'/></svg>");
|
|
100
86
|
}
|
|
101
87
|
|
|
@@ -46,8 +46,13 @@
|
|
|
46
46
|
--_d-border: 100%;
|
|
47
47
|
--_d-elevation: 1;
|
|
48
48
|
|
|
49
|
-
/* `.p-sm`/`.p-lg` are the canonical names; `.compact`/`.spacious`
|
|
50
|
-
first-class aliases -- see the identical note in `button.css`. */
|
|
49
|
+
/* `.p-xs`/`.p-sm`/`.p-lg` are the canonical names; `.dense`/`.compact`/`.spacious`
|
|
50
|
+
are first-class aliases -- see the identical note in `button.css`. */
|
|
51
|
+
&.p-xs,
|
|
52
|
+
&.dense {
|
|
53
|
+
--surface-padding: 0.5rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
&.p-sm,
|
|
52
57
|
&.compact {
|
|
53
58
|
--surface-padding: 0.75rem;
|
|
@@ -62,12 +67,73 @@
|
|
|
62
67
|
--surface-padding: 0;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/* `.soft` rests untinted for no named intent, rather than mixing its 12% --
|
|
71
|
+
measured on a neutral card, 12% of `--color-text` onto any of
|
|
72
|
+
`--color-background`, `--color-foreground`, or `--color-surface` lands
|
|
73
|
+
within a few units of the others (a near-black ink dominates the result
|
|
74
|
+
regardless of which near-white ground it is mixed onto), which is why
|
|
75
|
+
swapping the ground token alone does not read as quieter. A resting
|
|
76
|
+
no-intent `.card.soft` is instead `--color-foreground` with no ink mixed
|
|
77
|
+
in at all -- a plain step up from the page that marks a different
|
|
78
|
+
container without drawing attention to it, the way a card that is not
|
|
79
|
+
currently interesting should.
|
|
80
|
+
|
|
81
|
+
A named intent keeps its ordinary 12% wash rather than being untinted
|
|
82
|
+
unconditionally: 12% of an actual hue does not read as a slab the way
|
|
83
|
+
12% of near-black ink does -- measured, `.card.soft.destructive` at 12%
|
|
84
|
+
is a gentle `249, 225, 225`, nothing like the `217, 217, 217` a neutral
|
|
85
|
+
surface mixing the same 12% of its own ink used to read as -- and
|
|
86
|
+
untinting it anyway would silence a `.card.soft.destructive` at rest
|
|
87
|
+
rather than merely quiet it, the same reason `.panel`'s own carve-out
|
|
88
|
+
(below) is scoped the same way. `:not(...)` names every intent class for
|
|
89
|
+
that reason; a version of this fix that untinted every intent shipped
|
|
90
|
+
first and was corrected once `.panel` needed the identical, narrower
|
|
91
|
+
carve-out to keep a resting `.panel.destructive` legible.
|
|
92
|
+
|
|
93
|
+
`--_fill-cap: 0%` (box.css's override slot, not `--ui-fill` directly --
|
|
94
|
+
see the comment there) composed with `box-hover`'s own formula
|
|
95
|
+
(`min(100%, resting + 14%)`) is what reveals the intent on hover without
|
|
96
|
+
a second fill declaration, for the no-intent case: resting at 0% means
|
|
97
|
+
hover lands at exactly the hover step, 14%, in `--intent-hover` as
|
|
98
|
+
always. The ground steps to `--color-surface` at the same moment,
|
|
99
|
+
through the public `--ui-surface-ground` escape hatch rather than the
|
|
100
|
+
private `--_d-ground`, so a consumer's own override still wins over both
|
|
101
|
+
states the way it always has. Scoped to `.interactive` and `.hoverable`,
|
|
102
|
+
the two classes that already opt a card into `box-hover` -- a plain
|
|
103
|
+
`.card.soft` with neither has no hover state to reveal into and stays at
|
|
104
|
+
the quiet, untinted rest. A named intent needs neither special case: it
|
|
105
|
+
already rests tinted, so the same generic `box-hover` formula deepens it
|
|
106
|
+
on hover (12% to 26%) with no ground shift required. */
|
|
107
|
+
&.soft:not(.primary, .secondary, .success, .warning, .destructive, .danger, .error, .info) {
|
|
108
|
+
--_fill-cap: 0%;
|
|
109
|
+
--ui-surface-ground: var(--color-foreground);
|
|
110
|
+
|
|
111
|
+
&.interactive:hover,
|
|
112
|
+
&.hoverable:hover {
|
|
113
|
+
--ui-surface-ground: var(--color-surface);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
65
117
|
/* Opt-in, because a card is only sometimes a control. Applications still owe
|
|
66
118
|
it a real interactive element and an accessible name. Hover and press are
|
|
67
119
|
the derived ones every other pressable box gets; there is nothing
|
|
68
|
-
card-specific about either.
|
|
69
|
-
|
|
70
|
-
|
|
120
|
+
card-specific about either.
|
|
121
|
+
|
|
122
|
+
`.interactive` is the whole opt-in and stays that way; `.hoverable` and
|
|
123
|
+
`.pressable` are its halves, for a card that answers one gesture and not
|
|
124
|
+
the other. A card with its own nested controls takes `.hoverable` for the
|
|
125
|
+
hover warmth without the press-scale that reads as "you activated the
|
|
126
|
+
card" when the click really landed on a nested button. `.hoverable` alone
|
|
127
|
+
does not set the pointer cursor: that travels with being a click target,
|
|
128
|
+
i.e. with `.pressable` / `.interactive`. */
|
|
129
|
+
&.interactive,
|
|
130
|
+
&.hoverable {
|
|
131
|
+
@apply box-hover;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
&.interactive,
|
|
135
|
+
&.pressable {
|
|
136
|
+
@apply box-active cursor-pointer;
|
|
71
137
|
}
|
|
72
138
|
}
|
|
73
139
|
|
|
@@ -82,6 +148,11 @@
|
|
|
82
148
|
--_d-border: 0%;
|
|
83
149
|
--_d-elevation: 0;
|
|
84
150
|
|
|
151
|
+
&.p-xs,
|
|
152
|
+
&.dense {
|
|
153
|
+
--surface-padding: 0.25rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
85
156
|
&.p-sm,
|
|
86
157
|
&.compact {
|
|
87
158
|
--surface-padding: 0.5rem;
|
|
@@ -95,4 +166,50 @@
|
|
|
95
166
|
&.flush {
|
|
96
167
|
--surface-padding: 0;
|
|
97
168
|
}
|
|
169
|
+
|
|
170
|
+
/* Neutral panels are the common case -- most are plain layout wells,
|
|
171
|
+
sidebars, and toolbars with no named intent -- and 12% of `--color-text`
|
|
172
|
+
(near-black) mixed onto the page is a far more assertive move than 12%
|
|
173
|
+
of an actual hue: measured, `217, 217, 217` against a `250, 250, 250`
|
|
174
|
+
page, the same "neutral ink reads louder" effect `.card.soft`'s fix
|
|
175
|
+
already answered, and the same reason `--intent-fill-max` stops
|
|
176
|
+
neutral's fill short everywhere else in the model.
|
|
177
|
+
|
|
178
|
+
Scoped to no named intent rather than untinted unconditionally the way
|
|
179
|
+
`data-table` is: a panel is not interactive, so there is no hover to
|
|
180
|
+
reveal a colored intent back into later, and it is edgeless by default,
|
|
181
|
+
so the fill is the only channel a `.primary` bulk-actions bar or a
|
|
182
|
+
`.destructive` danger-zone panel has to say what it is. Untinting those
|
|
183
|
+
unconditionally would silence them, not just quiet them -- measured, a
|
|
184
|
+
resting `.panel.destructive` is a gentle `249, 225, 225` warm wash,
|
|
185
|
+
nothing like the neutral slab above. `.card.soft` carries the identical
|
|
186
|
+
no-named-intent scoping now, corrected to match once this carve-out
|
|
187
|
+
shipped -- see the comment on `.card`'s own `&.soft`, above. `:not(...)`
|
|
188
|
+
names every intent class for that reason, plus `.solid`/`.ghost` so an
|
|
189
|
+
explicit presentation on the element is never clamped to a cap it did
|
|
190
|
+
not ask for -- the same guard `data-table`'s own `:not(.solid, .ghost)`
|
|
191
|
+
uses.
|
|
192
|
+
|
|
193
|
+
Grounds on `--color-foreground` through the public `--ui-surface-ground`
|
|
194
|
+
escape hatch, not the private `--_d-ground`, the same token `.card.soft`
|
|
195
|
+
steps to -- so a consumer's own override still wins, and a bare `.panel`
|
|
196
|
+
rests at exactly the plate a `.card.soft` does (measured: `245, 245, 245`
|
|
197
|
+
light, `23, 23, 23` dark, identical both ways). Permanent rather than
|
|
198
|
+
hover-revealed: neither a panel nor `data-table` has an interaction
|
|
199
|
+
state to reveal a tint into, so both rest quiet rather than reveal.
|
|
200
|
+
|
|
201
|
+
The one cost, inherited rather than new: an own declaration always beats
|
|
202
|
+
an inherited one, so this also beats `.glass` setting the same token on
|
|
203
|
+
an ancestor for its translucent ground -- a neutral panel under `.glass`
|
|
204
|
+
is quiet but opaque rather than blurred. `.card.soft` has carried this
|
|
205
|
+
exact trade-off since it shipped; nothing exercised it there because
|
|
206
|
+
`tests/browser/aesthetics.spec.ts`'s glass coverage tests a default
|
|
207
|
+
(ghost) card, never a soft one. Accepted rather than solved differently:
|
|
208
|
+
there is no way to ask "did an ancestor already set this token" in CSS,
|
|
209
|
+
only which declaration wins, and the quiet neutral rest is the more
|
|
210
|
+
common case a panel needs to get right. */
|
|
211
|
+
&:not(.primary, .secondary, .success, .warning, .destructive, .danger, .error, .info, .solid, .ghost) {
|
|
212
|
+
--_fill-cap: 0%;
|
|
213
|
+
--ui-surface-ground: var(--color-foreground);
|
|
214
|
+
}
|
|
98
215
|
}
|