@robr0/design-system 0.13.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 (91) hide show
  1. package/README.md +15 -6
  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/Banner/Banner.css +110 -0
  8. package/components/Banner/Banner.d.ts +36 -0
  9. package/components/Banner/Banner.js +59 -0
  10. package/components/Button/Button.css +51 -0
  11. package/components/Button/Button.d.ts +2 -2
  12. package/components/Chart/Chart.css +4 -1
  13. package/components/CircularButton/CircularButton.css +46 -0
  14. package/components/CircularButton/CircularButton.d.ts +13 -2
  15. package/components/CircularButton/CircularButton.js +19 -16
  16. package/components/CommandPalette/CommandPalette.css +26 -1
  17. package/components/CommandPalette/CommandPalette.js +11 -7
  18. package/components/Composer/Composer.css +18 -0
  19. package/components/Composer/Composer.d.ts +4 -2
  20. package/components/Composer/Composer.js +1 -0
  21. package/components/ContributionGraph/ContributionGraph.css +28 -1
  22. package/components/ContributionGraph/ContributionGraph.d.ts +9 -1
  23. package/components/ContributionGraph/ContributionGraph.js +66 -42
  24. package/components/EmptyState/EmptyState.css +5 -0
  25. package/components/Figure/Figure.css +14 -4
  26. package/components/Figure/Figure.js +11 -20
  27. package/components/FunnelChart/FunnelChart.css +11 -35
  28. package/components/FunnelChart/FunnelChart.d.ts +26 -15
  29. package/components/FunnelChart/FunnelChart.js +82 -34
  30. package/components/Gauge/Gauge.css +39 -12
  31. package/components/Gauge/Gauge.d.ts +16 -8
  32. package/components/Gauge/Gauge.js +78 -62
  33. package/components/HoverCard/HoverCard.css +97 -0
  34. package/components/HoverCard/HoverCard.d.ts +29 -0
  35. package/components/HoverCard/HoverCard.js +83 -0
  36. package/components/ImageCompare/ImageCompare.css +112 -0
  37. package/components/ImageCompare/ImageCompare.d.ts +40 -0
  38. package/components/ImageCompare/ImageCompare.js +134 -0
  39. package/components/LinkList/LinkList.d.ts +3 -1
  40. package/components/LinkList/LinkList.js +4 -3
  41. package/components/Meter/Meter.css +98 -0
  42. package/components/Meter/Meter.d.ts +36 -0
  43. package/components/Meter/Meter.js +48 -0
  44. package/components/NotificationCenter/NotificationCenter.css +11 -3
  45. package/components/ProgressBar/ProgressBar.css +1 -1
  46. package/components/Rating/Rating.css +74 -0
  47. package/components/Rating/Rating.d.ts +41 -0
  48. package/components/Rating/Rating.js +124 -0
  49. package/components/SegmentedControl/SegmentedControl.css +23 -0
  50. package/components/SegmentedControl/SegmentedControl.d.ts +3 -1
  51. package/components/SegmentedControl/SegmentedControl.js +3 -1
  52. package/components/Slider/Slider.css +1 -1
  53. package/components/Slider/Slider.js +1 -1
  54. package/components/SourceTrail/SourceTrail.css +209 -0
  55. package/components/SourceTrail/SourceTrail.d.ts +49 -0
  56. package/components/SourceTrail/SourceTrail.js +108 -0
  57. package/components/Sparkline/Sparkline.css +26 -2
  58. package/components/Sparkline/Sparkline.d.ts +5 -3
  59. package/components/Sparkline/Sparkline.js +32 -2
  60. package/components/Spinner/Spinner.css +1 -1
  61. package/components/SplitButton/SplitButton.css +93 -0
  62. package/components/SplitButton/SplitButton.d.ts +43 -0
  63. package/components/SplitButton/SplitButton.js +67 -0
  64. package/components/StreamingText/StreamingText.d.ts +27 -9
  65. package/components/StreamingText/StreamingText.js +17 -29
  66. package/components/StreamingText/useStreamReveal.d.ts +70 -0
  67. package/components/StreamingText/useStreamReveal.js +121 -0
  68. package/components/ToggleGroup/ToggleGroup.css +17 -0
  69. package/components/ToggleGroup/ToggleGroup.d.ts +2 -0
  70. package/components/ToggleGroup/ToggleGroup.js +2 -0
  71. package/components/ToolCall/ToolCall.css +7 -2
  72. package/components/Tooltip/Tooltip.css +24 -7
  73. package/components/Tooltip/Tooltip.d.ts +2 -1
  74. package/components/UsageCard/UsageCard.css +30 -0
  75. package/components/UsageCard/UsageCard.d.ts +48 -0
  76. package/components/UsageCard/UsageCard.js +54 -0
  77. package/components/registry.d.ts +8 -2
  78. package/components/registry.json +89 -14
  79. package/components/registry.json.d.ts +89 -14
  80. package/components/registry.json.js +1 -1
  81. package/index.d.ts +8 -1
  82. package/index.js +19 -2
  83. package/package.json +5 -1
  84. package/tokens/motion.d.ts +9 -4
  85. package/tokens/motion.js +5 -1
  86. package/tokens/registry.json +4 -0
  87. package/tokens/registry.json.d.ts +4 -0
  88. package/tokens/registry.json.js +1 -1
  89. package/tokens/tokens-dark.css +11 -2
  90. package/tokens/tokens-light.css +25 -11
  91. package/tokens/tokens-primitives.css +3 -0
