@redseed/redseed-ui-tailwindcss 7.55.0 → 7.56.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/colors.css CHANGED
@@ -565,7 +565,20 @@
565
565
  --Colors-Warm-Mauve-800: #855878;
566
566
  --Colors-Warm-Mauve-900: #6a4660;
567
567
  --Colors-Warm-Mauve-950: #4c3145;
568
- /* Secondary brand accents (single tokens, Figma Brand palette · Secondary) */
568
+ /* Secondary brand accents (single tokens, Figma Brand palette · Secondary)
569
+
570
+ DO NOT USE these in product surfaces. Components and pages draw only on
571
+ the six primary identity ramps above — Journey Green, Deep Teal, Modern
572
+ Bone, Warm Yellow, Soft Lilac, Warm Mauve — plus the Grey ramp and the
573
+ semantic tokens (success / warning / error / info).
574
+
575
+ They are defined here for completeness against the Figma palette, which
576
+ is why they have no ramp: single values only, so they cannot carry text
577
+ either. Cornflower measures 3.2:1 on white and Apricot Lift 2.25:1, both
578
+ short of the 4.5:1 that WCAG 1.4.3 asks of body text.
579
+
580
+ One accepted exception: --color-button-primary-hover consumes Plum Ink
581
+ (see tokens.css). That is deliberate and predates this note. */
569
582
  --Colors-Apricot-Lift: #e89b6c;
570
583
  --Colors-Cornflower: #6e8fd6;
571
584
  --Colors-Marigold: #f2b33c;
@@ -1,19 +1,30 @@
1
1
  .rsui-badge {
2
- @apply inline-flex items-center justify-center gap-x-1;
2
+ /* whitespace-nowrap: a badge is a pill, so its label must stay on one line. Without it
3
+ a badge in a narrow column wrapped to two lines and grew to 42px tall — seen in
4
+ production on the Twinsies home page, where "1 answer to redo" and "2 answers to redo"
5
+ both broke across two lines inside a rounded-full chip. Every other chip-shaped
6
+ control in the system already declares this: Pill, ButtonSlot, TabSliderItem and
7
+ SwitcherItem. Badge was the only one that did not. */
8
+ @apply inline-flex items-center justify-center gap-x-1 whitespace-nowrap;
3
9
  @apply w-fit h-fit rounded-full;
4
10
  @apply text-sm font-medium leading-4;
5
11
  }
6
12
 
13
+ /* Horizontal padding grows with the size: it used to sit at px-2 for xs, sm and md alike,
14
+ so a md badge was exactly as wide as an xs one and its 8/8 padding collapsed to square —
15
+ on a rounded-full shape that puts the label closer to the curved ends than to the flat
16
+ top and bottom. The 8 / 10 / 12 progression matches Pill, the other chip in the system.
17
+ Vertical padding is untouched, so no badge changes height. */
7
18
  .rsui-badge--xs {
8
19
  @apply text-xs leading-3 px-2 py-0.5;
9
20
  }
10
21
 
11
22
  .rsui-badge--sm {
12
- @apply px-2 py-1;
23
+ @apply px-2.5 py-1;
13
24
  }
14
25
 
15
26
  .rsui-badge--md {
16
- @apply px-2 py-2;
27
+ @apply px-3 py-2;
17
28
  }
18
29
 
19
30
  .rsui-badge--secondary {
@@ -40,6 +51,12 @@
40
51
  @apply text-brand-500 bg-brand-50 border border-brand-200;
41
52
  }
42
53
 
54
+ /* Classic and Coach still ride the pre-rebrand pink and redseed-blue ramps rather than the
55
+ current brand ones (Skillscheck above already moved to Journey Green). Left as-is on
56
+ purpose: both categories are due to be phased out, so re-homing them onto brand colours
57
+ would be churn. There is also only one unclaimed brand ramp (Deep Teal) against two
58
+ categories — Journey Green, Warm Mauve and Modern Bone are taken by brand, AI and page
59
+ grounds. Revisit only if the phase-out stalls. */
43
60
  .rsui-badge--classic {
44
61
  @apply text-pink-500 bg-pink-50 border border-pink-200;
45
62
  }
@@ -1,5 +1,8 @@
1
+ /* ButtonCard now takes the family's tonal hover from .rsui-card--hoverable, which it opts
2
+ into via :hoverable="!disabled". It previously cancelled the card shadow and darkened the
3
+ border instead — the only bespoke hover treatment in the family, and the reason hovering
4
+ a ButtonCard looked nothing like hovering a Card. */
1
5
  .rsui-button-card {
2
- @apply hover:shadow-none hover:border-text-primary;
3
6
  }
4
7
 
5
8
  .rsui-button-card--disabled {
@@ -53,8 +53,13 @@
53
53
  }
54
54
 
55
55
  /* modifier xs size */
56
+ /* px-2.5, not px-1.5: horizontal padding steps 10/12/14/16/18/20 across xs..2xl, and
57
+ xs was the only size where px equalled py — every other size runs 1.4-1.6x wider than
58
+ tall, so an xs label sat 6px from the border and read visibly cramped. Height is
59
+ unchanged (py untouched); only the button's width grows. --xs-icon keeps its own p-1.5
60
+ because a square icon button is correct at that size. */
56
61
  .rsui-button-slot--xs {
57
- @apply text-sm px-1.5 py-1.5 gap-1;
62
+ @apply text-sm px-2.5 py-1.5 gap-1;
58
63
  }
59
64
 
60
65
  /* modifier xs size with icon */
@@ -3,13 +3,20 @@
3
3
  @apply text-button-tertiary bg-transparent border-transparent;
4
4
 
5
5
  /* default hover state */
