@redseed/redseed-ui-tailwindcss 7.68.0 → 7.70.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/modal.css +50 -4
- package/components/page_header.css +184 -10
- package/components/section_header.css +11 -0
- package/components/toast.css +188 -0
- package/components.css +1 -0
- package/package.json +1 -1
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 {
|
|
@@ -208,26 +208,181 @@
|
|
|
208
208
|
@apply flex-1 flex flex-wrap gap-space-xs items-center justify-end;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
.rsui-page-header__meta-action {
|
|
212
|
-
@apply flex lg:hidden;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
211
|
.rsui-page-header__search {
|
|
216
212
|
@apply w-full pt-space-xs lg:pt-0 lg:w-auto;
|
|
217
213
|
}
|
|
218
214
|
|
|
219
|
-
|
|
220
|
-
|
|
215
|
+
/* ---------------------------------------------------------------------------
|
|
216
|
+
A WIDE SUBTITLE MUST NOT PUSH THE ACTIONS ONTO THEIR OWN LINE (#386)
|
|
217
|
+
---------------------------------------------------------------------------
|
|
218
|
+
`__top` is flex-wrap and becomes a row at lg. `__title` has no grow and keeps its
|
|
219
|
+
default `min-width: auto`, so the block is as wide as its widest line — which is
|
|
220
|
+
usually the subtitle, not the h1. `__status-actions` is `flex-1`. Once the title's
|
|
221
|
+
CONTENT width plus the actions exceeds the row, the actions wrap underneath, right
|
|
222
|
+
aligned, leaving a band of empty space above the page content.
|
|
223
|
+
|
|
224
|
+
Reported against the Manager Self-Review capture screen and the 180 Feedback
|
|
225
|
+
self-assessment, where it wrapped the Ask Archie AI button. The LMS shipped a local
|
|
226
|
+
fix for it (redseedtraining/redseed#5239) and then took it back out, because the
|
|
227
|
+
behaviour belongs here.
|
|
228
|
+
|
|
229
|
+
The fix is to let the title SHRINK rather than letting it set the row's width:
|
|
230
|
+
`flex: 1 1 0%` with `min-width: 0` means the subtitle wraps inside the title block
|
|
231
|
+
instead of forcing the actions down. The actions stop being greedy (`flex: none`)
|
|
232
|
+
so they take only the space they need.
|
|
233
|
+
|
|
234
|
+
`align-self: flex-start` because `__top` is `lg:items-center`, which would otherwise
|
|
235
|
+
centre the actions against the whole title-plus-subtitle block and leave them
|
|
236
|
+
floating below the h1 they belong to. This is the visible change for any header that
|
|
237
|
+
has both a subtitle and actions, and it is the correct one: SectionHeader already
|
|
238
|
+
top-aligns its toolbar the same way (`__header` is `items-start`), so this closes a
|
|
239
|
+
disagreement between the two headers rather than opening one.
|
|
240
|
+
|
|
241
|
+
THREE EXCLUSIONS, all of which already own their row behaviour:
|
|
242
|
+
|
|
243
|
+
--profile bottom-aligns its actions on purpose (`lg:self-end lg:pb-space-lg`)
|
|
244
|
+
:has(__stats) already sets the title to `lg:grow lg:min-w-0 lg:basis-full`
|
|
245
|
+
:has(__search) puts a full-width search control in the row below lg
|
|
246
|
+
|
|
247
|
+
MEASURED, and worth being precise about which case is which.
|
|
248
|
+
|
|
249
|
+
The WRAP needs a genuinely wide action group — a status badge plus two default-size
|
|
250
|
+
buttons, which is the shape `__status-actions` takes on the reported screens, since it
|
|
251
|
+
is `justify-between` and carries the status as well as the actions:
|
|
252
|
+
|
|
253
|
+
1024 today: WRAPS, 112px with this rule: no wrap, 140px
|
|
254
|
+
|
|
255
|
+
That 28px is the cost of the fix in the wrapping case: the subtitle takes more lines
|
|
256
|
+
inside a narrower title block instead of the actions taking a whole extra row. It is
|
|
257
|
+
the right way round, because the extra row also left a band of dead space above the
|
|
258
|
+
page content and put the button somewhere nobody looks for it.
|
|
259
|
+
|
|
260
|
+
With a SMALLER action group — two buttons and no status, which is the
|
|
261
|
+
LongSubtitleWithActions story — nothing wraps at any lg width, either before or after:
|
|
262
|
+
|
|
263
|
+
1024-1100 today: 92px with this rule: 94px
|
|
264
|
+
1200+ today: 70px with this rule: 70px
|
|
265
|
+
|
|
266
|
+
So in the common case the rule costs 2px at the narrow end of lg and nothing above it,
|
|
267
|
+
while still doing the visible half of the job: the actions stop being greedy (measured
|
|
268
|
+
409px wide -> 82px, taking only what they need) and sit level with the h1 instead of
|
|
269
|
+
14px below it.
|
|
270
|
+
|
|
271
|
+
Scoped to lg and up. Below lg `__top` is a column, where every child is full width
|
|
272
|
+
and none of this applies. */
|
|
273
|
+
@media (min-width: 64rem) {
|
|
274
|
+
.rsui-page-header:not(.rsui-page-header--profile)
|
|
275
|
+
.rsui-page-header__top:not(:has(.rsui-page-header__stats)):not(:has(.rsui-page-header__search))
|
|
276
|
+
.rsui-page-header__title {
|
|
277
|
+
flex: 1 1 0%;
|
|
278
|
+
min-width: 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.rsui-page-header:not(.rsui-page-header--profile)
|
|
282
|
+
.rsui-page-header__top:not(:has(.rsui-page-header__stats)):not(:has(.rsui-page-header__search))
|
|
283
|
+
.rsui-page-header__status-actions {
|
|
284
|
+
flex: none;
|
|
285
|
+
align-self: flex-start;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ---------------------------------------------------------------------------
|
|
290
|
+
Meta
|
|
291
|
+
|
|
292
|
+
Visible at every width. It was `hidden lg:flex`, so the facts a header exists
|
|
293
|
+
to carry vanished below 1024px and a modal reproduced them — on a phone, where
|
|
294
|
+
a reader has least context to spare and least patience for a dialog.
|
|
295
|
+
|
|
296
|
+
A grid, not a flex row, and content-width rather than full-bleed: meta reads
|
|
297
|
+
as one cluster beside the title rather than a band across the page. Four
|
|
298
|
+
columns from sm up; two on a phone, because four 16px labels in 375px is not a
|
|
299
|
+
grid, it is a queue.
|
|
300
|
+
|
|
301
|
+
The cap is `:nth-child` rather than a count in JS. `#meta` is a slot and every
|
|
302
|
+
consumer fills it with a `v-for`, so the component cannot know how many facts
|
|
303
|
+
there are — but CSS does not need to know. Past the fourth, they are hidden
|
|
304
|
+
until the toggle expands the row.
|
|
305
|
+
--------------------------------------------------------------------------- */
|
|
306
|
+
/* The region is the grid. The facts sit inside a `display: contents` wrapper so
|
|
307
|
+
they become direct grid items without the toggle joining their `:nth-child`
|
|
308
|
+
sequence — see the note in PageHeader.vue. */
|
|
309
|
+
.rsui-page-header__meta-region {
|
|
310
|
+
@apply grid w-fit max-w-full items-start;
|
|
311
|
+
@apply gap-x-space-lg gap-y-space-md;
|
|
312
|
+
|
|
313
|
+
/* Columns sized to content, and the SAME count in both states.
|
|
314
|
+
|
|
315
|
+
An earlier version went five columns collapsed and four expanded, so the
|
|
316
|
+
toggle would sit in the row rather than wrap. That moved every fact
|
|
317
|
+
sideways when pressed — 13.7px with `1fr`, up to 40px with `max-content`,
|
|
318
|
+
because both make a column's width depend on something the toggle changes.
|
|
319
|
+
Holding the count fixed removes the question: the toggle occupies the
|
|
320
|
+
fifth cell while collapsed, and the fifth fact takes it once expanded.
|
|
321
|
+
|
|
322
|
+
The width has to be FIXED, not `max-content`. Holding the count steady is
|
|
323
|
+
not enough on its own: `max-content` sizes each column to whatever lands in
|
|
324
|
+
it, and expanding puts different facts in different columns, so they still
|
|
325
|
+
moved — 27px, measured. A fixed track depends on neither the count nor the
|
|
326
|
+
content.
|
|
327
|
+
|
|
328
|
+
6rem, against a widest-ink measurement of 90px across every label and value
|
|
329
|
+
in the busiest real header. Anything longer wraps inside its own cell
|
|
330
|
+
rather than pushing its neighbours. */
|
|
331
|
+
grid-template-columns: repeat(2, 6rem);
|
|
221
332
|
}
|
|
222
333
|
|
|
223
|
-
|
|
224
|
-
|
|
334
|
+
@media (min-width: 40rem) {
|
|
335
|
+
.rsui-page-header__meta-region {
|
|
336
|
+
grid-template-columns: repeat(5, 6rem);
|
|
337
|
+
}
|
|
225
338
|
}
|
|
226
339
|
|
|
227
340
|
.rsui-page-header__meta {
|
|
228
|
-
|
|
341
|
+
display: contents;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* Past the cap, hidden until expanded. `> *` rather than a component class so it
|
|
345
|
+
does not care what a consumer puts in the slot — MetaInfo today, something
|
|
346
|
+
else tomorrow. The toggle is excluded by its own rule below, which comes
|
|
347
|
+
later and wins. */
|
|
348
|
+
.rsui-page-header__meta > *:nth-child(n + 5) {
|
|
349
|
+
@apply hidden;
|
|
229
350
|
}
|
|
230
351
|
|
|
352
|
+
.rsui-page-header__meta--expanded > *:nth-child(n + 5) {
|
|
353
|
+
@apply block;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* The toggle sits where the fifth fact would be, so pressing it expands the row
|
|
357
|
+
around it rather than moving it. Always shown, whatever the cap is doing. */
|
|
358
|
+
.rsui-page-header__meta-toggle {
|
|
359
|
+
/* Centred against the fact beside it, not `self-start`. A fact is a two-line
|
|
360
|
+
unit — a 16px label over a 16px value, 36px in all — so a 20px toggle
|
|
361
|
+
pinned to the top reads as belonging to the LABELS rather than to the pair.
|
|
362
|
+
Centre also survives the expanded state, where the toggle wraps to its own
|
|
363
|
+
row and has no value to sit level with. */
|
|
364
|
+
@apply inline-flex w-fit items-center whitespace-nowrap self-center;
|
|
365
|
+
@apply text-sm font-semibold no-underline cursor-pointer transition-colors;
|
|
366
|
+
@apply text-button-tertiary hover:text-button-tertiary-hover;
|
|
367
|
+
@apply hover:underline underline-offset-4 decoration-2;
|
|
368
|
+
@apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-button-tertiary outline-hidden;
|
|
369
|
+
|
|
370
|
+
/* min-width, because the label changes length as it toggles — "+2 more"
|
|
371
|
+
against "Show fewer" — and without a floor the row shifts sideways under
|
|
372
|
+
the pointer mid-press. Same fix CategoryList carries (#355). */
|
|
373
|
+
@apply min-w-20 justify-center;
|
|
374
|
+
|
|
375
|
+
/* The negative margin pulls it back from the grid's 32px column gap to the
|
|
376
|
+
16px CategoryList uses. 32px is right between two FACTS, which read as
|
|
377
|
+
separate label/value pairs; it is too far for an affordance trailing them,
|
|
378
|
+
and the two rows sit one above the other in the same header — `+4 more`
|
|
379
|
+
under categories and `+3 more` under meta must not be spaced differently. */
|
|
380
|
+
@apply -ms-space-md;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
|
|
231
386
|
.rsui-page-header__avatar {
|
|
232
387
|
@apply shrink-0 size-12;
|
|
233
388
|
}
|
|
@@ -439,7 +594,10 @@
|
|
|
439
594
|
color: var(--rsui-page-header-accent, var(--rsui-page-header-ink));
|
|
440
595
|
}
|
|
441
596
|
|
|
442
|
-
|
|
597
|
+
/* On the REGION, not on `__meta`. The facts element is `display: contents` so it
|
|
598
|
+
generates no box at all — a border declared there paints nothing, and this
|
|
599
|
+
separator silently disappeared when the grid moved up a level. Measured. */
|
|
600
|
+
.rsui-page-header--toned .rsui-page-header__meta-region {
|
|
443
601
|
border-top: 1px solid var(--rsui-page-header-rule);
|
|
444
602
|
|
|
445
603
|
@apply mt-space-md pt-space-md;
|
|
@@ -484,3 +642,19 @@
|
|
|
484
642
|
.rsui-page-header--toned .rsui-category-list__more {
|
|
485
643
|
color: var(--rsui-page-header-ink);
|
|
486
644
|
}
|
|
645
|
+
|
|
646
|
+
/* The meta toggle borrows the same tertiary green and disappears on the same dark
|
|
647
|
+
band — measured as near-invisible on Journey Green 800. Same fix, same reason:
|
|
648
|
+
an invisible control is worse than reaching for the tone's ink.
|
|
649
|
+
|
|
650
|
+
Hover included, because the hover colour is a second tertiary green and would
|
|
651
|
+
make the control vanish again the moment a pointer reached it. */
|
|
652
|
+
.rsui-page-header--toned .rsui-page-header__meta-toggle {
|
|
653
|
+
color: var(--rsui-page-header-ink);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* Hover separately: the hover colour is a second tertiary green, so without this
|
|
657
|
+
the control vanishes again the moment a pointer reaches it. */
|
|
658
|
+
.rsui-page-header--toned .rsui-page-header__meta-toggle:hover {
|
|
659
|
+
color: var(--rsui-page-header-ink);
|
|
660
|
+
}
|
|
@@ -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 {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* A brief confirmation that an action completed. Bottom-left, per the standards, and
|
|
2
|
+
distinct from MessageBox in every dimension: fixed rather than in the page flow,
|
|
3
|
+
auto-dismissing rather than persistent (unless it carries an action), and about an
|
|
4
|
+
action rather than about the state of the thing on screen. */
|
|
5
|
+
|
|
6
|
+
/* SURFACE. A dark overlay, which is Material's snackbar and the reason the standards
|
|
7
|
+
point at it — a toast has to read as something laid ON the page rather than as part of
|
|
8
|
+
it.
|
|
9
|
+
|
|
10
|
+
Measured against the real page ground, Modern Bone 500 #edebe2 (named in
|
|
11
|
+
storybook/.storybook/preview.ts), because this was nearly shipped as a light grey pill:
|
|
12
|
+
|
|
13
|
+
background-primary-invert (Grey 300) 1.20:1 against the page — no separation
|
|
14
|
+
background-primary-contrast (Grey 800) 11.88:1, with white text on it at 14.20:1
|
|
15
|
+
|
|
16
|
+
1.20:1 is not a surface. It looked like a disabled chip someone had left behind.
|
|
17
|
+
|
|
18
|
+
ALL THREE VARIANTS SHARE THIS SURFACE, and that is the important part. They did not:
|
|
19
|
+
the default was dark while success and error were the library's light status tints, so
|
|
20
|
+
`variant` flipped the surface polarity as well as choosing the meaning. That made the
|
|
21
|
+
action slot's contract unstateable — `on-color` gives a button white ink, which is
|
|
22
|
+
14.20:1 on the dark default and 1.05:1 on the mint success ground. One rule could not
|
|
23
|
+
be true across the axis, and the story confidently told consumers the wrong half.
|
|
24
|
+
|
|
25
|
+
So semantics move off the ground and onto an accent (below), the ground stays constant,
|
|
26
|
+
and `on-color` is correct everywhere without qualification. The cost is that a red
|
|
27
|
+
toast no longer matches the red tint used by badges and MessageBox; the gain is a slot
|
|
28
|
+
contract that fits in one sentence and cannot be followed into a contrast failure. */
|
|
29
|
+
.rsui-toast {
|
|
30
|
+
@apply fixed z-50 flex items-center gap-x-space-sm;
|
|
31
|
+
@apply px-space-md py-space-sm rounded-xl shadow-full-light;
|
|
32
|
+
@apply text-sm font-medium;
|
|
33
|
+
@apply bg-background-primary-contrast text-text-on-color border border-transparent;
|
|
34
|
+
|
|
35
|
+
/* BOTTOM-LEFT, and it must clear two things the toast cannot see.
|
|
36
|
+
|
|
37
|
+
--rsui-toast-offset is the sticky action bar. The standards are explicit that a
|
|
38
|
+
toast sits ABOVE such a bar and never over it — the bar owns the bottom edge —
|
|
39
|
+
but only the page knows whether it has one, and how tall. Set it on :root, NOT on
|
|
40
|
+
a wrapper: the toast is teleported to the body, so a custom property declared on
|
|
41
|
+
an ancestor in the consumer's tree never reaches it. The default of 0 means an
|
|
42
|
+
ordinary page needs nothing.
|
|
43
|
+
|
|
44
|
+
env(safe-area-inset-bottom) is the home indicator. A toast is the last thing above
|
|
45
|
+
the bottom edge on a phone, so the inset is a FLOOR under the spacing rather than
|
|
46
|
+
an addition to it — the same max() shape drawer.css and modal.css already use. */
|
|
47
|
+
left: 1rem;
|
|
48
|
+
bottom: calc(max(1rem, env(safe-area-inset-bottom)) + var(--rsui-toast-offset, 0px));
|
|
49
|
+
|
|
50
|
+
/* Brief, so it is never a paragraph. Wide enough for a sentence, capped so it does
|
|
51
|
+
not stretch across a desktop, and held off both edges on a narrow screen. */
|
|
52
|
+
max-width: min(28rem, calc(100vw - 2rem));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* The message is the only part allowed to grow, so the action and the close button keep
|
|
56
|
+
their size and stay where the eye expects them between one toast and the next. */
|
|
57
|
+
.rsui-toast__message {
|
|
58
|
+
@apply flex-1 min-w-0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* The icon takes the toast's ink, for the same reason the close glyph does: `<Icon>`
|
|
62
|
+
* paints its child directly and cannot see what it is sitting on. `<Icon success>` renders
|
|
63
|
+
* text-success #067647 — a green drawn for a LIGHT ground — which measures 2.49:1 here and
|
|
64
|
+
* fails the 3:1 WCAG 1.4.11 asks of a meaningful non-text indicator. Measured, after this
|
|
65
|
+
* caught the close glyph and the action button the same way.
|
|
66
|
+
*
|
|
67
|
+
* The default inherits the toast's white; the semantic variants repaint it below. */
|
|
68
|
+
.rsui-toast__icon {
|
|
69
|
+
@apply flex items-center justify-center shrink-0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.rsui-toast__icon .rsui-icon > *,
|
|
73
|
+
.rsui-toast__icon svg {
|
|
74
|
+
color: currentColor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* The action sits before the close button, so Tab reaches Undo first — it is the reason
|
|
78
|
+
the toast is still on screen, and the close button sits right beside it.
|
|
79
|
+
|
|
80
|
+
THE ACTION MUST BE PASSED `on-color`, and now that every variant shares one dark
|
|
81
|
+
surface this is unconditionally true. A plain <ButtonTertiary> renders brand green here
|
|
82
|
+
(Brand 600 #135847) and measures 1.70:1 against the surface — a straight WCAG 1.4.3
|
|
83
|
+
failure, and what the first draft of this component's own story shipped with.
|
|
84
|
+
|
|
85
|
+
Not forced from here. Reaching in to restyle a consumer's button would override a
|
|
86
|
+
variant they chose, which is the same call section_header.css documents and declines to
|
|
87
|
+
make for sizes.
|
|
88
|
+
|
|
89
|
+
No --rsui-button-on-color-ink override any more: it used to be set per variant, and on
|
|
90
|
+
the light surfaces it was a no-op anyway, because that variable only reaches the FILLED
|
|
91
|
+
treatment and button_slot.css already falls back to the same value. One surface means
|
|
92
|
+
one behaviour, and nothing to override. */
|
|
93
|
+
.rsui-toast__action {
|
|
94
|
+
@apply shrink-0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.rsui-toast__close {
|
|
98
|
+
@apply flex items-center justify-center shrink-0 cursor-pointer;
|
|
99
|
+
@apply text-current opacity-70 hover:opacity-100 transition-opacity;
|
|
100
|
+
/* Reset native <button> chrome, matching .rsui-meta-info__help. */
|
|
101
|
+
@apply bg-transparent border-0 p-0 rounded outline-hidden;
|
|
102
|
+
/* The ring takes the current ink rather than border-primary: on this dark surface the
|
|
103
|
+
standard ring is near-invisible against the ground it sits on. */
|
|
104
|
+
@apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-current focus-visible:ring-offset-transparent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* The X takes the button's ink, which it does not do on its own.
|
|
108
|
+
*
|
|
109
|
+
* `<Icon>` falls back to `primary` when the consumer names no colour, and
|
|
110
|
+
* `.rsui-icon--primary > *` PAINTS the svg directly — a declaration beats the white this
|
|
111
|
+
* button inherits, however specific the ancestor. So the close glyph rendered as Grey 900
|
|
112
|
+
* at the button's own 70% opacity: 1.15:1 as actually composited. Invisible, and it was
|
|
113
|
+
* on the branch that way through review. With currentColor it composites to 7.73:1, clear
|
|
114
|
+
* of the 3:1 WCAG 1.4.11 asks of an icon-only control.
|
|
115
|
+
*
|
|
116
|
+
* currentColor rather than a named token, so the rule holds if a variant ever changes its
|
|
117
|
+
* ink. Same mechanism as the override meta_info.css:68 makes for its label icon — but
|
|
118
|
+
* unscoped, where meta_info deliberately scopes to --primary. That difference is the
|
|
119
|
+
* interesting part: meta_info protects a consumer's `<Icon success>` because a status
|
|
120
|
+
* marker on a label is intent, whereas a close glyph has no legitimate reason to carry a
|
|
121
|
+
* semantic colour, so there is nothing to protect. */
|
|
122
|
+
.rsui-toast__close .rsui-icon > * {
|
|
123
|
+
color: currentColor;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* VARIANTS. Only three, and deliberately not MessageBox's six.
|
|
127
|
+
*
|
|
128
|
+
* A toast says an action completed. "Info" and "warning" are statements about the state
|
|
129
|
+
* of something, which is MessageBox's job and belongs in the page flow where it can stay.
|
|
130
|
+
* An `ai` toast is not a thing. Offering them here would invite a toast to be used as a
|
|
131
|
+
* banner that happens to disappear.
|
|
132
|
+
*
|
|
133
|
+
* CARRIED BY THE ICON, and by nothing else. Two rejected alternatives, both measured:
|
|
134
|
+
*
|
|
135
|
+
* A left accent bar. Drawn as an inset box-shadow it is clipped by the toast's own
|
|
136
|
+
* 12px corner radius — on a 46px-tall toast the radius eats 12px off each end, so only
|
|
137
|
+
* 22px of a 4px bar is ever full width and it tapers away at both ends. It renders as a
|
|
138
|
+
* crescent, not a bar.
|
|
139
|
+
*
|
|
140
|
+
* Tinting the dark surface toward the hue. Keeps one polarity, so the ink stays
|
|
141
|
+
* correct, but the tints are indistinguishable from the plain surface: mixing the
|
|
142
|
+
* accent in at 20% gives 1.49:1 (success) and 1.35:1 (error) against the default
|
|
143
|
+
* ground. Nobody would tell those apart. Pushing further only turns the surface muddy.
|
|
144
|
+
*
|
|
145
|
+
* So the ground is identical for every variant and the icon carries the meaning, at the
|
|
146
|
+
* full accent against a constant dark surface:
|
|
147
|
+
*
|
|
148
|
+
* success Success 400 #47cd8a 7.01:1
|
|
149
|
+
* error Error 400 #f97066 5.10:1
|
|
150
|
+
*
|
|
151
|
+
* Both clear of the 3:1 WCAG 1.4.11 asks of a meaningful non-text indicator.
|
|
152
|
+
*
|
|
153
|
+
* This means a success or error toast with NO icon looks exactly like a default one, and
|
|
154
|
+
* that is the accepted cost. The library's convention is that a semantic glyph belongs to
|
|
155
|
+
* the consumer — the note above .rsui-message-box's icon says so, and Pill, MetricCard,
|
|
156
|
+
* ListItem and SectionHeader all take theirs through a slot — so the component will not
|
|
157
|
+
* hardcode one. Colour is never the only signal in any case (WCAG 1.4.1): the message
|
|
158
|
+
* names what happened, and an error additionally takes role="alert".
|
|
159
|
+
*
|
|
160
|
+
* THESE TWO REACH PAST THE SEMANTIC TIER INTO THE RAW PALETTE, deliberately, and they are
|
|
161
|
+
* the only place in this file that does — the surface goes through
|
|
162
|
+
* `background-primary-contrast` like everything else.
|
|
163
|
+
*
|
|
164
|
+
* The semantic tier has no status colour for a dark ground. Every candidate, measured
|
|
165
|
+
* against this surface:
|
|
166
|
+
*
|
|
167
|
+
* --color-text-success #067647 2.49:1 fail
|
|
168
|
+
* --color-text-error #b42318 2.16:1 fail
|
|
169
|
+
* --color-background-success-invert #abefc6 10.74:1 pass
|
|
170
|
+
* --color-background-error-invert #fecdca 10.00:1 pass
|
|
171
|
+
* --color-border-success #abefc6 10.74:1 pass
|
|
172
|
+
* --color-border-error #fecdca 10.00:1 pass
|
|
173
|
+
*
|
|
174
|
+
* Every `text-*` status token is drawn for a light ground and fails outright here. The
|
|
175
|
+
* ones that pass are pale pastels named for a background or a border — using them would
|
|
176
|
+
* buy tier purity with a naming stretch and a washed-out glyph on a dark pill.
|
|
177
|
+
*
|
|
178
|
+
* The proper fix is a semantic token meaning "a status colour on a contrast surface".
|
|
179
|
+
* That is an addition to tokens.css and a design decision rather than a component one, so
|
|
180
|
+
* until it exists this is the honest version: raw palette, named, measured, in one place,
|
|
181
|
+
* and easy to swap when the token lands. */
|
|
182
|
+
.rsui-toast--success .rsui-toast__icon {
|
|
183
|
+
color: var(--Colors-Success-400);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.rsui-toast--error .rsui-toast__icon {
|
|
187
|
+
color: var(--Colors-Error-400);
|
|
188
|
+
}
|
package/components.css
CHANGED
|
@@ -108,6 +108,7 @@ Please be careful when adding new components and updating the order.
|
|
|
108
108
|
@import './components/th.css';
|
|
109
109
|
@import './components/td.css';
|
|
110
110
|
@import './components/td_user.css';
|
|
111
|
+
@import './components/toast.css';
|
|
111
112
|
@import './components/toggle.css';
|
|
112
113
|
@import './components/tooltip.css';
|
|
113
114
|
@import './components/two_column_layout.css';
|