@robr0/design-system 0.14.0 → 0.16.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 (88) hide show
  1. package/README.md +11 -8
  2. package/behaviors/focusable.d.ts +26 -0
  3. package/behaviors/focusable.js +18 -0
  4. package/behaviors/useFocusScope.d.ts +38 -0
  5. package/behaviors/useFocusScope.js +82 -0
  6. package/behaviors/useLayer.d.ts +43 -0
  7. package/behaviors/useLayer.js +55 -0
  8. package/behaviors/useMounted.d.ts +7 -0
  9. package/behaviors/useMounted.js +14 -0
  10. package/behaviors/useScrollLock.d.ts +55 -0
  11. package/behaviors/useScrollLock.js +59 -0
  12. package/charts.d.ts +1 -0
  13. package/charts.js +2 -0
  14. package/components/AgentPlan/AgentPlan.css +12 -3
  15. package/components/AiButton/AiButton.css +327 -0
  16. package/components/AiButton/AiButton.d.ts +42 -0
  17. package/components/AiButton/AiButton.js +140 -16
  18. package/components/AlertDialog/AlertDialog.js +13 -58
  19. package/components/AnchorNav/AnchorNav.css +74 -0
  20. package/components/AnchorNav/AnchorNav.d.ts +7 -0
  21. package/components/AnchorNav/AnchorNav.js +74 -21
  22. package/components/Button/Button.css +54 -0
  23. package/components/Button/Button.d.ts +2 -2
  24. package/components/Chart/Chart.css +4 -1
  25. package/components/CircularButton/CircularButton.css +46 -0
  26. package/components/CircularButton/CircularButton.d.ts +13 -2
  27. package/components/CircularButton/CircularButton.js +19 -16
  28. package/components/Combobox/Combobox.js +2 -1
  29. package/components/CommandPalette/CommandPalette.css +36 -8
  30. package/components/CommandPalette/CommandPalette.js +9 -54
  31. package/components/Composer/Composer.css +72 -0
  32. package/components/Composer/Composer.d.ts +25 -6
  33. package/components/Composer/Composer.js +8 -0
  34. package/components/ContributionGraph/ContributionGraph.css +21 -0
  35. package/components/ContributionGraph/ContributionGraph.js +18 -10
  36. package/components/Dialog/Dialog.js +9 -62
  37. package/components/Drawer/Drawer.js +9 -62
  38. package/components/Dropdown/Dropdown.js +2 -1
  39. package/components/DropdownMenu/DropdownMenu.js +11 -3
  40. package/components/Figure/Figure.css +14 -4
  41. package/components/Figure/Figure.js +11 -20
  42. package/components/FunnelChart/FunnelChart.css +11 -36
  43. package/components/FunnelChart/FunnelChart.d.ts +19 -16
  44. package/components/FunnelChart/FunnelChart.js +67 -39
  45. package/components/Gauge/Gauge.css +11 -1
  46. package/components/Meter/Meter.css +10 -1
  47. package/components/ModelPicker/ModelPicker.js +2 -1
  48. package/components/Popover/Popover.js +3 -0
  49. package/components/ProgressBar/ProgressBar.css +1 -1
  50. package/components/SegmentedControl/SegmentedControl.css +27 -2
  51. package/components/SegmentedControl/SegmentedControl.d.ts +3 -1
  52. package/components/SegmentedControl/SegmentedControl.js +3 -1
  53. package/components/Slider/Slider.css +1 -1
  54. package/components/Slider/Slider.js +1 -1
  55. package/components/SourceTrail/SourceTrail.css +209 -0
  56. package/components/SourceTrail/SourceTrail.d.ts +49 -0
  57. package/components/SourceTrail/SourceTrail.js +108 -0
  58. package/components/Sparkline/Sparkline.css +26 -2
  59. package/components/Sparkline/Sparkline.js +2 -1
  60. package/components/Spinner/Spinner.css +1 -1
  61. package/components/SplitButton/SplitButton.d.ts +1 -1
  62. package/components/SplitButton/SplitButton.js +1 -0
  63. package/components/TimePicker/TimePicker.js +2 -1
  64. package/components/ToggleGroup/ToggleGroup.css +17 -0
  65. package/components/ToggleGroup/ToggleGroup.d.ts +2 -0
  66. package/components/ToggleGroup/ToggleGroup.js +2 -0
  67. package/components/ToolCall/ToolCall.css +16 -3
  68. package/components/Tooltip/Tooltip.css +24 -7
  69. package/components/Tooltip/Tooltip.d.ts +2 -1
  70. package/components/Tooltip/Tooltip.js +6 -0
  71. package/components/UsageCard/UsageCard.css +30 -0
  72. package/components/UsageCard/UsageCard.d.ts +48 -0
  73. package/components/UsageCard/UsageCard.js +54 -0
  74. package/components/registry.d.ts +8 -2
  75. package/components/registry.json +43 -16
  76. package/components/registry.json.d.ts +43 -16
  77. package/components/registry.json.js +1 -1
  78. package/index.d.ts +2 -1
  79. package/index.js +4 -2
  80. package/package.json +5 -1
  81. package/tokens/motion.d.ts +10 -4
  82. package/tokens/motion.js +7 -1
  83. package/tokens/registry.json +10 -0
  84. package/tokens/registry.json.d.ts +10 -0
  85. package/tokens/registry.json.js +1 -1
  86. package/tokens/tokens-dark.css +22 -2
  87. package/tokens/tokens-light.css +38 -11
  88. package/tokens/tokens-primitives.css +3 -0
