@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
@@ -80,15 +80,19 @@ const CommandPalette = ({
80
80
  }
81
81
  }, [open]);
82
82
  useEffect(() => {
83
- if (open) {
84
- document.body.style.overflow = "hidden";
85
- } else {
86
- document.body.style.overflow = "";
87
- }
83
+ if (!open) return;
84
+ const block = (event) => {
85
+ const target = event.target instanceof Element ? event.target : null;
86
+ if (target?.closest(`.${baseClass}__list`)) return;
87
+ event.preventDefault();
88
+ };
89
+ document.addEventListener("wheel", block, { passive: false });
90
+ document.addEventListener("touchmove", block, { passive: false });
88
91
  return () => {
89
- document.body.style.overflow = "";
92
+ document.removeEventListener("wheel", block);
93
+ document.removeEventListener("touchmove", block);
90
94
  };
91
- }, [open]);
95
+ }, [open, baseClass]);
92
96
  useEffect(() => {
93
97
  if (!open) return;
94
98
  const handleTrapKeyDown = (e) => {
@@ -140,6 +140,24 @@
140
140
  }
141
141
  }
142
142
 
143
+ /* Working: while a response streams, the ring stays lit and keeps turning
144
+ whether or not the field holds focus — the visible half of the `streaming`
145
+ contract for glowing composers. The pairing is defined in design.md's
146
+ Composer section; streaming without aiGlow changes nothing. */
147
+ .ds-composer--ai-glow.ds-composer--streaming {
148
+ border-color: transparent;
149
+ }
150
+
151
+ .ds-composer--ai-glow.ds-composer--streaming::before {
152
+ opacity: 1;
153
+ animation-play-state: running;
154
+ }
155
+
156
+ .ds-composer--ai-glow.ds-composer--streaming::after {
157
+ opacity: 0.3;
158
+ animation-play-state: running;
159
+ }
160
+
143
161
  /* Reduced motion: the ring and glow stay, the turning stops. */
144
162
  @media (prefers-reduced-motion: reduce) {
145
163
  .ds-composer--ai-glow::before,
@@ -21,7 +21,8 @@ type ComposerOwnProps = {
21
21
  onSubmit?: (value: string) => void;
22
22
  /**
23
23
  * A response is streaming: the send button becomes a stop button, submit
24
- * is blocked, and Enter is inert.
24
+ * is blocked, and Enter is inert. On a glowing composer (`aiGlow`) the
25
+ * gradient ring also stays lit and keeps turning while this is true.
25
26
  */
26
27
  streaming?: boolean;
27
28
  /** Fires when the stop button is pressed while `streaming`. */
@@ -32,7 +33,8 @@ type ComposerOwnProps = {
32
33
  * While focused, the shell wears AiButton's slowly rotating gradient ring
33
34
  * and glow in place of the plain selected border — the system's "a model
34
35
  * answers here" signal, for composers whose messages are answered by one.
35
- * Off by default.
36
+ * While `streaming`, the ring stays lit and turning whether or not the
37
+ * field holds focus. Off by default.
36
38
  */
37
39
  aiGlow?: boolean;
38
40
  /**
@@ -77,6 +77,7 @@ const Composer = React.forwardRef(
77
77
  const classes = [
78
78
  baseClass,
79
79
  aiGlow ? `${baseClass}--ai-glow` : "",
80
+ streaming ? `${baseClass}--streaming` : "",
80
81
  disabled ? `${baseClass}--disabled` : "",
81
82
  className
82
83
  ].filter(Boolean).join(" ");
@@ -79,6 +79,27 @@
79
79
  width: 100%;
80
80
  height: auto;
81
81
  aspect-ratio: 1 / 1;
82
+ /* The year sweeps in left to right on mount: each column's delay steps by
83
+ a fraction of the entrance duration (--ds-contribution-week, set inline
84
+ per column). Token-composed, so reduced motion collapses the sweep. */
85
+ animation: ds-contribution-fade var(--motion-duration-slow) var(--motion-ease-standard)
86
+ backwards;
87
+ animation-delay: calc(
88
+ var(--ds-contribution-week, 0) * var(--motion-duration-slower) / 40
89
+ );
90
+ }
91
+
92
+ @keyframes ds-contribution-fade {
93
+ from {
94
+ opacity: 0;
95
+ }
96
+ }
97
+
98
+ /* Hover lifts the day under the pointer, so the title tooltip has a visible
99
+ anchor. */
100
+ .ds-contribution-graph__week .ds-contribution-graph__cell:not(.ds-contribution-graph__cell--empty):hover {
101
+ outline: var(--border-xs) solid var(--color-text-tertiary);
102
+ outline-offset: calc(-1 * var(--border-xs));
82
103
  }
83
104
 
84
105
  .ds-contribution-graph__cell--empty {
@@ -112,16 +112,24 @@ const ContributionGraph = ({
112
112
  style: { gridTemplateColumns: columnTemplate },
113
113
  role: "img",
114
114
  "aria-label": caption ?? "Contribution activity",
115
- children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__week", children: week.map(
116
- (day, dayIndex) => day ? /* @__PURE__ */ jsx(
117
- "div",
118
- {
119
- className: `ds-contribution-graph__cell ds-contribution-graph__cell--level-${day.level}`,
120
- title: formatTooltip(day)
121
- },
122
- day.date
123
- ) : /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__cell ds-contribution-graph__cell--empty" }, `pad-${dayIndex}`)
124
- ) }, weekIndex))
115
+ children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx(
116
+ "div",
117
+ {
118
+ className: "ds-contribution-graph__week",
119
+ style: { "--ds-contribution-week": weekIndex },
120
+ children: week.map(
121
+ (day, dayIndex) => day ? /* @__PURE__ */ jsx(
122
+ "div",
123
+ {
124
+ className: `ds-contribution-graph__cell ds-contribution-graph__cell--level-${day.level}`,
125
+ title: formatTooltip(day)
126
+ },
127
+ day.date
128
+ ) : /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__cell ds-contribution-graph__cell--empty" }, `pad-${dayIndex}`)
129
+ )
130
+ },
131
+ weekIndex
132
+ ))
125
133
  }
