@redseed/redseed-ui-tailwindcss 7.69.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/page_header.css +74 -0
- package/components/toast.css +188 -0
- package/components.css +1 -0
- package/package.json +1 -1
|
@@ -212,6 +212,80 @@
|
|
|
212
212
|
@apply w-full pt-space-xs lg:pt-0 lg:w-auto;
|
|
213
213
|
}
|
|
214
214
|
|
|
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
|
+
|
|
215
289
|
/* ---------------------------------------------------------------------------
|
|
216
290
|
Meta
|
|
217
291
|
|
|
@@ -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';
|