6
- @apply hover:text-button-tertiary-hover hover:bg-transparent hover:border-button-tertiary-hover;
6
+ /* A ghost button fills faintly on hover; it must not sprout an outline. This used to
7
+ set border-button-tertiary-hover, taking the border from transparent to solid
8
+ #124943 — so hovering a borderless button made a hard dark rule appear, while the
9
+ fill stayed transparent. That is the inverse of the intended ghost behaviour, and
10
+ the opposite of primary (fill darkens) and secondary (outline darkens). */
11
+ @apply hover:text-button-tertiary-hover hover:bg-background-secondary hover:border-transparent;
7
12
 
8
13
  /* default focus state */
9
14
  @apply focus-visible:ring-button-tertiary focus-visible:border-transparent;
10
15
 
11
16
  /* default active state */
12
- @apply active:bg-transparent active:border-transparent;
17
+ /* Pressed goes one step darker than hover (transparent -> Grey-50 -> Grey-100) so the
18
+ press reads as deeper rather than clearing the hover fill back to nothing. */
19
+ @apply active:text-button-tertiary-active active:bg-background-tertiary-invert active:border-transparent;
13
20
 
14
21
  /* default disabled state */
15
22
  @apply disabled:bg-transparent disabled:border-transparent disabled:text-text-disabled;
@@ -68,6 +68,9 @@
68
68
  @apply relative border-0;
69
69
  }
70
70
 
71
+ /* Positioning only. The dashed outline itself is drawn by the <rect> inside this element —
72
+ it is an <svg>, and the rect strokes 1px with stroke-dasharray 8 16. Do not add CSS border
73
+ utilities here: they paint a second dashed edge on top of the SVG stroke. */
71
74
  .rsui-card__draft-border {
72
75
  @apply absolute inset-0 size-full pointer-events-none rounded-xl border-border-secondary;
73
76
  }
@@ -79,8 +82,20 @@
79
82
  @apply overflow-x-hidden;
80
83
  }
81
84
 
82
- .rsui-card--hoverable {
83
- @apply hover:shadow-md;
85
+ /* Hover state layer. The card family is an Outlined system — flat, 1px border, no resting
86
+ elevation — so hover tints the surface rather than lifting it. Two reasons this is a
87
+ ::before scrim and not a background-color swap: every colour variant (secondary, brand,
88
+ success, info, warning, error, draft, ai) has to survive underneath it, and a scrim
89
+ composites over the ai variant's gradient where a flat colour could not. ::before rather
90
+ than ::after so it paints beneath the card's content in DOM order. */
91
+ .rsui-card--hoverable::before {
92
+ content: '';
93
+ @apply absolute inset-0 rounded-xl pointer-events-none;
94
+ @apply bg-background-primary-contrast opacity-0 transition-opacity duration-200;
95
+ }
96
+
97
+ .rsui-card--hoverable:hover::before {
98
+ @apply opacity-5;
84
99
  }
85
100
 
86
101
  .rsui-card--clickable {
@@ -105,6 +120,12 @@
105
120
  colour alone (WCAG 1.4.1). */
106
121
  .rsui-card__action-layer--active {
107
122
  @apply ring-2 ring-offset-2 ring-brand-500;
123
+
124
+ /* Hold the brand ring through focus. Without this the focus-visible rule above repaints
125
+ the ring border-primary grey, so focusing a selected card silently dropped its
126
+ selection cue — the ring is the same geometry for both states, so only colour carries
127
+ the difference. */
128
+ @apply focus-visible:ring-brand-500;
108
129
  }
109
130
 
110
131
  .rsui-card__action-layer-label {
@@ -28,6 +28,17 @@
28
28
  @apply @md/card-header:items-center;
29
29
  }
30
30
 
31
+ /* Centring only holds while the text block is a lone title. `--single-line` means the *title*
32
+ is clamped to one line, not that the header is one row — add a subtitle and a badge and the
33
+ block is 78px tall, where centring a 48px avatar left it sitting 15px low against the text
34
+ it belongs to. Seen in production on the Twinsies meeting cards. Top-align as soon as the
35
+ block carries more than the title; a lone title is shorter than the avatar, so centring
36
+ stays correct there. */
37
+ .rsui-card-header__header--single-line:has(.rsui-card-header__subtitle),
38
+ .rsui-card-header__header--single-line:has(.rsui-card-header__badge) {
39
+ @apply @md/card-header:items-start;
40
+ }
41
+
31
42
  .rsui-card-header__avatar {
32
43
  @apply w-12 overflow-hidden flex items-center justify-center;
33
44
  }
@@ -83,7 +83,10 @@
83
83
  /* Success state styling */
84
84
  /* Increase specificity so it overrides base .rsui-card styles */
85
85
  .rsui-card.rsui-checkbox-card--success {
86
- @apply bg-success-100 border-border-success;
86
+ /* bg-background-success, not the raw bg-success-100 palette step: this was the only
87
+ success surface in the library painting #DCFAE6 while Card, Badge, MessageBox and
88
+ Section all use the #ECFDF3 token — two different success greens, same border. */
89
+ @apply bg-background-success border-border-success;
87
90
  }
88
91
 
89
92
  .rsui-checkbox-card--success .rsui-checkbox-card__title-text {
@@ -78,7 +78,9 @@
78
78
  }
79
79
 
80
80
  .rsui-comment__body {
81
- @apply py-3 px-4 rounded-xl border;
81
+ /* 14px vertical, matching the row rhythm ListItem uses — a comment is a row of prose
82
+ and read tight against its neighbour at 12px. */
83
+ @apply py-3.5 px-4 rounded-xl border;
82
84
  @apply rounded-tl-none ml-8;
83
85
  @apply text-text-primary bg-background-primary;
84
86
  @apply border-border-primary;
@@ -7,7 +7,9 @@
7
7
  }
8
8
 
9
9
  .rsui-form-field-checkbox__check {
10
- @apply size-6 shrink-0 relative bg-background-primary rounded-lg cursor-pointer;
10
+ /* Centres the tick. The input is absolutely positioned so it stays out of flow and keeps
11
+ filling the box; only the SVG is laid out here. */
12
+ @apply size-6 shrink-0 relative flex items-center justify-center bg-background-primary rounded-lg cursor-pointer;
11
13
  @apply has-[:disabled]:text-text-disabled has-[:disabled]:cursor-default;
12
14
  }
13
15
 
@@ -16,11 +18,21 @@
16
18
  @apply w-full h-full text-text-primary border border-border-primary text-transparent bg-none;
17
19
  @apply focus-visible:ring-3 focus-visible:ring-border-primary focus-visible:border-border-primary;
18
20
  @apply invalid:border-border-error invalid:ring-0;
21
+
22
+ /* Checked fills the box and the tick inverts, which is how every mainstream checkbox
23
+ reads. It used to stay transparent, so a checked box was a white square with a dark
24
+ tick — and the success variant already filled, so the default was the odd one out. */
25
+ @apply checked:bg-text-primary checked:border-text-primary;
26
+
19
27
  @apply disabled:text-text-disabled disabled:bg-background-disabled disabled:cursor-default;
28
+ @apply disabled:checked:bg-background-disabled disabled:checked:border-border-disabled;
20
29
  }
21
30
 
31
+ /* The tick is inset rather than size-full. At size-full a 24px glyph sat in a 24px box with
32
+ no breathing room, so its strokes ran into the border and the control read cramped. Each
33
+ size keeps roughly a 4px optical inset — see the sm/xs overrides further down. */
22
34
  .rsui-form-field-checkbox__check svg {
23
- @apply size-full relative z-1 pointer-events-none stroke-[2.5] origin-center;
35
+ @apply size-4 relative z-1 pointer-events-none stroke-[2.5] origin-center;
24
36
  /* Short, cute draw-on: the tick strokes itself in with a tiny pop.
25
37
  dasharray is set a touch above the check path length so the resting
26
38
  state has no gap. Disabled under reduced-motion below. */
@@ -28,6 +40,12 @@
28
40
  animation: rsui-checkbox-tick 200ms ease-out;
29
41
  }
30
42
 
43
+ /* Invert the tick against the filled box. Scoped to :not(:disabled) because a disabled
44
+ checked box keeps the pale disabled fill, where an inverted tick would disappear. */
45
+ .rsui-form-field-checkbox__check:has(input:checked:not(:disabled)) svg {
46
+ @apply text-text-on-color;
47
+ }
48
+
31
49
  @keyframes rsui-checkbox-tick {
32
50
  0% { stroke-dashoffset: 30; transform: scale(0.8); }
33
51
  60% { transform: scale(1.06); }
@@ -53,6 +71,10 @@
53
71
  @apply rounded-md;
54
72
  }
55
73
 
74
+ .rsui-form-field-checkbox__check--sm svg {
75
+ @apply size-3;
76
+ }
77
+
56
78
  .rsui-form-field-checkbox__label--sm {
57
79
  @apply leading-5 text-sm;
58
80
  }
@@ -65,6 +87,10 @@
65
87
  @apply rounded;
66
88
  }
