@redseed/redseed-ui-tailwindcss 7.56.0 → 7.57.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.
@@ -153,6 +153,38 @@
153
153
  @apply hover:bg-background-brand-contrast;
154
154
  }
155
155
 
156
+ /*
157
+ * Square off the edge each endpoint faces the range on, so the band closes.
158
+ *
159
+ * The base `.rsui-form-field-date-range__day` is `rounded-lg` on all four
160
+ * corners and the endpoints only override the background, so an endpoint used to
161
+ * sit between two square in-range fills as a rounded square — leaving a white
162
+ * notch at each corner where its curve pulled away from its neighbour. The band
163
+ * read as interrupted by the endpoint rather than containing it.
164
+ *
165
+ * MUI solves the same problem by drawing a continuous highlight band behind the
166
+ * cells and putting the endpoint circle on top of it. That needs a second
167
+ * element per cell; squaring the inner edge of the endpoint's own fill gets the
168
+ * same join out of the existing one.
169
+ */
170
+ .rsui-form-field-date-range__day--range-start {
171
+ @apply rounded-r-none;
172
+ }
173
+
174
+ .rsui-form-field-date-range__day--range-end {
175
+ @apply rounded-l-none;
176
+ }
177
+
178
+ /*
179
+ * Both modifiers land on the same day for a single-day window — `today`, or a
180
+ * range picked start-and-end on one date. There is no band to join, so restore
181
+ * the full radius. This selector is deliberately last and doubly-classed so it
182
+ * outranks the two above.
183
+ */
184
+ .rsui-form-field-date-range__day--range-start.rsui-form-field-date-range__day--range-end {
185
+ @apply rounded-lg;
186
+ }
187
+
156
188
  /*
157
189
  * The day the presets resolve against — "today" as the server sees it.
158
190
  *
@@ -88,42 +88,52 @@
88
88
 
89
89
  .rsui-icon--primary {
90
90
  @apply *:text-text-primary;
91
+ --rsui-icon-pulse: var(--color-text-primary);
91
92
  }
92
93
 
93
94
  .rsui-icon--secondary {
94
95
  @apply *:text-text-secondary;
96
+ --rsui-icon-pulse: var(--color-text-secondary);
95
97
  }
96
98
 
97
99
  .rsui-icon--brand {
98
100
  @apply *:text-text-brand;
101
+ --rsui-icon-pulse: var(--color-text-brand);
99
102
  }
100
103
 
101
104
  .rsui-icon--success {
102
105
  @apply *:text-text-success;
106
+ --rsui-icon-pulse: var(--color-text-success);
103
107
  }
104
108
 
105
109
  .rsui-icon--info {
106
110
  @apply *:text-text-info;
111
+ --rsui-icon-pulse: var(--color-text-info);
107
112
  }
108
113
 
109
114
  .rsui-icon--warning {
110
115
  @apply *:text-text-warning;
116
+ --rsui-icon-pulse: var(--color-text-warning);
111
117
  }
112
118
 
113
119
  .rsui-icon--error {
114
120
  @apply *:text-text-error;
121
+ --rsui-icon-pulse: var(--color-text-error);
115
122
  }
116
123
 
117
124
  .rsui-icon--ai {
118
125
  @apply *:text-text-ai;
126
+ --rsui-icon-pulse: var(--color-text-ai);
119
127
  }
120
128
 
121
129
  .rsui-icon--disabled {
122
130
  @apply *:text-text-disabled;
131
+ --rsui-icon-pulse: var(--color-text-disabled);
123
132
  }
124
133
 
125
134
  .rsui-icon--classic {
126
135
  @apply *:text-pink-500;
136
+ --rsui-icon-pulse: var(--color-pink-500);
127
137
  }
128
138
 
129
139
  .rsui-icon--primary.rsui-icon--invert {
@@ -216,52 +226,145 @@
216
226
  @apply *:text-text-on-color;
217
227
  }
218
228
 
229
+ /*
230
+ * Both flags together is the one combination that puts the glyph back on its
231
+ * variant colour, against a tinted fill — so each of these restates the pulse
232
+ * token alongside it, undoing the on-colour default set below. Kept here rather
233
+ * than in a block of its own so the ring colour is edited in the same place as
234
+ * the glyph colour it has to match.
235
+ */
219
236
  .rsui-icon--primary.rsui-icon--background.rsui-icon--invert {
220
237
  @apply bg-background-primary-invert;
221
238
  @apply *:text-text-primary;
239
+ --rsui-icon-pulse: var(--color-text-primary);
222
240
  }
223
241
 
224
242
  .rsui-icon--secondary.rsui-icon--background.rsui-icon--invert {
225
243
  @apply bg-background-secondary-invert;
226
244
  @apply *:text-text-secondary;
245
+ --rsui-icon-pulse: var(--color-text-secondary);
227
246
  }
228
247
 
229
248
  .rsui-icon--brand.rsui-icon--background.rsui-icon--invert {
230
249
  @apply bg-background-brand-invert;
231
250
  @apply *:text-text-brand;
251
+ --rsui-icon-pulse: var(--color-text-brand);
232
252
  }
