@robr0/design-system 0.11.0 → 0.12.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 (56) hide show
  1. package/README.md +4 -4
  2. package/charts.d.ts +1 -0
  3. package/charts.js +2 -0
  4. package/components/AppLayout/AppLayout.d.ts +7 -1
  5. package/components/AppLayout/AppLayout.js +2 -1
  6. package/components/AppSidebar/AppSidebar.css +104 -40
  7. package/components/AppSidebar/AppSidebar.d.ts +14 -1
  8. package/components/AppSidebar/AppSidebar.js +26 -17
  9. package/components/Badge/Badge.css +4 -5
  10. package/components/Chart/AreaChart.d.ts +5 -1
  11. package/components/Chart/AreaChart.js +5 -4
  12. package/components/Chart/BarChart.d.ts +3 -1
  13. package/components/Chart/BarChart.js +3 -3
  14. package/components/Chart/Chart.css +45 -0
  15. package/components/Chart/ComboChart.d.ts +40 -0
  16. package/components/Chart/ComboChart.js +142 -0
  17. package/components/Chart/LineChart.d.ts +3 -1
  18. package/components/Chart/LineChart.js +3 -3
  19. package/components/Chart/PieChart.d.ts +3 -1
  20. package/components/Chart/PieChart.js +3 -3
  21. package/components/Chart/RadarChart.d.ts +3 -1
  22. package/components/Chart/RadarChart.js +3 -3
  23. package/components/Chart/RadialChart.d.ts +7 -1
  24. package/components/Chart/RadialChart.js +51 -43
  25. package/components/Chart/ScatterChart.d.ts +3 -1
  26. package/components/Chart/ScatterChart.js +3 -3
  27. package/components/Chart/StackedBarChart.d.ts +3 -1
  28. package/components/Chart/StackedBarChart.js +3 -3
  29. package/components/Chart/Treemap.d.ts +3 -1
  30. package/components/Chart/Treemap.js +3 -3
  31. package/components/Chart/palette.js +1 -3
  32. package/components/FunnelChart/FunnelChart.css +41 -0
  33. package/components/FunnelChart/FunnelChart.d.ts +40 -0
  34. package/components/FunnelChart/FunnelChart.js +48 -0
  35. package/components/LegendTile/LegendTile.css +60 -0
  36. package/components/LegendTile/LegendTile.d.ts +30 -0
  37. package/components/LegendTile/LegendTile.js +21 -0
  38. package/components/Panel/Panel.css +26 -0
  39. package/components/Panel/Panel.d.ts +22 -0
  40. package/components/Panel/Panel.js +19 -0
  41. package/components/Stat/Stat.css +35 -2
  42. package/components/Stat/Stat.d.ts +3 -1
  43. package/components/Stat/Stat.js +7 -1
  44. package/components/registry.json +33 -0
  45. package/components/registry.json.d.ts +33 -0
  46. package/components/registry.json.js +1 -1
  47. package/index.d.ts +3 -0
  48. package/index.js +6 -0
  49. package/package.json +1 -1
  50. package/tokens/registry.json +4 -1
  51. package/tokens/registry.json.d.ts +4 -1
  52. package/tokens/registry.json.js +1 -1
  53. package/tokens/tokens-dark.css +3 -0
  54. package/tokens/tokens-light.css +11 -0
  55. package/tokens/tokens-primitives.css +2 -0
  56. package/tokens/tokens-typography.css +12 -5
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 -->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)** |
21
+ | **Design system** (`/src`) | <!-- component-count -->109<!-- /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 · 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
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 · Combo chart · 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 · Funnel chart · Globe · Input · Instructions · Interrupt card · Kbd · Legend tile · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · 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
@@ -151,10 +151,10 @@ Answers are measured, not assumed. `evals/chat` holds a golden set that runs thr
151
151
 
152
152
  ## Quality & CI
153
153
 
