@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.
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-tailwindcss",
3
- "version": "7.69.0",
3
+ "version": "7.70.0",
4
4
  "description": "RedSeed UI Tailwindcss",
5
5
  "main": "index.js",
6
6
  "style": "index.css",