233
253
 
234
254
  .rsui-icon--success.rsui-icon--background.rsui-icon--invert {
235
255
  @apply bg-background-success-invert;
236
256
  @apply *:text-text-success;
257
+ --rsui-icon-pulse: var(--color-text-success);
237
258
  }
238
259
 
239
260
  .rsui-icon--info.rsui-icon--background.rsui-icon--invert {
240
261
  @apply bg-background-info-invert;
241
262
  @apply *:text-text-info;
263
+ --rsui-icon-pulse: var(--color-text-info);
242
264
  }
243
265
 
244
266
  .rsui-icon--warning.rsui-icon--background.rsui-icon--invert {
245
267
  @apply bg-background-warning-invert;
246
268
  @apply *:text-text-warning;
269
+ --rsui-icon-pulse: var(--color-text-warning);
247
270
  }
248
271
 
249
272
  .rsui-icon--error.rsui-icon--background.rsui-icon--invert {
250
273
  @apply bg-background-error-invert;
251
274
  @apply *:text-text-error;
275
+ --rsui-icon-pulse: var(--color-text-error);
252
276
  }
253
277
 
254
278
  .rsui-icon--ai.rsui-icon--background.rsui-icon--invert {
255
279
  @apply bg-background-ai-invert;
256
280
  @apply *:text-text-ai;
281
+ --rsui-icon-pulse: var(--color-text-ai);
257
282
  }
258
283
 
259
284
  .rsui-icon--disabled.rsui-icon--background.rsui-icon--invert {
260
285
  @apply bg-background-disabled-invert;
261
286
  @apply *:text-text-disabled;
287
+ --rsui-icon-pulse: var(--color-text-disabled);
262
288
  }
263
289
 
264
290
  .rsui-icon--classic.rsui-icon--background.rsui-icon--invert {
265
291
  @apply bg-pink-50;
266
292
  @apply *:text-pink-500;
267
- }
293
+ --rsui-icon-pulse: var(--color-pink-500);
294
+ }
295
+ /*
296
+ * On a filled or inverted icon the glyph is repainted to an on-colour token, so
297
+ * the ring follows it rather than staying on a variant colour it no longer
298
+ * matches. Same specificity as the base variant rules and declared after them,
299
+ * so it wins on source order — but it loses to the two-flag rules above, which
300
+ * is the point: those put the glyph back on its variant colour and take the ring
301
+ * with them.
302
+ */
303
+ .rsui-icon--background,
304
+ .rsui-icon--invert {
305
+ --rsui-icon-pulse: var(--color-text-on-color);
306
+ }
307
+
308
+ /*
309
+ * Optional attention pulse: a ring expands out from the icon and fades, a few
310
+ * times, when the icon arrives.
311
+ *
312
+ * Deliberately a fixed number of iterations rather than `infinite`. An animation
313
+ * that repeats for more than five seconds needs a pause control (WCAG 2.2.2), and
314
+ * an icon has nowhere sensible to put one — three bursts draw the eye on arrival
315
+ * without taking on that obligation.
316
+ *
317
+ * Duration x iterations is the number that matters there, and it must stay under
318
+ * five seconds: 1.4s x 3 leaves 800ms of headroom. Slowing the ring to 1.8s to
319
+ * soften it further would put the total at 5.4s and trip the criterion, so the
320
+ * softening is done with opacity, border width and travel instead.
321
+ *
322
+ * The ring takes its colour from `--rsui-icon-pulse`, which each colour variant
323
+ * sets alongside its glyph rule.
324
+ *
325
+ * A custom property rather than `currentColor`: the variants colour the SVG child
326
+ * (`*:text-…`) and not the wrapper, so an inherited ring comes out near-black.
327
+ * Setting `color` on the wrapper instead would fix the base variants but silently
328
+ * miss `--invert` and `--background`, which repaint the glyph and would leave the
329
+ * ring on the old colour. A property the combination rules can override keeps the
330
+ * answer in one place — and, unlike a wrapper colour, it is inert for everything
331
+ * that does not read it.
332
+ *
333
+ * `pointer-events-none` because the ring overflows the icon's box and would
334
+ * otherwise sit over whatever is beside it.
335
+ */
336
+ .rsui-icon--pulse {
337
+ @apply relative;
338
+ }
339
+
340
+ .rsui-icon--pulse::after {
341
+ @apply content-[''] absolute inset-0 rounded-full pointer-events-none;
342
+ border: 1px solid var(--rsui-icon-pulse, currentColor);
343
+ /* Invisible at rest, in both directions. Without this the ring reverts to its
344
+ un-animated state once the iterations finish — full opacity at scale 1 —
345
+ and a solid ring stays welded around the icon for the life of the page. */
346
+ @apply opacity-0;
347
+ animation: rsui-icon-pulse 1.4s ease-out 3;
348
+ }
349
+
350
+ @keyframes rsui-icon-pulse {
351
+ 0% {
352
+ opacity: 0.35;
353
+ transform: scale(1);
354
+ }
355
+ 100% {
356
+ opacity: 0;
357
+ transform: scale(1.8);
358
+ }
359
+ }
360
+
361
+ /*
362
+ * The pulse is decoration on top of an icon that is already visible, so removing
363
+ * it costs nothing — no state is conveyed by the animation alone.
364
+ */
365
+ @media (prefers-reduced-motion: reduce) {
366
+ .rsui-icon--pulse::after {
367
+ animation: none;
368
+ display: none;
369
+ }
370
+ }
@@ -1,5 +1,46 @@
1
+ /*
2
+ * A row: optional icon, the text column, optional close. `items-start` so the
3
+ * icon and the close button align with the first line rather than centring
4
+ * against a tall body.
5
+ */
1
6
  .rsui-message-box {
2
- @apply border rounded-lg overflow-hidden p-4 flex flex-col gap-y-3;
7
+ @apply border rounded-lg overflow-hidden p-4 flex items-start gap-x-3;
8
+ }
9
+
10
+ /*
11
+ * The text column. Carries the vertical rhythm the box itself used to own, so a
12
+ * box with no icon and no actions lays out exactly as it did before.
13
+ */
14
+ .rsui-message-box__main {
15
+ @apply flex-1 flex flex-col gap-y-3;
16
+ }
17
+
18
+ /*
19
+ * Narrow and with an icon: the icon and the close button take a row of their own
20
+ * and the text runs full width beneath them, rather than indenting past a column
21
+ * that costs more than it gives at this width.
22
+ *
23
+ * Ordering is what makes it work — the DOM runs icon, text, close, so without it
24
+ * the full-width text column would wrap between the two and strand the close
25
+ * button on a third row. Scoped to `--with-icon` because a box with no icon has
26
+ * no column to escape: its close button already sits beside the title, and a
27
+ * lone X on its own row would be a wasted line.
28
+ *
29
+ * The box itself sets only `gap-x`, which buys nothing once these become two
30
+ * flex lines — so the row gap is set here, matching the text column's own
31
+ * rhythm. Inert at `sm:flex-nowrap`, where there is only ever one line.
32
+ */
33
+ .rsui-message-box--with-icon {
34
+ @apply flex-wrap gap-y-3 sm:flex-nowrap;
35
+ }
36
+
37
+ .rsui-message-box--with-icon .rsui-message-box__close {
38
+ @apply order-2 ms-auto sm:order-3 sm:ms-0;
39
+ }
40
+
41
+ /* `sm:basis-0` rather than `basis-auto`, to restore exactly what `flex-1` sets. */
42
+ .rsui-message-box--with-icon .rsui-message-box__main {
43
+ @apply order-3 basis-full sm:order-2 sm:basis-0;
3
44
  }
