@redseed/redseed-ui-tailwindcss 7.67.0 → 7.69.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/components/category_list.css +19 -5
- package/components/metric_card.css +58 -10
- package/components/modal.css +50 -4
- package/components/page_header.css +365 -10
- package/components/section_header.css +11 -0
- package/package.json +1 -1
- package/tokens.css +8 -1
|
@@ -30,16 +30,23 @@
|
|
|
30
30
|
|
|
31
31
|
/* A linked category has to look like a link. A badge with no pointer and no
|
|
32
32
|
hover reads as a label, which is what most categories are, so the ones that
|
|
33
|
-
genuinely filter a list get
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
genuinely filter a list get a pointer, a hover state on the pill, and the
|
|
34
|
+
underline on hover.
|
|
35
|
+
|
|
36
|
+
Underline on hover rather than at rest, which is where this started and was
|
|
37
|
+
wrong: it borrowed LinkSlot's always-on rule, but a pill is not a run of text
|
|
38
|
+
in a paragraph. Its own shape already separates it from what is around it, so
|
|
39
|
+
a resting underline reads as a mistake rather than as a link — the same
|
|
40
|
+
treatment a table cell link gets, where colour and weight carry it and the
|
|
41
|
+
underline is the hover affordance. */
|
|
36
42
|
.rsui-category-list__link {
|
|
37
43
|
@apply block rounded-full no-underline;
|
|
38
44
|
@apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-primary outline-hidden;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
.rsui-category-list__link .rsui-badge {
|
|
42
|
-
@apply cursor-pointer underline
|
|
48
|
+
@apply cursor-pointer no-underline transition-colors;
|
|
49
|
+
@apply hover:underline underline-offset-2;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
/* One grey step darker, not the usual `hover:bg-background-secondary`. That is the
|
|
@@ -56,8 +63,15 @@
|
|
|
56
63
|
|
|
57
64
|
Sibling to the list rather than inside it, so it wraps onto its own line at
|
|
58
65
|
phone widths — which is where the cap does most of its work. */
|
|
66
|
+
/* min-width, because the label changes length as it toggles: "+4 more" is 57px
|
|
67
|
+
and "Show fewer" is 80px, so without a floor the whole row shifted 23px
|
|
68
|
+
sideways under the pointer mid-press. Measured. The value is the wider of the
|
|
69
|
+
two states, so the button never grows and never shrinks; text-align keeps the
|
|
70
|
+
shorter label from drifting left inside it.
|
|
71
|
+
|
|
72
|
+
Same fix the page header's meta toggle already carries, for the same reason. */
|
|
59
73
|
.rsui-category-list__more {
|
|
60
|
-
@apply inline-flex w-fit items-center whitespace-nowrap;
|
|
74
|
+
@apply inline-flex w-fit min-w-20 justify-center items-center whitespace-nowrap;
|
|
61
75
|
@apply text-sm font-semibold no-underline cursor-pointer transition-colors;
|
|
62
76
|
@apply text-button-tertiary hover:text-button-tertiary-hover;
|
|
63
77
|
@apply hover:underline underline-offset-4 decoration-2;
|
|
@@ -167,7 +167,8 @@
|
|
|
167
167
|
on it — and the alternative is inventing a second readable ink per tone to
|
|
168
168
|
preserve a nicety. tone-light keeps its semantic colour, so it keeps the split. */
|
|
169
169
|
.rsui-metric-card--tone-green .rsui-metric-card__comparison-context,
|
|
170
|
-
.rsui-metric-card--tone-teal .rsui-metric-card__comparison-context
|
|
170
|
+
.rsui-metric-card--tone-teal .rsui-metric-card__comparison-context,
|
|
171
|
+
.rsui-metric-card--tone-bloom .rsui-metric-card__comparison-context {
|
|
171
172
|
color: var(--rsui-metric-card-ink-muted);
|
|
172
173
|
}
|
|
173
174
|
|
|
@@ -247,6 +248,43 @@
|
|
|
247
248
|
--rsui-metric-card-rule: rgba(255, 255, 255, 0.18);
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/* For a MetricCard sitting on PageHeader's bloom band, and the one tone whose surface is
|
|
252
|
+
NOT opaque.
|
|
253
|
+
|
|
254
|
+
This is not the header's own stats strip — that is PageHeaderStat, a 150px tile with a
|
|
255
|
+
serif figure, and the two are different objects. This tone is for the case where a full
|
|
256
|
+
MetricCard, with its comparison and trend, is the content sitting on a bloom surface.
|
|
257
|
+
|
|
258
|
+
The band it sits on runs Journey Green at one end to Warm Mauve at the other, so an
|
|
259
|
+
opaque tile is the wrong colour at one end of its own row whichever step it takes.
|
|
260
|
+
Warm Mauve 900 was tried first and reads as a mauve panel dropped on the green end.
|
|
261
|
+
A white scrim instead lets the band through, so each tile picks up the part of the
|
|
262
|
+
sweep it is standing on and the row reads as one thing.
|
|
263
|
+
|
|
264
|
+
THE COST, MEASURED, AND WHAT IT DECIDES. A scrim can only lighten, so the tile is
|
|
265
|
+
always lighter than the band beneath it, and the band's lightest point is already the
|
|
266
|
+
tightest place on it. Compositing 9% white over that point — rgb(133, 107, 129):
|
|
267
|
+
|
|
268
|
+
white 4.77:1 PASS
|
|
269
|
+
Grey 200 3.97:1 fails
|
|
270
|
+
Grey 300 3.34:1 fails
|
|
271
|
+
|
|
272
|
+
So this is the one tone with NO muted ink: --rsui-metric-card-ink-muted is white too,
|
|
273
|
+
and the step between a tile's number and its label is carried by size and weight, which
|
|
274
|
+
is what the design does anyway. Nothing darker than white clears AA through a scrim on
|
|
275
|
+
this band, so a muted colour here would be a quiet failure rather than a choice.
|
|
276
|
+
|
|
277
|
+
9% is the ceiling, not a taste value: 12% takes white itself to 4.46:1. A hoverable
|
|
278
|
+
tile is safe because Card's hover scrim is DARK — it moves the tile away from the
|
|
279
|
+
limit, not towards it. Raise this figure and white goes under; re-measure rather than
|
|
280
|
+
nudge it. */
|
|
281
|
+
.rsui-metric-card--tone-bloom {
|
|
282
|
+
--rsui-metric-card-surface: rgba(255, 255, 255, 0.09);
|
|
283
|
+
--rsui-metric-card-ink: var(--Colors-Base-white);
|
|
284
|
+
--rsui-metric-card-ink-muted: var(--Colors-Base-white);
|
|
285
|
+
--rsui-metric-card-rule: rgba(255, 255, 255, 0.18);
|
|
286
|
+
}
|
|
287
|
+
|
|
250
288
|
/* The white-label case: same layout, inverted ink. */
|
|
251
289
|
.rsui-metric-card--tone-light {
|
|
252
290
|
--rsui-metric-card-surface: var(--Colors-Modern-Bone-200);
|
|
@@ -273,17 +311,23 @@
|
|
|
273
311
|
color: var(--rsui-metric-card-ink-muted);
|
|
274
312
|
}
|
|
275
313
|
|
|
276
|
-
/* The semantic status inks are unreadable on the
|
|
314
|
+
/* The semantic status inks are unreadable on the DARK tiles, measured:
|
|
315
|
+
|
|
316
|
+
ink tone-green tone-teal tone-bloom
|
|
317
|
+
#0e5942 #0f373c (scrim)
|
|
318
|
+
--color-text-success #067647 1.46:1 2.26:1 1.20:1
|
|
319
|
+
--color-text-error #b42318 1.26:1 1.96:1 1.38:1
|
|
277
320
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
--color-text-error #b42318 1.26:1 1.96:1
|
|
321
|
+
tone-bloom is measured on the lightest tile the scrim can produce,
|
|
322
|
+
rgb(133, 107, 129) — the band's lightest point seen through 9% white.
|
|
281
323
|
|
|
282
|
-
|
|
324
|
+
All invisible — the same failure this file already documents for text-brand
|
|
283
325
|
on a toned tile. So the comparison gives up its colour there and takes the
|
|
284
326
|
tile's muted ink, exactly as the active state gives up its brand number, and
|
|
285
327
|
the arrow direction plus the consumer's word carry the meaning on their own.
|
|
286
|
-
Journey Green 200 measures 5.92:1 and Deep Teal 300 6.36:1, both clearing AA
|
|
328
|
+
Journey Green 200 measures 5.92:1 and Deep Teal 300 6.36:1, both clearing AA;
|
|
329
|
+
on tone-bloom the muted ink IS white, for the reason that tone documents, so
|
|
330
|
+
the comparison lands on white at 4.75:1 there.
|
|
287
331
|
|
|
288
332
|
tone-light is deliberately NOT in this list. Its surface is Modern Bone 200
|
|
289
333
|
(#f9f8f6), where success measures 5.36:1 and error 6.19:1 — both pass AA, so
|
|
@@ -291,21 +335,25 @@
|
|
|
291
335
|
|
|
292
336
|
(0,2,0), so it outranks the (0,1,0) sentiment modifiers wherever they sit. */
|
|
293
337
|
.rsui-metric-card--tone-green .rsui-metric-card__comparison,
|
|
294
|
-
.rsui-metric-card--tone-teal .rsui-metric-card__comparison
|
|
338
|
+
.rsui-metric-card--tone-teal .rsui-metric-card__comparison,
|
|
339
|
+
.rsui-metric-card--tone-bloom .rsui-metric-card__comparison {
|
|
295
340
|
color: var(--rsui-metric-card-ink-muted);
|
|
296
341
|
}
|
|
297
342
|
|
|
298
343
|
/* The solid badge goes with it. Filling it with the error ink on a dark tile would
|
|
299
344
|
reproduce exactly the 1.26:1 the rule above exists to avoid, so it takes the
|
|
300
345
|
tile's muted ink and puts the arrow in the tile's own surface colour — the
|
|
301
|
-
same 5.92:1
|
|
346
|
+
same 5.92:1 / 6.36:1 pairing, read the other way round. On tone-bloom the
|
|
347
|
+
surface is a scrim rather than a colour, so the knocked-out arrow shows the
|
|
348
|
+
band through it, which is the same effect one step further out.
|
|
302
349
|
|
|
303
350
|
The ringed badge needs no rule of its own: its border is `currentColor`, and the
|
|
304
351
|
rule above has already moved the comparison's ink to the tile's muted value, so
|
|
305
352
|
the ring follows it. This is the tone where solid-vs-ringed does its real work,
|
|
306
353
|
because once the colour is stripped it is the only sentiment signal left. */
|
|
307
354
|
.rsui-metric-card--tone-green .rsui-metric-card__comparison--negative .rsui-metric-card__comparison-icon,
|
|
308
|
-
.rsui-metric-card--tone-teal .rsui-metric-card__comparison--negative .rsui-metric-card__comparison-icon
|
|
355
|
+
.rsui-metric-card--tone-teal .rsui-metric-card__comparison--negative .rsui-metric-card__comparison-icon,
|
|
356
|
+
.rsui-metric-card--tone-bloom .rsui-metric-card__comparison--negative .rsui-metric-card__comparison-icon {
|
|
309
357
|
background-color: var(--rsui-metric-card-ink-muted);
|
|
310
358
|
color: var(--rsui-metric-card-surface);
|
|
311
359
|
}
|
package/components/modal.css
CHANGED
|
@@ -61,8 +61,29 @@
|
|
|
61
61
|
@apply sticky top-0 z-1 p-0 bg-background-secondary shrink-0 text-lg font-semibold;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/* ---------------------------------------------------------------------------
|
|
65
|
+
Padding, and why it is written as three shorthands rather than @apply p-*
|
|
66
|
+
|
|
67
|
+
Each region used to pay for all four of its own sides — `p-space-md` on all
|
|
68
|
+
three — so the 16px below the header met the 16px above the body: 32px
|
|
69
|
+
between a dialog's title and its first line, 32px again before the buttons,
|
|
70
|
+
against 16px at the edges. The seams ended up twice the margin, which is
|
|
71
|
+
what you get when neighbours both pay for the gap between them.
|
|
72
|
+
|
|
73
|
+
So the two INNER edges are zeroed and the body carries them. Each region
|
|
74
|
+
still behaves when one of the others is absent, which matters because all
|
|
75
|
+
three modifiers are independent props a consumer can turn off.
|
|
76
|
+
|
|
77
|
+
The values are Material's dialog: 24 at every edge, 16 from the headline to
|
|
78
|
+
the supporting text, 24 from the supporting text to the buttons.
|
|
79
|
+
|
|
80
|
+
Longhand rather than Tailwind's `p-*` utilities on purpose — these are
|
|
81
|
+
three-value shorthands with a different value per edge, and `@apply
|
|
82
|
+
pt-space-lg px-space-lg pb-0` is four utilities to say what one declaration
|
|
83
|
+
says, with the seam buried in the last one.
|
|
84
|
+
--------------------------------------------------------------------------- */
|
|
64
85
|
.rsui-modal__header--padded {
|
|
65
|
-
|
|
86
|
+
padding: 1.5rem 1.5rem 0;
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
.rsui-modal__body {
|
|
@@ -71,8 +92,11 @@
|
|
|
71
92
|
scrollbar-width: none;
|
|
72
93
|
}
|
|
73
94
|
|
|
95
|
+
/* Carries both seams — 16px up to the header, 24px down to the footer — as well
|
|
96
|
+
as its own side edges. With no footer the 24px is simply the bottom of the
|
|
97
|
+
dialog, which is the same number, so nothing special is needed for that case. */
|
|
74
98
|
.rsui-modal__body--padded {
|
|
75
|
-
|
|
99
|
+
padding: 1rem 1.5rem 1.5rem;
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
.rsui-modal__body::-webkit-scrollbar {
|
|
@@ -85,9 +109,31 @@
|
|
|
85
109
|
padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
|
|
86
110
|
}
|
|
87
111
|
|
|
112
|
+
/* The footer pays for ALL FOUR of its own sides, unlike the header above.
|
|
113
|
+
|
|
114
|
+
That asymmetry is deliberate and it is about the banding. The header and body
|
|
115
|
+
share no visible boundary in the places it matters, so zeroing the header's
|
|
116
|
+
bottom edge and letting the body carry it reads as one gap. The footer does
|
|
117
|
+
not: it has its own background (background-secondary against the body's
|
|
118
|
+
primary), so the boundary is a visible edge and the buttons sit INSIDE a band
|
|
119
|
+
of their own. Borrowing the space above them from the body left them jammed
|
|
120
|
+
against the top of that band with 24px below — measured at 0 above, 24 below.
|
|
121
|
+
|
|
122
|
+
So the seam here is the body's 24px bottom plus the footer's 24px top. That is
|
|
123
|
+
more than the 24px between the title and the body, which is the right way
|
|
124
|
+
round: a button row is a different kind of thing from a paragraph, and the
|
|
125
|
+
band is already announcing that.
|
|
126
|
+
|
|
127
|
+
If the banding ever goes (the other half of #373), this should drop back to
|
|
128
|
+
`0 1.5rem 1.5rem` and let the body carry the seam, the way the header does.
|
|
129
|
+
|
|
130
|
+
The bottom keeps its safe-area floor — a footer is the last thing above the
|
|
131
|
+
home indicator on a phone, so the value is a minimum rather than a fixed
|
|
132
|
+
amount, and it rises with the padding (1rem -> 1.5rem) or a footer would sit
|
|
133
|
+
closer to the bottom edge than to its own buttons. */
|
|
88
134
|
.rsui-modal__footer--padded {
|
|
89
|
-
|
|
90
|
-
padding-bottom: max(
|
|
135
|
+
padding: 1.5rem;
|
|
136
|
+
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
|
91
137
|
}
|
|
92
138
|
|
|
93
139
|
.rsui-modal__footer--start {
|
|
@@ -62,6 +62,27 @@
|
|
|
62
62
|
@apply lg:text-[2.5rem] lg:leading-[1.15] lg:tracking-[-0.01em] lg:font-black;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/* An eyebrow above the h1, matching card_header.css's: xs, semibold, uppercase, wide
|
|
66
|
+
tracking, in the secondary ink so it labels the title rather than competing with it.
|
|
67
|
+
The two are deliberately the same recipe — an eyebrow means the same thing on a page
|
|
68
|
+
as it does on a card, and a second type style for it would be a second answer to one
|
|
69
|
+
question.
|
|
70
|
+
|
|
71
|
+
8px down to the title, the brand's smallest step, carried as a margin on this element
|
|
72
|
+
rather than as a gap on the parent so an absent eyebrow contributes nothing. Same
|
|
73
|
+
reason card_header.css gives. */
|
|
74
|
+
.rsui-page-header__eyebrow {
|
|
75
|
+
@apply flex items-center gap-x-space-xs mb-space-xs;
|
|
76
|
+
@apply text-xs font-semibold uppercase tracking-wider text-text-secondary;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* A badge brings its own case, size and tracking, so it must not inherit the uppercase
|
|
80
|
+
treatment meant for plain text. Badge sets no text-transform of its own, so without
|
|
81
|
+
this the uppercase leaks in. */
|
|
82
|
+
.rsui-page-header__eyebrow .rsui-badge {
|
|
83
|
+
@apply normal-case tracking-normal;
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
.rsui-page-header__title-text-container {
|
|
66
87
|
@apply flex flex-row gap-x-space-lg items-center;
|
|
67
88
|
}
|
|
@@ -75,6 +96,110 @@
|
|
|
75
96
|
@apply text-base leading-normal text-text-secondary;
|
|
76
97
|
}
|
|
77
98
|
|
|
99
|
+
/* ── The stats row ────────────────────────────────────────────────────────────
|
|
100
|
+
Ported from the /home/v2 twin rather than re-derived, because that band is the
|
|
101
|
+
thing this is meant to reproduce and it had already been worked out at every
|
|
102
|
+
width. Every number below is its number.
|
|
103
|
+
|
|
104
|
+
A wrapping row rather than a scrolling one: these are a handful of counts, and
|
|
105
|
+
a count you cannot see is not a summary.
|
|
106
|
+
|
|
107
|
+
Full width below lg, where __top is a column and the tiles sit under the title;
|
|
108
|
+
auto from lg up, where __top is a row and they sit beside it.
|
|
109
|
+
--------------------------------------------------------------------------- */
|
|
110
|
+
.rsui-page-header__stats {
|
|
111
|
+
/* stretch, not start: a two-line label must not leave its tile shorter than
|
|
112
|
+
the one beside it. */
|
|
113
|
+
@apply w-full flex items-stretch gap-[10px];
|
|
114
|
+
@apply lg:w-auto lg:shrink-0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* The band carries four things in one row at desktop width and needs two rows
|
|
118
|
+
before that. The twin wrapped at 1280 rather than at 1024 because the three
|
|
119
|
+
counts plus a trailing action need about 600px, which squeezes the title hard
|
|
120
|
+
long before the layout actually breaks. */
|
|
121
|
+
.rsui-page-header__top:has(.rsui-page-header__stats) {
|
|
122
|
+
@apply flex-wrap gap-[18px] lg:gap-[20px] xl:gap-[40px];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* The title takes the rest of the row and the tiles keep their size, rather than
|
|
126
|
+
the two sharing it. min-width: 0 so a long title wraps instead of forcing the
|
|
127
|
+
tiles out of the row. */
|
|
128
|
+
.rsui-page-header__top:has(.rsui-page-header__stats) .rsui-page-header__title {
|
|
129
|
+
@apply lg:grow lg:min-w-0 lg:basis-full xl:basis-auto;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* One tile. 150px is the twin's width, at which its three labels sit on one line.
|
|
133
|
+
|
|
134
|
+
The leading edge is reserved whether or not it is painted, so a tile carrying a
|
|
135
|
+
rail and one without still line up — the mistake worth avoiding here is adding
|
|
136
|
+
the border only on the accented tile and knocking that tile 3px out of the row. */
|
|
137
|
+
.rsui-page-header__stat {
|
|
138
|
+
@apply flex flex-col items-start gap-px;
|
|
139
|
+
@apply w-full px-[16px] py-[12px] text-left;
|
|
140
|
+
@apply border-0 border-s-[3px] border-solid border-s-transparent rounded-[10px];
|
|
141
|
+
@apply lg:w-[150px];
|
|
142
|
+
|
|
143
|
+
background: var(--rsui-page-header-tile, var(--color-background-secondary));
|
|
144
|
+
border-inline-start-color: var(--rsui-page-header-stat-accent, transparent);
|
|
145
|
+
font: inherit;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Below lg the row is full width, so the tiles share it rather than sitting at a
|
|
149
|
+
fixed size with the rest of the row empty beside them. */
|
|
150
|
+
.rsui-page-header__stats > .rsui-page-header__stat {
|
|
151
|
+
@apply flex-1 lg:flex-none;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Only a tile that DOES something gets the pointer and the hover. A stat that
|
|
155
|
+
only reports a number is not a control, and PageHeaderStat renders it as a div
|
|
156
|
+
precisely so it does not pretend to be one. */
|
|
157
|
+
.rsui-page-header__stat--interactive {
|
|
158
|
+
@apply cursor-pointer no-underline transition-colors duration-150;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Hover DARKENS on the dark tones rather than lightening, which is the opposite of the
|
|
162
|
+
twin and deliberate. Lifting a scrim from 9% to 16% takes white itself to 4.10:1 on the
|
|
163
|
+
band's lightest point — the hover state would be the one that fails. Darkening moves
|
|
164
|
+
every ink on the tile away from its limit instead, and still reads as a state change.
|
|
165
|
+
tone-light darkens too, through its own solid step. */
|
|
166
|
+
.rsui-page-header__stat--interactive:hover {
|
|
167
|
+
background: var(--rsui-page-header-tile-hover, var(--color-background-secondary-contrast));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* The figure is the one place the serif is right at this size: it is a number
|
|
171
|
+
being read as a headline, which is the range the brand keeps Newsreader for.
|
|
172
|
+
tabular-nums so a row of counts does not shift as the numbers change. */
|
|
173
|
+
.rsui-page-header__stat-value {
|
|
174
|
+
@apply font-redseed-heading text-[2.875rem] font-semibold leading-none tabular-nums;
|
|
175
|
+
|
|
176
|
+
color: var(--rsui-page-header-ink, var(--color-text-primary));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* The label takes the tone's INK rather than its muted ink, and the step down to it is
|
|
180
|
+
carried by size and weight — 46px semibold against 12px medium is already a long way.
|
|
181
|
+
|
|
182
|
+
That is a contrast result, not a preference. A tile is a scrim, so it is always lighter
|
|
183
|
+
than the band under it, and a muted ink measured against the BAND does not survive the
|
|
184
|
+
tile. Composited over each tone's own worst ground:
|
|
185
|
+
|
|
186
|
+
tone tile ground its muted ink white
|
|
187
|
+
green rgb(32, 92, 74) JourneyGreen300 4.18 7.79
|
|
188
|
+
teal rgb(34, 67, 71) DeepTeal300 5.32 10.75
|
|
189
|
+
bloom rgb(133, 107, 129) Grey 200 3.97 4.77
|
|
190
|
+
|
|
191
|
+
Two of the three fail, and bloom's is the band's lightest point — reachable, because a
|
|
192
|
+
header with no actions lets the title grow and pushes the tiles to the right edge, which
|
|
193
|
+
is exactly where that point is. In situ at 1440 with an action present the same tile
|
|
194
|
+
measures 6.83:1, but a library component does not get to assume the layout it is given.
|
|
195
|
+
|
|
196
|
+
tone-light resolves this to Grey 900 through the same rule, at 16.08:1 on its tile. */
|
|
197
|
+
.rsui-page-header__stat-label {
|
|
198
|
+
@apply text-xs font-medium leading-[1.3];
|
|
199
|
+
|
|
200
|
+
color: var(--rsui-page-header-ink, var(--color-text-primary));
|
|
201
|
+
}
|
|
202
|
+
|
|
78
203
|
.rsui-page-header__status-actions {
|
|
79
204
|
@apply flex-1 flex gap-x-space-xs items-center justify-between lg:pr-space-xs;
|
|
80
205
|
}
|
|
@@ -83,26 +208,107 @@
|
|
|
83
208
|
@apply flex-1 flex flex-wrap gap-space-xs items-center justify-end;
|
|
84
209
|
}
|
|
85
210
|
|
|
86
|
-
.rsui-page-header__meta-action {
|
|
87
|
-
@apply flex lg:hidden;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
211
|
.rsui-page-header__search {
|
|
91
212
|
@apply w-full pt-space-xs lg:pt-0 lg:w-auto;
|
|
92
213
|
}
|
|
93
214
|
|
|
94
|
-
|
|
95
|
-
|
|
215
|
+
/* ---------------------------------------------------------------------------
|
|
216
|
+
Meta
|
|
217
|
+
|
|
218
|
+
Visible at every width. It was `hidden lg:flex`, so the facts a header exists
|
|
219
|
+
to carry vanished below 1024px and a modal reproduced them — on a phone, where
|
|
220
|
+
a reader has least context to spare and least patience for a dialog.
|
|
221
|
+
|
|
222
|
+
A grid, not a flex row, and content-width rather than full-bleed: meta reads
|
|
223
|
+
as one cluster beside the title rather than a band across the page. Four
|
|
224
|
+
columns from sm up; two on a phone, because four 16px labels in 375px is not a
|
|
225
|
+
grid, it is a queue.
|
|
226
|
+
|
|
227
|
+
The cap is `:nth-child` rather than a count in JS. `#meta` is a slot and every
|
|
228
|
+
consumer fills it with a `v-for`, so the component cannot know how many facts
|
|
229
|
+
there are — but CSS does not need to know. Past the fourth, they are hidden
|
|
230
|
+
until the toggle expands the row.
|
|
231
|
+
--------------------------------------------------------------------------- */
|
|
232
|
+
/* The region is the grid. The facts sit inside a `display: contents` wrapper so
|
|
233
|
+
they become direct grid items without the toggle joining their `:nth-child`
|
|
234
|
+
sequence — see the note in PageHeader.vue. */
|
|
235
|
+
.rsui-page-header__meta-region {
|
|
236
|
+
@apply grid w-fit max-w-full items-start;
|
|
237
|
+
@apply gap-x-space-lg gap-y-space-md;
|
|
238
|
+
|
|
239
|
+
/* Columns sized to content, and the SAME count in both states.
|
|
240
|
+
|
|
241
|
+
An earlier version went five columns collapsed and four expanded, so the
|
|
242
|
+
toggle would sit in the row rather than wrap. That moved every fact
|
|
243
|
+
sideways when pressed — 13.7px with `1fr`, up to 40px with `max-content`,
|
|
244
|
+
because both make a column's width depend on something the toggle changes.
|
|
245
|
+
Holding the count fixed removes the question: the toggle occupies the
|
|
246
|
+
fifth cell while collapsed, and the fifth fact takes it once expanded.
|
|
247
|
+
|
|
248
|
+
The width has to be FIXED, not `max-content`. Holding the count steady is
|
|
249
|
+
not enough on its own: `max-content` sizes each column to whatever lands in
|
|
250
|
+
it, and expanding puts different facts in different columns, so they still
|
|
251
|
+
moved — 27px, measured. A fixed track depends on neither the count nor the
|
|
252
|
+
content.
|
|
253
|
+
|
|
254
|
+
6rem, against a widest-ink measurement of 90px across every label and value
|
|
255
|
+
in the busiest real header. Anything longer wraps inside its own cell
|
|
256
|
+
rather than pushing its neighbours. */
|
|
257
|
+
grid-template-columns: repeat(2, 6rem);
|
|
96
258
|
}
|
|
97
259
|
|
|
98
|
-
|
|
99
|
-
|
|
260
|
+
@media (min-width: 40rem) {
|
|
261
|
+
.rsui-page-header__meta-region {
|
|
262
|
+
grid-template-columns: repeat(5, 6rem);
|
|
263
|
+
}
|
|
100
264
|
}
|
|
101
265
|
|
|
102
266
|
.rsui-page-header__meta {
|
|
103
|
-
|
|
267
|
+
display: contents;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Past the cap, hidden until expanded. `> *` rather than a component class so it
|
|
271
|
+
does not care what a consumer puts in the slot — MetaInfo today, something
|
|
272
|
+
else tomorrow. The toggle is excluded by its own rule below, which comes
|
|
273
|
+
later and wins. */
|
|
274
|
+
.rsui-page-header__meta > *:nth-child(n + 5) {
|
|
275
|
+
@apply hidden;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.rsui-page-header__meta--expanded > *:nth-child(n + 5) {
|
|
279
|
+
@apply block;
|
|
104
280
|
}
|
|
105
281
|
|
|
282
|
+
/* The toggle sits where the fifth fact would be, so pressing it expands the row
|
|
283
|
+
around it rather than moving it. Always shown, whatever the cap is doing. */
|
|
284
|
+
.rsui-page-header__meta-toggle {
|
|
285
|
+
/* Centred against the fact beside it, not `self-start`. A fact is a two-line
|
|
286
|
+
unit — a 16px label over a 16px value, 36px in all — so a 20px toggle
|
|
287
|
+
pinned to the top reads as belonging to the LABELS rather than to the pair.
|
|
288
|
+
Centre also survives the expanded state, where the toggle wraps to its own
|
|
289
|
+
row and has no value to sit level with. */
|
|
290
|
+
@apply inline-flex w-fit items-center whitespace-nowrap self-center;
|
|
291
|
+
@apply text-sm font-semibold no-underline cursor-pointer transition-colors;
|
|
292
|
+
@apply text-button-tertiary hover:text-button-tertiary-hover;
|
|
293
|
+
@apply hover:underline underline-offset-4 decoration-2;
|
|
294
|
+
@apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-button-tertiary outline-hidden;
|
|
295
|
+
|
|
296
|
+
/* min-width, because the label changes length as it toggles — "+2 more"
|
|
297
|
+
against "Show fewer" — and without a floor the row shifts sideways under
|
|
298
|
+
the pointer mid-press. Same fix CategoryList carries (#355). */
|
|
299
|
+
@apply min-w-20 justify-center;
|
|
300
|
+
|
|
301
|
+
/* The negative margin pulls it back from the grid's 32px column gap to the
|
|
302
|
+
16px CategoryList uses. 32px is right between two FACTS, which read as
|
|
303
|
+
separate label/value pairs; it is too far for an affordance trailing them,
|
|
304
|
+
and the two rows sit one above the other in the same header — `+4 more`
|
|
305
|
+
under categories and `+3 more` under meta must not be spaced differently. */
|
|
306
|
+
@apply -ms-space-md;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
106
312
|
.rsui-page-header__avatar {
|
|
107
313
|
@apply shrink-0 size-12;
|
|
108
314
|
}
|
|
@@ -151,11 +357,36 @@
|
|
|
151
357
|
@apply p-space-lg;
|
|
152
358
|
}
|
|
153
359
|
|
|
360
|
+
/* The band reaches the viewport edges; its text does not. calc(50% - 50vw) pulls each
|
|
361
|
+
side out to the edge and the matching padding puts the content back on the container's
|
|
362
|
+
inner edge, so the greeting lines up with the logo and nav above it.
|
|
363
|
+
|
|
364
|
+
Measured in JS first, which is the wrong answer: a measured value goes stale whenever
|
|
365
|
+
the element resizes without the trigger you chose firing, and the band silently insets
|
|
366
|
+
itself again. CSS cannot go stale.
|
|
367
|
+
|
|
368
|
+
The vertical padding is the toned header's own and is not touched here, so this is
|
|
369
|
+
purely a horizontal escape. See the prop for the `overflow-x: clip` the document has to
|
|
370
|
+
carry. */
|
|
371
|
+
.rsui-page-header--bleed {
|
|
372
|
+
/* width: auto is load-bearing. The base rule sets w-full, and a box with an explicit
|
|
373
|
+
width does not grow when its margins go negative — it just shifts. With w-full held
|
|
374
|
+
the band moved left to the viewport edge and stopped short on the right by exactly
|
|
375
|
+
the amount it had moved, which looks like a one-sided bug and is not one. */
|
|
376
|
+
@apply w-auto;
|
|
377
|
+
|
|
378
|
+
margin-inline: calc(50% - 50vw);
|
|
379
|
+
padding-inline: calc(50vw - 50%);
|
|
380
|
+
}
|
|
381
|
+
|
|
154
382
|
.rsui-page-header--tone-green {
|
|
155
383
|
--rsui-page-header-bg: var(--Colors-Journey-Green-800);
|
|
156
384
|
--rsui-page-header-ink: var(--Colors-Base-white);
|
|
157
385
|
--rsui-page-header-ink-muted: var(--Colors-Journey-Green-300);
|
|
158
386
|
--rsui-page-header-rule: rgba(255, 255, 255, 0.18);
|
|
387
|
+
--rsui-page-header-accent: var(--Colors-Warm-Yellow-400);
|
|
388
|
+
--rsui-page-header-tile: rgba(255, 255, 255, 0.09);
|
|
389
|
+
--rsui-page-header-tile-hover: rgba(0, 0, 0, 0.12);
|
|
159
390
|
}
|
|
160
391
|
|
|
161
392
|
.rsui-page-header--tone-teal {
|
|
@@ -163,6 +394,60 @@
|
|
|
163
394
|
--rsui-page-header-ink: var(--Colors-Base-white);
|
|
164
395
|
--rsui-page-header-ink-muted: var(--Colors-Deep-Teal-300);
|
|
165
396
|
--rsui-page-header-rule: rgba(255, 255, 255, 0.18);
|
|
397
|
+
--rsui-page-header-accent: var(--Colors-Warm-Yellow-400);
|
|
398
|
+
--rsui-page-header-tile: rgba(255, 255, 255, 0.09);
|
|
399
|
+
--rsui-page-header-tile-hover: rgba(0, 0, 0, 0.12);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* The brand's hero treatment: three soft radial washes over a green-to-mauve
|
|
403
|
+
sweep, so the band warms across its width instead of running dark green end to
|
|
404
|
+
end. Every colour is a primary identity ramp — Journey Green and Deep Teal
|
|
405
|
+
carry the base, Warm Yellow / Soft Lilac / Warm Mauve are the washes. No
|
|
406
|
+
secondary accents.
|
|
407
|
+
|
|
408
|
+
A TONE rather than a boolean layered over green or teal. The alphas below are
|
|
409
|
+
a contrast budget measured against THIS base sweep; put the same washes over a
|
|
410
|
+
different background and the measurement is void, so there is nothing to
|
|
411
|
+
compose with.
|
|
412
|
+
|
|
413
|
+
THE ALPHAS ARE MEASURED, NOT CHOSEN. All four layers were composited on a
|
|
414
|
+
200x200 grid across eight band sizes from 1920x150 down to 375x320, and the
|
|
415
|
+
lightest point anywhere in the band taken as the worst case — the right test
|
|
416
|
+
for a library component, where a consumer may put text anywhere along the
|
|
417
|
+
band, rather than the worst point under one particular text box.
|
|
418
|
+
|
|
419
|
+
The lightest point is the Soft Lilac centre at 96% 30%, where the washes
|
|
420
|
+
combine to alpha 0.320 and the surface resolves to rgb(121, 92, 117).
|
|
421
|
+
Against it:
|
|
422
|
+
|
|
423
|
+
Base white 5.82:1 PASS
|
|
424
|
+
Grey 200 4.84:1 PASS <- the muted ink below
|
|
425
|
+
Deep Teal 200 4.00:1 large text only
|
|
426
|
+
Journey Green 300 3.12:1 large text only
|
|
427
|
+
|
|
428
|
+
Which is why the muted ink is NOT Journey Green 300, the muted step the green
|
|
429
|
+
tone uses: it fails AA at the lightest point of this band. Move a centre or
|
|
430
|
+
raise an alpha and this has to be measured again; do not eyeball it. */
|
|
431
|
+
.rsui-page-header--tone-bloom {
|
|
432
|
+
--rsui-page-header-bg:
|
|
433
|
+
/* Warm Yellow 500 — above the band, left of centre where the base is greenest */
|
|
434
|
+
radial-gradient(50% 130% at 44% -20%, rgba(238, 220, 91, 0.34) 0%, rgba(238, 220, 91, 0) 62%),
|
|
435
|
+
/* Soft Lilac 500 — the right edge, the pink */
|
|
436
|
+
radial-gradient(55% 130% at 96% 30%, rgba(233, 184, 222, 0.32) 0%, rgba(233, 184, 222, 0) 62%),
|
|
437
|
+
/* Warm Mauve 500 — below the band, deepening the purple end */
|
|
438
|
+
radial-gradient(70% 125% at 88% 112%, rgba(185, 125, 168, 0.38) 0%, rgba(185, 125, 168, 0) 65%),
|
|
439
|
+
/* base sweep: Journey Green 800 -> Deep Teal 700 -> Warm Mauve 950 */
|
|
440
|
+
linear-gradient(115deg, var(--Colors-Journey-Green-800) 0%, var(--Colors-Deep-Teal-700) 42%, var(--Colors-Warm-Mauve-950) 100%);
|
|
441
|
+
--rsui-page-header-ink: var(--Colors-Base-white);
|
|
442
|
+
--rsui-page-header-ink-muted: var(--Colors-Grey-200);
|
|
443
|
+
--rsui-page-header-rule: rgba(255, 255, 255, 0.18);
|
|
444
|
+
/* Warm Yellow 400, the twin's accent: 5.37:1 on this band. */
|
|
445
|
+
--rsui-page-header-accent: var(--Colors-Warm-Yellow-400);
|
|
446
|
+
/* A scrim rather than a colour, so each tile picks up the part of the sweep
|
|
447
|
+
it is standing on and a row of them reads as one thing rather than as
|
|
448
|
+
panels dropped on a band. */
|
|
449
|
+
--rsui-page-header-tile: rgba(255, 255, 255, 0.09);
|
|
450
|
+
--rsui-page-header-tile-hover: rgba(0, 0, 0, 0.12);
|
|
166
451
|
}
|
|
167
452
|
|
|
168
453
|
/* The white-label case. Same layout, inverted ink — a client's own background
|
|
@@ -172,6 +457,10 @@
|
|
|
172
457
|
--rsui-page-header-ink: var(--Colors-Grey-900);
|
|
173
458
|
--rsui-page-header-ink-muted: var(--Colors-Grey-600);
|
|
174
459
|
--rsui-page-header-rule: var(--Colors-Grey-300);
|
|
460
|
+
/* A scrim is invisible on a light band, so this tone names solid steps. */
|
|
461
|
+
--rsui-page-header-accent: var(--Colors-Journey-Green-700);
|
|
462
|
+
--rsui-page-header-tile: var(--Colors-Modern-Bone-200);
|
|
463
|
+
--rsui-page-header-tile-hover: var(--Colors-Grey-200);
|
|
175
464
|
}
|
|
176
465
|
|
|
177
466
|
/* Ink application is tone-agnostic: only the variable values differ, so the
|
|
@@ -184,7 +473,57 @@
|
|
|
184
473
|
color: var(--rsui-page-header-ink-muted);
|
|
185
474
|
}
|
|
186
475
|
|
|
187
|
-
|
|
476
|
+
/* The eyebrow takes the muted ink for the same reason the subtitle does: it is
|
|
477
|
+
the quieter of the two lines around the title, and text-text-secondary is a
|
|
478
|
+
dark grey that disappears on every tone but light. */
|
|
479
|
+
.rsui-page-header--toned .rsui-page-header__eyebrow {
|
|
480
|
+
color: var(--rsui-page-header-ink-muted);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* ── The band's type ──────────────────────────────────────────────────────────
|
|
484
|
+
Ported from the /home/v2 twin, which is the design this tone reproduces.
|
|
485
|
+
|
|
486
|
+
A DELIBERATE DEPARTURE from the h1 rule at the top of this file, and worth a
|
|
487
|
+
reviewer's eye rather than a quiet landing. That rule drops Newsreader for the
|
|
488
|
+
sans steps, citing the book's restriction of Newsreader to Header 1 and its
|
|
489
|
+
own sizes — and it is right for a page title. This is not a page title; it is
|
|
490
|
+
a hero band, the same surface the marketing site sets in Newsreader, and it
|
|
491
|
+
carries a greeting rather than the name of a record. The departure is scoped
|
|
492
|
+
to this one tone, so every other header keeps the sans title.
|
|
493
|
+
|
|
494
|
+
27px below lg, 36px from lg up — the twin's two sizes.
|
|
495
|
+
--------------------------------------------------------------------------- */
|
|
496
|
+
.rsui-page-header--tone-bloom .rsui-page-header__title h1 {
|
|
497
|
+
@apply font-redseed-heading text-[1.6875rem] font-semibold;
|
|
498
|
+
@apply leading-[1.1] tracking-[-0.01em];
|
|
499
|
+
@apply lg:text-[2.25rem] lg:leading-[1.1] lg:font-semibold;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* The eyebrow sits tight to the greeting — 3px, not the 8px a card eyebrow takes
|
|
503
|
+
— because the two read as one block of address here rather than as a label and
|
|
504
|
+
the thing it labels. */
|
|
505
|
+
.rsui-page-header--tone-bloom .rsui-page-header__eyebrow {
|
|
506
|
+
@apply mb-[3px] tracking-[0.06em];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* And the line under the title sits away from it, at 7px and one step down in
|
|
510
|
+
size: it is a second sentence, not a subtitle wrapping the first. */
|
|
511
|
+
.rsui-page-header--tone-bloom .rsui-page-header__subtitle {
|
|
512
|
+
@apply mt-[7px] text-sm;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* The one word in that line that is the point of it. Available on every tone,
|
|
516
|
+
because a band with a highlight in it is not specific to this one. */
|
|
517
|
+
.rsui-page-header--toned .rsui-page-header__subtitle strong {
|
|
518
|
+
@apply font-semibold;
|
|
519
|
+
|
|
520
|
+
color: var(--rsui-page-header-accent, var(--rsui-page-header-ink));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* On the REGION, not on `__meta`. The facts element is `display: contents` so it
|
|
524
|
+
generates no box at all — a border declared there paints nothing, and this
|
|
525
|
+
separator silently disappeared when the grid moved up a level. Measured. */
|
|
526
|
+
.rsui-page-header--toned .rsui-page-header__meta-region {
|
|
188
527
|
border-top: 1px solid var(--rsui-page-header-rule);
|
|
189
528
|
|
|
190
529
|
@apply mt-space-md pt-space-md;
|
|
@@ -229,3 +568,19 @@
|
|
|
229
568
|
.rsui-page-header--toned .rsui-category-list__more {
|
|
230
569
|
color: var(--rsui-page-header-ink);
|
|
231
570
|
}
|
|
571
|
+
|
|
572
|
+
/* The meta toggle borrows the same tertiary green and disappears on the same dark
|
|
573
|
+
band — measured as near-invisible on Journey Green 800. Same fix, same reason:
|
|
574
|
+
an invisible control is worse than reaching for the tone's ink.
|
|
575
|
+
|
|
576
|
+
Hover included, because the hover colour is a second tertiary green and would
|
|
577
|
+
make the control vanish again the moment a pointer reached it. */
|
|
578
|
+
.rsui-page-header--toned .rsui-page-header__meta-toggle {
|
|
579
|
+
color: var(--rsui-page-header-ink);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* Hover separately: the hover colour is a second tertiary green, so without this
|
|
583
|
+
the control vanishes again the moment a pointer reaches it. */
|
|
584
|
+
.rsui-page-header--toned .rsui-page-header__meta-toggle:hover {
|
|
585
|
+
color: var(--rsui-page-header-ink);
|
|
586
|
+
}
|
|
@@ -22,8 +22,19 @@
|
|
|
22
22
|
@apply border-border-primary;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/* A clickable header is a control, so it takes the library's focus ring rather
|
|
26
|
+
than the browser's. Without `outline-hidden` a `tabindex="0"` div falls through
|
|
27
|
+
to the UA default ring, which is the wrong colour and shape for this library.
|
|
28
|
+
|
|
29
|
+
Keyboard only, verified with a real click rather than a dispatched event: the
|
|
30
|
+
browser knows a genuine mouse press from a synthetic one, and `:focus-visible`
|
|
31
|
+
is false after the real thing. (A dispatched MouseEvent reports true even on a
|
|
32
|
+
plain <button>, so a probe built that way proves nothing.)
|
|
33
|
+
|
|
34
|
+
Same declaration CardHeader already carries for the same situation. */
|
|
25
35
|
.rsui-section-header--clickable {
|
|
26
36
|
@apply cursor-pointer;
|
|
37
|
+
@apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-primary outline-hidden;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
.rsui-section-header__header {
|
package/package.json
CHANGED
package/tokens.css
CHANGED
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
--color-text-success: #067647;
|
|
12
12
|
--color-text-warning: #b54708;
|
|
13
13
|
--color-text-error: #b42318;
|
|
14
|
-
--color-
|
|
14
|
+
/* #0077cc measured 4.46:1 on its own fill (--color-background-info, #f6fbfd),
|
|
15
|
+
just under the 4.5 AA threshold for body text. #0070c0 is the smallest step
|
|
16
|
+
that clears it — 4.94:1 — so the info blue moves as little as it can while
|
|
17
|
+
being legible. Every other ground it sits on improves too: 4.66 -> 5.15 on
|
|
18
|
+
white.
|
|
19
|
+
--color-background-info-contrast keeps #0077cc deliberately: it is a
|
|
20
|
+
BACKGROUND, so darkening it would change surfaces rather than fix text. */
|
|
21
|
+
--color-text-info: #0070c0;
|
|
15
22
|
/* Warm-Mauve-800, not -600: the AI treatments set this over the pale AI gradient
|
|
16
23
|
(#ebe0fa -> #fff6eb), where -600 measured 2.93:1 and failed WCAG AA for 14px text.
|
|
17
24
|
-800 clears it at 4.52:1 at the gradient's worst point. Consumed by Badge AI,
|