@robr0/design-system 0.10.0 → 0.11.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/README.md +7 -6
- package/components/AgentPlan/AgentPlan.css +14 -3
- package/components/AiButton/AiButton.css +1 -1
- package/components/Alert/Alert.css +9 -0
- package/components/AlertDialog/AlertDialog.css +3 -1
- package/components/AlertDialog/AlertDialog.js +2 -3
- package/components/AppSidebar/AppSidebar.css +9 -0
- package/components/Card/Card.css +7 -2
- package/components/CardStack/CardStack.css +106 -0
- package/components/CardStack/CardStack.d.ts +44 -0
- package/components/CardStack/CardStack.js +140 -0
- package/components/Carousel/Carousel.css +9 -0
- package/components/Carousel/Carousel.js +37 -4
- package/components/Chart/AreaChart.js +6 -5
- package/components/Chart/LineChart.js +4 -3
- package/components/Chart/PieChart.js +2 -15
- package/components/Chart/RadarChart.js +5 -4
- package/components/Chart/RadialChart.js +2 -15
- package/components/Chart/ScatterChart.js +3 -13
- package/components/Chart/StackedBarChart.js +2 -15
- package/components/Chart/Treemap.js +3 -17
- package/components/Chart/palette.d.ts +1 -0
- package/components/Chart/palette.js +19 -0
- package/components/ChatMarker/ChatMarker.css +9 -0
- package/components/ChatMessage/ChatMessage.css +16 -0
- package/components/ChatThread/ChatThread.js +2 -2
- package/components/Chip/Chip.css +9 -0
- package/components/CodeBlock/CodeBlock.css +10 -1
- package/components/CodeBlock/CodeBlock.js +2 -1
- package/components/ColorPicker/ColorPicker.js +10 -0
- package/components/Combobox/Combobox.css +9 -0
- package/components/CommandPalette/CommandPalette.css +9 -0
- package/components/CommandPalette/CommandPalette.js +31 -5
- package/components/Composer/Composer.css +118 -0
- package/components/Composer/Composer.d.ts +7 -0
- package/components/Composer/Composer.js +7 -1
- package/components/ContactCard/ContactCard.css +9 -0
- package/components/ContextMenu/ContextMenu.js +39 -3
- package/components/DatePicker/DatePicker.js +16 -0
- package/components/Dialog/Dialog.css +3 -1
- package/components/Dialog/Dialog.js +5 -4
- package/components/DocumentChip/DocumentChip.css +9 -0
- package/components/Dropdown/Dropdown.js +22 -3
- package/components/DropdownMenu/DropdownMenu.js +43 -6
- package/components/EmptyState/EmptyState.css +9 -0
- package/components/FileInput/FileInput.css +9 -0
- package/components/Globe/Globe.css +155 -0
- package/components/Globe/Globe.d.ts +96 -0
- package/components/Globe/Globe.js +415 -0
- package/components/InterruptCard/InterruptCard.css +27 -17
- package/components/InterruptCard/InterruptCard.d.ts +1 -1
- package/components/InterruptCard/InterruptCard.js +1 -1
- package/components/MapCallout/MapCallout.css +46 -0
- package/components/MapCallout/MapCallout.d.ts +29 -0
- package/components/MapCallout/MapCallout.js +17 -0
- package/components/MapLegend/MapLegend.css +88 -0
- package/components/MapLegend/MapLegend.d.ts +39 -0
- package/components/MapLegend/MapLegend.js +68 -0
- package/components/MessageActions/MessageActions.css +9 -0
- package/components/MessageCard/MessageCard.css +68 -16
- package/components/MessageCard/MessageCard.d.ts +1 -1
- package/components/MessageCard/MessageCard.js +6 -4
- package/components/ModelPicker/ModelPicker.css +4 -1
- package/components/ModelPicker/ModelPicker.js +43 -1
- package/components/NotificationCenter/NotificationCenter.css +9 -0
- package/components/NotificationCenter/NotificationCenter.js +26 -10
- package/components/NumberInput/NumberInput.css +9 -0
- package/components/Popover/Popover.css +3 -1
- package/components/Popover/Popover.js +2 -1
- package/components/SourceChip/SourceChip.css +9 -0
- package/components/Stat/Stat.css +9 -0
- package/components/Stepper/Stepper.css +9 -0
- package/components/TagInput/TagInput.css +9 -0
- package/components/TimePicker/TimePicker.js +4 -1
- package/components/Toast/Toast.css +32 -11
- package/components/Toast/Toast.js +5 -4
- package/components/ToggleGroup/ToggleGroup.css +9 -0
- package/components/ToolCall/ToolCall.css +18 -6
- package/components/Tooltip/Tooltip.js +20 -16
- package/components/registry.json +37 -0
- package/components/registry.json.d.ts +37 -0
- package/components/registry.json.js +2 -2
- package/index.d.ts +4 -0
- package/index.js +8 -0
- package/package.json +5 -1
- package/tokens/motion.d.ts +23 -0
- package/tokens/motion.js +14 -0
- package/tokens/registry.json +17 -0
- package/tokens/registry.json.d.ts +17 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +11 -0
- package/tokens/tokens-light.css +11 -0
- package/tokens/tokens-typography.css +20 -0
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
18
18
|
| Part | Description |
|
|
19
19
|
|---|---|
|
|
20
20
|
| **Portfolio website** (`/website`) | Next.js app with case studies, work history, writing, and about pages, all built exclusively with the design system components below. **[Live site →](https://robertritacca.com/)** |
|
|
21
|
-
| **Design system** (`/src`) | <!-- component-count -->
|
|
21
|
+
| **Design system** (`/src`) | <!-- component-count -->105<!-- /component-count --> React components, a three-tier token architecture, dark mode, a WebGL2 ambient background that themes itself from your tokens, and a full documentation site. Built to production standards. **[Storybook →](https://design-system-iota-one.vercel.app/?path=/docs/robr0-ds--docs)** |
|
|
22
22
|
| **AI layer** (`ai` components in `/src`, chat in `/website`, `/evals`) | A site-wide chat that answers questions about the work: built from the library's own `ai` components, grounded in a corpus generated from the site's published content, and scored by a golden-set eval. |
|
|
23
23
|
|
|
24
24
|
---
|
|
@@ -28,7 +28,7 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
28
28
|
### Components
|
|
29
29
|
|
|
30
30
|
<!-- component-list:start -->
|
|
31
|
-
Accordion · Agent plan · Agent status · AI button · Alert · Alert dialog · Anchor nav · App layout · App sidebar · Area chart · Avatar · Badge · Bar chart · Breadcrumb · Button · Button group · Card · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Input · Instructions · Interrupt card · Kbd · Line chart · Link list · Message actions · Message card · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Pie chart · Pin input · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Reasoning · Scatter chart · Section title · Segmented control · Selection card · Shader field · Skeleton · Slider · Source chip · Sparkline · Spinner · Stacked bar chart · Stat · Stepper · Swatch · Table · Tabs · Tag input · Textarea · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Tooltip · Tree view · Treemap
|
|
31
|
+
Accordion · Agent plan · Agent status · AI button · Alert · Alert dialog · Anchor nav · App layout · App sidebar · Area chart · Avatar · Badge · Bar chart · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Globe · Input · Instructions · Interrupt card · Kbd · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Pie chart · Pin input · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Reasoning · Scatter chart · Section title · Segmented control · Selection card · Shader field · Skeleton · Slider · Source chip · Sparkline · Spinner · Stacked bar chart · Stat · Stepper · Swatch · Table · Tabs · Tag input · Textarea · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Tooltip · Tree view · Treemap
|
|
32
32
|
<!-- component-list:end -->
|
|
33
33
|
|
|
34
34
|
### Ambient background
|
|
@@ -50,7 +50,7 @@ Eight parameters describe the look, and a composition is a table of token names
|
|
|
50
50
|
|
|
51
51
|
It asks two things of you, both deliberate. It fills a positioned ancestor you provide, because where a background sits is a layout decision rather than a rendering one. And it never decides what to paint instead of itself: it reports `pending`, `active` or `unavailable`, and one fallback covers every way it can fail (no WebGL2, a blocked or lost GPU context, a renderer that stalls before its first frame, or `enabled={false}` as a kill switch). `pending` is the state worth handling. Paint neither layer while the context comes up, or the swap a frame later reads as two backgrounds loading in sequence.
|
|
52
52
|
|
|
53
|
-
It
|
|
53
|
+
It checks `prefers-reduced-motion` in JavaScript rather than through the motion tokens, because the CSS guard cannot see a JS-driven loop. Set the preference and it draws a single static frame.
|
|
54
54
|
|
|
55
55
|
The background behind this site is that component, with eight blurred CSS discs kept painted underneath as its fallback. The **[component page](https://robertritacca.com/components/shader-field)** is a live demo, and the **[setup guide](https://robertritacca.com/docs/get-started)** has the wiring.
|
|
56
56
|
|
|
@@ -97,7 +97,7 @@ import { BarChart, LineChart } from '@robr0/design-system/charts';
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Icons use a bundled Material Symbols Rounded variable font (woff2),
|
|
100
|
+
Icons use a bundled Material Symbols Rounded variable font (woff2): components import it themselves, so they need no extra setup. If you render raw `.material-symbols-rounded` spans of your own, import `@robr0/design-system/fonts/material-symbols.css` once. Nunito Sans, the system's default typeface, is intentionally *not* bundled: load it yourself (e.g. Google Fonts or `next/font`) or override `--font-family-primary`.
|
|
101
101
|
|
|
102
102
|
### Token architecture
|
|
103
103
|
|
|
@@ -164,11 +164,12 @@ CI also guards against documentation drift: generated surfaces (this README's co
|
|
|
164
164
|
## Running locally
|
|
165
165
|
|
|
166
166
|
```bash
|
|
167
|
+
npm install # once, at the root: the website is an npm workspace, so this installs both
|
|
168
|
+
|
|
167
169
|
# Storybook (the library's dev sandbox)
|
|
168
170
|
npm run storybook # http://localhost:6006
|
|
169
171
|
|
|
170
|
-
# Portfolio + documentation website
|
|
171
|
-
npm install
|
|
172
|
+
# Portfolio + documentation website
|
|
172
173
|
npm run dev --workspace website # http://localhost:3000
|
|
173
174
|
```
|
|
174
175
|
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
width: 100%;
|
|
17
17
|
background-color: var(--color-bg-container-primary);
|
|
18
18
|
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
19
|
-
|
|
19
|
+
/* The chat furniture's shared 24px shell — see ToolCall. */
|
|
20
|
+
border-radius: var(--radius-xl);
|
|
20
21
|
overflow: hidden;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
align-items: center;
|
|
30
31
|
gap: var(--gap-sm);
|
|
31
32
|
width: 100%;
|
|
32
|
-
|
|
33
|
+
/* 20px sides — the card family's shared content inset. */
|
|
34
|
+
padding: var(--padding-sm) var(--padding-lg);
|
|
33
35
|
background: none;
|
|
34
36
|
border: none;
|
|
35
37
|
text-align: left;
|
|
@@ -121,7 +123,7 @@
|
|
|
121
123
|
display: flex;
|
|
122
124
|
flex-direction: column;
|
|
123
125
|
margin: 0;
|
|
124
|
-
padding: var(--padding-sm-md) var(--padding-
|
|
126
|
+
padding: var(--padding-sm-md) var(--padding-lg);
|
|
125
127
|
list-style: none;
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -229,3 +231,12 @@
|
|
|
229
231
|
white-space: nowrap;
|
|
230
232
|
border: 0;
|
|
231
233
|
}
|
|
234
|
+
|
|
235
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
236
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
237
|
+
.material-symbols-rounded itself (a common global default) would
|
|
238
|
+
otherwise override inheritance and detach the icon from the state
|
|
239
|
+
colours this component sets on its parents. */
|
|
240
|
+
.ds-agent-plan .material-symbols-rounded {
|
|
241
|
+
color: inherit;
|
|
242
|
+
}
|
|
@@ -250,3 +250,12 @@
|
|
|
250
250
|
.ds-alert--neutral .ds-alert__dismiss {
|
|
251
251
|
color: var(--color-status-neutral-text);
|
|
252
252
|
}
|
|
253
|
+
|
|
254
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
255
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
256
|
+
.material-symbols-rounded itself (a common global default) would
|
|
257
|
+
otherwise override inheritance and detach the icon from the state
|
|
258
|
+
colours this component sets on its parents. */
|
|
259
|
+
.ds-alert .material-symbols-rounded {
|
|
260
|
+
color: inherit;
|
|
261
|
+
}
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
align-items: center;
|
|
12
12
|
justify-content: center;
|
|
13
13
|
opacity: 0;
|
|
14
|
+
visibility: hidden; /* keeps panel controls out of the tab order when closed */
|
|
14
15
|
pointer-events: none;
|
|
15
|
-
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
16
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard), visibility var(--motion-duration-base) var(--motion-ease-standard);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
.ds-alert-dialog--open {
|
|
19
20
|
opacity: 1;
|
|
21
|
+
visibility: visible;
|
|
20
22
|
pointer-events: auto;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -7,6 +7,7 @@ import "./AlertDialog.css";
|
|
|
7
7
|
import "../../fonts/material-symbols.css";
|
|
8
8
|
const emptySubscribe = () => () => {
|
|
9
9
|
};
|
|
10
|
+
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
10
11
|
const AlertDialog = ({
|
|
11
12
|
open,
|
|
12
13
|
onOpenChange,
|
|
@@ -53,9 +54,7 @@ const AlertDialog = ({
|
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
if (e.key === "Tab" && panel) {
|
|
56
|
-
const focusable = panel.querySelectorAll(
|
|
57
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
58
|
-
);
|
|
57
|
+
const focusable = panel.querySelectorAll(FOCUSABLE);
|
|
59
58
|
if (focusable.length === 0) return;
|
|
60
59
|
const first = focusable[0];
|
|
61
60
|
const last = focusable[focusable.length - 1];
|
|
@@ -442,3 +442,12 @@
|
|
|
442
442
|
padding: var(--padding-lg) 0;
|
|
443
443
|
}
|
|
444
444
|
}
|
|
445
|
+
|
|
446
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
447
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
448
|
+
.material-symbols-rounded itself (a common global default) would
|
|
449
|
+
otherwise override inheritance and detach the icon from the state
|
|
450
|
+
colours this component sets on its parents. */
|
|
451
|
+
.ds-app-sidebar .material-symbols-rounded {
|
|
452
|
+
color: inherit;
|
|
453
|
+
}
|
package/components/Card/Card.css
CHANGED
|
@@ -76,7 +76,9 @@
|
|
|
76
76
|
.ds-card--case-study {
|
|
77
77
|
height: auto;
|
|
78
78
|
min-width: 0;
|
|
79
|
-
|
|
79
|
+
/* The cover is inset, not full-bleed: 12px of card around a 12px-radius
|
|
80
|
+
cover keeps the corners concentric with the card's 24px shell. */
|
|
81
|
+
padding: var(--padding-sm-md);
|
|
80
82
|
gap: 0;
|
|
81
83
|
overflow: hidden;
|
|
82
84
|
cursor: pointer;
|
|
@@ -103,6 +105,7 @@
|
|
|
103
105
|
width: 100%;
|
|
104
106
|
aspect-ratio: 940 / 480;
|
|
105
107
|
overflow: hidden;
|
|
108
|
+
border-radius: var(--radius-md);
|
|
106
109
|
background: var(--color-bg-container-secondary);
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -142,7 +145,9 @@
|
|
|
142
145
|
display: flex;
|
|
143
146
|
flex-direction: column;
|
|
144
147
|
gap: var(--gap-sm);
|
|
145
|
-
|
|
148
|
+
/* 8px on top of the card's 12px inset: text lands 20px from the card edge
|
|
149
|
+
(the old full-bleed body padding) and sits slightly inside the cover. */
|
|
150
|
+
padding: var(--padding-md) var(--padding-sm) var(--padding-sm-md);
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
.ds-card--case-study__eyebrow {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
CARD STACK COMPONENT
|
|
3
|
+
A deck showing one card, the rest peeking out
|
|
4
|
+
beneath. Forward flips lift the top card away;
|
|
5
|
+
backward flips drop it in from above. Depth
|
|
6
|
+
offsets are percentages of the card itself, so
|
|
7
|
+
the choreography scales with whatever card the
|
|
8
|
+
deck holds.
|
|
9
|
+
============================================ */
|
|
10
|
+
|
|
11
|
+
/* Base */
|
|
12
|
+
|
|
13
|
+
.ds-card-stack {
|
|
14
|
+
/* How far each buried card peeks out below the one above it. */
|
|
15
|
+
--ds-card-stack-peek: var(--gap-md);
|
|
16
|
+
|
|
17
|
+
position: relative;
|
|
18
|
+
display: grid;
|
|
19
|
+
border-radius: var(--radius-xl);
|
|
20
|
+
padding-bottom: calc(var(--ds-card-stack-peek) * var(--ds-card-stack-depth, 0));
|
|
21
|
+
outline: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ds-card-stack:focus-visible {
|
|
25
|
+
outline: var(--border-md) solid var(--color-action-primary-bg);
|
|
26
|
+
outline-offset: var(--padding-xxs);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Items — every card occupies the same cell; position is one transform. */
|
|
30
|
+
|
|
31
|
+
.ds-card-stack__item {
|
|
32
|
+
grid-area: 1 / 1;
|
|
33
|
+
/* An opaque page-colour backing behind each card: the system's cards sit on
|
|
34
|
+
translucent surfaces, and without this the buried cards read through the
|
|
35
|
+
top one. Rounded to Card's radius; a deck of differently shaped cards
|
|
36
|
+
repoints --ds-card-stack-card-radius. */
|
|
37
|
+
background: var(--color-bg-page-primary);
|
|
38
|
+
border-radius: var(--ds-card-stack-card-radius, var(--radius-xl));
|
|
39
|
+
transform-origin: 50% 100%;
|
|
40
|
+
transform: translateY(calc(var(--ds-card-stack-peek) * var(--ds-card-stack-pos, 0)))
|
|
41
|
+
scale(calc(1 - 0.04 * var(--ds-card-stack-pos, 0)));
|
|
42
|
+
transition:
|
|
43
|
+
transform var(--motion-duration-deliberate) var(--motion-ease-emphasized),
|
|
44
|
+
opacity var(--motion-duration-deliberate) var(--motion-ease-emphasized);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Deeper than the visible peek: parked at the last slot, faded out. */
|
|
48
|
+
.ds-card-stack__item--buried {
|
|
49
|
+
opacity: 0;
|
|
50
|
+
transform: translateY(calc(var(--ds-card-stack-peek) * var(--ds-card-stack-depth, 0)))
|
|
51
|
+
scale(calc(1 - 0.04 * var(--ds-card-stack-depth, 0)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Flipped away with loop off: lifted out of view, above the deck. */
|
|
55
|
+
.ds-card-stack__item--gone {
|
|
56
|
+
opacity: 0;
|
|
57
|
+
transform: translateY(-12%) scale(0.98);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Flip choreography — the lift is a fraction of the card's own height. */
|
|
61
|
+
|
|
62
|
+
.ds-card-stack__item--leaving {
|
|
63
|
+
animation: ds-card-stack-leave var(--motion-duration-deliberate) var(--motion-ease-emphasized)
|
|
64
|
+
both;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ds-card-stack__item--entering {
|
|
68
|
+
animation: ds-card-stack-enter var(--motion-duration-deliberate) var(--motion-ease-entrance)
|
|
69
|
+
both;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@keyframes ds-card-stack-leave {
|
|
73
|
+
from {
|
|
74
|
+
transform: translateY(0) rotate(0deg) scale(1);
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
to {
|
|
78
|
+
transform: translateY(-18%) rotate(-3deg) scale(0.98);
|
|
79
|
+
opacity: 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes ds-card-stack-enter {
|
|
84
|
+
from {
|
|
85
|
+
transform: translateY(-18%) rotate(-3deg) scale(0.98);
|
|
86
|
+
opacity: 0;
|
|
87
|
+
}
|
|
88
|
+
to {
|
|
89
|
+
transform: translateY(0) rotate(0deg) scale(1);
|
|
90
|
+
opacity: 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Screen-reader announcement of the deck position. */
|
|
95
|
+
|
|
96
|
+
.ds-card-stack__sr-only {
|
|
97
|
+
position: absolute;
|
|
98
|
+
width: 1px;
|
|
99
|
+
height: 1px;
|
|
100
|
+
padding: 0;
|
|
101
|
+
margin: -1px;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
clip: rect(0, 0, 0, 0);
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
border: 0;
|
|
106
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by CardStack itself — everything else falls through to the root `<div>`. */
|
|
3
|
+
type CardStackOwnProps = {
|
|
4
|
+
/** The cards, in order. The first child starts on top. */
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Controlled top card, as an index into `children`. Pair with
|
|
8
|
+
* `onIndexChange`. Omit to let the stack own its position.
|
|
9
|
+
*/
|
|
10
|
+
index?: number;
|
|
11
|
+
/** Initial top card when uncontrolled. */
|
|
12
|
+
defaultIndex?: number;
|
|
13
|
+
/** Fires when the top card changes, with the new index. */
|
|
14
|
+
onIndexChange?: (index: number) => void;
|
|
15
|
+
/** How many card edges peek out behind the top card. */
|
|
16
|
+
peek?: number;
|
|
17
|
+
/** Wrap from the last card back to the first. Off, the stack stops at both ends. */
|
|
18
|
+
loop?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Clicking the top card flips to the next. Clicks that land on a link or
|
|
21
|
+
* control inside the card are left alone, so a card can still carry its
|
|
22
|
+
* own actions.
|
|
23
|
+
*/
|
|
24
|
+
advanceOnClick?: boolean;
|
|
25
|
+
/** Accessible name for the stack, e.g. "Open roles". */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Additional CSS classes */
|
|
28
|
+
className?: string;
|
|
29
|
+
};
|
|
30
|
+
export interface CardStackProps extends CardStackOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof CardStackOwnProps> {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* CardStack — a deck of cards showing one at a time, the rest peeking out
|
|
34
|
+
* beneath it. Flipping forward lifts the top card up and away while the next
|
|
35
|
+
* one rises into place; flipping back drops the previous card in from above.
|
|
36
|
+
* The stack is the pattern, not the cards: any children work, Card and
|
|
37
|
+
* EntityCard are the intended fillings, and every card should share one size
|
|
38
|
+
* so the deck keeps its shape.
|
|
39
|
+
*
|
|
40
|
+
* Flip by clicking the top card, or with the arrow keys when the stack has
|
|
41
|
+
* focus. Cards behind the top one are inert until their turn.
|
|
42
|
+
*/
|
|
43
|
+
export declare const CardStack: React.ForwardRefExoticComponent<CardStackProps & React.RefAttributes<HTMLDivElement>>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState, useRef, useCallback } from "react";
|
|
4
|
+
import "./CardStack.css";
|
|
5
|
+
const INTERACTIVE_SELECTOR = 'a, button, input, select, textarea, summary, [role="button"], [role="link"], [tabindex]';
|
|
6
|
+
const NEXT_KEYS = /* @__PURE__ */ new Set(["ArrowRight", "ArrowDown", " ", "Enter"]);
|
|
7
|
+
const PREV_KEYS = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowUp"]);
|
|
8
|
+
const CardStack = React.forwardRef(
|
|
9
|
+
({
|
|
10
|
+
children,
|
|
11
|
+
index: indexProp,
|
|
12
|
+
defaultIndex = 0,
|
|
13
|
+
onIndexChange,
|
|
14
|
+
peek = 2,
|
|
15
|
+
loop = true,
|
|
16
|
+
advanceOnClick = true,
|
|
17
|
+
label,
|
|
18
|
+
className = "",
|
|
19
|
+
onClick,
|
|
20
|
+
onKeyDown,
|
|
21
|
+
style,
|
|
22
|
+
...rest
|
|
23
|
+
}, ref) => {
|
|
24
|
+
const baseClass = "ds-card-stack";
|
|
25
|
+
const items = React.Children.toArray(children);
|
|
26
|
+
const count = items.length;
|
|
27
|
+
const isControlled = indexProp !== void 0;
|
|
28
|
+
const [innerIndex, setInnerIndex] = useState(defaultIndex);
|
|
29
|
+
const index = Math.max(0, Math.min(isControlled ? indexProp : innerIndex, count - 1));
|
|
30
|
+
const [leaving, setLeaving] = useState(false);
|
|
31
|
+
const [entering, setEntering] = useState(false);
|
|
32
|
+
const pendingRef = useRef(null);
|
|
33
|
+
const commit = useCallback(
|
|
34
|
+
(next2) => {
|
|
35
|
+
const normalized = (next2 % count + count) % count;
|
|
36
|
+
if (!isControlled) setInnerIndex(normalized);
|
|
37
|
+
onIndexChange?.(normalized);
|
|
38
|
+
},
|
|
39
|
+
[count, isControlled, onIndexChange]
|
|
40
|
+
);
|
|
41
|
+
const next = useCallback(() => {
|
|
42
|
+
if (count < 2 || leaving || entering) return;
|
|
43
|
+
if (!loop && index >= count - 1) return;
|
|
44
|
+
pendingRef.current = index + 1;
|
|
45
|
+
setLeaving(true);
|
|
46
|
+
}, [count, leaving, entering, loop, index]);
|
|
47
|
+
const prev = useCallback(() => {
|
|
48
|
+
if (count < 2 || leaving || entering) return;
|
|
49
|
+
if (!loop && index <= 0) return;
|
|
50
|
+
commit(index - 1);
|
|
51
|
+
setEntering(true);
|
|
52
|
+
}, [count, leaving, entering, loop, index, commit]);
|
|
53
|
+
const handleTopAnimationEnd = (e) => {
|
|
54
|
+
if (e.target !== e.currentTarget) return;
|
|
55
|
+
if (leaving) {
|
|
56
|
+
setLeaving(false);
|
|
57
|
+
if (pendingRef.current !== null) commit(pendingRef.current);
|
|
58
|
+
pendingRef.current = null;
|
|
59
|
+
}
|
|
60
|
+
if (entering) setEntering(false);
|
|
61
|
+
};
|
|
62
|
+
const handleClick = (e) => {
|
|
63
|
+
onClick?.(e);
|
|
64
|
+
if (!advanceOnClick || e.defaultPrevented) return;
|
|
65
|
+
const target = e.target;
|
|
66
|
+
const control = target.closest(INTERACTIVE_SELECTOR);
|
|
67
|
+
if (control && control !== e.currentTarget) return;
|
|
68
|
+
next();
|
|
69
|
+
};
|
|
70
|
+
const handleKeyDown = (e) => {
|
|
71
|
+
onKeyDown?.(e);
|
|
72
|
+
if (e.defaultPrevented || e.target !== e.currentTarget) return;
|
|
73
|
+
if (NEXT_KEYS.has(e.key)) {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
next();
|
|
76
|
+
} else if (PREV_KEYS.has(e.key)) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
prev();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const visiblePeek = Math.max(0, Math.min(peek, count - 1));
|
|
82
|
+
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
83
|
+
return /* @__PURE__ */ jsxs(
|
|
84
|
+
"div",
|
|
85
|
+
{
|
|
86
|
+
...rest,
|
|
87
|
+
ref,
|
|
88
|
+
className: classes,
|
|
89
|
+
role: "group",
|
|
90
|
+
"aria-roledescription": "card stack",
|
|
91
|
+
"aria-label": label,
|
|
92
|
+
tabIndex: count > 1 ? 0 : void 0,
|
|
93
|
+
style: { ...style, "--ds-card-stack-depth": visiblePeek },
|
|
94
|
+
onClick: handleClick,
|
|
95
|
+
onKeyDown: handleKeyDown,
|
|
96
|
+
children: [
|
|
97
|
+
items.map((child, k) => {
|
|
98
|
+
const raw = k - index;
|
|
99
|
+
const pos = loop ? (raw % count + count) % count : raw;
|
|
100
|
+
const gone = pos < 0;
|
|
101
|
+
const buried = pos > visiblePeek;
|
|
102
|
+
const isTop = pos === 0;
|
|
103
|
+
const itemClasses = [
|
|
104
|
+
`${baseClass}__item`,
|
|
105
|
+
gone ? `${baseClass}__item--gone` : "",
|
|
106
|
+
buried ? `${baseClass}__item--buried` : "",
|
|
107
|
+
isTop && leaving ? `${baseClass}__item--leaving` : "",
|
|
108
|
+
isTop && entering ? `${baseClass}__item--entering` : ""
|
|
109
|
+
].filter(Boolean).join(" ");
|
|
110
|
+
return /* @__PURE__ */ jsx(
|
|
111
|
+
"div",
|
|
112
|
+
{
|
|
113
|
+
className: itemClasses,
|
|
114
|
+
style: {
|
|
115
|
+
"--ds-card-stack-pos": gone ? 0 : Math.min(pos, visiblePeek),
|
|
116
|
+
zIndex: count - pos
|
|
117
|
+
},
|
|
118
|
+
inert: !isTop || void 0,
|
|
119
|
+
"aria-hidden": !isTop || void 0,
|
|
120
|
+
onAnimationEnd: isTop ? handleTopAnimationEnd : void 0,
|
|
121
|
+
children: child
|
|
122
|
+
},
|
|
123
|
+
k
|
|
124
|
+
);
|
|
125
|
+
}),
|
|
126
|
+
count > 1 && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__sr-only`, role: "status", children: [
|
|
127
|
+
"Card ",
|
|
128
|
+
index + 1,
|
|
129
|
+
" of ",
|
|
130
|
+
count
|
|
131
|
+
] })
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
CardStack.displayName = "CardStack";
|
|
138
|
+
export {
|
|
139
|
+
CardStack
|
|
140
|
+
};
|
|
@@ -110,3 +110,12 @@
|
|
|
110
110
|
outline: 2px solid var(--color-action-primary-bg);
|
|
111
111
|
outline-offset: 2px;
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
115
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
116
|
+
.material-symbols-rounded itself (a common global default) would
|
|
117
|
+
otherwise override inheritance and detach the icon from the state
|
|
118
|
+
colours this component sets on its parents. */
|
|
119
|
+
.ds-carousel .material-symbols-rounded {
|
|
120
|
+
color: inherit;
|
|
121
|
+
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
-
import React, { useState, useRef, useCallback, useEffect } from "react";
|
|
3
|
+
import React, { useState, useSyncExternalStore, useRef, useCallback, useEffect } from "react";
|
|
4
|
+
import { MOTION_AUTOPLAY_INTERVAL_MS } from "../../tokens/motion.js";
|
|
4
5
|
import "./Carousel.css";
|
|
5
6
|
import "../../fonts/material-symbols.css";
|
|
7
|
+
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
8
|
+
const subscribeReducedMotion = (callback) => {
|
|
9
|
+
const query = window.matchMedia(REDUCED_MOTION_QUERY);
|
|
10
|
+
query.addEventListener("change", callback);
|
|
11
|
+
return () => query.removeEventListener("change", callback);
|
|
12
|
+
};
|
|
13
|
+
const getReducedMotion = () => window.matchMedia(REDUCED_MOTION_QUERY).matches;
|
|
14
|
+
const getServerReducedMotion = () => false;
|
|
6
15
|
const Carousel = ({
|
|
7
16
|
children,
|
|
8
17
|
showDots = true,
|
|
9
18
|
showArrows = true,
|
|
10
19
|
autoPlay = false,
|
|
11
|
-
autoPlayInterval =
|
|
20
|
+
autoPlayInterval = MOTION_AUTOPLAY_INTERVAL_MS,
|
|
12
21
|
loop = false,
|
|
13
22
|
onSlideChange,
|
|
14
23
|
className = ""
|
|
@@ -17,6 +26,11 @@ const Carousel = ({
|
|
|
17
26
|
const slideCount = slides.length;
|
|
18
27
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
19
28
|
const [paused, setPaused] = useState(false);
|
|
29
|
+
const reducedMotion = useSyncExternalStore(
|
|
30
|
+
subscribeReducedMotion,
|
|
31
|
+
getReducedMotion,
|
|
32
|
+
getServerReducedMotion
|
|
33
|
+
);
|
|
20
34
|
const containerRef = useRef(null);
|
|
21
35
|
const baseClass = "ds-carousel";
|
|
22
36
|
const goTo = useCallback(
|
|
@@ -35,10 +49,10 @@ const Carousel = ({
|
|
|
35
49
|
const goNext = useCallback(() => goTo(activeIndex + 1), [activeIndex, goTo]);
|
|
36
50
|
const goPrev = useCallback(() => goTo(activeIndex - 1), [activeIndex, goTo]);
|
|
37
51
|
useEffect(() => {
|
|
38
|
-
if (!autoPlay || paused || slideCount <= 1) return;
|
|
52
|
+
if (!autoPlay || paused || reducedMotion || slideCount <= 1) return;
|
|
39
53
|
const interval = setInterval(goNext, autoPlayInterval);
|
|
40
54
|
return () => clearInterval(interval);
|
|
41
|
-
}, [autoPlay, paused, autoPlayInterval, goNext, slideCount]);
|
|
55
|
+
}, [autoPlay, paused, reducedMotion, autoPlayInterval, goNext, slideCount]);
|
|
42
56
|
const handleKeyDown = (e) => {
|
|
43
57
|
if (e.key === "ArrowLeft") {
|
|
44
58
|
e.preventDefault();
|
|
@@ -48,6 +62,21 @@ const Carousel = ({
|
|
|
48
62
|
goNext();
|
|
49
63
|
}
|
|
50
64
|
};
|
|
65
|
+
const handleDotKeyDown = (e, index) => {
|
|
66
|
+
let target;
|
|
67
|
+
if (e.key === "ArrowRight") {
|
|
68
|
+
target = loop ? (index + 1) % slideCount : Math.min(index + 1, slideCount - 1);
|
|
69
|
+
} else if (e.key === "ArrowLeft") {
|
|
70
|
+
target = loop ? (index - 1 + slideCount) % slideCount : Math.max(index - 1, 0);
|
|
71
|
+
}
|
|
72
|
+
if (target === void 0) return;
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
goTo(target);
|
|
76
|
+
const dotList = e.target.parentElement;
|
|
77
|
+
const dots = dotList?.querySelectorAll(`.${baseClass}__dot`);
|
|
78
|
+
dots?.[target]?.focus();
|
|
79
|
+
};
|
|
51
80
|
const canGoPrev = loop || activeIndex > 0;
|
|
52
81
|
const canGoNext = loop || activeIndex < slideCount - 1;
|
|
53
82
|
const containerClasses = [baseClass, className].filter(Boolean).join(" ");
|
|
@@ -59,6 +88,8 @@ const Carousel = ({
|
|
|
59
88
|
ref: containerRef,
|
|
60
89
|
onMouseEnter: () => setPaused(true),
|
|
61
90
|
onMouseLeave: () => setPaused(false),
|
|
91
|
+
onFocus: () => setPaused(true),
|
|
92
|
+
onBlur: () => setPaused(false),
|
|
62
93
|
onKeyDown: handleKeyDown,
|
|
63
94
|
role: "region",
|
|
64
95
|
"aria-roledescription": "carousel",
|
|
@@ -111,8 +142,10 @@ const Carousel = ({
|
|
|
111
142
|
{
|
|
112
143
|
className: `${baseClass}__dot${index === activeIndex ? ` ${baseClass}__dot--active` : ""}`,
|
|
113
144
|
onClick: () => goTo(index),
|
|
145
|
+
onKeyDown: (e) => handleDotKeyDown(e, index),
|
|
114
146
|
role: "tab",
|
|
115
147
|
"aria-selected": index === activeIndex,
|
|
148
|
+
tabIndex: index === activeIndex ? 0 : -1,
|
|
116
149
|
"aria-label": `Go to slide ${index + 1}`,
|
|
117
150
|
type: "button"
|
|
118
151
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback } from "react";
|
|
3
3
|
import { ResponsiveContainer, AreaChart as AreaChart$1, CartesianGrid, XAxis, YAxis, Tooltip, Area, Legend } from "recharts";
|
|
4
|
+
import { getChartSeriesColors } from "./palette.js";
|
|
4
5
|
import "./Chart.css";
|
|
5
6
|
function getCSSVar(name, fallback) {
|
|
6
7
|
if (typeof window === "undefined") return fallback;
|
|
@@ -32,7 +33,7 @@ const AreaChart = ({
|
|
|
32
33
|
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
33
34
|
const textSecondary = getCSSVar("--color-text-secondary", "#A2A2A2");
|
|
34
35
|
const gridColor = getCSSVar("--color-divider", "#232323");
|
|
35
|
-
const
|
|
36
|
+
const seriesColors = getChartSeriesColors();
|
|
36
37
|
const renderTooltip = useCallback(
|
|
37
38
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
38
39
|
(props) => /* @__PURE__ */ jsx(AreaTooltip, { ...props }),
|
|
@@ -50,8 +51,8 @@ const AreaChart = ({
|
|
|
50
51
|
] }, i)) })
|
|
51
52
|
] }),
|
|
52
53
|
/* @__PURE__ */ jsx("div", { className: `${baseClass}__body`, children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height, children: /* @__PURE__ */ jsxs(AreaChart$1, { data, margin: { top: 8, right: 4, bottom: 0, left: -12 }, children: [
|
|
53
|
-
/* @__PURE__ */ jsx("defs", { children: series.map((s) => {
|
|
54
|
-
const color = s.color ||
|
|
54
|
+
/* @__PURE__ */ jsx("defs", { children: series.map((s, i) => {
|
|
55
|
+
const color = s.color || seriesColors[i % seriesColors.length];
|
|
55
56
|
return /* @__PURE__ */ jsxs("linearGradient", { id: `area-gradient-${s.dataKey}`, x1: "0", y1: "0", x2: "0", y2: "1", children: [
|
|
56
57
|
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: 0.4 }),
|
|
57
58
|
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: 0.05 })
|
|
@@ -76,13 +77,13 @@ const AreaChart = ({
|
|
|
76
77
|
}
|
|
77
78
|
),
|
|
78
79
|
/* @__PURE__ */ jsx(Tooltip, { content: renderTooltip }),
|
|
79
|
-
series.map((s) => /* @__PURE__ */ jsx(
|
|
80
|
+
series.map((s, i) => /* @__PURE__ */ jsx(
|
|
80
81
|
Area,
|
|
81
82
|
{
|
|
82
83
|
type: "monotone",
|
|
83
84
|
dataKey: s.dataKey,
|
|
84
85
|
name: s.label,
|
|
85
|
-
stroke: s.color ||
|
|
86
|
+
stroke: s.color || seriesColors[i % seriesColors.length],
|
|
86
87
|
strokeWidth: 2,
|
|
87
88
|
fill: `url(#area-gradient-${s.dataKey})`,
|
|
88
89
|
fillOpacity: s.fillOpacity ?? 1,
|