67
89
 
90
+ .rsui-form-field-checkbox__check--xs svg {
91
+ @apply size-2;
92
+ }
93
+
68
94
  .rsui-form-field-checkbox__label--xs {
69
95
  @apply leading-4 text-xs;
70
96
  }
@@ -1,4 +1,15 @@
1
+ /* The field group owns its own spacing. It used to render a bare list of siblings and
2
+ inherit spacing from `.rsui-form-slot__content > *`, so anywhere that ancestor was absent —
3
+ including every one of its own stories — the fields and the submit button collapsed flush
4
+ against each other with a 0px gap. gap-space-md (16px) matches the mt-4 that rule applied,
5
+ so the group is unchanged inside FormSlot and correct everywhere else. */
6
+ .rsui-form-fields-login {
7
+ @apply flex flex-col gap-space-md;
8
+ }
9
+
10
+ /* pt-4 dropped: the group's gap-space-md now supplies the 16px above this link, and
11
+ keeping both stacked them into 32px. */
1
12
  .rsui-form-login__signup {
2
- @apply text-sm pt-4;
13
+ @apply text-sm;
3
14
  }
4
15
 
@@ -1,4 +1,15 @@
1
+ /* The field group owns its own spacing. It used to render a bare list of siblings and
2
+ inherit spacing from `.rsui-form-slot__content > *`, so anywhere that ancestor was absent —
3
+ including every one of its own stories — the fields and the submit button collapsed flush
4
+ against each other with a 0px gap. gap-space-md (16px) matches the mt-4 that rule applied,
5
+ so the group is unchanged inside FormSlot and correct everywhere else. */
6
+ .rsui-form-fields-register {
7
+ @apply flex flex-col gap-space-md;
8
+ }
9
+
10
+ /* pt-4 dropped: the group's gap-space-md now supplies the 16px above this link, and
11
+ keeping both stacked them into 32px. */
1
12
  .rsui-form-register__signin {
2
- @apply text-sm pt-4;
13
+ @apply text-sm;
3
14
  }
4
15
 
@@ -1,5 +1,9 @@
1
1
  .rsui-list-item {
2
- @apply relative flex flex-col rounded-xl p-3 overflow-hidden;
2
+ /* 14px vertical rather than the 12px of a flat p-3: at 12px a two-line row reads as
3
+ cramped against its neighbours, which is the note that came back from laying these
4
+ out on a real page. Horizontal padding is unchanged. Half-steps are already used
5
+ across the button sizes, so 3.5 is in keeping. */
6
+ @apply relative flex flex-col rounded-xl px-3 py-3.5 overflow-hidden;
3
7
  }
