@robr0/design-system 0.9.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.
Files changed (99) hide show
  1. package/README.md +16 -10
  2. package/components/AgentPlan/AgentPlan.css +14 -3
  3. package/components/AiButton/AiButton.css +1 -1
  4. package/components/Alert/Alert.css +9 -0
  5. package/components/AlertDialog/AlertDialog.css +3 -1
  6. package/components/AlertDialog/AlertDialog.js +2 -3
  7. package/components/AppSidebar/AppSidebar.css +22 -3
  8. package/components/AppSidebar/AppSidebar.d.ts +6 -2
  9. package/components/AppSidebar/AppSidebar.js +67 -36
  10. package/components/Card/Card.css +7 -2
  11. package/components/CardStack/CardStack.css +106 -0
  12. package/components/CardStack/CardStack.d.ts +44 -0
  13. package/components/CardStack/CardStack.js +140 -0
  14. package/components/Carousel/Carousel.css +9 -0
  15. package/components/Carousel/Carousel.js +37 -4
  16. package/components/Chart/AreaChart.js +6 -5
  17. package/components/Chart/BarChart.js +1 -1
  18. package/components/Chart/LineChart.js +4 -3
  19. package/components/Chart/PieChart.js +2 -15
  20. package/components/Chart/RadarChart.js +5 -4
  21. package/components/Chart/RadialChart.js +2 -15
  22. package/components/Chart/ScatterChart.js +3 -13
  23. package/components/Chart/StackedBarChart.js +2 -15
  24. package/components/Chart/Treemap.js +3 -17
  25. package/components/Chart/palette.d.ts +1 -0
  26. package/components/Chart/palette.js +19 -0
  27. package/components/ChatMarker/ChatMarker.css +9 -0
  28. package/components/ChatMessage/ChatMessage.css +16 -0
  29. package/components/ChatThread/ChatThread.js +2 -2
  30. package/components/Chip/Chip.css +9 -0
  31. package/components/CodeBlock/CodeBlock.css +10 -1
  32. package/components/CodeBlock/CodeBlock.js +2 -1
  33. package/components/ColorPicker/ColorPicker.js +10 -0
  34. package/components/Combobox/Combobox.css +9 -0
  35. package/components/CommandPalette/CommandPalette.css +9 -0
  36. package/components/CommandPalette/CommandPalette.js +31 -5
  37. package/components/Composer/Composer.css +118 -0
  38. package/components/Composer/Composer.d.ts +7 -0
  39. package/components/Composer/Composer.js +7 -1
  40. package/components/ContactCard/ContactCard.css +9 -0
  41. package/components/ContextMenu/ContextMenu.js +39 -3
  42. package/components/DatePicker/DatePicker.js +16 -0
  43. package/components/Dialog/Dialog.css +3 -1
  44. package/components/Dialog/Dialog.js +5 -4
  45. package/components/DocumentChip/DocumentChip.css +9 -0
  46. package/components/Dropdown/Dropdown.js +22 -3
  47. package/components/DropdownMenu/DropdownMenu.js +43 -6
  48. package/components/EmptyState/EmptyState.css +9 -0
  49. package/components/FileInput/FileInput.css +9 -0
  50. package/components/Globe/Globe.css +155 -0
  51. package/components/Globe/Globe.d.ts +96 -0
  52. package/components/Globe/Globe.js +415 -0
  53. package/components/InterruptCard/InterruptCard.css +27 -17
  54. package/components/InterruptCard/InterruptCard.d.ts +1 -1
  55. package/components/InterruptCard/InterruptCard.js +1 -1
  56. package/components/MapCallout/MapCallout.css +46 -0
  57. package/components/MapCallout/MapCallout.d.ts +29 -0
  58. package/components/MapCallout/MapCallout.js +17 -0
  59. package/components/MapLegend/MapLegend.css +88 -0
  60. package/components/MapLegend/MapLegend.d.ts +39 -0
  61. package/components/MapLegend/MapLegend.js +68 -0
  62. package/components/MessageActions/MessageActions.css +9 -0
  63. package/components/MessageCard/MessageCard.css +68 -16
  64. package/components/MessageCard/MessageCard.d.ts +1 -1
  65. package/components/MessageCard/MessageCard.js +6 -4
  66. package/components/ModelPicker/ModelPicker.css +4 -1
  67. package/components/ModelPicker/ModelPicker.js +43 -1
  68. package/components/NotificationCenter/NotificationCenter.css +9 -0
  69. package/components/NotificationCenter/NotificationCenter.js +26 -10
  70. package/components/NumberInput/NumberInput.css +9 -0
  71. package/components/Popover/Popover.css +3 -1
  72. package/components/Popover/Popover.js +2 -1
  73. package/components/SectionTitle/SectionTitle.css +8 -0
  74. package/components/SectionTitle/SectionTitle.d.ts +3 -1
  75. package/components/SectionTitle/SectionTitle.js +6 -1
  76. package/components/SourceChip/SourceChip.css +9 -0
  77. package/components/Stat/Stat.css +9 -0
  78. package/components/Stepper/Stepper.css +9 -0
  79. package/components/TagInput/TagInput.css +9 -0
  80. package/components/TimePicker/TimePicker.js +4 -1
  81. package/components/Toast/Toast.css +32 -11
  82. package/components/Toast/Toast.js +5 -4
  83. package/components/ToggleGroup/ToggleGroup.css +9 -0
  84. package/components/ToolCall/ToolCall.css +18 -6
  85. package/components/Tooltip/Tooltip.js +20 -16
  86. package/components/registry.json +37 -0
  87. package/components/registry.json.d.ts +37 -0
  88. package/components/registry.json.js +2 -2
  89. package/index.d.ts +4 -0
  90. package/index.js +8 -0
  91. package/package.json +5 -1
  92. package/tokens/motion.d.ts +23 -0
  93. package/tokens/motion.js +14 -0
  94. package/tokens/registry.json +17 -0
  95. package/tokens/registry.json.d.ts +17 -0
  96. package/tokens/registry.json.js +1 -1
  97. package/tokens/tokens-dark.css +28 -13
  98. package/tokens/tokens-light.css +26 -12
  99. 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 -->101<!-- /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)** |
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 is also the one place a `prefers-reduced-motion` check lives in JavaScript rather than in the motion tokens, because the CSS guard cannot see a `requestAnimationFrame` loop. Set the preference and it draws a single static frame.
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
 