@@ -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
  /* ============================================
@@ -60,9 +60,10 @@ const ModelPicker = React.forwardRef(
60
60
  }
61
61
  }, []);
62
62
  useEffect(() => {
63
+ if (!isOpen) return;
63
64
  document.addEventListener("mousedown", handleClickOutside);
64
65
  return () => document.removeEventListener("mousedown", handleClickOutside);
65
- }, [handleClickOutside]);
66
+ }, [isOpen, handleClickOutside]);
66
67
  const handleTriggerKeyDown = (e) => {
67
68
  if (disabled) return;
68
69
  switch (e.key) {
@@ -27,6 +27,9 @@ const Popover = ({
27
27
  }
28
28
  onOpenChange?.(next);
29
29
  }, [isControlled, onOpenChange]);
30
+ useEffect(() => {
31
+ return () => clearTimeout(hoverTimeoutRef.current);
32
+ }, []);
30
33
  useEffect(() => {
31
34
  if (!isOpen || trigger !== "click") return;
32
35
  const handler = (e) => {
@@ -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
  }
@@ -55,12 +55,14 @@
55
55
  transition: color var(--motion-duration-base) var(--motion-ease-standard);
56
56
  }
57
57
 
58
- /* Icon */
58
+ /* Icon — same idle tier as the label, so an icon never reads dimmer than
59
+ the text beside it, and an icon-only segment (the site's theme toggle)
60
+ holds label-strength contrast in both modes. */
59
61
  .ds-segmented-control__icon {
60
62
  flex-shrink: 0;
61
63
  --icon-size: var(--icon-size-md);
62
64
  line-height: 1;
63
- color: var(--color-icon-secondary);
65
+ color: var(--color-text-secondary);
64
66
  transition: color var(--motion-duration-base) var(--motion-ease-standard);
65
67
  }
66
68
 
@@ -92,6 +94,29 @@
92
94
  background-color: var(--color-action-primary-bg-hover);
93
95
  }
94
96
 
