@robr0/design-system 0.14.0 → 0.15.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 (62) hide show
  1. package/README.md +8 -5
  2. package/charts.d.ts +1 -0
  3. package/charts.js +2 -0
  4. package/components/AnchorNav/AnchorNav.css +74 -0
  5. package/components/AnchorNav/AnchorNav.d.ts +7 -0
  6. package/components/AnchorNav/AnchorNav.js +74 -21
  7. package/components/Button/Button.css +51 -0
  8. package/components/Button/Button.d.ts +2 -2
  9. package/components/Chart/Chart.css +4 -1
  10. package/components/CircularButton/CircularButton.css +46 -0
  11. package/components/CircularButton/CircularButton.d.ts +13 -2
  12. package/components/CircularButton/CircularButton.js +19 -16
  13. package/components/CommandPalette/CommandPalette.css +26 -1
  14. package/components/CommandPalette/CommandPalette.js +11 -7
  15. package/components/Composer/Composer.css +18 -0
  16. package/components/Composer/Composer.d.ts +4 -2
  17. package/components/Composer/Composer.js +1 -0
  18. package/components/ContributionGraph/ContributionGraph.css +21 -0
  19. package/components/ContributionGraph/ContributionGraph.js +18 -10
  20. package/components/Figure/Figure.css +14 -4
  21. package/components/Figure/Figure.js +11 -20
  22. package/components/FunnelChart/FunnelChart.css +11 -36
  23. package/components/FunnelChart/FunnelChart.d.ts +19 -16
  24. package/components/FunnelChart/FunnelChart.js +67 -39
  25. package/components/Gauge/Gauge.css +11 -1
  26. package/components/Meter/Meter.css +10 -1
  27. package/components/ProgressBar/ProgressBar.css +1 -1
  28. package/components/SegmentedControl/SegmentedControl.css +23 -0
  29. package/components/SegmentedControl/SegmentedControl.d.ts +3 -1
  30. package/components/SegmentedControl/SegmentedControl.js +3 -1
  31. package/components/Slider/Slider.css +1 -1
  32. package/components/Slider/Slider.js +1 -1
  33. package/components/SourceTrail/SourceTrail.css +209 -0
  34. package/components/SourceTrail/SourceTrail.d.ts +49 -0
  35. package/components/SourceTrail/SourceTrail.js +108 -0
  36. package/components/Sparkline/Sparkline.css +26 -2
  37. package/components/Sparkline/Sparkline.js +2 -1
  38. package/components/Spinner/Spinner.css +1 -1
  39. package/components/SplitButton/SplitButton.d.ts +1 -1
  40. package/components/SplitButton/SplitButton.js +1 -0
  41. package/components/ToggleGroup/ToggleGroup.css +17 -0
  42. package/components/ToggleGroup/ToggleGroup.d.ts +2 -0
  43. package/components/ToggleGroup/ToggleGroup.js +2 -0
  44. package/components/ToolCall/ToolCall.css +7 -2
  45. package/components/Tooltip/Tooltip.css +24 -7
  46. package/components/Tooltip/Tooltip.d.ts +2 -1
  47. package/components/UsageCard/UsageCard.css +30 -0
  48. package/components/UsageCard/UsageCard.d.ts +48 -0
  49. package/components/UsageCard/UsageCard.js +54 -0
  50. package/components/registry.d.ts +8 -2
  51. package/components/registry.json +41 -14
  52. package/components/registry.json.d.ts +41 -14
  53. package/components/registry.json.js +1 -1
  54. package/index.d.ts +2 -1
  55. package/index.js +4 -2
  56. package/package.json +1 -1
  57. package/tokens/registry.json +4 -0
  58. package/tokens/registry.json.d.ts +4 -0
  59. package/tokens/registry.json.js +1 -1
  60. package/tokens/tokens-dark.css +11 -2
  61. package/tokens/tokens-light.css +25 -11
  62. package/tokens/tokens-primitives.css +3 -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 -->120<!-- /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 -->122<!-- /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. An MCP endpoint serves the same public data, plus the component prop API, to any agent. |
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 · Avatar group · Badge · Banner · 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 · Filter bar · Funnel chart · Gauge · Globe · Hover card · Image compare · Input · Instructions · Interrupt card · Kbd · Legend tile · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · 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 · Rating · Reasoning · Scatter chart · Section title · Segmented control · Selection card · Shader field · Skeleton · Slider · Source chip · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Stepper · Streaming text · 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 · Avatar group · Badge · Banner · 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 · Filter bar · Funnel chart · Gauge · Globe · Hover card · Image compare · Input · Instructions · Interrupt card · Kbd · Legend tile · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · 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 · Rating · Reasoning · Scatter chart · Section title · Segmented control · Selection card · Shader field · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Tooltip · Tree view · Treemap · Usage card
32
32
  <!-- component-list:end -->