154
- Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/workflows/ci.yml)) with four jobs: lint + library build, story tests, Storybook build, and website lint + build. The story tests render every Storybook story in headless Chromium via Vitest and run an axe accessibility audit on each, so a violation fails the build exactly like a render error. The same checklist runs locally with one command:
154
+ Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/workflows/ci.yml)) with four jobs: lint + library build, story tests, Storybook build, and website lint + build followed by the two built-HTML checks (rendered spacing, chat-corpus coverage). The story tests render every Storybook story in headless Chromium via Vitest and run an axe accessibility audit on each, so a violation fails the build exactly like a render error. The same checklist runs locally with one command:
155
155
 
156
156
  ```bash
157
- npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build
157
+ npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build + built-HTML checks
158
158
  ```
159
159
 
160
160
  CI also guards against documentation drift: generated surfaces (this README's component count and list, the website's skills pages, the published blueprint copies of the root markdown specs) are rebuilt from their source registries on every build, and CI fails if the committed copies are stale. The numbers on the site are never hand-written.
package/charts.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  export * from './components/Chart/AreaChart';
7
7
  export * from './components/Chart/BarChart';
8
+ export * from './components/Chart/ComboChart';
8
9
  export * from './components/Chart/LineChart';
9
10
  export * from './components/Chart/PieChart';
10
11
  export * from './components/Chart/RadarChart';
package/charts.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { AreaChart } from "./components/Chart/AreaChart.js";
2
2
  import { BarChart } from "./components/Chart/BarChart.js";
3
+ import { ComboChart } from "./components/Chart/ComboChart.js";
3
4
  import { LineChart } from "./components/Chart/LineChart.js";
4
5
  import { PieChart } from "./components/Chart/PieChart.js";
5
6
  import { RadarChart } from "./components/Chart/RadarChart.js";