@@ -0,0 +1,74 @@
1
+ /* ============================================
2
+ RATING COMPONENT
3
+ Star-scale rating control
4
+ ============================================ */
5
+
6
+ .ds-rating {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ gap: var(--gap-xxs);
10
+ }
11
+
12
+ /* ============================================
13
+ STEPS
14
+ ============================================ */
15
+
16
+ .ds-rating__step {
17
+ display: inline-flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ padding: 0;
21
+ border: none;
22
+ background: none;
23
+ cursor: pointer;
24
+ color: var(--color-icon-secondary);
25
+ border-radius: var(--radius-xs);
26
+ transition: color var(--motion-duration-fast) var(--motion-ease-standard),
27
+ transform var(--motion-duration-fast) var(--motion-ease-standard);
28
+ }
29
+
30
+ .ds-rating__icon {
31
+ --icon-size: var(--icon-size-md);
32
+ }
33
+
34
+ .ds-rating--compact .ds-rating__icon {
35
+ --icon-size: var(--icon-size-sm);
36
+ }
37
+
38
+ .ds-rating__step--filled {
39
+ color: var(--color-core-accent-gold);
40
+ }
41
+
42
+ .ds-rating__step--filled .ds-rating__icon {
43
+ --material-symbols-fill: 1;
44
+ }
45
+
46
+ /* ============================================
47
+ STATES
48
+ ============================================ */
49
+
50
+ .ds-rating__step:hover {
51
+ transform: scale(1.1);
52
+ }
53
+
54
+ .ds-rating__step:focus-visible {
55
+ outline: 2px solid var(--color-action-primary-bg);
56
+ outline-offset: 2px;
57
+ }
58
+
59
+ .ds-rating--read-only .ds-rating__step {
60
+ cursor: default;
61
+ }
62
+
63
+ .ds-rating--read-only .ds-rating__step:hover {
64
+ transform: none;
65
+ }
66
+
67
+ .ds-rating--disabled .ds-rating__step {
68
+ opacity: 0.4;
69
+ cursor: not-allowed;
70
+ }
71
+
72
+ .ds-rating--disabled .ds-rating__step:hover {
73
+ transform: none;
74
+ }
@@ -0,0 +1,41 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by Rating itself — everything else falls through to the root node. */
3
+ type RatingOwnProps = {
4
+ /** Current rating (controlled). 0 means no rating. */
5
+ value?: number;
6
+ /** Initial rating for uncontrolled use. 0 means no rating. */
7
+ defaultValue?: number;
8
+ /** Number of steps on the scale */
9
+ max?: number;
10
+ /**
11
+ * Convenience callback receiving the new rating directly.
12
+ * Fires on every selection, including a clear back to 0 via `allowClear`.
13
+ */
14
+ onValueChange?: (value: number) => void;
15
+ /** Display-only mode — renders the current rating with no interaction */
16
+ readOnly?: boolean;
17
+ /** Whether the control is disabled */
18
+ disabled?: boolean;
19
+ /** Selecting the already-selected step clears the rating back to 0 */
20
+ allowClear?: boolean;
21
+ /** Material Symbol drawn for each step */
22
+ icon?: string;
23
+ /** Component size */
24
+ size?: 'default' | 'compact';
25
+ /** Accessible name for the group, and the base of each step's label */
26
+ label?: string;
27
+ /** Additional CSS classes */
28
+ className?: string;
29
+ };
30
+ export interface RatingProps extends RatingOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof RatingOwnProps> {
31
+ }
32
+ /**
33
+ * Star-scale rating control. Behaves as a radio group: each step is a
34
+ * `role="radio"` button, arrow keys move the selection, and the current
35
+ * step holds the roving tab stop. `readOnly` renders the same row as a
36
+ * static image with a spoken "N out of M" label.
37
+ *
38
+ * Forwards a ref to the root element and spreads unrecognised props onto it.
39
+ */
40
+ export declare const Rating: React.ForwardRefExoticComponent<RatingProps & React.RefAttributes<HTMLDivElement>>;
41
+ export {};
@@ -0,0 +1,124 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import React, { useState } from "react";
4
+ import "./Rating.css";
5
+ import "../../fonts/material-symbols.css";
6
+ const Rating = React.forwardRef(
7
+ ({
8
+ value,
9
+ defaultValue = 0,
10
+ max = 5,
11
+ onValueChange,
12
+ readOnly = false,
13
+ disabled = false,
14
+ allowClear = false,
15
+ icon = "star",
16
+ size = "default",
17
+ label = "Rating",
18
+ className = "",
19
+ ...rest
20
+ }, ref) => {
21
+ const [internalValue, setInternalValue] = useState(defaultValue);
22
+ const [hoverValue, setHoverValue] = useState(0);
23
+ const baseClass = "ds-rating";
24
+ const currentValue = value ?? internalValue;
25
+ const displayValue = !readOnly && !disabled && hoverValue > 0 ? hoverValue : currentValue;
26
+ const classes = [
27
+ baseClass,
28
+ `${baseClass}--${size}`,
29
+ readOnly ? `${baseClass}--read-only` : "",
30
+ disabled ? `${baseClass}--disabled` : "",
31
+ className
32
+ ].filter(Boolean).join(" ");
33
+ const setValue = (next) => {
34
+ const resolved = allowClear && next === currentValue ? 0 : next;
35
+ if (value === void 0) setInternalValue(resolved);
36
+ onValueChange?.(resolved);
37
+ };
38
+ const handleKeyDown = (event) => {
39
+ let next = null;
40
+ switch (event.key) {
41
+ case "ArrowRight":
42
+ case "ArrowUp":
43
+ next = Math.min(max, currentValue + 1);
44
+ break;
45
+ case "ArrowLeft":
46
+ case "ArrowDown":
47
+ next = Math.max(1, currentValue - 1);
48
+ break;
49
+ case "Home":
50
+ next = 1;
51
+ break;
52
+ case "End":
53
+ next = max;
54
+ break;
55
+ }
56
+ if (next !== null) {
57
+ event.preventDefault();
58
+ if (value === void 0) setInternalValue(next);
59
+ onValueChange?.(next);
60
+ const steps2 = event.currentTarget.querySelectorAll('[role="radio"]');
61
+ steps2[next - 1]?.focus();
62
+ }
63
+ };
64
+ const steps = Array.from({ length: max }, (_, i) => i + 1);
65
+ if (readOnly) {
66
+ return /* @__PURE__ */ jsx(
67
+ "div",
68
+ {
69
+ ...rest,
70
+ ref,
71
+ className: classes,
72
+ role: "img",
73
+ "aria-label": rest["aria-label"] ?? `${label}: ${currentValue} out of ${max}`,
74
+ children: steps.map((step) => /* @__PURE__ */ jsx(
75
+ "span",
76
+ {
77
+ className: `${baseClass}__step ${step <= displayValue ? `${baseClass}__step--filled` : ""}`,
78
+ "aria-hidden": "true",
79
+ children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, children: icon })
80
+ },
81
+ step
82
+ ))
83
+ }
84
+ );
85
+ }
86
+ return /* @__PURE__ */ jsx(
87
+ "div",
88
+ {
89
+ ...rest,
90
+ ref,
91
+ className: classes,
92
+ role: "radiogroup",
93
+ "aria-label": rest["aria-label"] ?? label,
94
+ "aria-disabled": disabled || void 0,
95
+ onKeyDown: disabled ? void 0 : handleKeyDown,
96
+ onMouseLeave: () => setHoverValue(0),
97
+ children: steps.map((step) => {
98
+ const checked = step === currentValue;
99
+ const tabStop = checked || currentValue === 0 && step === 1;
100
+ return /* @__PURE__ */ jsx(
101
+ "button",
102
+ {
103
+ type: "button",
104
+ role: "radio",
105
+ "aria-checked": checked,
106
+ "aria-label": label ? `${label}: ${step} out of ${max}` : `${step} out of ${max}`,
107
+ tabIndex: tabStop ? 0 : -1,
108
+ disabled,
109
+ className: `${baseClass}__step ${step <= displayValue ? `${baseClass}__step--filled` : ""}`,
110
+ onClick: () => setValue(step),
111
+ onMouseEnter: () => setHoverValue(step),
112
+ children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon })
113
+ },
114
+ step
115
+ );
116
+ })
117
+ }
118
+ );
119
+ }
120
+ );
121
+ Rating.displayName = "Rating";
122
+ export {
123
+ Rating
124
+ };
@@ -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
  }