33
33
 
34
34
  ### Ambient background
@@ -71,7 +71,7 @@ import '@robr0/design-system/tokens/tokens.css';
71
71
  import { Button, Card, Badge } from '@robr0/design-system';
72
72
  ```
73
73
 
74
- Deep imports work too (`@robr0/design-system/components/Button/Button`). The Recharts-backed charts live behind a separate entry so the optional `recharts` peer dependency is only needed if you use them; Sparkline and ContributionGraph are dependency-free and export from the main barrel:
74
+ Deep imports work too (`@robr0/design-system/components/Button/Button`). The Recharts-backed charts live behind a separate entry so the optional `recharts` peer dependency is only needed if you use them; the dependency-free charts export from the main barrel with everything else:
75
75
 
76
76
  ```tsx
77
77
  import { BarChart, LineChart } from '@robr0/design-system/charts';
@@ -84,7 +84,7 @@ 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. 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:
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), and the six fill steps below are the core of a rebrand:
88
88
  ```css
89
89
  :root {
90
90
  --primitive-teal-08: #6D31D3; /* light fill */
@@ -96,6 +96,7 @@ import { BarChart, LineChart } from '@robr0/design-system/charts';
96
96
  --primitive-radius-full: 12px; /* pill buttons become rounded rectangles */
97
97
  }
98
98
  ```