4
45
 
5
46
  .rsui-message-box--default {
@@ -37,17 +78,76 @@
37
78
  }
38
79
 
39
80
  .rsui-message-box__head {
40
- @apply flex justify-between items-center gap-x-2;
81
+ @apply flex items-center gap-x-2;
41
82
  }
42
83
 
43
84
  .rsui-message-box__content {
44
- @apply flex justify-between items-start gap-x-2;
85
+ @apply flex items-start gap-x-2;
86
+ }
87
+
88
+ /*
89
+ * `items-start` so the icon aligns with the first line of the message rather
90
+ * than centring against a multi-line body. `shrink-0` keeps it from being
91
+ * squeezed by long text.
92
+ */
93
+ .rsui-message-box__icon {
94
+ @apply shrink-0 flex items-center h-6;
95
+ }
96
+
97
+ /*
98
+ * Actions sit under the message, indented to the body rather than the icon, so
99
+ * they read as belonging to the text.
100
+ *
101
+ * Neutral, not variant-coloured: a coloured action on a coloured tint is a
102
+ * contrast problem, and the action is a way out of the message rather than a
103
+ * restatement of its severity. The dismiss is quieter than whatever the consumer
104
+ * puts beside it, which is the only hierarchy this row needs.
105
+ */
106
+ .rsui-message-box__actions {
107
+ @apply flex flex-wrap items-center gap-x-4 gap-y-2 pt-1;
108
+ }
109
+
110
+ .rsui-message-box__dismiss,
111
+ .rsui-message-box__action {
112
+ @apply bg-transparent border-0 p-0 cursor-pointer;
113
+ @apply text-sm font-semibold transition-colors;
114
+ @apply hover:underline underline-offset-4 decoration-2;
115
+ @apply outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-primary rounded-sm;
116
+ }
117
+
118
+ /*
119
+ * Two tiers, and the only hierarchy this row needs: dismiss is the quieter of
120
+ * the pair, whatever the consumer puts beside it is the one they want pressed.
121
+ */
122
+ .rsui-message-box__dismiss {
123
+ @apply text-text-secondary hover:text-text-primary;
124
+ }
125
+
126
+ .rsui-message-box__action {
127
+ @apply text-text-primary;
45
128
  }
46
129
 
