@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.
@@ -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
- @apply p-space-md;
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
- @apply p-space-md;
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
- @apply p-space-md;
90
- padding-bottom: max(1rem, env(safe-area-inset-bottom));
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
- .rsui-page-header__meta-modal {
220
- @apply lg:hidden;
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
- .rsui-page-header__meta-modal__body {
224
- @apply flex flex-col gap-y-space-md;
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
- @apply hidden lg:flex gap-x-space-xl;
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
- .rsui-page-header--toned .rsui-page-header__meta {
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-tailwindcss",
3
- "version": "7.68.0",
3
+ "version": "7.70.0",
4
4
  "description": "RedSeed UI Tailwindcss",
5
5
  "main": "index.js",
6
6
  "style": "index.css",