99
+ A complete rebrand re-keys the whole teal ramp (01-10): the remaining steps carry the primary label, active icons, hover/selected input borders, and the AI gradient, so stopping at the six fills leaves those teal. The easiest way to get all of it right is the [playground](https://www.robertritacca.com/playground): restyle the system live and copy out a complete, paste-ready override.
99
100
 
100
101
  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
102
 
@@ -142,6 +143,8 @@ The AI layer also serves machines directly. The live site exposes a Model Contex
142
143
  claude mcp add --transport http robr0-ds https://robertritacca.com/api/mcp
143
144
  ```
144
145
 
146
+ The same generated data is served as files too: every component's prop contract lives at `https://robertritacca.com/components/<slug>.md` beside its docs page, and a generated agent skill (a `SKILL.md` plus its `references/components.md` catalogue, under `https://robertritacca.com/skill/robr0-design-system/`) can be saved as a pair into a project's own skills folder so a coding agent carries the library's install, theming and catalogue rules into every session. The [setup guide](https://robertritacca.com/docs/get-started) has the two-command install.
147
+
145
148
  ---
146
149
 
147
150
  ## Tech
@@ -163,7 +166,7 @@ Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/w
163
166
  npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build + built-HTML checks
164
167
  ```
165
168
 
166
- 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.
169
+ CI also guards against documentation drift: every generated surface (the `validate-registry` entry in the root `package.json` is the authoritative list, from this README's component count to the per-component markdown and the consumer agent skill) is rebuilt from its source registries on every build, and CI fails if the committed copies are stale. The numbers on the site are never hand-written.
167
170
 
168
171
  ---
169
172
 
package/charts.d.ts CHANGED
@@ -13,3 +13,4 @@ export * from './components/Chart/RadialChart';
13
13
  export * from './components/Chart/ScatterChart';
14
14
  export * from './components/Chart/StackedBarChart';
15
15
  export * from './components/Chart/Treemap';
16
+ export * from './components/FunnelChart/FunnelChart';
package/charts.js CHANGED
@@ -8,10 +8,12 @@ import { RadialChart } from "./components/Chart/RadialChart.js";
8
8
  import { ScatterChart } from "./components/Chart/ScatterChart.js";
9
9
  import { StackedBarChart } from "./components/Chart/StackedBarChart.js";
10
10
  import { Treemap } from "./components/Chart/Treemap.js";
11
+ import { FunnelChart } from "./components/FunnelChart/FunnelChart.js";
11
12
  export {
12
13
  AreaChart,
13
14
  BarChart,
14
15
  ComboChart,
16
+ FunnelChart,
15
17
  LineChart,
16
18
  PieChart,
17
19
  RadarChart,
@@ -61,3 +61,77 @@
61
61
  font-weight: var(--font-paragraph-em-weight);
62
62
  border-left-color: var(--color-text-primary);
63
63
  }
64
+
65
+ /* ============================================
66
+ FLOATING VARIANT
67
+ Collapsed to a minimap of short lines; the
68
+ full panel expands over it on hover, focus
69
+ or tap. The caller positions the nav itself
70
+ (typically fixed on the right page edge).
71
+ ============================================ */
72
+ .ds-anchor-nav--floating {
73
+ position: relative;
74
+ display: inline-block;
75
+ }
76
+
77
+ .ds-anchor-nav__lines {
78
+ display: flex;
79
+ flex-direction: column;
80
+ align-items: flex-end;
81
+ gap: var(--gap-sm);
82
+ /* A comfortable hover/tap target around the thin lines */
83
+ padding: var(--padding-sm);
84
+ cursor: pointer;
85
+ }
86
+
87
+ .ds-anchor-nav__line {
88
+ /* One line per section: bespoke minimap geometry, specced in design.md */
89
+ width: 16px;
90
+ height: var(--border-md);
91
+ border-radius: var(--radius-full);
92
+ background-color: var(--color-divider);
93
+ transition:
94
+ width var(--motion-duration-fast) var(--motion-ease-standard),
95
+ background-color var(--motion-duration-fast) var(--motion-ease-standard);
96
+ }
97
+
98
+ .ds-anchor-nav__line--active {
99
+ width: 24px;
100
+ background-color: var(--color-text-primary);
101
+ }
102
+
103
+ .ds-anchor-nav__panel {
104
+ position: absolute;
105
+ z-index: 10;
106
+ top: 50%;
107
+ right: 0;
108
+ min-width: 200px;
109
+ max-width: 280px;
110
+ /* A long list scrolls inside the panel rather than past the viewport */
111
+ max-height: 70vh;
112
+ overflow-y: auto;
113
+ padding: var(--padding-md);
114
+ border: var(--border-xs) solid var(--color-bg-container-border);
115
+ border-radius: var(--radius-sm);
116
+ /* Page background, not container: the container fill is semi-transparent
117
+ in dark mode, and a floating panel must not show text through itself */
118
+ background-color: var(--color-bg-page-primary);
119
+ box-shadow: var(--shadow-floating);
120
+
121
+ /* Hidden by default — but never visibility: hidden, because the links must
122
+ stay focusable so tabbing into the nav expands it via :focus-within */
123
+ opacity: 0;
124
+ pointer-events: none;
125
+ transform: translate(var(--gap-xxs), -50%);
126
+ transition:
127
+ opacity var(--motion-duration-fast) var(--motion-ease-standard),
128
+ transform var(--motion-duration-fast) var(--motion-ease-standard);
129
+ }
130
+
131
+ .ds-anchor-nav--floating:hover .ds-anchor-nav__panel,
132
+ .ds-anchor-nav--floating:focus-within .ds-anchor-nav__panel,
133
+ .ds-anchor-nav--open .ds-anchor-nav__panel {
134
+ opacity: 1;
135
+ pointer-events: auto;
136
+ transform: translate(0, -50%);
137
+ }
@@ -8,6 +8,8 @@ export interface AnchorNavItem {
8
8
  type AnchorNavOwnProps = {
9
9
  /** The on-page sections to list, in document order */
10
10
  items: AnchorNavItem[];
11
+ /** Visual form: `list` renders the inline "On this page" rail; `floating` collapses to a stack of short lines that expands into a panel on hover, keyboard focus, or tap, so it can ride a page edge without taking column width */
12
+ variant?: 'list' | 'floating';
11
13
  /** Header text above the list; pass an empty string to render no header */
12
14
  title?: string;
13
15
  /** Material Symbols icon name beside the header; pass an empty string for none */
@@ -28,6 +30,11 @@ export interface AnchorNavProps extends AnchorNavOwnProps, Omit<React.ComponentP
28
30
  * has passed `offset` as current. Pass `activeId` to control the highlight
29
31
  * instead (no listener is attached). The active link carries
30
32
  * `aria-current="location"`.
33
+ *
34
+ * The `floating` variant collapses the list to a minimap of short lines and
35
+ * expands the full panel on hover or keyboard focus (the links stay focusable
36
+ * while collapsed, so tabbing in opens it). A tap opens it where hover
37
+ * doesn't exist; Escape or a click elsewhere closes it again.
31
38
  */
32
39
  export declare const AnchorNav: React.ForwardRefExoticComponent<AnchorNavProps & React.RefAttributes<HTMLElement>>;
33
40
  export {};
@@ -1,20 +1,24 @@
1
1
  "use client";
2
- import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import React, { useRef, useImperativeHandle, useState, useCallback, useEffect } from "react";
4
4
  import "./AnchorNav.css";
5
5
  const AnchorNav = React.forwardRef(
6
6
  ({
7
7
  items,
8
+ variant = "list",
8
9
  title = "On this page",
9
10
  icon = "toc",
10
11
  activeId,
11
12
  onActiveChange,
12
13
  offset = 96,
13
14
  className,
15
+ onKeyDown,
14
16
  ...rest
15
17
  }, ref) => {
16
18
  const navRef = useRef(null);
17
19
  useImperativeHandle(ref, () => navRef.current);
20
+ const floating = variant === "floating";
21
+ const [open, setOpen] = useState(false);
18
22
  const isControlled = activeId !== void 0;
19
23
  const [trackedId, setTrackedId] = useState(void 0);
20
24
  const currentId = isControlled ? activeId : trackedId;
@@ -54,26 +58,75 @@ const AnchorNav = React.forwardRef(
54
58
  if (frame !== 0) window.cancelAnimationFrame(frame);
55
59
  };
56
60
  }, [isControlled, items, offset, setActive]);
57
- const classes = ["ds-anchor-nav", className].filter(Boolean).join(" ");
58
- return /* @__PURE__ */ jsxs("nav", { ref: navRef, "aria-label": title || "On this page", className: classes, ...rest, children: [
59
- title && /* @__PURE__ */ jsxs("p", { className: "ds-anchor-nav__header", children: [
60
- icon && /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded ds-anchor-nav__icon", "aria-hidden": "true", children: icon }),
61
- title
62
- ] }),
63
- /* @__PURE__ */ jsx("ul", { className: "ds-anchor-nav__list", children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
64
- "a",
65
- {
66
- href: `#${item.id}`,
67
- className: item.id === currentId ? "ds-anchor-nav__link ds-anchor-nav__link--active" : "ds-anchor-nav__link",
68
- "aria-current": item.id === currentId ? "location" : void 0,
69
- onClick: () => {
70
- if (!isControlled) setActive(item.id);
71
- else onActiveChangeRef.current?.(item.id);
72
- },
73
- children: item.label
74
- }
75
- ) }, item.id)) })
76
- ] });
61
+ useEffect(() => {
62
+ if (!open) return;
63
+ const onPointerDown = (event) => {
64
+ if (navRef.current && !navRef.current.contains(event.target)) setOpen(false);
65
+ };
66
+ document.addEventListener("pointerdown", onPointerDown);
67
+ return () => document.removeEventListener("pointerdown", onPointerDown);
68
+ }, [open]);
69
+ const handleKeyDown = (event) => {
70
+ onKeyDown?.(event);
71
+ if (floating && event.key === "Escape") {
72
+ setOpen(false);
73
+ const focused = document.activeElement;
74
+ if (focused instanceof HTMLElement && navRef.current?.contains(focused)) focused.blur();
75
+ }
76
+ };
77
+ const classes = [
78
+ "ds-anchor-nav",
79
+ floating && "ds-anchor-nav--floating",
80
+ floating && open && "ds-anchor-nav--open",
81
+ className
82
+ ].filter(Boolean).join(" ");
83
+ const header = title ? /* @__PURE__ */ jsxs("p", { className: "ds-anchor-nav__header", children: [
84
+ icon && /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded ds-anchor-nav__icon", "aria-hidden": "true", children: icon }),
85
+ title
86
+ ] }) : null;
87
+ const list = /* @__PURE__ */ jsx("ul", { className: "ds-anchor-nav__list", children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
88
+ "a",
89
+ {
90
+ href: `#${item.id}`,
91
+ className: item.id === currentId ? "ds-anchor-nav__link ds-anchor-nav__link--active" : "ds-anchor-nav__link",
92
+ "aria-current": item.id === currentId ? "location" : void 0,
93
+ onClick: (event) => {
94
+ if (!isControlled) setActive(item.id);
95
+ else onActiveChangeRef.current?.(item.id);
96
+ if (floating) {
97
+ setOpen(false);
98
+ event.currentTarget.blur();
99
+ }
100
+ },
101
+ children: item.label
102
+ }
103
+ ) }, item.id)) });
104
+ return /* @__PURE__ */ jsx(
105
+ "nav",
106
+ {
107
+ ref: navRef,
108
+ "aria-label": title || "On this page",
109
+ className: classes,
110
+ ...rest,
111
+ onKeyDown: handleKeyDown,
112
+ children: floating ? /* @__PURE__ */ jsxs(Fragment, { children: [
113
+ /* @__PURE__ */ jsx("div", { className: "ds-anchor-nav__lines", "aria-hidden": "true", onClick: () => setOpen(true), children: items.map((item) => /* @__PURE__ */ jsx(
114
+ "span",
115
+ {
116
+ className: item.id === currentId ? "ds-anchor-nav__line ds-anchor-nav__line--active" : "ds-anchor-nav__line"
117
+ },
118
+ item.id
119
+ )) }),
120
+ /* @__PURE__ */ jsxs("div", { className: "ds-anchor-nav__panel", children: [
121
+ header,
122
+ list
123
+ ] })
124
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
125
+ header,
126
+ list
127
+ ] })
128
+ }
129
+ );
77
130
  }