4
8
 
5
9
  .rsui-list-item--clickable {
@@ -69,6 +73,65 @@
69
73
  @apply mt-2 flex flex-wrap gap-x-4 gap-y-2;
70
74
  }
71
75
 
76
+ /* Leading icon. Inline, unlike .rsui-list-item__avatar above, which hangs outside the row
77
+ box on a negative offset — an avatar identifies a person, an icon classifies the row, so
78
+ they sit differently and can be used together. */
79
+ .rsui-list-item__icon {
80
+ @apply shrink-0 size-8 rounded-lg overflow-hidden;
81
+ @apply flex items-center justify-center;
82
+ @apply bg-background-secondary;
83
+ }
84
+
85
+ .rsui-list-item__icon svg {
86
+ @apply size-4;
87
+ }
88
+
89
+ /* Accent rail — a pseudo-element, so an accented row has the same DOM as a plain one.
90
+ Needs z-1 to sit above .rsui-list-item__bg, which is absolute and would otherwise paint
91
+ over it; the root's rounded-xl clips the rail into the corner. */
92
+ .rsui-list-item--accented::before {
93
+ content: '';
94
+ @apply absolute left-0 top-0 bottom-0 w-1 z-1 pointer-events-none;
95
+ }
96
+
97
+ .rsui-list-item--accent-warning::before {
98
+ @apply bg-background-warning-contrast;
99
+ }
100
+
101
+ .rsui-list-item--accent-info::before {
102
+ @apply bg-background-info-contrast;
103
+ }
104
+
105
+ .rsui-list-item--accent-success::before {
106
+ @apply bg-background-success-contrast;
107
+ }
108
+
109
+ .rsui-list-item--accent-error::before {
110
+ @apply bg-background-error-contrast;
111
+ }
112
+
113
+ .rsui-list-item--accent-neutral::before {
114
+ @apply bg-background-primary-contrast;
115
+ }
116
+
117
+ /* The icon tile takes the accent's own tint when both are used, so the rail and the tile
118
+ read as one mark rather than two. */
119
+ .rsui-list-item--accent-warning .rsui-list-item__icon {
120
+ @apply bg-background-warning;
121
+ }
122
+
123
+ .rsui-list-item--accent-info .rsui-list-item__icon {
124
+ @apply bg-background-info;
125
+ }
126
+
127
+ .rsui-list-item--accent-success .rsui-list-item__icon {
128
+ @apply bg-background-success;
129
+ }
130
+
131
+ .rsui-list-item--accent-error .rsui-list-item__icon {
132
+ @apply bg-background-error;
133
+ }
134
+
72
135
  .rsui-list-item__description {
73
136
  @apply line-clamp-2 mt-2 text-base;
74
137
  }
@@ -22,6 +22,20 @@
22
22
  @apply bg-background-error border-border-error text-text-primary;
23
23
  }
24
24
 
25
+ /* AI. Same surface language ButtonAI and .rsui-card--ai already use — the warm
26
+ gradient with the Warm Mauve border — so an AI message reads as the same
27
+ family as an AI action. The title carries text-ai for identity; the body stays
28
+ text-primary, because a whole paragraph in Warm Mauve is harder to read and
29
+ the border plus gradient already say "AI". */
30
+ .rsui-message-box--ai {
31
+ @apply bg-linear-to-tr from-background-ai-from to-background-ai-to;
32
+ @apply border-border-ai text-text-primary;
33
+ }
34
+
35
+ .rsui-message-box--ai .rsui-message-box__title {
36
+ @apply text-text-ai;
37
+ }
38
+
25
39
  .rsui-message-box__head {
26
40
  @apply flex justify-between items-center gap-x-2;
27
41
  }
@@ -2,16 +2,23 @@
2
2
  @apply flex flex-col gap-y-1;
3
3
  }
4
4
 
5
+ /* gap-x rather than space-x: space-* works by adding margins to children, which is the
6
+ legacy pattern, double-counts against any gap and breaks once the row wraps. Every other
7
+ component in the library spaces with gap. */
5
8
  .rsui-meta-info__top {
6
- @apply flex space-x-1 items-center;
9
+ @apply flex gap-x-1 items-center;
7
10
  }
8
11
 
9
12
  .rsui-meta-info__label-icon {
10
13
  @apply flex items-center justify-center;
11
14
  }
12
15
 
16
+ /* The label is the descriptor, so it recedes: medium weight in the tertiary tone. It used to
17
+ be semibold near-black while the value was regular grey, which inverted the hierarchy — the
18
+ eye landed on "Age" instead of on the number. Grey-600 on white measures 7.26:1, so it stays
19
+ comfortably readable at 12px. */
13
20
  .rsui-meta-info__label {
14
- @apply font-semibold text-xs text-text-primary;
21
+ @apply font-medium text-xs text-text-tertiary;
15
22
  }
16
23
 
17
24
  .rsui-meta-info__help {
@@ -22,9 +29,10 @@
22
29
  @apply focus-visible:ring-offset-2 focus-visible:ring-2 focus-visible:ring-border-primary;
23
30
  }
24
31
 
32
+ /* The value carries the information, so it takes the emphasis the label gave up. */
25
33
  .rsui-meta-info__value {
26
34
  @apply flex items-center gap-x-space-xs;
27
- @apply text-xs text-text-secondary;
35
+ @apply text-xs font-semibold text-text-primary;
28
36
  }
29
37
 