@@ -84,24 +84,29 @@ import { BarChart, LineChart } from '@robr0/design-system/charts';
84
84
  ```css
85
85
  :root { --font-family-primary: 'Inter', sans-serif; }
86
86
  ```
87
- - **Colours, radius, spacing**: every semantic token chains to a primitive, so overriding a primitive re-themes everything built on it:
87
+ - **Colours, radius, spacing**: every semantic token chains to a primitive, so overriding a primitive re-themes everything built on it. The action colour is theme-split by design (light fills run teal-08/09/10, dark inverts to teal-05/04/03), so rebranding it means re-keying those steps:
88
88
  ```css
89
89
  :root {
90
- --primitive-teal-07: #7C3AED; /* your brand color becomes the action color */
90
+ --primitive-teal-08: #6D31D3; /* light fill */
91
+ --primitive-teal-09: #4C2293; /* light hover */
92
+ --primitive-teal-10: #2E1560; /* light active, dark label */
93
+ --primitive-teal-05: #A78BFA; /* dark fill */
94
+ --primitive-teal-04: #C4B5FD; /* dark hover */
95
+ --primitive-teal-03: #DDD6FE; /* dark active */
91
96
  --primitive-radius-full: 12px; /* pill buttons become rounded rectangles */
92
97
  }
93
98
  ```
94
99
 
95
- Icons use a bundled Material Symbols Rounded variable font (woff2), with no extra setup. 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`.
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`.
96
101
 
97
102
  ### Token architecture
98
103
 
99
104
  Tokens flow in one direction, and primitives are never referenced directly in components:
100
105
 
101
106
  ```
102
- tokens-primitives.css --primitive-teal-07: #118AB2
107
+ tokens-primitives.css --primitive-teal-08: #0E6E8F
103
108
 
104
- tokens-light/dark.css --color-action-primary-bg: var(--primitive-teal-07)
109
+ tokens-light/dark.css --color-action-primary-bg: var(--primitive-teal-08)
105
110
 
106
111
  Component CSS background-color: var(--color-action-primary-bg)
107
112
  ```
@@ -159,11 +164,12 @@ CI also guards against documentation drift: generated surfaces (this README's co
159
164
  ## Running locally
160
165
 
161
166
  ```bash
167
+ npm install # once, at the root: the website is an npm workspace, so this installs both
168
+
162
169
  # Storybook (the library's dev sandbox)
163
170
  npm run storybook # http://localhost:6006
164
171
 
165
- # Portfolio + documentation website (npm workspace: install once at the root)
166
- npm install
172
+ # Portfolio + documentation website
167
173
  npm run dev --workspace website # http://localhost:3000
168
174
  ```
169
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
- border-radius: var(--radius-md);
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
- padding: var(--padding-sm) var(--padding-md);
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-md);
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
+ }
@@ -60,7 +60,7 @@
60
60
  position: absolute;