78
131
  );
79
132
  AnchorNav.displayName = "AnchorNav";
@@ -182,6 +182,35 @@
182
182
  cursor: not-allowed;
183
183
  }
184
184
 
185
+ /* ============================================
186
+ NEUTRAL BUTTON (SOLID GREY)
187
+ Filled like primary but in the neutral greys —
188
+ unlike tertiary, the background is always shown.
189
+ ============================================ */
190
+
191
+ .ds-button--neutral {
192
+ background-color: var(--color-action-neutral-bg);
193
+ border: none;
194
+ }
195
+
196
+ .ds-button--neutral .ds-button__text {
197
+ color: var(--color-action-neutral-text);
198
+ }
199
+
200
+ .ds-button--neutral.ds-button--hover {
201
+ background-color: var(--color-action-neutral-bg-hover);
202
+ }
203
+
204
+ .ds-button--neutral.ds-button--active {
205
+ background-color: var(--color-action-neutral-bg-active);
206
+ }
207
+
208
+ .ds-button--neutral.ds-button--disabled {
209
+ background-color: var(--color-action-neutral-bg);
210
+ opacity: 0.4;
211
+ cursor: not-allowed;
212
+ }
213
+
185
214
  /* ============================================
186
215
  DESTRUCTIVE BUTTON
187
216
  For dangerous actions: delete, remove, etc.
@@ -266,6 +295,10 @@
266
295
  color: var(--color-icon-primary);
267
296
  }
268
297
 
298
+ .ds-button--neutral .ds-button__icon {
299
+ color: var(--color-action-neutral-text);
300
+ }
301
+
269
302
  .ds-button--destructive .ds-button__icon {
270
303
  color: var(--color-core-accent-coral);
271
304
  }
@@ -315,6 +348,10 @@
315
348
  background-color: var(--color-action-passive-bg-hover);
316
349
  }
317
350
 
351
+ .ds-button--neutral:not(.ds-button--disabled):hover {
352
+ background-color: var(--color-action-neutral-bg-hover);
353
+ }
354
+
318
355
  .ds-button--destructive:not(.ds-button--disabled):hover {
319
356
  background-color: var(--color-core-accent-coral);
320
357
  }
@@ -359,6 +396,10 @@
359
396
  background-color: var(--color-action-passive-bg-active);
360
397
  }
361
398
 
399
+ .ds-button--neutral:not(.ds-button--disabled):active {
400
+ background-color: var(--color-action-neutral-bg-active);
401
+ }
402
+
362
403
  .ds-button--destructive:not(.ds-button--disabled):active {
363
404
  background-color: var(--color-core-accent-coral);
364
405
  }
@@ -382,6 +423,16 @@
382
423
  pointer-events: none;
383
424
  }
384
425
 
426
+ /* The spinner is a box, not a glyph: left inline in the icon slot, it sits
427
+ on the text baseline and the strut's descender pushes it above centre.
428
+ Flex centres it geometrically — the same fix as CircularButton's loading
429
+ icon. */
430
+ .ds-button--loading .ds-button__icon {
431
+ display: inline-flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ }
435
+
385
436
  /* The generic icon-slot svg sizing must not distort the spinner —
386
437
  it sizes itself from the Spinner size prop */