130
+ /*
131
+ * `-my-2` so the 40px hit target centres on the first 24px line rather than
132
+ * hanging 8px below it. Under `items-start` the button's own box starts at the
133
+ * padding edge, which put its glyph at y=36 against a title centred at y=28 —
134
+ * and, with an icon shown, two icons in the same row not lining up. The negative
135
+ * margin keeps the target size while claiming only 24px of layout height.
136
+ */
47
137
  .rsui-message-box__close {
48
- @apply size-10;
138
+ @apply size-10 shrink-0 -my-2;
49
139
  }
50
140
 
141
+ /*
142
+ * The glyph colour is set on the Icon itself (`secondary`), not here — the
143
+ * variant rules target the SVG child directly, so a colour on this button would
144
+ * lose to them.
145
+ *
146
+ * It was `disabled` before, which painted a working control in the disabled text
147
+ * colour — Grey-400, 2.41:1 on white — so it looked permanently greyed out and
148
+ * fell under the 3:1 WCAG 1.4.11 asks of a control. Grey-700 reads as quiet
149
+ * without reading as dead.
150
+ */
51
151
  .rsui-message-box__close-icon {
52
152
  @apply size-10 cursor-pointer rounded-lg flex items-center justify-center hover:bg-border-secondary transition duration-200;
53
153
  /* Reset native <button> chrome so it matches the prior inline affordance ... */
@@ -17,3 +17,71 @@
17
17
  @apply flex-1;
18
18
  }
19
19
 
20
+ /*
21
+ * The view-all sits in its own region so it can centre, rather than joining the
22
+ * right-aligned action row.
23
+ *
24
+ * `order-first md:order-last` keeps it trailing at both breakpoints. The footer
25
+ * is `flex-col-reverse` below `md`, so the last DOM child paints at the top —
26
+ * appending the region put the view-all *above* the actions and the content on
27
+ * mobile, which is the opposite of what every comment about it claims. Ordering
28
+ * it first puts it back at the bottom there, and `md:order-last` restores it to
29
+ * the end of the desktop row.
30
+ */
31
+ .rsui-section-footer__view-all-region {
32
+ @apply flex items-center justify-center order-first md:order-last;
33
+ }
34
+
35
+ /*
36
+ * `flex-1` only when the view-all has the footer to itself, which is the case
37
+ * that wants centring. Applying it unconditionally made the region a second
38
+ * greedy sibling alongside `content-start`: the two split the row and squeezed
39
+ * the action buttons towards the middle instead of the right edge — measured at
40
+ * x=599 of 1265. With actions present the region stays content-sized and
41
+ * `justify-between` puts it at the end of the row where it belongs.
42
+ */
43
+ .rsui-section-footer__view-all-region--full {
44
+ @apply flex-1 w-full;
45
+ }
46
+
47
+ /*
48
+ * The trailing "view all" link. Matches the "See all" treatment already used in
49
+ * section headers: `font-semibold text-sm` on the tertiary-button colour, no
50
+ * underline, no arrow. That pairing is what makes it read as an action without
51
+ * competing with the section's own type.
52
+ *
53
+ * Deliberately not LinkSlot, which sits at `font-medium`, carries an always-on
54
+ * underline rule, and pins a font size per variant — all three wrong for this.
55
+ * No `text-*` here either: the size comes from whatever encloses the footer, so
56
+ * the link sits a step below the section's own type wherever it lands rather than
57
+ * locking to one scale. The semibold is what makes it read as an action.
58
+ *
59
+ * Colour comes from `--color-button-tertiary` rather than a literal brand value,
60
+ * so it tracks whatever tertiary actions are doing. It is overridden per section
61
+ * variant below rather than hardcoded, because a brand-coloured link is
62
+ * invisible on a brand-coloured surface.
63
+ *
64
+ * `w-fit` so the link stays the width of its own text. Its region centres it, and
65
+ * an underline that ran the width of the footer would read as a rule rather than
66
+ * as part of the link.
67
+ */
68
+ .rsui-section-footer__view-all {
69
+ @apply inline-flex w-fit items-center whitespace-nowrap;
70
+ @apply font-semibold no-underline cursor-pointer transition-colors;
71
+ @apply text-button-tertiary hover:text-button-tertiary-hover;
72
+ /* Underline on hover only: the resting state stays as clean as the header's
73
+ "See all", but the link still confirms itself as one under the pointer. */
74
+ @apply hover:underline underline-offset-4 decoration-2;
75
+ @apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-button-tertiary outline-hidden;
76
+ }
77
+
78
+ /*
79
+ * No dark-surface override here, deliberately. Every plain Section variant is a
80
+ * light tint, so the tertiary colour reads on all nine. The one dark surface in
81
+ * the package is the slider's `--featured` (brand-600), where a brand link would
82
+ * be brand on brand — but `SectionSlider` renders no footer and does not forward
83
+ * Section's `#footer` slot, so no consumer can put this link there. A rule for it
84
+ * would be unreachable, untested, and would imply a capability that does not
85
+ * exist. Add it alongside a footer slot on the slider, if that is ever wanted.
86
+ */
87
+
@@ -24,12 +24,314 @@
24
24
 