@@ -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-text);
165
+ }
166
+
167
+ .ds-source-trail__row--done .ds-source-trail__row-indicator {
168
+ color: var(--color-status-positive-text);
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
+ }
@@ -0,0 +1,49 @@
1
+ import { default as React } from 'react';
2
+ export type SourceTrailStatus = 'pending' | 'active' | 'done';
3
+ /** One source the agent opened while answering. */
4
+ export interface SourceTrailItem {
5
+ /** The source name, passed through to the row's SourceChip. */
6
+ title: string;
7
+ /** Optional link to the source — the chip renders as an anchor. */
8
+ href?: string;
9
+ /** Leading icon for the chip — a Material Symbol name (string) or custom element (ReactNode). */
10
+ icon?: string | React.ReactNode;
11
+ /** Where the visit has got to. Drives the row's indicator; defaults to done. */
12
+ status?: SourceTrailStatus;
13
+ /** Supporting line, e.g. what the source contributed to the answer. */
14
+ detail?: string;
15
+ }
16
+ /** Props owned by SourceTrail itself — everything else falls through to the root element. */
17
+ type SourceTrailOwnProps = {
18
+ /** The sources, in the order the agent opened them. */
19
+ items: SourceTrailItem[];
20
+ /**
21
+ * Header text. Left unset, it is computed from the items —
22
+ * "Reading 3 sources" while any is still pending or active,
23
+ * "Read 3 sources" once every visit has settled.
24
+ */
25
+ title?: string;
26
+ /** Still researching: newly appended rows animate in. */
27
+ streaming?: boolean;
28
+ /** Open state for controlled use. Pair with `onOpenChange`. */
29
+ open?: boolean;
30
+ /** Open state for uncontrolled use. */
31
+ defaultOpen?: boolean;
32
+ /** Fires whenever the panel opens or closes. */
33
+ onOpenChange?: (open: boolean) => void;
34
+ /** Additional CSS classes */
35
+ className?: string;
36
+ };
37
+ export interface SourceTrailProps extends SourceTrailOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof SourceTrailOwnProps> {
38
+ }
39
+ /**
40
+ * SourceTrail is the research trail under an agent's answer: the sources it
41
+ * opened, in order, each with a status and an optional note on what it
42
+ * contributed. SourceChip is the atom; this is the list — a count header
43
+ * with a disclosure, so a long trail collapses to one line.
44
+ *
45
+ * While `streaming`, appended rows animate in (pure CSS, so the global
46
+ * reduced-motion guard covers it) and the active row carries a spinner.
47
+ */
48
+ export declare const SourceTrail: React.ForwardRefExoticComponent<SourceTrailProps & React.RefAttributes<HTMLDivElement>>;
49
+ export {};
@@ -0,0 +1,108 @@
1
+ "use client";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
+ import React, { useId, useState, useCallback } from "react";
4
+ import { SourceChip } from "../SourceChip/SourceChip.js";
5
+ import { Spinner } from "../Spinner/Spinner.js";
6
+ import "./SourceTrail.css";
7
+ import "../../fonts/material-symbols.css";
8
+ const STATUS_ICONS = {
9
+ pending: "circle",
10
+ done: "check_circle"
11
+ };
12
+ const STATUS_TEXT = {
13
+ pending: "Not opened yet",
14
+ active: "Reading now",
15
+ done: "Read"
16
+ };
17
+ const SourceTrail = React.forwardRef(
18
+ ({
19
+ items,
20
+ title,
21
+ streaming = false,
22
+ open,
23
+ defaultOpen = true,
24
+ onOpenChange,
25
+ className = "",
26
+ ...rest
27
+ }, ref) => {
28
+ const baseClass = "ds-source-trail";
29
+ const id = useId();
30
+ const panelId = `${id}-panel`;
31
+ const triggerId = `${id}-trigger`;
32
+ const isControlled = open !== void 0;
33
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
34
+ const isOpen = isControlled ? open : uncontrolledOpen;
35
+ const toggle = useCallback(() => {
36
+ const next = !isOpen;
37
+ if (!isControlled) setUncontrolledOpen(next);
38
+ onOpenChange?.(next);
39
+ }, [isOpen, isControlled, onOpenChange]);
40
+ const unsettled = items.some(
41
+ (item) => item.status === "pending" || item.status === "active"
42
+ );
43
+ const count = items.length;
44
+ const heading = title ?? (unsettled || streaming ? `Reading ${count} source${count === 1 ? "" : "s"}` : `Read ${count} source${count === 1 ? "" : "s"}`);
45
+ const classes = [
46
+ baseClass,
47
+ isOpen ? `${baseClass}--open` : "",
48
+ streaming ? `${baseClass}--streaming` : "",
49
+ className
50
+ ].filter(Boolean).join(" ");
51
+ return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
52
+ /* @__PURE__ */ jsxs(
53
+ "button",
54
+ {
55
+ type: "button",
56
+ id: triggerId,
57
+ className: `${baseClass}__header`,
58
+ "aria-expanded": isOpen,
59
+ "aria-controls": panelId,
60
+ onClick: toggle,
61
+ children: [
62
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__indicator`, "aria-hidden": "true", children: unsettled || streaming ? /* @__PURE__ */ jsx(Spinner, { size: "sm", variant: "inherit" }) : /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: "travel_explore" }) }),
63
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__heading`, children: heading }),
64
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__chevron material-symbols-rounded`, "aria-hidden": "true", children: "expand_more" })
65
+ ]
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx(
69
+ "div",
70
+ {
71
+ className: `${baseClass}__panel`,
72
+ id: panelId,
73
+ role: "region",
74
+ "aria-labelledby": triggerId,
75
+ children: /* @__PURE__ */ jsx("ol", { className: `${baseClass}__list`, children: items.map((item, index) => {
76
+ const status = item.status ?? "done";
77
+ return /* @__PURE__ */ jsxs(
78
+ "li",
79
+ {
80
+ className: `${baseClass}__row ${baseClass}__row--${status}`,
81
+ children: [
82
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__row-indicator`, "aria-hidden": "true", children: status === "active" ? /* @__PURE__ */ jsx(Spinner, { size: "sm", variant: "inherit" }) : /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: STATUS_ICONS[status] }) }),
83
+ /* @__PURE__ */ jsx(
84
+ SourceChip,
85
+ {
86
+ title: item.title,
87
+ href: item.href,
88
+ icon: item.icon,
89
+ target: item.href ? "_blank" : void 0,
90
+ rel: item.href ? "noopener noreferrer" : void 0
91
+ }
92
+ ),
93
+ item.detail && /* @__PURE__ */ jsx("span", { className: `${baseClass}__detail`, children: item.detail }),
94
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__sr`, children: STATUS_TEXT[status] })
95
+ ]
96
+ },
97
+ `${item.title}-${index}`
98
+ );
99
+ }) })
100
+ }
101
+ )
102
+ ] });
103
+ }
104
+ );
105
+ SourceTrail.displayName = "SourceTrail";
106
+ export {
107
+ SourceTrail
108
+ };