387
438
  .ds-button .ds-button__icon .ds-spinner__circle {
@@ -8,7 +8,7 @@ type ButtonOwnProps = {
8
8
  /** Icon for right side — Material Symbol name (string) or custom element (ReactNode) */
9
9
  iconRight?: string | React.ReactNode;
10
10
  /** Visual treatment */
11
- variant?: 'primary' | 'secondary' | 'tertiary' | 'destructive';
11
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'neutral' | 'destructive';
12
12
  /** Button size */
13
13
  size?: 'default' | 'compact';
14
14
  /** Whether the button is disabled */
@@ -63,7 +63,7 @@ export interface ButtonProps extends ButtonOwnProps, Omit<React.ComponentPropsWi
63
63
  }
64
64
  /**
65
65
  * Button component from Figma design system.
66
- * Supports primary, secondary, tertiary and destructive variants.
66
+ * Supports primary, secondary, tertiary, neutral and destructive variants.
67
67
  * Icons can be placed on the left and/or right side of the text.
68
68
  *
69
69
  * Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
@@ -149,7 +149,10 @@
149
149
 
150
150
  .ds-chart__tooltip {
151
151
  padding: var(--padding-sm) var(--padding-md);
152
- background-color: var(--color-bg-container-primary);
152
+ background-color: var(--color-bg-glass);
153
+ /* The glass token is deliberately translucent; this blur is what keeps
154
+ the chart's marks readable through the overlay. */
155
+ backdrop-filter: blur(24px);
153
156
  border: var(--border-xs) solid var(--color-bg-container-border);
154
157
  border-radius: var(--radius-sm); /* 8px */
155
158
  box-shadow: var(--shadow-floating);
@@ -196,6 +196,43 @@
196
196
  background-color: var(--color-action-passive-bg-active);
197
197
  }