30
38
  .rsui-meta-info--md .rsui-meta-info__label {
@@ -37,4 +37,73 @@
37
37
  selection ring (WCAG 1.4.1); the brand number is colour reinforcement only. */
38
38
  .rsui-metric-card--active .rsui-metric-card__number {
39
39
  @apply text-text-brand;
40
- }
40
+ }
41
+
42
+ /* ---------------------------------------------------------------------------
43
+ Tonal variants
44
+
45
+ Same vocabulary and same mechanism as .rsui-page-header — green / teal /
46
+ light, with surface, ink, muted ink and rule moving together as a named set,
47
+ carried on --rsui-metric-card-* custom properties. Plain properties rather
48
+ than new @theme keys, for the reasons in #285.
49
+
50
+ The tile is a step LIGHTER than the band it sits on (Journey Green 700 on an
51
+ 800 band), so it reads as a raised panel. Each tone names an explicit surface
52
+ rather than a translucent white, so a toned tile still works standing alone
53
+ where there is no band beneath it to show through.
54
+
55
+ metric_card.css imports after card.css, so these single-class rules out-rank
56
+ .rsui-card's own surface at equal specificity. Combining a tone with one of
57
+ Card's semantic variants (brand, success, …) is not supported — pick one.
58
+ --------------------------------------------------------------------------- */
59
+ .rsui-metric-card--toned {
60
+ background: var(--rsui-metric-card-surface, var(--color-background-secondary));
61
+ border-color: var(--rsui-metric-card-rule, var(--color-border-primary));
62
+ }
63
+
64
+ .rsui-metric-card--tone-green {
65
+ --rsui-metric-card-surface: var(--Colors-Journey-Green-700);
66
+ --rsui-metric-card-ink: var(--Colors-Base-white);
67
+ /* 200, not the 300 PageHeader uses. The tile surface is a step lighter than the
68
+ header band it sits on, and 300 on 700 measures 4.46:1 — a hair under AA. */
69
+ --rsui-metric-card-ink-muted: var(--Colors-Journey-Green-200);
70
+ --rsui-metric-card-rule: rgba(255, 255, 255, 0.18);
71
+ }
72
+
73
+ .rsui-metric-card--tone-teal {
74
+ --rsui-metric-card-surface: var(--Colors-Deep-Teal-600);
75
+ --rsui-metric-card-ink: var(--Colors-Base-white);
76
+ --rsui-metric-card-ink-muted: var(--Colors-Deep-Teal-300);
77
+ --rsui-metric-card-rule: rgba(255, 255, 255, 0.18);
78
+ }
79
+
80
+ /* The white-label case: same layout, inverted ink. */
81
+ .rsui-metric-card--tone-light {
82
+ --rsui-metric-card-surface: var(--Colors-Modern-Bone-200);
83
+ --rsui-metric-card-ink: var(--Colors-Grey-900);
84
+ --rsui-metric-card-ink-muted: var(--Colors-Grey-600);
85
+ --rsui-metric-card-rule: var(--Colors-Grey-300);
86
+ }
87
+
88
+ /* Tone-agnostic: only the variable values differ, so the light tone resolves
89
+ these same selectors to dark ink. */
90
+ .rsui-metric-card--toned .rsui-metric-card__number {
91
+ color: var(--rsui-metric-card-ink);
92
+ }
93
+
94
+ .rsui-metric-card--toned .rsui-metric-card__label {
95
+ color: var(--rsui-metric-card-ink-muted);
96
+ }
97
+
98
+ .rsui-metric-card--toned .rsui-metric-card__icon {
99
+ color: var(--rsui-metric-card-ink-muted);
100
+ }
101
+
102
+ /* The active state's brand-coloured number is colour reinforcement only — the
103
+ Card action layer carries the actual selection ring (WCAG 1.4.1). On a tonal
104
+ surface that reinforcement has to go: text-brand is Brand-500 (#136c52),
105
+ which against the green tile's #0e5942 is about 1.3:1 and simply invisible.
106
+ The ring still does the work. */
107
+ .rsui-metric-card--toned.rsui-metric-card--active .rsui-metric-card__number {
108
+ color: var(--rsui-metric-card-ink);
109
+ }
@@ -77,3 +77,79 @@
77
77
  @apply flex flex-col gap-1 lg:flex-row lg:items-center lg:gap-4;
78
78
  }
79
79
 