61
61
  inset: 0;
62
62
  border-radius: inherit;
63
- padding: var(--border-xs);
63
+ padding: var(--border-md);
64
64
  background: conic-gradient(
65
65
  from var(--ds-ai-button-angle),
66
66
  var(--color-ai-gradient-start),
@@ -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];
@@ -166,12 +166,17 @@
166
166
  cursor: pointer;
167
167
  overflow: hidden;
168
168
  text-align: left;
169
- transition: background-color var(--motion-duration-fast) var(--motion-ease-standard), padding var(--motion-duration-slow) var(--motion-ease-emphasized);
169
+ /* The row renders as an <a> when the item carries an href. */
170
+ text-decoration: none;
171
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-standard), padding var(--motion-duration-slow) var(--motion-ease-emphasized), margin var(--motion-duration-slow) var(--motion-ease-emphasized);
170
172
  }
171
173
 
174
+ /* Inset via margin + reduced padding (12 + 8 = the 20px the label sat at
175
+ before), so the pill floats off the rail edges while the icon and label
176
+ keep their exact x-positions against the category labels above. */
172
177
  .ds-app-sidebar--expanded .ds-app-sidebar__btn {
173
- width: 100%;
174
- padding: var(--padding-sm) var(--padding-lg);
178
+ margin: 0 var(--padding-sm-md);
179
+ padding: var(--padding-sm);
175
180
  justify-content: space-between;
176
181
  }
177
182
 
@@ -282,6 +287,9 @@
282
287
  align-items: center;
283
288
  gap: 8px;
284
289
  height: 40px;
290
+ /* The same edge inset the parent pills get; sub-items only render while
291
+ the rail is expanded, so this needs no state selector. */
292
+ margin-right: var(--padding-sm-md);
285
293
  padding: var(--padding-sm);
286
294
  border-radius: var(--radius-full);
287
295
  background: transparent;
@@ -292,6 +300,8 @@
292
300
  text-align: left;
293
301
  overflow: hidden;
294
302
  white-space: nowrap;
303
+ /* The row renders as an <a> when the sub-item carries an href. */
304
+ text-decoration: none;
295
305
  transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
296
306
  }
297
307
 
@@ -432,3 +442,12 @@
432
442
  padding: var(--padding-lg) 0;
433
443
  }
434
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
+ }
@@ -4,8 +4,10 @@ export interface AppSidebarSubItem {
4
4
  key: string;
5
5
  /** Display label */
6
6
  label: string;
7
- /** Click handler */
7
+ /** Click handler — also fires on a link sub-item, so a consumer can route client-side */
8
8
  onClick?: () => void;
9
+ /** Renders the sub-item as a real link to this URL instead of a button */
10
+ href?: string;
9
11
  }
10
12
  export interface AppSidebarItem {
11
13
  /** Unique key for this item */
@@ -14,8 +16,10 @@ export interface AppSidebarItem {
14
16
  icon: string;
15
17
  /** Display label (shown when expanded) */
16
18
  label: string;
17
- /** Click handler */
19
+ /** Click handler — also fires on a link item, so a consumer can route client-side */
18
20
  onClick?: () => void;
21
+ /** Renders the item as a real link to this URL instead of a button. Ignored when the item has children, whose row is the accordion toggle */
22
+ href?: string;
19
23
  /** Sub-items — turns this into an accordion */
20
24
  children?: AppSidebarSubItem[];
21
25
  }
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useCallback } from "react";
4
4
  import "./AppSidebar.css";
5
5
  import "../../fonts/material-symbols.css";