25
25
  .rsui-section-slider__action {
26
26
  @apply shrink-0 inline-flex items-center justify-center size-14 rounded-full no-underline transition-all disabled:opacity-50 disabled:text-grey-400 hover:cursor-pointer disabled:hover:cursor-default border border-grey-400 bg-transparent;
27
+ @apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-primary;
27
28
  }
28
29
 
30
+ /*
31
+ * The trailing "view all" card. The element carries `rsui-card
32
+ * rsui-card--bordered` plus any `rsui-card--<variant>`, so radius, background,
33
+ * border colour and the transition all come from Card — including every
34
+ * variant's border colour, which is the point of composing rather than
35
+ * restating. This file adds only what Card has no opinion about.
36
+ *
37
+ * The dashed edge is the one deliberate departure, and it is what marks the card
38
+ * as an opening rather than content. It follows form_field_uploader.css, the
39
+ * package's only other dashed affordance. `section_slider.css` is imported after
40
+ * `card.css`, so this wins on source order at equal specificity.
41
+ *
42
+ * `hover:shadow-md` rather than a background change, mirroring
43
+ * `rsui-card--hoverable`: it reads the same on every variant, whereas shifting
44
+ * the background would fight whatever colour the variant just set.
45
+ *
46
+ * No `color` here on purpose. Card does not set one either — its text comes from
47
+ * whichever Section encloses it — and Tailwind's preflight already gives buttons
48
+ * `color: inherit`, so the card picks up the section the same way a real card
49
+ * does. Setting it explicitly produced the same pixels today but would have
50
+ * ignored a section that ever chose a different text colour.
51
+ *
52
+ * Height is not declared: the rail item is a flex container that stretches, so
53
+ * the shell takes its height from the tallest card in the track. A fixed height
54
+ * would go short or overhang the moment consumer cards changed.
55
+ *
56
+ * Width is a floor rather than a fixed value. The rail item has no intrinsic
57
+ * width — consumer cards supply their own — so `w-full` here would resolve
58
+ * against content and collapse to the label. `min-w-64` matches the common card
59
+ * width and still grows if the consumer puts something wider inside.
60
+ */
61
+ .rsui-section-slider__view-all {
62
+ @apply min-w-64 items-center justify-center gap-3 border-dashed p-space-lg text-center;
63
+ @apply font-semibold;
64
+ @apply hover:cursor-pointer hover:shadow-md;
65
+ @apply focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-primary outline-hidden;
66
+ }
67
+
68
+ /*
69
+ * The `ai` variant is the exception: Card gives it an animated gradient, which is
70
+ * a lot of movement for a card that is meant to be an opening rather than a
71
+ * destination. Cleared so the section's own background — itself the ai gradient
72
+ * when the slider is in that variant — shows through instead.
73
+ *
74
+ * `bg-none` and not `bg-transparent`: Card paints this one with
75
+ * `background-image`, which a background-colour reset leaves untouched. That is
76
+ * also why the other variants need no rule here — theirs are background colours,
77
+ * and they are meant to stay.
78
+ *
79
+ * Card has no `--bordered.--ai` companion the way the other variants do, so the
80
+ * border would otherwise fall back to the base grey. Brand-500 matches the border
81
+ * the `--ai` section itself uses.
82
+ */
83
+ .rsui-section-slider__view-all.rsui-card--ai {
84
+ @apply bg-none border-brand-500;
85
+ }
86
+
87
+ /*
88
+ * The icon slot, stacked above the label the way Empty stacks its image. Sizing
89
+ * belongs to whichever Icon component the consumer puts inside — those size
90
+ * their own SVG child.
91
+ *
92
+ * Colour, though, has to be taken back. Icon's variants pin the SVG to a fixed
93
+ * token (`--primary` is `text-text-primary`), which would leave the icon sitting
94
+ * still while the label darkens on hover, and dark on the brand-600 surface of
95
+ * `--featured` where the rest of the card has inverted. Forcing `currentColor`
96
+ * onto the SVG makes the icon inherit the button, so it moves and inverts with
97
+ * the label without the consumer having to pick a matching Icon variant.
98
+ *
99
+ * `IconCircleBackground` is excluded, though — it already carries a deliberate
100
+ * on-colour pairing, and forcing `currentColor` would put a dark glyph on its dark
101
+ * contrast-coloured circle.
102
+ *
103
+ * Both branches of the `:is()` need the guard. A bare `svg` alternative looks
104
+ * harmless but matches the heroicon inside a background icon just the same, and
105
+ * with the wrapper class in front the rule is (0,3,0) — above the (0,2,0) that
106
+ * paints the glyph — so it would win outright and defeat the exclusion this
107
+ * comment claims to make.
108
+ */
109
+ .rsui-section-slider__view-all-icon {
110
+ @apply flex items-center justify-center;
111
+ }
112
+
113
+ .rsui-section-slider__view-all-icon :is(.rsui-icon:not(.rsui-icon--background) svg, .rsui-icon:not(.rsui-icon--background) > *) {
114
+ color: currentColor;
115
+ }
116
+
117
+ /*
118
+ * Card's tokens are built for a light surface, and this variant is brand-600, so
119
+ * they invert here — the same thing the header title and subtitle already do for
120
+ * `--featured`.
121
+ *
122
+ * `bg-transparent` is load-bearing, not tidying. `.rsui-card` paints
123
+ * `bg-background-primary`, which is white, and nothing else in this cascade
124
+ * resets it — inverting only the foreground leaves white text and a white dashed
125
+ * border on a white card, i.e. an invisible card on a brand-600 slider. The card
126
+ * has to sit on the brand surface for the inversion to mean anything.
127
+ */
128
+ .rsui-section-slider--featured .rsui-section-slider__view-all {
129
+ @apply bg-transparent border-white/60 text-white/80;
130
+ @apply hover:border-white hover:bg-white/10 hover:text-white;
131
+ @apply focus-visible:ring-white;
132
+ }
133
+
134
+ /*
135
+ * Same reasoning on the `--ai` section, minus the inversion. That gradient is
136
+ * light enough for the default dark label to stay legible, so only the fill has
137
+ * to go — Card's white would otherwise sit on the gradient as a flat patch, which
138
+ * is the one place a ghost card should be showing what is behind it.
139
+ *
140
+ * `bg-transparent` and not `bg-none` here: the card's own background is a colour
141
+ * at this point, since `--ai` describes the section rather than the card. The
142
+ * `bg-none` rule above is for `viewAllVariant="ai"`, where Card paints a gradient
143
+ * via `background-image` instead.
144
+ */
145
+ .rsui-section-slider--ai .rsui-section-slider__view-all {
146
+ @apply bg-transparent;
147
+ @apply hover:bg-white/40;
148
+ }
149
+
150
+ /*
151
+ * The `focus-visible` ring below is not decorative. Below `lg` this is a real
152
+ * scroll port, and Chrome can make scroll containers keyboard-focusable so they
153
+ * can be arrow-keyed — measured at 500px, this one is focusable with
154
+ * `outline-style: none`, i.e. a focus stop with no visible indicator (WCAG
155
+ * 2.4.7). At `lg` and up the track is `overflow-x-hidden`, which is not
156
+ * user-scrollable and so not focusable, and the rule never fires.
157
+ *
158
+ * `ring-inset` rather than an offset ring: the slider root is `overflow-x-hidden`
159
+ * with `px-0`, so the track sits flush against it and an outset ring would be
160
+ * clipped away on both edges — and clipped a second time by the mask in
161
+ * `--hover-actions` mode. Same reason card_group.css and form_field_date_range.css
162
+ * use it.
163
+ */
29
164
  .rsui-section-slider__container {
30
165
  @apply relative flex gap-x-3 lg:gap-x-6 overflow-x-auto lg:overflow-x-hidden focus:outline-hidden scroll-smooth snap-x snap-mandatory scroll-pl-space-lg;
166
+ @apply focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-primary;
167
+ }
168
+
169
+ /*
170
+ * Position announcements for screen readers. The rail is a list of cards, not a
171
+ * carousel, so there is no slide to name -- what a non-sighted user cannot
172
+ * recover is simply how far along the rail they now are.
173
+ */
174
+ .rsui-section-slider__announcer {
175
+ @apply sr-only;
31
176
  }