80
+
81
+ /* ---------------------------------------------------------------------------
82
+ Tonal variants
83
+
84
+ White-labelling is why these are named sets rather than a background prop.
85
+ Pass a colour alone and a client who picks a light one gets white text on
86
+ white; background, ink, muted ink and rule have to move together, the way
87
+ Card handles its variants.
88
+
89
+ Plain custom properties rather than new @theme keys, for two reasons: the
90
+ four values must travel as a set, and a new theme key does not reliably
91
+ generate a utility — the trap in #285 that made every focus ring invisible.
92
+ Same pattern .rsui-stage-dial already uses for its accent.
93
+
94
+ A toned header carries its own padding, because the band has to read as a
95
+ band. Place it outside the page's content gutter so it bleeds to the edges;
96
+ an untoned header is unchanged and still inherits the layout's padding.
97
+ --------------------------------------------------------------------------- */
98
+ .rsui-page-header--toned {
99
+ /* Fallbacks keep an unrecognised tone legible rather than transparent. */
100
+ background: var(--rsui-page-header-bg, var(--color-background-secondary));
101
+
102
+ @apply py-space-lg px-space-xl;
103
+ }
104
+
105
+ .rsui-page-header--tone-green {
106
+ --rsui-page-header-bg: var(--Colors-Journey-Green-800);
107
+ --rsui-page-header-ink: var(--Colors-Base-white);
108
+ --rsui-page-header-ink-muted: var(--Colors-Journey-Green-300);
109
+ --rsui-page-header-rule: rgba(255, 255, 255, 0.18);
110
+ }
111
+
112
+ .rsui-page-header--tone-teal {
113
+ --rsui-page-header-bg: var(--Colors-Deep-Teal-700);
114
+ --rsui-page-header-ink: var(--Colors-Base-white);
115
+ --rsui-page-header-ink-muted: var(--Colors-Deep-Teal-300);
116
+ --rsui-page-header-rule: rgba(255, 255, 255, 0.18);
117
+ }
118
+
119
+ /* The white-label case. Same layout, inverted ink — a client's own background
120
+ may be light, so the component cannot assume light-on-dark. */
121
+ .rsui-page-header--tone-light {
122
+ --rsui-page-header-bg: var(--Colors-Base-white);
123
+ --rsui-page-header-ink: var(--Colors-Grey-900);
124
+ --rsui-page-header-ink-muted: var(--Colors-Grey-600);
125
+ --rsui-page-header-rule: var(--Colors-Grey-300);
126
+ }
127
+
128
+ /* Ink application is tone-agnostic: only the variable values differ, so the
129
+ light tone resolves these to dark ink through the same rules. */
130
+ .rsui-page-header--toned .rsui-page-header__title h1 {
131
+ color: var(--rsui-page-header-ink);
132
+ }
133
+
134
+ .rsui-page-header--toned .rsui-page-header__subtitle {
135
+ color: var(--rsui-page-header-ink-muted);
136
+ }
137
+
138
+ .rsui-page-header--toned .rsui-page-header__meta {
139
+ border-top: 1px solid var(--rsui-page-header-rule);
140
+
141
+ @apply mt-space-md pt-space-md;
142
+ }
143
+
144
+ /* MetaInfo sets its own dark label and value tokens, which disappear on a dark
145
+ band. Reaching into another component's classes is not ideal, but an
146
+ invisible meta row is worse, and what a slot looks like on this component's
147
+ own tonal surface is this stylesheet's business. */
148
+ .rsui-page-header--toned .rsui-meta-info__label {
149
+ color: var(--rsui-page-header-ink);
150
+ }
151
+
152
+ .rsui-page-header--toned .rsui-meta-info__value,
153
+ .rsui-page-header--toned .rsui-meta-info__help {
154
+ color: var(--rsui-page-header-ink-muted);
155
+ }
@@ -1,25 +1,50 @@
1
1
  .rsui-radio-card {
2
2
  @apply relative cursor-pointer transition-all;
3
- @apply rounded-xl shadow-xs outline-hidden select-none;
4
- @apply flex flex-col gap-3 p-3 max-w-80 w-80;
3
+ /* No resting shadow: this was the only card in the family that sat lifted while the
4
+ other seven are flat. Padding matches the family's 24/32 rhythm rather than the 12/12
5
+ it used to carry alone. ring-offset-2 matches the focus geometry every other card
6
+ uses — this ring previously hugged the border with no offset. */
7
+ @apply rounded-xl outline-hidden select-none;
8
+ @apply flex flex-col gap-3 py-space-lg px-space-xl max-w-80 w-80;
5
9
  @apply bg-background-primary border border-border-primary;
6
- @apply focus-visible:ring-2 focus-visible:ring-border-primary;
10
+ @apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-primary;
11
+ }
12
+
13
+ /* Hover state layer, matching .rsui-card--hoverable. ::before because ::after already
14
+ carries the disabled scrim below. */
15
+ .rsui-radio-card::before {
16
+ content: '';
17
+ @apply absolute inset-0 rounded-xl pointer-events-none;
18
+ @apply bg-background-primary-contrast opacity-0 transition-opacity duration-200;
19
+ }
20
+
21
+ .rsui-radio-card:hover::before {
22
+ @apply opacity-5;
23
+ }
24
+
25
+ .rsui-radio-card--disabled:hover::before {
26
+ @apply opacity-0;
7
27
  }
8
28
 
9
29
  .rsui-radio-card--flexible {
10
30
  @apply max-w-full w-full;
11
31
  }
12
32
 
33
+ /* Selection is the family's brand ring — the same ring-2 / ring-offset-2 / brand-500 that
34
+ Card's action layer and CardGroup use. This used to be a 10% translucent scrim, a third
35
+ distinct idiom for "selected" in one component family. The ring is a shape boundary
36
+ (present vs absent) rather than colour alone, which is the WCAG 1.4.1 property Card's own
37
+ comment calls out, and it holds its brand colour through focus. */
13
38
  .rsui-radio-card--selected {
14
- @apply focus-visible:ring-0 border-border-primary;
15
-
16
- @apply after:content-[''] after:absolute after:inset-0 after:rounded-md after:opacity-10 after:bg-background-primary-contrast;
39
+ @apply ring-2 ring-offset-2 ring-brand-500 border-border-primary;
40
+ @apply focus-visible:ring-brand-500;
17
41
  }
18
42
 
19
43
  .rsui-radio-card--disabled {
20
44
  @apply focus-visible:ring-0 border-border-disabled cursor-default;
21
45
 
22
- @apply after:content-[''] after:absolute after:inset-0 after:rounded-md after:opacity-60 after:bg-background-disabled;
46
+ /* Same corner correction as --selected above, and more visible here at 60% opacity. */
47
+ @apply after:content-[''] after:absolute after:inset-0 after:rounded-xl after:opacity-60 after:bg-background-disabled;
23
48
  }
24
49
 
25
50
  .rsui-radio-card__title-row {
@@ -44,12 +44,24 @@
44
44
  @apply basis-full;
45
45
  }
46
46
 
47
+ /* Redundant now that the base badge carries no margin — kept as a guard so re-adding one
48
+ cannot break the stacked layout, where the badge starts a new line and must sit flush. */
47
49
  .rsui-section-header__title--stacked .rsui-section-header__badge {
48
50
  @apply ml-0;
49
51
  }
50
52
 