198
198
 
199
+ /* ============================================
200
+ NEUTRAL VARIANT (SOLID GREY)
201
+ Filled like primary but in the neutral greys —
202
+ unlike tertiary, the background is always shown.
203
+ ============================================ */
204
+
205
+ .ds-circular-button--neutral {
206
+ background-color: var(--color-action-neutral-bg);
207
+ border: none;
208
+ }
209
+
210
+ .ds-circular-button--neutral .ds-circular-button__icon {
211
+ color: var(--color-action-neutral-text);
212
+ }
213
+
214
+ .ds-circular-button--neutral.ds-circular-button--hover {
215
+ background-color: var(--color-action-neutral-bg-hover);
216
+ }
217
+
218
+ .ds-circular-button--neutral.ds-circular-button--active {
219
+ background-color: var(--color-action-neutral-bg-active);
220
+ }
221
+
222
+ .ds-circular-button--neutral.ds-circular-button--disabled {
223
+ opacity: 0.4;
224
+ cursor: not-allowed;
225
+ }
226
+
227
+ /* Interactive hover/active */
228
+ .ds-circular-button--neutral:not(.ds-circular-button--disabled):hover {
229
+ background-color: var(--color-action-neutral-bg-hover);
230
+ }
231
+
232
+ .ds-circular-button--neutral:not(.ds-circular-button--disabled):active {
233
+ background-color: var(--color-action-neutral-bg-active);
234
+ }
235
+
199
236
  /* ============================================
200
237
  LOADING
201
238
  Keeps the variant's full-colour appearance (no disabled dim);
@@ -207,6 +244,15 @@
207
244
  pointer-events: none;
208
245
  }
209
246
 
247
+ /* The spinner is a box, not a glyph: left inline, it sits on the text
248
+ baseline and the strut's descender pushes it ~1px above the button's
249
+ centre. Flex centres it geometrically. */
250
+ .ds-circular-button--loading .ds-circular-button__icon {
251
+ display: inline-flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ }
255
+
210
256
  /* ============================================
211
257
  FOCUS
212
258
  ============================================ */