32
177
 
33
178
  .rsui-section-slider__container::-webkit-scrollbar {
34
179
  @apply hidden;
35
180
  }
181
+
182
+ /*
183
+ * Edge fades for `hover-actions`. With the arrows held back until hover, a
184
+ * resting rail has nothing at all to say it moves, and the peeking last card is
185
+ * doing that work alone. Fading the edge that has content behind it gives the
186
+ * rail a permanent affordance that costs no hover, no touch and no semantics.
187
+ *
188
+ * A mask rather than a gradient overlay, because the fade has to work over four
189
+ * different section backgrounds — default, `--featured` (brand-600) and `--ai`
190
+ * (itself a gradient). Colour-matching an overlay to a gradient is not possible;
191
+ * masking the content to transparent is background-agnostic and needs no variant
192
+ * of its own.
193
+ *
194
+ * Both widths rest at 0, which collapses the gradient to fully opaque, so the
195
+ * mask is inert until Vue adds a fade modifier. Not transitioned: the widths
196
+ * would need registering via @property to animate at all, and the switch happens
197
+ * mid-scroll where the movement already covers it.
198
+ */
199
+ .rsui-section-slider__container {
200
+ --rsui-section-slider-fade-start: 0px;
201
+ --rsui-section-slider-fade-end: 0px;
202
+ }
203
+
204
+ .rsui-section-slider--hover-actions .rsui-section-slider__container {
205
+ mask-image: linear-gradient(
206
+ to right,
207
+ transparent 0,
208
+ black var(--rsui-section-slider-fade-start),
209
+ black calc(100% - var(--rsui-section-slider-fade-end)),
210
+ transparent 100%
211
+ );
212
+ }
213
+
214
+ .rsui-section-slider__container--fade-start {
215
+ --rsui-section-slider-fade-start: 3rem;
216
+ }
217
+
218
+ .rsui-section-slider__container--fade-end {
219
+ --rsui-section-slider-fade-end: 3rem;
220
+ }
221
+
222
+ /*
223
+ * Positioning context for the `hover-actions` overlay. Deliberately a wrapper
224
+ * around the track rather than the track itself: the container is the scroll
225
+ * port, so anything absolutely positioned inside it scrolls away with the cards.
226
+ */
227
+ .rsui-section-slider__viewport {
228
+ @apply relative;
229
+ }
230
+
231
+ /*
232
+ * Overlay navigation layer — one full-bleed layer rather than two individually
233
+ * positioned buttons, so `justify-between` places the pair on the two edges and
234
+ * `items-center` centres them on the rail without any transform maths.
235
+ *
236
+ * The layer spans the cards, so it has to stay transparent to the pointer;
237
+ * `pointer-events-none` here with `pointer-events-auto` back on the buttons
238
+ * keeps the cards underneath clickable.
239
+ *
240
+ * Hidden below `lg` on purpose. Up to that breakpoint the track is
241
+ * `overflow-x-auto` and swipe already moves it, so arrows over the artwork would
242
+ * be covering content to duplicate a gesture the user already has.
243
+ */
244
+ .rsui-section-slider__actions {
245
+ @apply hidden lg:flex absolute inset-0 z-10 items-center justify-between px-3 pointer-events-none;
246
+ }
247
+
248
+ /*
249
+ * Overlaid arrows sit on card artwork, so the header's transparent pill would
250
+ * leave the chevron competing with whatever is behind it. Give these ones a
251
+ * solid surface — scoped to the overlay so header arrows keep their existing
252
+ * look.
253
+ *
254
+ * This single-tone border does not *guarantee* 1.4.11 against arbitrary
255
+ * photography: its outer edge always has an unknown image beside it, so it fails
256
+ * on mid-tone imagery. It narrows the gap rather than closing it, helped by the
257
+ * fade behind the pill — though note the fade only covers the outer part of the
258
+ * pill's backdrop (the fade runs 0–3rem from the edge, the pill roughly
259
+ * 0.75–4.25rem), and only partially at that.
260
+ *
261
+ * A two-tone indicator would actually close it: two adjacent bands with 3:1
262
+ * against each other means at least one of them clears 3:1 against whatever is
263
+ * behind, whatever that turns out to be. Deliberately not done here — it is a
264
+ * visible design change to the control rather than a correctness fix, so it wants
265
+ * a decision rather than a commit.
266
+ */
267
+ .rsui-section-slider__actions .rsui-section-slider__action {
268
+ @apply pointer-events-auto bg-background-primary border-grey-600 shadow-xl;
269
+ }
270
+
271
+ @media (hover: hover) {
272
+ /*
273
+ * Rest hidden, reveal on the rail. Opacity rather than `display`/`hidden` so
274
+ * the buttons keep their box (no reflow as they appear) and stay reachable
275
+ * by keyboard — a focusable control that cannot be seen fails WCAG 2.4.7,
276
+ * which is what `:focus-within` below is for. The base class already carries
277
+ * `transition-all`, so the fade needs nothing added here.
278
+ *
279
+ * Gated on `(hover: hover)`: from `lg` up the track is `overflow-x-hidden`,
280
+ * so these arrows are the only way to move the rail. A touch device at a
281
+ * desktop width — iPad in landscape, touchscreen laptop — has no swipe and
282
+ * no hover, and would be left with no way to navigate at all. Those devices
283
+ * keep the arrows on permanently.
284
+ */
285
+ .rsui-section-slider__actions .rsui-section-slider__action {
286
+ @apply opacity-0;
287
+ }
288
+
289
+ /*
290
+ * `:enabled` so only arrows that can actually move the rail come forward.
291
+ * In rail mode that tracks scroll position, so a resting rail shows no
292
+ * previous arrow at all rather than one pointing into nothing, and the pair
293
+ * disappears entirely when every card already fits. The button stays in the
294
+ * DOM and in the a11y tree as disabled, which tells a screen reader user
295
+ * "start of the rail" — something an unmounted button cannot do.
296
+ */
297
+ .rsui-section-slider__viewport:hover .rsui-section-slider__action:enabled,
298
+ .rsui-section-slider__viewport:focus-within .rsui-section-slider__action:enabled {
299
+ @apply opacity-100;
300
+ }
301
+
302
+ /*
303
+ * Escape hides the arrows again while the pointer is still on the rail —
304
+ * WCAG 1.4.13 wants hover-revealed content that obscures other content to be
305
+ * dismissable without moving the pointer, and these do sit over the cards.
306
+ *
307
+ * `:not(:focus-within)` because dismissal must not win over the focus reveal
308
+ * above. Without it, pressing Escape with the pointer on the rail and focus on
309
+ * an arrow hid a control that was still focused and still clickable — the
310
+ * exact 2.4.7 failure the reveal rule exists to prevent.
311
+ *
312
+ * `:enabled` carries no extra weight of its own here: this selector and the
313
+ * reveal above both compute to (0,4,0), so it only brings this level and the
314
+ * cascade decides on source order. That is why this block has to stay last
315
+ * inside the query.
316
+ */
317
+ .rsui-section-slider__viewport--navigation-dismissed:not(:focus-within):hover .rsui-section-slider__action:enabled {
318
+ @apply opacity-0;
319
+ }
320
+ }
321
+
322
+ /*
323
+ * Reduced motion. This rule covers swipe and keyboard scrolling, which never go
324
+ * through JavaScript. Arrow presses are handled separately in scrollToPosition,
325
+ * which passes an explicit `'instant'` — a scripted `behavior` other than `auto`
326
+ * overrides `scroll-behavior`, so the two paths do not overlap and neither is
327
+ * redundant.
328
+ */
329
+ @media (prefers-reduced-motion: reduce) {
330
+ .rsui-section-slider__container {
331
+ scroll-behavior: auto;
332
+ }
333
+
334
+ .rsui-section-slider__action {
335
+ transition: none;
336
+ }
337
+ }
@@ -15,6 +15,66 @@
15
15
  @apply w-full sm:w-fit;