53
+ /* ACTION SIZE: section-header actions belong at `sm`, not the button default of `md`. A md
54
+ button is 42px against a 32px title box, so a cluster of them outweighs the heading they
55
+ belong to. This is guidance rather than something enforced here: constraining button height
56
+ from inside the header would silently override a size prop the consumer passed. The stories
57
+ demonstrate the `sm` pairing.
58
+
59
+ TITLE SIZE: text-3xl from lg up. At a flat text-2xl the title box was 32px while even an sm
60
+ button is 38px, so the heading never led its own actions. Responsive rather than flat, and
61
+ matching how PageHeader scales, because mobile stacks the actions anyway (see
62
+ __actions-mobile) and the imbalance only really shows on wide screens. */
51
63
  .rsui-section-header__title-text {
52
- @apply text-2xl font-bold line-clamp-2 md:line-clamp-1 text-text-primary;
64
+ @apply text-2xl lg:text-3xl font-bold line-clamp-2 md:line-clamp-1 text-text-primary;
53
65
  /* Reset heading UA margins so an <h1>-<h6> title matches the previous <div> */
54
66
  /* min-w-0 lets the title shrink and truncate first; the badge is shrink-0 so */
55
67
  /* it keeps its size. No flex-1 — a growing title pushes the See-all badge up */
@@ -62,8 +74,10 @@
62
74
  @apply line-clamp-none md:line-clamp-none;
63
75
  }
64
76
 
77
+ /* No ml-space-xs here: the parent is a flex row that already supplies gap-space-xs, so the
78
+ margin stacked on top of it and the badge sat 16px from its label instead of 8px. */
65
79
  .rsui-section-header__badge {
66
- @apply ml-space-xs flex items-center shrink-0;
80
+ @apply flex items-center shrink-0;
67
81
  }
68
82
 
69
83
  .rsui-section-header__subtitle {
@@ -53,6 +53,28 @@
53
53
  @apply absolute inset-[15%] flex items-center justify-center text-center;
54
54
  }
55
55
 