@@ -187,14 +187,43 @@ const AppSidebar = ({
187
187
  const hasChildren = item.children && item.children.length > 0;
188
188
  const isOpen = openKeys.has(item.key);
189
189
  const isActive = activeKey === item.key;
190
- return /* @__PURE__ */ jsxs(React.Fragment, { children: [
191
- /* @__PURE__ */ jsxs(
192
- "button",
190
+ const isLink = Boolean(item.href) && !hasChildren;
191
+ const itemClasses = [
192
+ `${baseClass}__btn`,
193
+ isActive ? `${baseClass}__btn--active` : ""
194
+ ].filter(Boolean).join(" ");
195
+ const itemContent = /* @__PURE__ */ jsxs(Fragment, { children: [
196
+ /* @__PURE__ */ jsxs("span", { className: `${baseClass}__btn-left`, children: [
197
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-icon material-symbols-rounded`, children: item.icon }),
198
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-label`, children: item.label })
199
+ ] }),
200
+ hasChildren && /* @__PURE__ */ jsx(
201
+ "span",
193
202
  {
194
203
  className: [
195
- `${baseClass}__btn`,
196
- isActive ? `${baseClass}__btn--active` : ""
204
+ `${baseClass}__btn-chevron material-symbols-rounded`,
205
+ isOpen ? `${baseClass}__btn-chevron--open` : ""
197
206
  ].filter(Boolean).join(" "),
207
+ children: "chevron_right"
208
+ }
209
+ )
210
+ ] });
211
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
212
+ isLink ? /* @__PURE__ */ jsx(
213
+ "a",
214
+ {
215
+ className: itemClasses,
216
+ href: item.href,
217
+ onClick: () => item.onClick?.(),
218
+ "aria-current": isActive ? "page" : void 0,
219
+ title: !isExpanded ? item.label : void 0,
220
+ children: itemContent
221
+ }
222
+ ) : /* @__PURE__ */ jsx(
223
+ "button",
224
+ {
225
+ type: "button",
226
+ className: itemClasses,
198
227
  onClick: () => {
199
228
  if (hasChildren && isExpanded) {
200
229
  toggleAccordion(item.key);
@@ -202,23 +231,9 @@ const AppSidebar = ({
202
231
  item.onClick?.();
203
232
  },
204
233
  "aria-expanded": hasChildren ? isOpen : void 0,
234
+ "aria-current": isActive ? "page" : void 0,
205
235
  title: !isExpanded ? item.label : void 0,
206
- children: [
207
- /* @__PURE__ */ jsxs("span", { className: `${baseClass}__btn-left`, children: [
208
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-icon material-symbols-rounded`, children: item.icon }),
209
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-label`, children: item.label })
210
- ] }),
211
- hasChildren && /* @__PURE__ */ jsx(
212
- "span",
213
- {
214
- className: [
215
- `${baseClass}__btn-chevron material-symbols-rounded`,
216
- isOpen ? `${baseClass}__btn-chevron--open` : ""
217
- ].filter(Boolean).join(" "),
218
- children: "chevron_right"
219
- }
220
- )
221
- ]
236
+ children: itemContent
222
237
  }
223
238
  ),
224
239
  hasChildren && /* @__PURE__ */ jsx(
@@ -228,20 +243,36 @@ const AppSidebar = ({
228
243
  `${baseClass}__sub-items`,
229
244
  isOpen && isExpanded ? `${baseClass}__sub-items--open` : ""
230
245
  ].filter(Boolean).join(" "),
231
- children: item.children.map((sub) => /* @__PURE__ */ jsxs("div", { className: `${baseClass}__sub-item`, children: [
232
- /* @__PURE__ */ jsx("div", { className: `${baseClass}__sub-line` }),
233
- /* @__PURE__ */ jsx(
234
- "button",
235
- {
236
- className: [
237
- `${baseClass}__sub-btn`,
238
- activeSubKey === sub.key ? `${baseClass}__sub-btn--active` : ""
239
- ].filter(Boolean).join(" "),
240
- onClick: sub.onClick,
241
- children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__sub-label`, children: sub.label })
242
- }
243
- )
244
- ] }, sub.key))
246
+ children: item.children.map((sub) => {
247
+ const subActive = activeSubKey === sub.key;
248
+ const subClasses = [
249
+ `${baseClass}__sub-btn`,
250
+ subActive ? `${baseClass}__sub-btn--active` : ""
251
+ ].filter(Boolean).join(" ");
252
+ const subLabel = /* @__PURE__ */ jsx("span", { className: `${baseClass}__sub-label`, children: sub.label });
253
+ return /* @__PURE__ */ jsxs("div", { className: `${baseClass}__sub-item`, children: [
254
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__sub-line` }),
255
+ sub.href ? /* @__PURE__ */ jsx(
256
+ "a",
257
+ {
258
+ className: subClasses,
259
+ href: sub.href,
260
+ onClick: sub.onClick,
261
+ "aria-current": subActive ? "page" : void 0,
262
+ children: subLabel
263
+ }
264
+ ) : /* @__PURE__ */ jsx(
265
+ "button",
266
+ {
267
+ type: "button",
268
+ className: subClasses,
269
+ onClick: sub.onClick,
270
+ "aria-current": subActive ? "page" : void 0,
271
+ children: subLabel
272
+ }
273
+ )
274
+ ] }, sub.key);
275
+ })
245
276
  }
246
277
  )
247
278
  ] }, item.key);
@@ -76,7 +76,9 @@
76
76
  .ds-card--case-study {
77
77
  height: auto;
78
78
  min-width: 0;
79
- padding: 0;
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
- padding: var(--padding-lg);
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 {};