16
16
  }
17
17
 
18
+ /* Bordered — for a switcher on a surface that is not white. Opt in with the
19
+ `bordered` prop.
20
+
21
+ The track is `background-disabled` (Grey-25, #fcfcfc) and a secondary Section is
22
+ `background-secondary` (Grey-50, #fafafa): 2/255 apart, and the track is the
23
+ *lighter* of the two, so on a grey surface it does not read as a container at all.
24
+
25
+ A border rather than a darker fill, so the tonal well stays the same grey
26
+ everywhere it already works. Note it does add 2px to each axis, so a bordered
27
+ switcher no longer aligns exactly with an unbordered sibling.
28
+
29
+ `:not(--chips)` on both new modifiers: chips deliberately has no track
30
+ (`bg-transparent p-0 rounded-none`), so a border there is a square hairline box
31
+ hugging the chips with a clipped shadow — broken-looking rather than opt-out-able.
32
+ Excluded here rather than reset in the chips block, so no existing chips rule
33
+ changes. */
34
+ .rsui-switcher--bordered:not(.rsui-switcher--chips) {
35
+ @apply border border-border-secondary;
36
+ }
37
+
38
+ /* The active item is already delineated by its own Grey-300 border
39
+ (`switcher_item.css`, `--active` sets `border-border-secondary` against a base of
40
+ `border-transparent`). Once the container gains a Grey-300 border too, the pill
41
+ edge and the track edge are the same colour and the nesting reads flat. The shadow
42
+ re-separates the two by depth rather than by adding a third colour.
43
+
44
+ `shadow-sm` rather than a raw `box-shadow`, which would be the only literal shadow
45
+ in the package — every other component uses a utility. */
46
+ .rsui-switcher--bordered:not(.rsui-switcher--chips) .rsui-switcher-item--active {
47
+ @apply shadow-sm;
48
+ }
49
+
50
+ /* Inline — one row below `sm`, split between the options. Opt in with the `inline`
51
+ prop, and pair it with `full`: `inline` sets the direction, `full` gives it a row
52
+ to split. On its own the container is still `w-fit` and shrink-wraps.
53
+
54
+ Scoped to `max-sm` on purpose: at `sm` and above the default is already a row, so
55
+ there is nothing to change and nothing to accidentally break — `--full`'s `flex-1`
56
+ in particular is left entirely alone. Only the sub-`sm` stack is overridden.
57
+
58
+ Three declarations, all load-bearing:
59
+ - `w-auto` undoes `w-full` from `.rsui-switcher .rsui-switcher-item`, which would
60
+ otherwise make each option claim the full width of a row it is now sharing.
61
+ This works by source order at equal specificity (0,2,0), so this block must
62
+ stay BELOW that base item rule.
63
+ - `flex-1` is what actually splits the row between the options.
64
+ - `min-w-0` is what stops it clipping. Flex items floor at `min-width: auto`, and
65
+ the item is `whitespace-nowrap`, so without this they refuse to shrink below
66
+ their label, the row outgrows the `overflow-hidden` track, and options are cut
67
+ off with no scrollbar and no way to reach them — measured at 166px of a 360px
68
+ track lost with three long labels. `overflow-hidden` on the item keeps a long
69
+ label inside its own button rather than spilling over its neighbour. */
70
+ .rsui-switcher--inline:not(.rsui-switcher--chips) {
71
+ @apply max-sm:flex-row max-sm:items-center;
72
+ }
73
+
74
+ .rsui-switcher--inline:not(.rsui-switcher--chips) .rsui-switcher-item {
75
+ @apply max-sm:w-auto max-sm:flex-1 max-sm:min-w-0 max-sm:overflow-hidden;
76
+ }
77
+
18
78
  /* Chips variant — a filter chip set rather than a segmented control. Opt in with
19
79
  the `chips` prop; the default is untouched.
20
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-tailwindcss",
3
- "version": "7.56.0",
3
+ "version": "7.57.0",
4
4
  "description": "RedSeed UI Tailwindcss",
5
5
  "main": "index.js",
6
6
  "style": "index.css",