@@ -4,7 +4,7 @@ type CircularButtonOwnProps = {
4
4
  /** Material Symbol icon name */
5
5
  icon: string;
6
6
  /** Visual treatment */
7
- variant?: 'primary' | 'secondary' | 'tertiary';
7
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'neutral';
8
8
  /**
9
9
  * Legacy alias for `variant`.
10
10
  *
@@ -34,6 +34,17 @@ type CircularButtonOwnProps = {
34
34
  loading?: boolean;
35
35
  /** Accessible label — required, because the button has no visible text */
36
36
  ariaLabel: string;
37
+ /**
38
+ * The hover/focus tooltip. An icon-only control names itself: by default
39
+ * the button wears a Tooltip carrying `ariaLabel`. Pass a string to show
40
+ * different wording, or `false` to opt out — for a host that labels the
41
+ * control another way, or one that owns the button's box directly
42
+ * (SplitButton's trigger stretches to its sibling segment, which the
43
+ * tooltip wrapper would block).
44
+ */
45
+ tooltip?: string | false;
46
+ /** Which side the tooltip opens on. */
47
+ tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
37
48
  /** Optional href — renders as <a> instead of <button> */
38
49
  href?: string;
39
50
  /** Optional target attribute for links */
@@ -48,7 +59,7 @@ export interface CircularButtonProps extends CircularButtonOwnProps, Omit<React.
48
59
  /**
49
60
  * Circular icon button component.
50
61
  * A round button containing a single icon, available in
51
- * primary, secondary and tertiary variants with default and compact sizes.
62
+ * primary, secondary, tertiary and neutral variants with default and compact sizes.
52
63
  *
53
64
  * Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
54
65
  * whichever element it renders, and spreads unrecognised props onto it.
@@ -1,6 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Spinner } from "../Spinner/Spinner.js";
4
+ import { Tooltip } from "../Tooltip/Tooltip.js";
4
5
  import "./CircularButton.css";
5
6
  import "../../fonts/material-symbols.css";
6
7
  const CircularButton = React.forwardRef(
@@ -13,6 +14,8 @@ const CircularButton = React.forwardRef(
13
14
  size = "default",
14
15
  loading,
15
16
  ariaLabel,
17
+ tooltip,
18
+ tooltipPosition = "top",
16
19
  href,
17
20
  target,
18
21
  rel,
@@ -33,22 +36,19 @@ const CircularButton = React.forwardRef(
33
36
  className
34
37
  ].filter(Boolean).join(" ");
35
38
  const children = isLoading ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Spinner, { size: size === "compact" ? "sm" : "md", variant: "inherit" }) }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon });
36
- if (href && !isDisabled && !isLoading) {
37
- return /* @__PURE__ */ jsx(
38
- "a",
39
- {
40
- ...rest,
41
- ref,
42
- className: classes,
43
- href,
44
- target,
45
- rel,
46
- "aria-label": ariaLabel,
47
- children
48
- }
49
- );
50
- }
51
- return /* @__PURE__ */ jsx(
39
+ const control = href && !isDisabled && !isLoading ? /* @__PURE__ */ jsx(
40
+ "a",
41
+ {
42
+ ...rest,
43
+ ref,
44
+ className: classes,
45
+ href,
46
+ target,
47
+ rel,
48
+ "aria-label": ariaLabel,
49
+ children
50
+ }
51
+ ) : /* @__PURE__ */ jsx(
52
52
  "button",
53
53
  {
54
54
  ...rest,
@@ -61,6 +61,9 @@ const CircularButton = React.forwardRef(
61
61
  children
62
62
  }
63
63
  );
64
+ const tooltipContent = tooltip === false ? null : tooltip ?? ariaLabel;
65
+ if (!tooltipContent) return control;
66
+ return /* @__PURE__ */ jsx(Tooltip, { content: tooltipContent, position: tooltipPosition, children: control });
64
67
  }
65
68
  );
