@robr0/design-system 0.10.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 (125) hide show
  1. package/README.md +9 -8
  2. package/charts.d.ts +1 -0
  3. package/charts.js +2 -0
  4. package/components/AgentPlan/AgentPlan.css +14 -3
  5. package/components/AiButton/AiButton.css +1 -1
  6. package/components/Alert/Alert.css +9 -0
  7. package/components/AlertDialog/AlertDialog.css +3 -1
  8. package/components/AlertDialog/AlertDialog.js +2 -3
  9. package/components/AppLayout/AppLayout.d.ts +7 -1
  10. package/components/AppLayout/AppLayout.js +2 -1
  11. package/components/AppSidebar/AppSidebar.css +113 -40
  12. package/components/AppSidebar/AppSidebar.d.ts +14 -1
  13. package/components/AppSidebar/AppSidebar.js +26 -17
  14. package/components/Badge/Badge.css +4 -5
  15. package/components/Card/Card.css +7 -2
  16. package/components/CardStack/CardStack.css +106 -0
  17. package/components/CardStack/CardStack.d.ts +44 -0
  18. package/components/CardStack/CardStack.js +140 -0
  19. package/components/Carousel/Carousel.css +9 -0
  20. package/components/Carousel/Carousel.js +37 -4
  21. package/components/Chart/AreaChart.d.ts +5 -1
  22. package/components/Chart/AreaChart.js +11 -9
  23. package/components/Chart/BarChart.d.ts +3 -1
  24. package/components/Chart/BarChart.js +3 -3
  25. package/components/Chart/Chart.css +45 -0
  26. package/components/Chart/ComboChart.d.ts +40 -0
  27. package/components/Chart/ComboChart.js +142 -0
  28. package/components/Chart/LineChart.d.ts +3 -1
  29. package/components/Chart/LineChart.js +7 -6
  30. package/components/Chart/PieChart.d.ts +3 -1
  31. package/components/Chart/PieChart.js +5 -18
  32. package/components/Chart/RadarChart.d.ts +3 -1
  33. package/components/Chart/RadarChart.js +8 -7
  34. package/components/Chart/RadialChart.d.ts +7 -1
  35. package/components/Chart/RadialChart.js +53 -58
  36. package/components/Chart/ScatterChart.d.ts +3 -1
  37. package/components/Chart/ScatterChart.js +6 -16
  38. package/components/Chart/StackedBarChart.d.ts +3 -1
  39. package/components/Chart/StackedBarChart.js +5 -18
  40. package/components/Chart/Treemap.d.ts +3 -1
  41. package/components/Chart/Treemap.js +6 -20
  42. package/components/Chart/palette.d.ts +1 -0
  43. package/components/Chart/palette.js +17 -0
  44. package/components/ChatMarker/ChatMarker.css +9 -0
  45. package/components/ChatMessage/ChatMessage.css +16 -0
  46. package/components/ChatThread/ChatThread.js +2 -2
  47. package/components/Chip/Chip.css +9 -0
  48. package/components/CodeBlock/CodeBlock.css +10 -1
  49. package/components/CodeBlock/CodeBlock.js +2 -1
  50. package/components/ColorPicker/ColorPicker.js +10 -0
  51. package/components/Combobox/Combobox.css +9 -0
  52. package/components/CommandPalette/CommandPalette.css +9 -0
  53. package/components/CommandPalette/CommandPalette.js +31 -5
  54. package/components/Composer/Composer.css +118 -0
  55. package/components/Composer/Composer.d.ts +7 -0
  56. package/components/Composer/Composer.js +7 -1
  57. package/components/ContactCard/ContactCard.css +9 -0
  58. package/components/ContextMenu/ContextMenu.js +39 -3
  59. package/components/DatePicker/DatePicker.js +16 -0
  60. package/components/Dialog/Dialog.css +3 -1
  61. package/components/Dialog/Dialog.js +5 -4
  62. package/components/DocumentChip/DocumentChip.css +9 -0
  63. package/components/Dropdown/Dropdown.js +22 -3
  64. package/components/DropdownMenu/DropdownMenu.js +43 -6
  65. package/components/EmptyState/EmptyState.css +9 -0
  66. package/components/FileInput/FileInput.css +9 -0
  67. package/components/FunnelChart/FunnelChart.css +41 -0
  68. package/components/FunnelChart/FunnelChart.d.ts +40 -0
  69. package/components/FunnelChart/FunnelChart.js +48 -0
  70. package/components/Globe/Globe.css +155 -0
  71. package/components/Globe/Globe.d.ts +96 -0
  72. package/components/Globe/Globe.js +415 -0
  73. package/components/InterruptCard/InterruptCard.css +27 -17
  74. package/components/InterruptCard/InterruptCard.d.ts +1 -1
  75. package/components/InterruptCard/InterruptCard.js +1 -1
  76. package/components/LegendTile/LegendTile.css +60 -0
  77. package/components/LegendTile/LegendTile.d.ts +30 -0
  78. package/components/LegendTile/LegendTile.js +21 -0
  79. package/components/MapCallout/MapCallout.css +46 -0
  80. package/components/MapCallout/MapCallout.d.ts +29 -0
  81. package/components/MapCallout/MapCallout.js +17 -0
  82. package/components/MapLegend/MapLegend.css +88 -0
  83. package/components/MapLegend/MapLegend.d.ts +39 -0
  84. package/components/MapLegend/MapLegend.js +68 -0
  85. package/components/MessageActions/MessageActions.css +9 -0
  86. package/components/MessageCard/MessageCard.css +68 -16
  87. package/components/MessageCard/MessageCard.d.ts +1 -1
  88. package/components/MessageCard/MessageCard.js +6 -4
  89. package/components/ModelPicker/ModelPicker.css +4 -1
  90. package/components/ModelPicker/ModelPicker.js +43 -1
  91. package/components/NotificationCenter/NotificationCenter.css +9 -0
  92. package/components/NotificationCenter/NotificationCenter.js +26 -10
  93. package/components/NumberInput/NumberInput.css +9 -0
  94. package/components/Panel/Panel.css +26 -0
  95. package/components/Panel/Panel.d.ts +22 -0
  96. package/components/Panel/Panel.js +19 -0
  97. package/components/Popover/Popover.css +3 -1
  98. package/components/Popover/Popover.js +2 -1
  99. package/components/SourceChip/SourceChip.css +9 -0
  100. package/components/Stat/Stat.css +44 -2
  101. package/components/Stat/Stat.d.ts +3 -1
  102. package/components/Stat/Stat.js +7 -1
  103. package/components/Stepper/Stepper.css +9 -0
  104. package/components/TagInput/TagInput.css +9 -0
  105. package/components/TimePicker/TimePicker.js +4 -1
  106. package/components/Toast/Toast.css +32 -11
  107. package/components/Toast/Toast.js +5 -4
  108. package/components/ToggleGroup/ToggleGroup.css +9 -0
  109. package/components/ToolCall/ToolCall.css +18 -6
  110. package/components/Tooltip/Tooltip.js +20 -16
  111. package/components/registry.json +70 -0
  112. package/components/registry.json.d.ts +70 -0
  113. package/components/registry.json.js +2 -2
  114. package/index.d.ts +7 -0
  115. package/index.js +14 -0
  116. package/package.json +5 -1
  117. package/tokens/motion.d.ts +23 -0
  118. package/tokens/motion.js +14 -0
  119. package/tokens/registry.json +21 -1
  120. package/tokens/registry.json.d.ts +21 -1
  121. package/tokens/registry.json.js +1 -1
  122. package/tokens/tokens-dark.css +14 -0
  123. package/tokens/tokens-light.css +22 -0
  124. package/tokens/tokens-primitives.css +2 -0
  125. package/tokens/tokens-typography.css +32 -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 -->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 -->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 · 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 · 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
@@ -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
 
@@ -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), 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`.
101
101
 
102
102
  ### Token architecture
103
103
 
@@ -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.
@@ -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 (npm workspace: install once at the root)
171
- npm install
172
+ # Portfolio + documentation website
172
173
  npm run dev --workspace website # http://localhost:3000
173
174
  ```
174
175
 
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,
@@ -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];
@@ -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
 
@@ -442,3 +506,12 @@
442
506
  padding: var(--padding-lg) 0;
443
507
  }
444
508
  }
509
+
510
+ /* Icons take the colour of the text around them. Explicit, not just
511
+ inherited by default: a consumer stylesheet that sets a colour on
512
+ .material-symbols-rounded itself (a common global default) would
513
+ otherwise override inheritance and detach the icon from the state
514
+ colours this component sets on its parents. */
515
+ .ds-app-sidebar .material-symbols-rounded {
516
+ color: inherit;
517
+ }
@@ -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
  };