56
+ /* On a coloured or dark surface. The centre content has no colour of its own and
57
+ inherits the page ink, which on a dark band is near-invisible — this is the
58
+ only thing the variant needs to change. Same mechanism as
59
+ .rsui-body-text--on-color, including the child selector, since the slot
60
+ usually holds its own elements.
61
+
62
+ The track is deliberately NOT changed, because measuring said it did not need
63
+ to be. Grey-400 against the two tonal bands:
64
+
65
+ on Journey Green 800 (#0a4c38) 4.13:1
66
+ on Deep Teal 700 (#0c3035) 5.84:1
67
+
68
+ both clear of the 3:1 WCAG 1.4.11 asks of a non-text graphic. The obvious
69
+ "improvement" would have made it worse: a translucent white track, which is
70
+ what this looked like in the mock, measures 1.64:1 at 18% and 2.61:1 at 35%
71
+ — both failures. If a consumer wants the track tinted to their band, that is
72
+ a --rsui-stage-dial-* style override, not a default. */
73
+ .rsui-stage-dial--on-color .rsui-stage-dial__content {
74
+ @apply text-text-on-color;
75
+ @apply *:text-text-on-color;
76
+ }
77
+
56
78
  @media (prefers-reduced-motion: reduce) {
57
79
  .rsui-stage-dial--animate .rsui-stage-dial__fill {
58
80
  transition: none;
@@ -14,3 +14,49 @@
14
14
  .rsui-switcher .rsui-switcher-item {
15
15
  @apply w-full sm:w-fit;
16
16
  }
17
+
18
+ /* Chips variant — a filter chip set rather than a segmented control. Opt in with
19
+ the `chips` prop; the default is untouched.
20
+
21
+ Two differences, and both are the reason it exists:
22
+ 1. No tonal container, so the chips sit directly on the page rather than in
23
+ a grey well.
24
+ 2. One row that scrolls sideways when it runs out of width, instead of
25
+ stacking. The default stacks below `sm` — documented and deliberate for a
26
+ segmented control — but a stacking chip set makes its container grow by an
27
+ unpredictable number of lines, which is what this avoids.
28
+
29
+ Every rule below has to out-specify a same-specificity rule above it, so this
30
+ block must stay last in the file. */
31
+ .rsui-switcher--chips {
32
+ @apply w-full flex-row items-center flex-nowrap gap-2;
33
+ @apply bg-transparent p-0 rounded-none;
34
+
35
+ /* The default is overflow-hidden; this row is meant to scroll. Scrollbar
36
+ hidden to match .rsui-tab-slider, which already does exactly this. */
37
+ @apply overflow-x-auto;
38
+ scrollbar-width: none;
39
+ }
40
+
41
+ .rsui-switcher--chips::-webkit-scrollbar {
42
+ display: none;
43
+ }
44
+
45
+ /* Overrides `w-full sm:w-fit` above: a chip is sized by its own label at every
46
+ width, never stretched to fill the row. */
47
+ .rsui-switcher--chips .rsui-switcher-item {
48
+ @apply w-fit rounded-full;
49
+ @apply bg-background-primary text-text-secondary border-border-primary;
50
+
51
+ /* 44px, so a chip clears the Apple HIG touch target on a phone. The default
52
+ item computes to 42px and is deliberately left alone. */
53
+ @apply min-h-11;
54
+ }
55
+
56
+ .rsui-switcher--chips .rsui-switcher-item--active {
57
+ @apply bg-background-primary-contrast text-text-on-color border-transparent;
58
+ }
59
+
60
+ .rsui-switcher--chips .rsui-switcher-item--disabled {
61
+ @apply bg-background-disabled text-text-disabled border-transparent;
62
+ }
@@ -1,7 +1,24 @@
1
+ /* The item is a native <button role="tab"> so it is keyboard operable and announces as a
2
+ tab (it used to be a click-handled <div>, which no keyboard could reach). These plain
3
+ declarations strip the UA button chrome so the visual design is unchanged — written out
4
+ rather than assumed from Tailwind preflight, because RSUI's index.css does not import
5
+ Tailwind itself and the consuming app may not ship preflight. */
1
6
  .rsui-switcher-item {
7
+ appearance: none;
8
+ background: none;
9
+ margin: 0;
10
+ border: 0 solid transparent;
11
+ font: inherit;
12
+ color: inherit;
13
+
2
14
  @apply h-fit inline-flex shrink-0 items-center justify-center select-none outline-hidden whitespace-nowrap will-change-transform cursor-pointer;
3
15
  @apply font-semibold gap-2 rounded-lg border border-transparent transition;
4
16
  @apply text-base px-3 py-2;
17
+
18
+ /* The class list above sets outline-hidden, which previously left the item with no focus
19
+ indicator at all once it became focusable. Drawn inset because .rsui-switcher is
20
+ overflow-hidden and would clip an offset ring. */
21
+ @apply focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-primary;
5
22
  }
6
23
 
7
24
  .rsui-switcher-item--active {
@@ -16,6 +33,8 @@
16
33
  @apply size-6;
17
34
  }
18
35
 
36
+ /* No ml-space-xs here: the parent is a flex row that already supplies gap-space-xs, so the
37
+ margin stacked on top of it and the badge sat 16px from its label instead of 8px. */
19
38
  .rsui-switcher-item__badge {
20
- @apply ml-space-xs flex items-center;
21
- }
39
+ @apply flex items-center;
40
+ }
@@ -1,7 +1,26 @@
1
+ /* The item is a native <button role="tab"> so it is keyboard operable and announces as a
2
+ tab (it used to be a click-handled <div>, which no keyboard could reach). These plain
3
+ declarations strip the UA button chrome so the visual design is byte-for-byte unchanged.
4
+ They are written out rather than assumed from Tailwind preflight because RSUI's index.css
5
+ does not import Tailwind itself — the consuming app does, and may not ship preflight.
6
+ The border reset matters most: the @apply below sets only `border-b-2`, so without an
7
+ all-sides width of 0 the UA's default `medium outset` border would remain on the other
8
+ three sides and widen every tab. */
1
9
  .rsui-tab-slider-item {
10
+ appearance: none;
11
+ background: none;
12
+ margin: 0;
13
+ border: 0 solid transparent;
14
+ font: inherit;
15
+ color: inherit;
16
+
2
17
  @apply inline-flex shrink-0 items-center justify-center select-none cursor-pointer whitespace-nowrap;
3
18
  @apply text-base px-4 py-3 gap-2 border-b-2 border-transparent -mb-px;
4
19
  @apply text-text-secondary;
20
+
21
+ /* Focus is drawn inside the box: .rsui-tab-slider is an overflow-x-auto scroller, so an
22
+ offset ring on the first or last tab would be clipped by it. */
23
+ @apply outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-primary;
5
24
  }
6
25
 
7
26
  .rsui-tab-slider-item--active {
@@ -16,6 +35,8 @@
16
35
  @apply size-5;
17
36
  }
18
37
 
38
+ /* No ml-space-xs here: the parent is a flex row that already supplies gap-space-xs, so the
39
+ margin stacked on top of it and the badge sat 16px from its label instead of 8px. */
19
40
  .rsui-tab-slider-item__badge {
20
- @apply ml-space-xs flex items-center;
41
+ @apply flex items-center;
21
42
  }
package/components.css CHANGED
@@ -54,7 +54,6 @@ Please be careful when adding new components and updating the order.
54
54
  @import './components/form_wrapper_build.css';
55
55
  @import './components/form_wrapper_lms.css';
56
56
  @import './components/grid_container.css';
57
- @import './components/hero_section.css';
58
57
  @import './components/icon.css';
59
58
  @import './components/image.css';
60
59
  @import './components/image_16by9.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-tailwindcss",
3
- "version": "7.55.0",
3
+ "version": "7.56.0",
4
4
  "description": "RedSeed UI Tailwindcss",
5
5
  "main": "index.js",
6
6
  "style": "index.css",
package/tokens.css CHANGED
@@ -12,7 +12,11 @@
12
12
  --color-text-warning: #b54708;
13
13
  --color-text-error: #b42318;
14
14
  --color-text-info: #0077cc;
15
- --color-text-ai: var(--Colors-Warm-Mauve-600);
15
+ /* Warm-Mauve-800, not -600: the AI treatments set this over the pale AI gradient
16
+ (#ebe0fa -> #fff6eb), where -600 measured 2.93:1 and failed WCAG AA for 14px text.
17
+ -800 clears it at 4.52:1 at the gradient's worst point. Consumed by Badge AI,
18
+ Button AI, the AI body-text modifier and the AI icon treatments. */
19
+ --color-text-ai: var(--Colors-Warm-Mauve-800);
16
20
  --color-text-on-color: var(--Colors-Base-white);
17
21
 
18
22
  --color-background-primary: var(--Colors-Base-white);
@@ -1,32 +0,0 @@
1
- .rsui-hero-section {
2
- @apply rounded-xl px-5 py-6 flex flex-col gap-4 md:gap-6 relative overflow-hidden bg-text-primary text-text-on-color;
3
- }
4
-
5
- .rsui-hero-section::before {
6
- content: '';
7
- position: absolute;
8
- inset: 0;
9
- background-size: auto;
10
- background-position: right center;
11
- background-repeat: no-repeat;
12
- opacity: 1;
13
- z-index: 0;
14
- }
15
-
16
- .rsui-hero-section > * {
17
- position: relative;
18
- z-index: 1;
19
- }
20
-
21
- .rsui-hero-section__cards {
22
- @apply grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6;
23
- }
24
-
25
- .rsui-hero-section__cards--hidden {
26
- @apply hidden;
27
- }
28
-
29
- .rsui-hero-section__content--hidden {
30
- @apply hidden;
31
- }
32
-