@@ -10,6 +11,7 @@ import { Treemap } from "./components/Chart/Treemap.js";
10
11
  export {
11
12
  AreaChart,
12
13
  BarChart,
14
+ ComboChart,
13
15
  LineChart,
14
16
  PieChart,
15
17
  RadarChart,
@@ -17,6 +17,12 @@ export interface AppLayoutProps {
17
17
  logo?: React.ReactNode;
18
18
  /** Page content — centred in the main area */
19
19
  children: React.ReactNode;
20
+ /**
21
+ * Colour scheme: 'dark' pins the layout to the dark theme (the historical
22
+ * behaviour and the default); 'inherit' drops the pin so the layout
23
+ * follows the surrounding data-theme like any other component.
24
+ */
25
+ theme?: 'dark' | 'inherit';
20
26
  /** Additional CSS classes on outer wrapper */
21
27
  className?: string;
22
28
  }
@@ -24,4 +30,4 @@ export interface AppLayoutProps {
24
30
  * AppLayout — full-page template with a collapsible sidebar
25
31
  * and a centred content area.
26
32
  */
27
- export declare const AppLayout: ({ sections, profile, activeKey, activeSubKey, defaultExpanded, logoText, logo, children, className, }: AppLayoutProps) => React.JSX.Element;
33
+ export declare const AppLayout: ({ sections, profile, activeKey, activeSubKey, defaultExpanded, logoText, logo, children, theme, className, }: AppLayoutProps) => React.JSX.Element;
@@ -12,6 +12,7 @@ const AppLayout = ({
12
12
  logoText,
13
13
  logo,
14
14
  children,
15
+ theme = "dark",
15
16
  className = ""
16
17
  }) => {
17
18
  const baseClass = "ds-app-layout";
@@ -24,7 +25,7 @@ const AppLayout = ({
24
25
  expanded ? `${baseClass}--expanded` : "",
25
26
  className
26
27
  ].filter(Boolean).join(" ");
27
- return /* @__PURE__ */ jsxs("div", { className: classes, "data-theme": "dark", children: [
28
+ return /* @__PURE__ */ jsxs("div", { className: classes, "data-theme": theme === "dark" ? "dark" : void 0, children: [
28
29
  /* @__PURE__ */ jsx(
29
30
  AppSidebar,
30
31
  {
@@ -27,6 +27,24 @@
27
27
  width: 280px;
28
28
  }
29
29
 
30
+ /* ============================================
31
+ FLOATING VARIANT — the rail as a glass card
32
+ inset from the viewport edges (the same
33
+ recipe as the site chat panel)
34
+ ============================================ */
35
+ .ds-app-sidebar--floating {
36
+ top: var(--ds-sidebar-float-inset, 20px);
37
+ left: var(--ds-sidebar-float-inset, 20px);
38
+ height: calc(100dvh - var(--ds-sidebar-float-inset, 20px) * 2);
39
+ background-color: var(--color-bg-glass);
40
+ border: var(--border-xs) solid var(--color-bg-container-border);
41
+ border-radius: var(--radius-xl);
42
+ box-shadow: var(--shadow-floating);
43
+ /* The glass token is deliberately translucent; this blur is what keeps
44
+ content behind the rail readable through it. */
45
+ backdrop-filter: blur(24px);
46
+ }
47
+
30
48
  /* ============================================
31
49
  TOP SECTION — logo + nav
32
50
  ============================================ */
@@ -52,15 +70,17 @@
52
70
  flex-shrink: 0;
53
71
  }
54
72
 
73
+ /* Expand/collapse choreography, here and on every row below: the rail's
74
+ clipping width and the text's opacity fade carry the whole transition.
75
+ Inner layout stays constant - text keeps its natural width with
76
+ min-width: 0 so it truncates under the sweeping rail instead of holding
77
+ rows at full width (width: 0 to auto cannot interpolate, and a gap that
78
+ snaps mid-glide reads as a bounce). */
55
79
  .ds-app-sidebar__logo-inner {
56
80
  display: flex;
57
81
  align-items: center;
58
- gap: 0;
59
- min-width: 0;
60
- }
61
-
62
- .ds-app-sidebar--expanded .ds-app-sidebar__logo-inner {
63
82
  gap: var(--gap-lg);
83
+ min-width: 0;
64
84
  }
65
85
 
66
86
  .ds-app-sidebar__logo-icon {
@@ -77,14 +97,14 @@
77
97
  letter-spacing: var(--font-paragraph-em-letter-spacing);
78
98
  color: var(--color-text-primary);
79
99
  white-space: nowrap;
80
- width: 0;
100
+ min-width: 0;
101
+ flex-shrink: 1;
81
102
  overflow: hidden;
82
103
  opacity: 0;
83
104
  transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
84
105
  }
85
106
 
86
107
  .ds-app-sidebar--expanded .ds-app-sidebar__logo-text {
87
- width: auto;
88
108
  opacity: 1;
89
109
  }
90
110
 
@@ -102,7 +122,8 @@
102
122
  padding: 0;
103
123
  overflow: hidden;
104
124
  opacity: 0;
105
- transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
125
+ /* Both widths are explicit (0 and 24px), so the width glides too. */
126
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard), width var(--motion-duration-slow) var(--motion-ease-emphasized);
106
127
  }
107
128
 
108
129
  .ds-app-sidebar--expanded .ds-app-sidebar__toggle {
@@ -123,10 +144,8 @@
123
144
  flex-direction: column;
124
145
  gap: var(--gap-xs);
125
146
  flex-shrink: 0;
126
- align-items: center;
127
- }
128
-
129
- .ds-app-sidebar--expanded .ds-app-sidebar__nav {
147
+ /* Stretch in both states: rows fill the rail and the collapsed icon is
148
+ centred by the row's own padding, so alignment never snaps. */
130
149
  align-items: stretch;
131
150
  }
132
151
 
@@ -156,8 +175,14 @@
156
175
  .ds-app-sidebar__btn {
157
176
  display: flex;
158
177
  align-items: center;
159
- justify-content: center;
178
+ justify-content: space-between;
160
179
  height: 40px;
180
+ min-width: 0;
181
+ /* The same inset in both states: in the collapsed 64px rail the 12px
182
+ margins leave a 40px row, so the hover pill is a perfect circle clear
183
+ of the rail edges, with the icon centred (12 + 8 + 24 + 8 + 12 = 64);
184
+ expanded, these are the pill's edge inset. Nothing to transition. */
185
+ margin: 0 var(--padding-sm-md);
161
186
  padding: var(--padding-sm);
162
187
  border-radius: var(--radius-full);
163
188
  background: transparent;
@@ -174,11 +199,6 @@
174
199
  /* Inset via margin + reduced padding (12 + 8 = the 20px the label sat at
175
200
  before), so the pill floats off the rail edges while the icon and label
176
201
  keep their exact x-positions against the category labels above. */
177
- .ds-app-sidebar--expanded .ds-app-sidebar__btn {
178
- margin: 0 var(--padding-sm-md);
179
- padding: var(--padding-sm);
180
- justify-content: space-between;
181
- }
182
202
 
183
203
  .ds-app-sidebar__btn:hover {
184
204
  background-color: var(--color-action-passive-bg-hover);
@@ -191,14 +211,10 @@
191
211
  .ds-app-sidebar__btn-left {
192
212
  display: flex;
193
213
  align-items: center;
194
- gap: 0;
214
+ gap: var(--gap-sm);
195
215
  min-width: 0;
196
216
  }
197
217
 
198
- .ds-app-sidebar--expanded .ds-app-sidebar__btn-left {
199
- gap: 8px;
200
- }
201
-
202
218
  .ds-app-sidebar__btn-icon {
203
219
  --icon-size: var(--icon-size-md);
204
220
  color: var(--color-icon-primary);
@@ -213,29 +229,48 @@
213
229
  letter-spacing: var(--font-paragraph-em-letter-spacing);
214
230
  color: var(--color-text-primary);
215
231
  white-space: nowrap;
216
- width: 0;
232
+ min-width: 0;
233
+ flex-shrink: 1;
217
234
  overflow: hidden;
218
235
  opacity: 0;
219
236
  transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
220
237
  }
221
238
 
222
239
  .ds-app-sidebar--expanded .ds-app-sidebar__btn-label {
223
- width: auto;
240
+ opacity: 1;
241
+ }
242
+
243
+ .ds-app-sidebar__btn-badge {
244
+ font-family: var(--font-caption-family);
245
+ font-size: var(--font-caption-size);
246
+ font-weight: var(--font-paragraph-sm-em-weight);
247
+ line-height: var(--font-caption-line-height);
248
+ color: var(--color-text-secondary);
249
+ background-color: var(--color-bg-container-tertiary);
250
+ border-radius: var(--radius-full);
251
+ padding: var(--padding-xxxs) var(--padding-sm);
252
+ min-width: 0;
253
+ flex-shrink: 1;
254
+ overflow: hidden;
255
+ opacity: 0;
256
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
257
+ }
258
+
259
+ .ds-app-sidebar--expanded .ds-app-sidebar__btn-badge {
224
260
  opacity: 1;
225
261
  }
226
262
 
227
263
  .ds-app-sidebar__btn-chevron {
228
264
  font-size: 24px;
229
265
  color: var(--color-icon-secondary);
230
- flex-shrink: 0;
231
- width: 0;
266
+ min-width: 0;
267
+ flex-shrink: 1;
232
268
  overflow: hidden;
233
269
  transition: transform var(--motion-duration-slow) var(--motion-ease-standard), opacity var(--motion-duration-base) var(--motion-ease-standard);
234
270
  opacity: 0;
235
271
  }
236
272
 
237
273
  .ds-app-sidebar--expanded .ds-app-sidebar__btn-chevron {
238
- width: auto;
239
274
  opacity: 1;
240
275
  }
241
276
 
@@ -323,31 +358,60 @@
323
358
  white-space: nowrap;
324
359
  }
325
360
 
361
+ /* ============================================
362
+ CONSUMER SLOTS + BOTTOM WRAPPER
363
+ Slots keep their height in both states so the
364
+ rail's layout never snaps; collapsed they fade
365
+ out and stop taking clicks.
366
+ ============================================ */
367
+ .ds-app-sidebar__bottom {
368
+ display: flex;
369
+ flex-direction: column;
370
+ gap: var(--gap-md);
371
+ min-width: 0;
372
+ flex-shrink: 0;
373
+ }
374
+
375
+ .ds-app-sidebar__top-slot,
376
+ .ds-app-sidebar__footer-slot {
377
+ padding: 0 var(--padding-md);
378
+ min-width: 0;
379
+ overflow: hidden;
380
+ opacity: 0;
381
+ pointer-events: none;
382
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
383
+ }
384
+
385
+ .ds-app-sidebar--expanded .ds-app-sidebar__top-slot,
386
+ .ds-app-sidebar--expanded .ds-app-sidebar__footer-slot {
387
+ opacity: 1;
388
+ pointer-events: auto;
389
+ }
390
+
326
391
  /* ============================================
327
392
  PROFILE
328
393
  ============================================ */
329
394
  .ds-app-sidebar__profile {
330
395
  display: flex;
331
396
  align-items: center;
332
- justify-content: center;
333
- padding: 0;
397
+ justify-content: space-between;
398
+ min-width: 0;
399
+ /* Centres the 40px avatar in the collapsed 64px rail; glides to the
400
+ expanded inset below. */
401
+ padding: 0 var(--padding-sm-md);
334
402
  flex-shrink: 0;
403
+ transition: padding var(--motion-duration-slow) var(--motion-ease-emphasized);
335
404
  }
336
405
 
337
406
  .ds-app-sidebar--expanded .ds-app-sidebar__profile {
338
- justify-content: space-between;
339
407
  padding: 0 var(--padding-lg);
340
408
  }
341
409
 
342
410
  .ds-app-sidebar__profile-left {
343
411
  display: flex;
344
412
  align-items: center;
345
- gap: 0;
346
- min-width: 0;
347
- }
348
-
349
- .ds-app-sidebar--expanded .ds-app-sidebar__profile-left {
350
413
  gap: 10px;
414
+ min-width: 0;
351
415
  }
352
416
 
353
417
  .ds-app-sidebar__avatar {
@@ -369,14 +433,13 @@
369
433
  display: flex;
370
434
  flex-direction: column;
371
435
  min-width: 0;
372
- width: 0;
436
+ flex-shrink: 1;
373
437
  overflow: hidden;
374
438
  opacity: 0;
375
439
  transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
376
440
  }
377
441
 
378
442
  .ds-app-sidebar--expanded .ds-app-sidebar__profile-info {
379
- width: auto;
380
443
  opacity: 1;
381
444
  }
382
445
 
@@ -416,11 +479,12 @@
416
479
  width: 0;
417
480
  overflow: hidden;
418
481
  opacity: 0;
419
- transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
482
+ /* Both widths are explicit (0 and 24px), so the width glides too. */
483
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard), width var(--motion-duration-slow) var(--motion-ease-emphasized);
420
484
  }
421
485
 
422
486
  .ds-app-sidebar--expanded .ds-app-sidebar__profile-more {
423
- width: auto;
487
+ width: 24px;
424
488
  opacity: 1;
425
489
  }
426
490
 
@@ -20,6 +20,8 @@ export interface AppSidebarItem {
20
20
  onClick?: () => void;
21
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
22
  href?: string;
23
+ /** Trailing count pill, e.g. unread items; hidden while the rail is collapsed */
24
+ badge?: string | number;
23
25
  /** Sub-items — turns this into an accordion */
24
26
  children?: AppSidebarSubItem[];
25
27
  }
@@ -54,6 +56,17 @@ export interface AppSidebarProps {
54
56
  onExpandedChange?: (expanded: boolean) => void;
55
57
  /** Callback when profile more button clicked */
56
58
  onProfileMore?: () => void;
59
+ /**
60
+ * Floats the rail off the viewport edges as a glass card: inset with
61
+ * rounded corners, the translucent glass fill over a backdrop blur, and
62
+ * the floating shadow. The inset defaults to 20px and is overridable via
63
+ * the --ds-sidebar-float-inset custom property.
64
+ */
65
+ floating?: boolean;
66
+ /** Rendered under the logo row, above the nav; fades out while collapsed */
67
+ topSlot?: React.ReactNode;
68
+ /** Rendered above the profile block; fades out while collapsed */
69
+ footerSlot?: React.ReactNode;
57
70
  /** Additional CSS classes */
58
71
  className?: string;
59
72
  /** Logo element — defaults to built-in robr0 logo */
@@ -68,4 +81,4 @@ export interface AppSidebarProps {
68
81
  * **Expanded** (280 px): labels, category headings, accordion
69
82
  * sub-items with tree-line connectors, and a profile section.
70
83
  */
71
- export declare const AppSidebar: ({ sections, profile, activeKey, activeSubKey, defaultExpanded, expanded: controlledExpanded, onExpandedChange, onProfileMore, className, logo, logoText, }: AppSidebarProps) => React.JSX.Element;
84
+ export declare const AppSidebar: ({ sections, profile, activeKey, activeSubKey, defaultExpanded, expanded: controlledExpanded, onExpandedChange, onProfileMore, floating, topSlot, footerSlot, className, logo, logoText, }: AppSidebarProps) => React.JSX.Element;
@@ -118,6 +118,9 @@ const AppSidebar = ({
118
118
  expanded: controlledExpanded,
119
119
  onExpandedChange,
120
120
  onProfileMore,
121
+ floating = false,
122
+ topSlot,
123
+ footerSlot,
121
124
  className = "",
122
125
  logo,
123
126
  logoText = "robr0"
@@ -145,6 +148,7 @@ const AppSidebar = ({
145
148
  const classes = [
146
149
  baseClass,
147
150
  isExpanded ? `${baseClass}--expanded` : "",
151
+ floating ? `${baseClass}--floating` : "",
148
152
  className
149
153
  ].filter(Boolean).join(" ");
150
154
  return /* @__PURE__ */ jsxs("nav", { className: classes, "aria-label": "App navigation", children: [
@@ -181,6 +185,7 @@ const AppSidebar = ({
181
185
  }
182
186
  )
183
187
  ] }),
188
+ topSlot && /* @__PURE__ */ jsx("div", { className: `${baseClass}__top-slot`, children: topSlot }),
184
189
  sections.map((section, si) => /* @__PURE__ */ jsxs("div", { className: `${baseClass}__nav`, children: [
185
190
  section.category && /* @__PURE__ */ jsx("div", { className: `${baseClass}__category`, children: section.category }),
186
191
  section.items.map((item) => {
@@ -197,6 +202,7 @@ const AppSidebar = ({
197
202
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-icon material-symbols-rounded`, children: item.icon }),
198
203
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-label`, children: item.label })
199
204
  ] }),
205
+ item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-badge`, children: item.badge }),
200
206
  hasChildren && /* @__PURE__ */ jsx(
201
207
  "span",
202
208
  {
@@ -279,23 +285,26 @@ const AppSidebar = ({
279
285
  })
280
286
  ] }, si))
281
287
  ] }),
282
- profile && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__profile`, children: [
283
- /* @__PURE__ */ jsxs("div", { className: `${baseClass}__profile-left`, children: [
284
- /* @__PURE__ */ jsx("div", { className: `${baseClass}__avatar`, children: profile.avatarUrl && /* @__PURE__ */ jsx("img", { src: profile.avatarUrl, alt: profile.name }) }),
285
- /* @__PURE__ */ jsxs("div", { className: `${baseClass}__profile-info`, children: [
286
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__profile-name`, children: profile.name }),
287
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__profile-email`, children: profile.email })
288
- ] })
289
- ] }),
290
- /* @__PURE__ */ jsx(
291
- "button",
292
- {
293
- className: `${baseClass}__profile-more`,
294
- onClick: onProfileMore,
295
- "aria-label": "Profile options",
296
- children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: "more_horiz" })
297
- }
298
- )
288
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__bottom`, children: [
289
+ footerSlot && /* @__PURE__ */ jsx("div", { className: `${baseClass}__footer-slot`, children: footerSlot }),
290
+ profile && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__profile`, children: [
291
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__profile-left`, children: [
292
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__avatar`, children: profile.avatarUrl && /* @__PURE__ */ jsx("img", { src: profile.avatarUrl, alt: profile.name }) }),
293
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__profile-info`, children: [
294
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__profile-name`, children: profile.name }),
295
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__profile-email`, children: profile.email })
296
+ ] })
297
+ ] }),
298
+ /* @__PURE__ */ jsx(
299
+ "button",
300
+ {
301
+ className: `${baseClass}__profile-more`,
302
+ onClick: onProfileMore,
303
+ "aria-label": "Profile options",
304
+ children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: "more_horiz" })
305
+ }
306
+ )
307
+ ] })
299
308
  ] })
300
309
  ] });
301
310
  };
@@ -12,6 +12,10 @@
12
12
  line-height: var(--font-paragraph-sm-em-line-height);
13
13
  letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
14
14
  padding: var(--padding-xxxs) var(--padding-sm); /* Figma: 2px 8px */
15
+ /* Badges are borderless by decision (2026-08-26): the tinted fill
16
+ carries the status on its own, and the old per-variant borders sat
17
+ below 3:1 against their fills. The transparent border keeps the
18
+ geometry so the box does not shift against bordered neighbours. */
15
19
  border: var(--border-xs) solid transparent;
16
20
  border-radius: var(--radius-xs); /* Figma: 4px */
17
21
  white-space: nowrap;
@@ -23,7 +27,6 @@
23
27
 
24
28
  .ds-badge--info {
25
29
  background-color: var(--color-status-info-bg);
26
- border-color: var(--color-status-info-border);
27
30
  color: var(--color-status-info-text);
28
31
  }
29
32
 
@@ -33,7 +36,6 @@
33
36
 
34
37
  .ds-badge--positive {
35
38
  background-color: var(--color-status-positive-bg);
36
- border-color: var(--color-status-positive-border);
37
39
  color: var(--color-status-positive-text);
38
40
  }
39
41
 
@@ -43,7 +45,6 @@
43
45
 
44
46
  .ds-badge--warning {
45
47
  background-color: var(--color-status-warning-bg);
46
- border-color: var(--color-status-warning-border);
47
48
  color: var(--color-status-warning-text);
48
49
  }
49
50
 
@@ -53,7 +54,6 @@
53
54
 
54
55
  .ds-badge--error {
55
56
  background-color: var(--color-status-error-bg);
56
- border-color: var(--color-status-error-border);
57
57
  color: var(--color-status-error-text);
58
58
  }
59
59
 
@@ -63,6 +63,5 @@
63
63
 
64
64
  .ds-badge--neutral {
65
65
  background-color: var(--color-status-neutral-bg);
66
- border-color: var(--color-status-neutral-border);
67
66
  color: var(--color-status-neutral-text);
68
67
  }
@@ -26,6 +26,10 @@ export interface AreaChartProps {
26
26
  summaryItems?: ChartSummaryItem[];
27
27
  /** Chart area height in pixels */
28
28
  height?: number;
29
+ /** Show the legend under a multi-series chart */
30
+ showLegend?: boolean;
31
+ /** Strip the card chrome (border, padding, fill) when the chart sits inside another panel that supplies the surface */
32
+ bare?: boolean;
29
33
  /** Additional CSS classes on the wrapper */
30
34
  className?: string;
31
35
  }
@@ -33,4 +37,4 @@ export interface AreaChartProps {
33
37
  * Area chart built on Recharts with design-system tokens.
34
38
  * Supports multiple stacked or overlapping series with gradient fills.
35
39
  */
36
- export declare const AreaChart: ({ data, xKey, series, stacked, title, subtitle, summaryItems, height, className, }: AreaChartProps) => import("react").JSX.Element;
40
+ export declare const AreaChart: ({ data, xKey, series, stacked, title, subtitle, summaryItems, height, showLegend, bare, className, }: AreaChartProps) => import("react").JSX.Element;
@@ -4,8 +4,7 @@ import { ResponsiveContainer, AreaChart as AreaChart$1, CartesianGrid, XAxis, YA
4
4
  import { getChartSeriesColors } from "./palette.js";
5
5
  import "./Chart.css";
6
6
  function getCSSVar(name, fallback) {
7
- if (typeof window === "undefined") return fallback;
8
- return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
7
+ return `var(${name}, ${fallback})`;
9
8
  }
10
9
  function AreaTooltip({ active, payload, label }) {
11
10
  if (!active || !payload?.length) return null;
@@ -27,10 +26,12 @@ const AreaChart = ({
27
26
  subtitle,
28
27
  summaryItems,
29
28
  height = 350,
29
+ showLegend = true,
30
+ bare = false,
30
31
  className = ""
31
32
  }) => {
32
33
  const baseClass = "ds-chart";
33
- const classes = [baseClass, className].filter(Boolean).join(" ");
34
+ const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
34
35
  const textSecondary = getCSSVar("--color-text-secondary", "#A2A2A2");
35
36
  const gridColor = getCSSVar("--color-divider", "#232323");
36
37
  const seriesColors = getChartSeriesColors();
@@ -91,7 +92,7 @@ const AreaChart = ({
91
92
  },
92
93
  s.dataKey
93
94
  )),
94
- series.length > 1 && /* @__PURE__ */ jsx(Legend, { wrapperStyle: { color: textSecondary, fontSize: 12 } })
95
+ showLegend && series.length > 1 && /* @__PURE__ */ jsx(Legend, { wrapperStyle: { color: textSecondary, fontSize: 12 } })
95
96
  ] }) }) })
96
97
  ] });
97
98
  };
@@ -23,6 +23,8 @@ export interface BarChartProps {
23
23
  barColor?: string;
24
24
  /** Chart area height in pixels */
25
25
  height?: number;
26
+ /** Strip the card chrome (border, padding, fill) when the chart sits inside another panel that supplies the surface */
27
+ bare?: boolean;
26
28
  /** Additional CSS classes on the wrapper */
27
29
  className?: string;
28
30
  }
@@ -31,4 +33,4 @@ export interface BarChartProps {
31
33
  * Part of the Chart component family — shares wrapper, header,
32
34
  * tooltip, and summary-stat styles with future chart types.
33
35
  */
34
- export declare const BarChart: ({ data, xKey, yKey, dataLabel, title, subtitle, summaryItems, barColor, height, className, }: BarChartProps) => import("react").JSX.Element;
36
+ export declare const BarChart: ({ data, xKey, yKey, dataLabel, title, subtitle, summaryItems, barColor, height, bare, className, }: BarChartProps) => import("react").JSX.Element;
@@ -3,8 +3,7 @@ import { useCallback } from "react";
3
3
  import { ResponsiveContainer, BarChart as BarChart$1, CartesianGrid, XAxis, YAxis, Tooltip, Bar } from "recharts";
4
4
  import "./Chart.css";
5
5
  function getCSSVar(name, fallback) {
6
- if (typeof window === "undefined") return fallback;
7
- return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
6
+ return `var(${name}, ${fallback})`;
8
7
  }
9
8
  function ChartTooltip({
10
9
  active,
@@ -33,10 +32,11 @@ const BarChart = ({
33
32
  summaryItems,
34
33
  barColor,
35
34
  height = 350,
35
+ bare = false,
36
36
  className = ""
37
37
  }) => {
38
38
  const baseClass = "ds-chart";
39
- const classes = [baseClass, className].filter(Boolean).join(" ");
39
+ const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
40
40
  const resolvedBarColor = barColor || getCSSVar("--color-action-primary-bg", "#0E6E8F");
41
41
  const textSecondary = getCSSVar("--color-text-secondary", "#A2A2A2");
42
42
  const gridColor = getCSSVar("--color-divider", "#232323");