66
69
  CircularButton.displayName = "CircularButton";
@@ -14,13 +14,20 @@
14
14
  opacity: 0;
15
15
  visibility: hidden; /* also removes the input from the tab order */
16
16
  pointer-events: none;
17
- transition: opacity var(--motion-duration-fast) var(--motion-ease-standard), visibility var(--motion-duration-fast) var(--motion-ease-standard);
17
+ /* Visibility waits out the fade only on close (duration token as delay);
18
+ on open it must flip instantly — the open effect focuses the input a
19
+ frame later, and focus() on a still-hidden element is a no-op, which
20
+ left the palette opening without its caret. */
21
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard),
22
+ visibility 0s linear var(--motion-duration-fast);
18
23
  }
19
24
 
20
25
  .ds-command-palette--open {
21
26
  opacity: 1;
22
27
  visibility: visible;
23
28
  pointer-events: auto;
29
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard),
30
+ visibility 0s;
24
31
  }
25
32
 
26
33
  /* ============================================
@@ -70,6 +77,13 @@
70
77
  padding: var(--padding-sm-md) var(--padding-md); /* 12px 16px */
71
78
  border-bottom: var(--border-xs) solid var(--color-divider);
72
79
  flex-shrink: 0;
80
+ transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
81
+ }
82
+
83
+ /* The focused field marks the whole row with the active input border, the
84
+ same signal a focused Input gives its own edge. */
85
+ .ds-command-palette__search:focus-within {
86
+ border-bottom-color: var(--color-input-border-selected);
73
87
  }
74
88
 
75
89
  .ds-command-palette__search-icon {
@@ -135,6 +149,17 @@
135
149
  min-height: 0;
136
150
  overflow-y: auto;
137
151
  padding: var(--padding-xxs);
152
+ /* Quiet scrollbar, ChatThread's recipe: thin and token-coloured instead
153
+ of the 15px native chrome rail a classic-scrollbar platform would draw
154
+ inside the panel. The stable gutter matters here because filtering
155
+ toggles overflow — without it every keystroke that crosses the
156
+ threshold reflows the rows sideways. */
157
+ scrollbar-width: thin;
158
+ scrollbar-gutter: stable;
159
+ scrollbar-color: var(--color-divider) transparent;
160
+ /* The scroll lock lets wheel and touch through only inside this list, so
161
+ hitting its ends must not chain the leftover delta to the page. */
162
+ overscroll-behavior: contain;
138
163
  }
139
164
 
140
165
  .ds-command-palette__status {