@redseed/redseed-ui-tailwindcss 7.56.0 → 7.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/button_card.css +4 -2
- package/components/button_slot.css +124 -1
- package/components/card.css +154 -26
- package/components/card_header.css +79 -19
- package/components/card_horizontal.css +15 -3
- package/components/checkbox_card.css +24 -11
- package/components/form_field_date_range.css +32 -0
- package/components/icon.css +104 -1
- package/components/message_box.css +104 -4
- package/components/meta_info.css +36 -10
- package/components/metric_card.css +1 -1
- package/components/page_header.css +51 -17
- package/components/radio_card.css +15 -8
- package/components/section_footer.css +68 -0
- package/components/section_header.css +58 -11
- package/components/section_slider.css +302 -0
- package/components/switcher.css +60 -0
- package/package.json +1 -1
- package/tokens.css +7 -0
|
@@ -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
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
.rsui-section-header {
|
|
2
|
-
@apply flex flex-col gap-x-space-
|
|
2
|
+
@apply flex flex-col gap-x-space-md gap-y-space-md transition-all;
|
|
3
3
|
@apply md:flex-row md:justify-between md:items-center;
|
|
4
|
+
@apply border-b border-transparent;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
/* overflow-hidden guards against a useResponsiveWidth feedback loop (mobile
|
|
@@ -11,22 +12,40 @@
|
|
|
11
12
|
@apply overflow-hidden;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/* The --divider class was being set by SectionHeader.vue with no rule behind it, so
|
|
16
|
+
showDivider did nothing. Implemented the way CardHeader does it — a transparent
|
|
17
|
+
bottom border on the base, coloured here — and the prop's default flips to false in
|
|
18
|
+
the same change, so every consumer's appearance is unchanged (they saw no rule
|
|
19
|
+
before and see none now) while the prop starts working for anyone who opts in.
|
|
20
|
+
Brand-wise a hairline between sections is right; a box around them is not. */
|
|
21
|
+
.rsui-section-header--divider {
|
|
22
|
+
@apply border-border-primary;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
.rsui-section-header--clickable {
|
|
15
26
|
@apply cursor-pointer;
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
.rsui-section-header__header {
|
|
19
|
-
@apply flex-1 min-w-0 flex justify-between items-start gap-x-space-
|
|
30
|
+
@apply flex-1 min-w-0 flex justify-between items-start gap-x-space-md;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
.rsui-section-header__icon {
|
|
23
34
|
@apply flex items-center justify-center;
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
/* No row-gap: one value cannot serve both a title/badge row and a title/subtitle pair.
|
|
38
|
+
The subtitle carries its own margin instead, which also means an absent subtitle
|
|
39
|
+
leaves no gap behind. Same structure as .rsui-card-header__text. */
|
|
26
40
|
.rsui-section-header__text {
|
|
27
|
-
@apply flex-1 flex flex-col
|
|
41
|
+
@apply flex-1 flex flex-col;
|
|
28
42
|
}
|
|
29
43
|
|
|
44
|
+
/* An optical nudge, not spacing — which is why it stays a raw 4px rather than being
|
|
45
|
+
rounded onto the 8px rhythm. It lines the title's cap height up with the button text
|
|
46
|
+
beside it; the correct value is whatever makes those two agree, and it is not a gap
|
|
47
|
+
between two elements. Same category as the control offsets in checkbox_card and
|
|
48
|
+
card_group, which are also deliberately raw. */
|
|
30
49
|
.rsui-section-header__text--with-toolbar {
|
|
31
50
|
@apply pt-1;
|
|
32
51
|
}
|
|
@@ -56,12 +75,35 @@
|
|
|
56
75
|
from inside the header would silently override a size prop the consumer passed. The stories
|
|
57
76
|
demonstrate the `sm` pairing.
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
BADGE SIZE: a badge beside the title belongs at `xs`. Measured against the Header 4
|
|
79
|
+
title box (26px): the `md` badge is 34px — 131% of it — and `sm`, which is Badge's
|
|
80
|
+
DEFAULT, is 26px, exactly 100%. Either reads as competing with the heading rather than
|
|
81
|
+
labelling it. `xs` is 18px, 69%. Like ACTION SIZE above this is guidance, not enforced:
|
|
82
|
+
scoping a size from inside the header would silently override a size prop the consumer
|
|
83
|
+
passed, and CSS cannot tell Badge's default `sm` from an explicit one. The stories
|
|
84
|
+
demonstrate the pairing.
|
|
85
|
+
|
|
86
|
+
Worth knowing that `sm` being the default makes `<Badge>` with no size 100% of a
|
|
87
|
+
Header 4 title box anywhere it sits beside one — CardHeader included. That is a
|
|
88
|
+
Badge-level question rather than something to patch per header.
|
|
89
|
+
|
|
90
|
+
TITLE SIZE: brand Header 4 (20/130%) below lg, Header 3 (28/120%) from lg up. It was
|
|
91
|
+
text-2xl -> text-3xl, which is 24 -> 30 and neither is a step on the scale at all
|
|
92
|
+
(16 / 20 / 28 / 40 / 64).
|
|
93
|
+
|
|
94
|
+
Responsive rather than flat is kept deliberately, and so is the reason: mobile stacks
|
|
95
|
+
the actions (see __actions-mobile) so the title does not have to out-weigh them there,
|
|
96
|
+
while on a wide screen it sits beside them and does. Read the note below before
|
|
97
|
+
flattening this.
|
|
98
|
+
|
|
99
|
+
Honest limit: the original concern was that a 32px title box lost to a 38px sm button.
|
|
100
|
+
28px at 120% is a 34px box, so it is better but not resolved — a title that decisively
|
|
101
|
+
leads an sm button needs Header 2 (40px), which is PageHeader's step and would compete
|
|
102
|
+
with it. The alternative is smaller action buttons. That is a design call, flagged
|
|
103
|
+
rather than taken here. */
|
|
63
104
|
.rsui-section-header__title-text {
|
|
64
|
-
@apply text-
|
|
105
|
+
@apply text-xl leading-[1.3] font-bold line-clamp-2 md:line-clamp-1 text-text-primary;
|
|
106
|
+
@apply lg:text-[1.75rem] lg:leading-[1.2];
|
|
65
107
|
/* Reset heading UA margins so an <h1>-<h6> title matches the previous <div> */
|
|
66
108
|
/* min-w-0 lets the title shrink and truncate first; the badge is shrink-0 so */
|
|
67
109
|
/* it keeps its size. No flex-1 — a growing title pushes the See-all badge up */
|
|
@@ -80,18 +122,23 @@
|
|
|
80
122
|
@apply flex items-center shrink-0;
|
|
81
123
|
}
|
|
82
124
|
|
|
125
|
+
/* TIGHT PAIR — 4px up to the title, deliberately off the brand's 8px rhythm. See the
|
|
126
|
+
note above .rsui-card-header__subtitle in card_header.css: line-height already
|
|
127
|
+
contributes half-leading at both ends, so 8px renders as roughly 14px and the pair
|
|
128
|
+
reads as two separate lines rather than one thing and its qualifier. */
|
|
83
129
|
.rsui-section-header__subtitle {
|
|
130
|
+
@apply mt-space-xxs;
|
|
84
131
|
@apply text-sm text-text-secondary;
|
|
85
132
|
}
|
|
86
133
|
|
|
87
134
|
.rsui-section-header__toolbar {
|
|
88
|
-
@apply flex gap-space-
|
|
135
|
+
@apply flex gap-space-xs md:items-center justify-end;
|
|
89
136
|
}
|
|
90
137
|
|
|
91
138
|
.rsui-section-header__actions-desktop {
|
|
92
|
-
@apply flex gap-space-
|
|
139
|
+
@apply flex gap-space-xs items-center;
|
|
93
140
|
}
|
|
94
141
|
|
|
95
142
|
.rsui-section-header__actions-mobile {
|
|
96
|
-
@apply flex flex-wrap gap-space-
|
|
143
|
+
@apply flex flex-wrap gap-space-xs items-center justify-start;
|
|
97
144
|
}
|
|
@@ -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
|
+
}
|
package/components/switcher.css
CHANGED
|
@@ -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
package/tokens.css
CHANGED
|
@@ -49,6 +49,13 @@
|
|
|
49
49
|
--color-background-ai-from: #ebe0fa;
|
|
50
50
|
--color-background-ai-to: var(--Colors-Background-AI-2); /* v1 defines ai-from only; ai-to kept as warm counterpart */
|
|
51
51
|
--color-background-ai-contrast: var(--Colors-Warm-Mauve-600);
|
|
52
|
+
/* The AI family at full strength, for a BOLD surface rather than a light tint.
|
|
53
|
+
ai-from/ai-to are a pale lilac wash; these are the dark end of the same ramp,
|
|
54
|
+
for a card that has to carry white type. On-ramp on purpose — the /home/v2
|
|
55
|
+
mock used #56374c/#3b2635, which are not Warm Mauve values. */
|
|
56
|
+
--color-background-ai-bold-from: var(--Colors-Warm-Mauve-900);
|
|
57
|
+
--color-background-ai-bold-via: var(--Colors-Warm-Mauve-950);
|
|
58
|
+
--color-background-ai-bold-to: var(--Colors-Grey-950);
|
|
52
59
|
--color-background-ai-invert: var(--Colors-Warm-Mauve-200);
|
|
53
60
|
|
|
54
61
|
--color-border-primary: var(--Colors-Grey-400);
|