126
134
  )
127
135
  ] }),
@@ -47,15 +47,25 @@
47
47
  ZOOMABLE (has onClick)
48
48
  ============================================ */
49
49
 
50
- .ds-figure--zoomable {
50
+ /* The zoom control is a real <button> wrapping the media (ARIA forbids
51
+ role="button" on <figure>), stripped back to a bare block. */
52
+ .ds-figure__zoom {
53
+ display: block;
54
+ width: 100%;
55
+ padding: 0;
56
+ border: 0;
57
+ background: none;
58
+ font: inherit;
51
59
  cursor: zoom-in;
52
60
  }
53
61
 
54
- .ds-figure--zoomable:hover .ds-figure__media img {
62
+ .ds-figure__zoom:hover .ds-figure__media img {
55
63
  opacity: 0.88;
56
64
  }
57
65
 
58
- .ds-figure--zoomable:focus-visible {
66
+ /* Inset ring: the button sits inside the figure's overflow clip, so an
67
+ outset outline would be cropped away. */
68
+ .ds-figure__zoom:focus-visible {
59
69
  outline: 2px solid var(--color-action-primary-bg);
60
- outline-offset: 2px;
70
+ outline-offset: -2px;
61
71
  }
@@ -9,26 +9,17 @@ const Figure = ({
9
9
  }) => {
10
10
  const baseClass = "ds-figure";
11
11
  const classes = [baseClass, onClick && `${baseClass}--zoomable`, className].filter(Boolean).join(" ");
12
- const onKeyDown = onClick ? (e) => {
13
- if (e.key === "Enter" || e.key === " ") {
14
- e.preventDefault();
15
- onClick();
16
- }
17
- } : void 0;
18
- return /* @__PURE__ */ jsxs(
19
- "figure",
20
- {
21
- className: classes,
22
- onClick,
23
- onKeyDown,
24
- role: onClick ? "button" : void 0,
25
- tabIndex: onClick ? 0 : void 0,
26
- children: [
27
- /* @__PURE__ */ jsx("div", { className: `${baseClass}__media`, children }),
28
- caption && /* @__PURE__ */ jsx("figcaption", { className: `${baseClass}__caption`, children: caption })
29
- ]
30
- }
31
- );
12
+ const media = /* @__PURE__ */ jsx("div", { className: `${baseClass}__media`, children });
13
+ return /* @__PURE__ */ jsxs("figure", { className: classes, children: [
14
+ onClick ? (
15
+ // A real button rather than role="button" on the figure: ARIA
16
+ // forbids the button role on <figure>, and the element brings
17
+ // Enter/Space activation itself. Its accessible name is the
18
+ // child image's alt text.
19
+ /* @__PURE__ */ jsx("button", { type: "button", className: `${baseClass}__zoom`, onClick, children: media })
20
+ ) : media,
21
+ caption && /* @__PURE__ */ jsx("figcaption", { className: `${baseClass}__caption`, children: caption })
22
+ ] });
32
23
  };
33
24
  export {
34
25
  Figure
@@ -1,42 +1,17 @@
1
1
  /* ============================================
2
2
  FUNNEL CHART COMPONENT
3
- Side-by-side vertical bars stepping down with
4
- each stage's share of the first. Geometry and
5
- colour arrive per stage as inline custom
6
- properties (--funnel-stage-size, a clamped
7
- percentage number; --funnel-stage-color, a
8
- chart-series token), so the rules here stay
9
- token-only.
3
+ Centred trapezoid funnel built on Recharts'
4
+ native Funnel. Card chrome, header, padding
5
+ and the tooltip surface all come from the
6
+ shared chart styles (Chart.css); colour
7
+ arrives per stage as a chart-series token
8
+ resolved live in SVG paint. This file owns
9
+ only what the shared styles cannot reach.
10
10
  ============================================ */
11
11
 
12
- /* Base card chrome, header, and padding come from the shared chart
13
- styles (Chart.css); this file owns the stage row alone. */
12
+ /* The stage-name labels draw as SVG text, which inherits no typography from
13
+ the shared chart classes pin them to the family's primary face. */
14
14
 
15
- .ds-funnel-chart__stages {
16
- display: flex;
17
- align-items: center;
18
- gap: var(--gap-xxs);
19
- }
20
-
21
- /* Parts */
22
-
23
- .ds-funnel-chart__stage {
24
- flex: 1;
25
- min-width: 0;
26
- height: calc(var(--funnel-stage-size) * 1%);
27
- border-radius: var(--radius-sm);
28
- background-color: var(--funnel-stage-color);
29
- display: grid;
30
- place-items: center;
31
- }
32
-
33
- .ds-funnel-chart__pct {
34
- background-color: var(--color-bg-container-primary);
35
- color: var(--color-text-primary);
36
- border-radius: var(--radius-full);
37
- padding: var(--padding-xxxs) var(--padding-xs);
38
- font-family: var(--font-caption-family);
39
- font-size: var(--font-caption-size);
40
- font-weight: var(--font-paragraph-sm-em-weight);
41
- line-height: var(--font-caption-line-height);
15
+ .ds-funnel-chart .recharts-label-list text {
16
+ font-family: var(--font-family-primary);
42
17
  }
@@ -1,19 +1,19 @@
1
1
  import { default as React } from 'react';
2
2
  /** One stage of the funnel, ordered widest first. */
3
3
  export interface FunnelStage {
4
- /** Stage name, read into the chart's accessible label. */
4
+ /** Stage name, shown beside the stage and read into the chart's accessible label. */
5
5
  label: string;
6
- /** Raw count for the stage — drives the bar's height as a share of the first stage. */
6
+ /** Raw count for the stage — drives the stage's width as a share of the first stage. */
7
7
  value: number;
8
8
  /**
9
- * Preformatted value ("96.4K") for the accessible label and any consumer
10
- * legend. Falls back to the raw `value`, locale-formatted.
9
+ * Preformatted value ("96.4K") for the tooltip and the accessible label.
10
+ * Falls back to the raw `value`, locale-formatted.
11
11
  */
12
12
  displayValue?: string;
13
13
  }
14
14
  /** Props owned by FunnelChart itself — everything else falls through to the root div. */
15
15
  type FunnelChartOwnProps = {
16
- /** Ordered stages, first stage widest. Each later stage's height is its share of the first. */
16
+ /** Ordered stages, first stage widest. Each later stage's width is its share of the first. */
17
17
  data: FunnelStage[];
18
18
  /** Chart title, in the shared chart header. */
19
19
  title?: string;
@@ -23,9 +23,12 @@ type FunnelChartOwnProps = {
23
23
  bare?: boolean;
24
24
  /** Chart area height in pixels. */
25
25
  height?: number;
26
+ /** Shows each stage's name beside its band. Turn off when a legend under the chart already carries the names. */
27
+ showLabels?: boolean;
26
28
  /**
27
- * Floor percentage for a stage's height, so steep drop-offs stay readable.
28
- * Shares are clamped to the range from this floor up to 100.
29
+ * Floor percentage from the stepped-bar rendering this chart used to have.
30
+ * @deprecated The funnel now draws true trapezoids sized by value, so a
31
+ * height floor no longer applies; the prop is ignored.
29
32
  */
30
33
  minStageShare?: number;
31
34
  /** Additional CSS classes */
@@ -34,15 +37,15 @@ type FunnelChartOwnProps = {
34
37
  export interface FunnelChartProps extends FunnelChartOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof FunnelChartOwnProps | 'children'> {
35
38
  }
36
39
  /**
37
- * FunnelChart — ordered stages as side-by-side vertical bars whose heights
38
- * step down with each stage's share of the first, each carrying a centred
39
- * percentage pill. Pure JSX and CSS computed from props: no recharts, no
40
- * hooks, no browser APIs, so it deliberately omits 'use client' and renders
41
- * from a Server Component. It wears the chart family's card chrome (title,
42
- * subtitle, padding) and takes `bare` to drop it inside a Panel, like every
43
- * other chart. Degenerate data stays safe: an empty array renders an empty
44
- * stage row, and a zero or negative first value draws every stage at the
45
- * floor height.
40
+ * FunnelChart — ordered stages as a centred funnel of trapezoid bands, built
41
+ * on Recharts' native Funnel so the stages sweep in on load and share the
42
+ * family's tooltip and palette. Each band's width is its stage's share of the
43
+ * first, the honest recharts geometry: the taper itself is the conversion
44
+ * story, and the glass tooltip carries each stage's reading and percentage.
45
+ * It wears the chart family's card chrome (title, subtitle, padding) and
46
+ * takes `bare` to drop it inside a Panel, like every other chart. The whole
47
+ * funnel is summarised in one accessible label, stage by stage. Degenerate
48
+ * data stays safe: an empty array renders an empty chart area.
46
49
  */
47
50
  export declare const FunnelChart: React.ForwardRefExoticComponent<FunnelChartProps & React.RefAttributes<HTMLDivElement>>;
48
51
  export {};
@@ -1,8 +1,31 @@
1
- import { jsxs, jsx } from "react/jsx-runtime";
2
- import React from "react";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback } from "react";
3
+ import { ResponsiveContainer, FunnelChart as FunnelChart$1, Tooltip, Funnel, LabelList } from "recharts";
4
+ import { getChartSeriesColors } from "../Chart/palette.js";
3
5
  import "../Chart/Chart.css";
4
6
  import "./FunnelChart.css";
5
- const SERIES_COUNT = 7;
7
+ function getCSSVar(name, fallback) {
8
+ return `var(${name}, ${fallback})`;
9
+ }
10
+ function FunnelTooltip({ active, payload }) {
11
+ if (!active || !payload?.length) return null;
12
+ const entry = payload[0];
13
+ const stage = entry.payload;
14
+ return /* @__PURE__ */ jsxs("div", { className: "ds-chart__tooltip", children: [
15
+ /* @__PURE__ */ jsx("div", { className: "ds-chart__tooltip-label", children: stage?.name }),
16
+ /* @__PURE__ */ jsxs("div", { className: "ds-chart__tooltip-row", children: [
17
+ /* @__PURE__ */ jsx(
18
+ "span",
19
+ {
20
+ className: "ds-chart__tooltip-dot",
21
+ style: { backgroundColor: stage?.fill }
22
+ }
23
+ ),
24
+ /* @__PURE__ */ jsx("span", { className: "ds-chart__tooltip-name", children: stage?.share !== void 0 ? `${stage.share}%` : "" }),
25
+ /* @__PURE__ */ jsx("span", { className: "ds-chart__tooltip-value", children: stage?.displayValue ?? entry.value?.toLocaleString() })
26
+ ] })
27
+ ] });
28
+ }
6
29
  const FunnelChart = React.forwardRef(
7
30
  ({
8
31
  data,
@@ -10,55 +33,60 @@ const FunnelChart = React.forwardRef(
10
33
  subtitle,
11
34
  bare = false,
12
35
  height = 190,
13
- minStageShare = 16,
36
+ showLabels = true,
37
+ minStageShare,
14
38
  className = "",
15
39
  ...rest
16
40
  }, ref) => {
17
41
  const baseClass = "ds-funnel-chart";
18
42
  const chartClass = "ds-chart";
19
- const classes = [
20
- chartClass,
21
- bare && `${chartClass}--bare`,
22
- baseClass,
23
- className
24
- ].filter(Boolean).join(" ");
43
+ const classes = [chartClass, bare && `${chartClass}--bare`, baseClass, className].filter(Boolean).join(" ");
44
+ const seriesColors = getChartSeriesColors();
45
+ const textSecondary = getCSSVar("--color-text-secondary", "#A2A2A2");
46
+ const surfaceColor = getCSSVar("--color-bg-container-primary", "#FFFFFF");
25
47
  const firstValue = data.length > 0 ? data[0].value : 0;
26
- const clampShare = (share) => Math.min(Math.max(share, minStageShare), 100);
27
48
  const stages = data.map((stage, i) => ({
28
- ...stage,
29
- share: clampShare(firstValue > 0 ? stage.value / firstValue * 100 : 0),
30
- color: `var(--color-chart-series-${i % SERIES_COUNT + 1})`
49
+ name: stage.label,
50
+ value: stage.value,
51
+ displayValue: stage.displayValue,
52
+ share: firstValue > 0 ? Math.round(stage.value / firstValue * 100) : 0,
53
+ fill: seriesColors[i % seriesColors.length]
31
54
  }));
32
- const ariaLabel = stages.length > 0 ? `Funnel: ${stages.map((s) => `${s.label} ${s.displayValue ?? s.value.toLocaleString()}`).join(", ")}` : "Funnel, no data";
55
+ const ariaLabel = stages.length > 0 ? `Funnel: ${stages.map((s) => `${s.name} ${s.displayValue ?? s.value.toLocaleString()}`).join(", ")}` : "Funnel, no data";
56
+ const renderTooltip = useCallback(
57
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
58
+ (props) => /* @__PURE__ */ jsx(FunnelTooltip, { ...props }),
59
+ []
60
+ );
33
61
  return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
34
62
  (title || subtitle) && /* @__PURE__ */ jsx("div", { className: `${chartClass}__header`, children: /* @__PURE__ */ jsxs("div", { className: `${chartClass}__header-text`, children: [
35
63
  title && /* @__PURE__ */ jsx("h3", { className: `${chartClass}__title`, children: title }),
36
64
  subtitle && /* @__PURE__ */ jsx("p", { className: `${chartClass}__subtitle`, children: subtitle })
37
65
  ] }) }),
38
- /* @__PURE__ */ jsx(
39
- "div",
40
- {
41
- className: `${chartClass}__body ${baseClass}__stages`,
42
- style: { height },
43
- role: "img",
44
- "aria-label": ariaLabel,
45
- children: stages.map((stage, i) => /* @__PURE__ */ jsx(
46
- "div",
47
- {
48
- className: `${baseClass}__stage`,
49
- style: {
50
- "--funnel-stage-size": stage.share,
51
- "--funnel-stage-color": stage.color
52
- },
53
- children: /* @__PURE__ */ jsxs("span", { className: `${baseClass}__pct`, children: [
54
- Math.round(firstValue > 0 ? stage.value / firstValue * 100 : 0),
55
- "%"
56
- ] })
57
- },
58
- `${stage.label}-${i}`
59
- ))
60
- }
61
- )
66
+ /* @__PURE__ */ jsx("div", { className: `${chartClass}__body`, role: "img", "aria-label": ariaLabel, children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height, children: /* @__PURE__ */ jsxs(FunnelChart$1, { margin: { top: 4, right: 4, bottom: 4, left: 4 }, children: [
67
+ /* @__PURE__ */ jsx(Tooltip, { content: renderTooltip }),
68
+ /* @__PURE__ */ jsx(
69
+ Funnel,
70
+ {
71
+ dataKey: "value",
72
+ nameKey: "name",
73
+ data: stages,
74
+ lastShapeType: "rectangle",
75
+ stroke: surfaceColor,
76
+ strokeWidth: 2,
77
+ children: showLabels && /* @__PURE__ */ jsx(
78
+ LabelList,
79
+ {
80
+ position: "right",
81
+ dataKey: "name",
82
+ fill: textSecondary,
83
+ stroke: "none",
84
+ fontSize: 12
85
+ }
86
+ )
87
+ }
88
+ )
89
+ ] }) }) })
62
90
  ] });
63
91
  }
64
92
  );
@@ -39,7 +39,7 @@
39
39
 
40
40
  .ds-gauge__track {
41
41
  fill: none;
42
- stroke: var(--color-bg-container-secondary);
42
+ stroke: var(--color-bg-container-tertiary);
43
43
  stroke-linecap: round;
44
44
  }
45
45
 
@@ -48,6 +48,16 @@
48
48
  stroke: currentColor;
49
49
  stroke-linecap: round;
50
50
  transition: stroke-dasharray var(--motion-duration-slow) var(--motion-ease-emphasized);
51
+ /* Sweep in from an empty dial on mount — the single keyframe animates to
52
+ the element's own dasharray, so one rule serves every reading. The
53
+ duration tokens collapse under reduced motion, guarding this for free. */
54
+ animation: ds-gauge-sweep var(--motion-duration-slower) var(--motion-ease-emphasized);
55
+ }
56
+
57
+ @keyframes ds-gauge-sweep {
58
+ from {
59
+ stroke-dasharray: 0 100;
60
+ }
51
61
  }
52
62
 
53
63
  .ds-gauge__center {
@@ -47,7 +47,7 @@
47
47
  width: 100%;
48
48
  height: 8px;
49
49
  border-radius: var(--radius-full);
50
- background-color: var(--color-bg-container-secondary);
50
+ background-color: var(--color-bg-container-tertiary);
51
51
  overflow: hidden;
52
52
  }
53
53
 
@@ -59,6 +59,15 @@
59
59
  height: 100%;
60
60
  border-radius: var(--radius-full);
61
61
  transition: width var(--motion-duration-base) var(--motion-ease-standard);
62
+ /* Grows to the current level on mount — the single keyframe animates to
63
+ the inline width, and the duration token collapses under reduced motion. */
64
+ animation: ds-meter-grow var(--motion-duration-slower) var(--motion-ease-emphasized);
65
+ }
66
+
67
+ @keyframes ds-meter-grow {
68
+ from {
69
+ width: 0;
70
+ }
62
71
  }
63
72
 
64
73
  /* ============================================
@@ -17,7 +17,7 @@
17
17
  .ds-progress-bar__track {
18
18
  flex: 1;
19
19
  height: 8px;
20
- background-color: var(--color-bg-container-secondary);
20
+ background-color: var(--color-bg-container-tertiary);
21
21
  border-radius: var(--radius-full);
22
22
  overflow: hidden;
23
23
  }
@@ -92,6 +92,29 @@
92
92
  background-color: var(--color-action-primary-bg-hover);
93
93
  }
94
94
 
95
+ /* ============================================
96
+ NEUTRAL VARIANT
97
+ The active pill fills grey instead of the
98
+ action teal — for sets where the selection
99
+ should not carry the action colour's weight.
100
+ ============================================ */
101
+
102
+ .ds-segmented-control--neutral .ds-segmented-control__segment--active {
103
+ background-color: var(--color-action-neutral-bg);
104
+ }
105
+
106
+ .ds-segmented-control--neutral .ds-segmented-control__segment--active .ds-segmented-control__label {
107
+ color: var(--color-action-neutral-text);
108
+ }
109
+
110
+ .ds-segmented-control--neutral .ds-segmented-control__segment--active .ds-segmented-control__icon {
111
+ color: var(--color-action-neutral-text);
112
+ }
113
+
114
+ .ds-segmented-control--neutral .ds-segmented-control__segment--active:hover {
115
+ background-color: var(--color-action-neutral-bg-hover);
116
+ }
117
+
95
118
  /* ============================================
96
119
  DISABLED STATE
97
120
  ============================================ */
@@ -18,6 +18,8 @@ export interface SegmentedControlProps {
18
18
  onSegmentChange?: (value: string) => void;
19
19
  /** Component size */
20
20
  size?: 'default' | 'compact';
21
+ /** Visual treatment of the active segment — teal by default, `neutral` fills it grey */
22
+ variant?: 'primary' | 'neutral';
21
23
  /** Full width — segments fill container */
22
24
  fullWidth?: boolean;
23
25
  /** Accessible label for the tablist */
@@ -30,4 +32,4 @@ export interface SegmentedControlProps {
30
32
  * A horizontal set of mutually exclusive options with a
31
33
  * pill-shaped active indicator. Supports icons and keyboard navigation.
32
34
  */
33
- export declare const SegmentedControl: ({ segments, activeSegment, onSegmentChange, size, fullWidth, ariaLabel, className, }: SegmentedControlProps) => React.JSX.Element;
35
+ export declare const SegmentedControl: ({ segments, activeSegment, onSegmentChange, size, variant, fullWidth, ariaLabel, className, }: SegmentedControlProps) => React.JSX.Element;
@@ -8,15 +8,17 @@ const SegmentedControl = ({
8
8
  activeSegment,
9
9
  onSegmentChange,
10
10
  size = "default",
11
+ variant = "primary",
11
12
  fullWidth = false,
12
13
  ariaLabel,
13
14
  className = ""
14
15
  }) => {
15
16
  const baseClass = "ds-segmented-control";
16
17
  const sizeClass = `${baseClass}--${size}`;
18
+ const variantClass = `${baseClass}--${variant}`;
17
19
  const fullWidthClass = fullWidth ? `${baseClass}--full-width` : "";
18
20
  const containerRef = useRef(null);
19
- const classes = [baseClass, sizeClass, fullWidthClass, className].filter(Boolean).join(" ");
21
+ const classes = [baseClass, sizeClass, variantClass, fullWidthClass, className].filter(Boolean).join(" ");
20
22
  const handleKeyDown = (e, idx) => {
21
23
  const enabledSegments = segments.filter((s) => !s.disabled);
22
24
  const currentEnabledIdx = enabledSegments.findIndex((s) => s.value === segments[idx].value);
@@ -21,7 +21,7 @@
21
21
  border-radius: var(--radius-full);
22
22
  outline: none;
23
23
  cursor: pointer;
24
- background: var(--color-bg-container-secondary);
24
+ background: var(--color-bg-container-tertiary);
25
25
  }
26
26
 
27
27
  .ds-slider--compact .ds-slider__input {
@@ -44,7 +44,7 @@ const Slider = React.forwardRef(
44
44
  onChange: handleChange,
45
45
  "aria-label": ariaLabel || rest["aria-label"],
46
46
  style: {
47
- background: `linear-gradient(to right, var(--color-action-primary-bg) ${percentage}%, var(--color-bg-container-secondary) ${percentage}%)`,
47
+ background: `linear-gradient(to right, var(--color-action-primary-bg) ${percentage}%, var(--color-bg-container-tertiary) ${percentage}%)`,
48
48
  ...style
49
49
  }
50
50
  }