97
+ /* ============================================
98
+ NEUTRAL VARIANT
99
+ The active pill fills grey instead of the
100
+ action teal — for sets where the selection
101
+ should not carry the action colour's weight.
102
+ ============================================ */
103
+
104
+ .ds-segmented-control--neutral .ds-segmented-control__segment--active {
105
+ background-color: var(--color-action-neutral-bg);
106
+ }
107
+
108
+ .ds-segmented-control--neutral .ds-segmented-control__segment--active .ds-segmented-control__label {
109
+ color: var(--color-action-neutral-text);
110
+ }
111
+
112
+ .ds-segmented-control--neutral .ds-segmented-control__segment--active .ds-segmented-control__icon {
113
+ color: var(--color-action-neutral-text);
114
+ }
115
+
116
+ .ds-segmented-control--neutral .ds-segmented-control__segment--active:hover {
117
+ background-color: var(--color-action-neutral-bg-hover);
118
+ }
119
+
95
120
  /* ============================================
96
121
  DISABLED STATE
97
122
  ============================================ */
@@ -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
  }
@@ -0,0 +1,209 @@
1
+ /* ============================================
2
+ SOURCE TRAIL COMPONENT
3
+ The research trail under an agent's answer:
4
+ a count header with a disclosure over the
5
+ sources it opened, in order.
6
+
7
+ Shares the chat furniture's 24px shell with
8
+ ToolCall — a collapsed trail reads as one
9
+ more row in the run, not a different species.
10
+ ============================================ */
11
+
12
+ .ds-source-trail {
13
+ display: flex;
14
+ flex-direction: column;
15
+ width: 100%;
16
+ background-color: var(--color-bg-container-primary);
17
+ border: var(--border-xs) solid var(--color-bg-container-border);
18
+ border-radius: var(--radius-xl);
19
+ overflow: hidden;
20
+ }
21
+
22
+ /* ============================================
23
+ HEADER
24
+ ============================================ */
25
+
26
+ .ds-source-trail__header {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: var(--gap-sm);
30
+ width: 100%;
31
+ padding: var(--padding-sm) var(--padding-lg);
32
+ background: none;
33
+ border: none;
34
+ /* Buttons default to the UA's buttontext, not the inherited colour, so
35
+ anything in the header that inherits would read black in both themes. */
36
+ color: var(--color-text-primary);
37
+ text-align: left;
38
+ cursor: pointer;
39
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
40
+ }
41
+
42
+ .ds-source-trail__header:hover {
43
+ background-color: var(--color-bg-container-secondary);
44
+ }
45
+
46
+ .ds-source-trail__header:focus-visible {
47
+ outline: 2px solid var(--color-action-primary-bg);
48
+ outline-offset: -2px;
49
+ }
50
+
51
+ .ds-source-trail__indicator {
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ flex: none;
56
+ color: var(--color-text-secondary);
57
+ }
58
+
59
+ .ds-source-trail__indicator .material-symbols-rounded {
60
+ --icon-size: var(--icon-size-sm);
61
+ }
62
+
63
+ .ds-source-trail__heading {
64
+ flex: 1 1 auto;
65
+ min-width: 0;
66
+ overflow: hidden;
67
+ text-overflow: ellipsis;
68
+ white-space: nowrap;
69
+ color: var(--color-text-primary);
70
+ font-family: var(--font-paragraph-sm-em-family);
71
+ font-size: var(--font-paragraph-sm-em-size);
72
+ font-weight: var(--font-paragraph-sm-em-weight);
73
+ line-height: var(--font-paragraph-sm-em-line-height);
74
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
75
+ }
76
+
77
+ .ds-source-trail__chevron {
78
+ --icon-size: var(--icon-size-sm);
79
+
80
+ flex: none;
81
+ color: var(--color-icon-secondary);
82
+ transition: transform var(--motion-duration-slow) var(--motion-ease-standard);
83
+ }
84
+
85
+ .ds-source-trail--open .ds-source-trail__chevron {
86
+ transform: rotate(180deg);
87
+ }
88
+
89
+ /* ============================================
90
+ PANEL
91
+ ============================================ */
92
+
93
+ .ds-source-trail__panel {
94
+ display: grid;
95
+ grid-template-rows: 0fr;
96
+ visibility: hidden;
97
+ transition:
98
+ grid-template-rows var(--motion-duration-slow) var(--motion-ease-standard),
99
+ visibility var(--motion-duration-slow) var(--motion-ease-standard);
100
+ }
101
+
102
+ .ds-source-trail--open .ds-source-trail__panel {
103
+ grid-template-rows: 1fr;
104
+ visibility: visible;
105
+ }
106
+
107
+ .ds-source-trail__list {
108
+ overflow: hidden;
109
+ min-height: 0;
110
+ margin: 0;
111
+ padding: 0;
112
+ list-style: none;
113
+ display: flex;
114
+ flex-direction: column;
115
+ }
116
+
117
+ .ds-source-trail--open .ds-source-trail__list {
118
+ padding: var(--padding-sm) var(--padding-lg) var(--padding-md);
119
+ border-top: var(--border-xs) solid var(--color-divider);
120
+ gap: var(--gap-sm);
121
+ }
122
+
123
+ /* ============================================
124
+ ROWS
125
+ ============================================ */
126
+
127
+ .ds-source-trail__row {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: var(--gap-sm);
131
+ min-width: 0;
132
+ }
133
+
134
+ /* Streaming: an appended row eases in. Pure CSS, so the global
135
+ reduced-motion guard collapses the duration tokens. */
136
+ .ds-source-trail--streaming .ds-source-trail__row {
137
+ animation: ds-source-trail-enter var(--motion-duration-slow) var(--motion-ease-standard);
138
+ }
139
+
140
+ @keyframes ds-source-trail-enter {
141
+ from {
142
+ opacity: 0;
143
+ transform: translateY(4px);
144
+ }
145
+ }
146
+
147
+ .ds-source-trail__row-indicator {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ flex: none;
152
+ color: var(--color-text-secondary);
153
+ }
154
+
155
+ .ds-source-trail__row-indicator .material-symbols-rounded {
156
+ --icon-size: var(--icon-size-sm);
157
+ }
158
+
159
+ .ds-source-trail__row--pending .ds-source-trail__row-indicator {
160
+ color: var(--color-text-tertiary);
161
+ }
162
+
163
+ .ds-source-trail__row--active .ds-source-trail__row-indicator {
164
+ color: var(--color-status-info-icon);
165
+ }
166
+
167
+ .ds-source-trail__row--done .ds-source-trail__row-indicator {
168
+ color: var(--color-status-positive-icon);
169
+ }
170
+
171
+ /* A pending source has not been opened yet, so its chip sits quieter */
172
+ .ds-source-trail__row--pending {
173
+ opacity: 0.6;
174
+ }
175
+
176
+ .ds-source-trail__detail {
177
+ flex: 1 1 auto;
178
+ min-width: 0;
179
+ overflow: hidden;
180
+ text-overflow: ellipsis;
181
+ white-space: nowrap;
182
+ color: var(--color-text-tertiary);
183
+ font-family: var(--font-paragraph-sm-family);
184
+ font-size: var(--font-paragraph-sm-size);
185
+ font-weight: var(--font-paragraph-sm-weight);
186
+ line-height: var(--font-paragraph-sm-line-height);
187
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
188
+ }
189
+
190
+ /* Screen-reader-only status text, matching AgentPlan's row announcement */
191
+ .ds-source-trail__sr {
192
+ position: absolute;
193
+ width: 1px;
194
+ height: 1px;
195
+ padding: 0;
196
+ margin: -1px;
197
+ overflow: hidden;
198
+ clip: rect(0, 0, 0, 0);
199
+ white-space: nowrap;
200
+ border: 0;
201
+ }
202
+
203
+ /* Icons follow the state colour set on their parents, even against a
204
+ consumer stylesheet that colours .material-symbols-rounded globally.
205
+ The chevron is excluded — it carries its own colour rule, which this
206
+ higher-specificity rule would otherwise silently override. */
207
+ .ds-source-trail .material-symbols-rounded:not(.ds-source-trail__chevron) {
208
+ color: inherit;
209
+ }