@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
@@ -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
+ };
@@ -17,20 +17,44 @@
17
17
 
18
18
  /* Parts */
19
19
 
20
+ /* The line draws itself on mount, the way the recharts lines do: the path is
21
+ normalised to 100 dash units (pathLength on the SVG side), and the offset
22
+ retreats from fully hidden to zero. The fill and end dot fade in behind
23
+ and after it. All durations are tokens, so reduced motion collapses the
24
+ whole sequence to the finished state for free. */
25
+
20
26
  .ds-sparkline__line {
21
27
  fill: none;
22
28
  stroke: currentColor;
23
29
  stroke-linecap: round;
24
30
  stroke-linejoin: round;
31
+ stroke-dasharray: 100;
32
+ animation: ds-sparkline-draw var(--motion-duration-slower) var(--motion-ease-emphasized);
25
33
  }
26
34
 
27
35
  .ds-sparkline__fill {
28
36
  fill: currentColor;
29
37
  opacity: 0.15;
38
+ animation: ds-sparkline-fade var(--motion-duration-slower) var(--motion-ease-standard);
30
39
  }
31
40
 
32
41
  .ds-sparkline__dot {
33
42
  fill: currentColor;
43
+ /* Waits out the draw, then pops in where the line ends. */
44
+ animation: ds-sparkline-fade var(--motion-duration-slow) var(--motion-ease-standard)
45
+ var(--motion-duration-slower) backwards;
46
+ }
47
+
48
+ @keyframes ds-sparkline-draw {
49
+ from {
50
+ stroke-dashoffset: 100;
51
+ }
52
+ }
53
+
54
+ @keyframes ds-sparkline-fade {
55
+ from {
56
+ opacity: 0;
57
+ }
34
58
  }
35
59
 
36
60
  /* Tones */
@@ -40,11 +64,11 @@
40
64
  }
41
65
 
42
66
  .ds-sparkline--positive {
43
- color: var(--color-status-positive-text);
67
+ color: var(--color-trend-up);
44
68
  }
45
69
 
46
70
  .ds-sparkline--negative {
47
- color: var(--color-status-error-text);
71
+ color: var(--color-trend-down);
48
72
  }
49
73
 
50
74
  .ds-sparkline--neutral {
@@ -92,7 +92,8 @@ const Sparkline = React.forwardRef(
92
92
  {
93
93
  className: `${baseClass}__line`,
94
94
  d: linePath,
95
- strokeWidth
95
+ strokeWidth,
96
+ pathLength: 100
96
97
  }
97
98
  ),
98
99
  showDot && lastPoint && /* @__PURE__ */ jsx(
@@ -39,7 +39,7 @@
39
39
  }
40
40
 
41
41
  .ds-spinner__track {
42
- stroke: var(--color-bg-container-secondary);
42
+ stroke: var(--color-bg-container-tertiary);
43
43
  }
44
44
 
45
45
  .ds-spinner__arc {
@@ -10,7 +10,7 @@ type SplitButtonOwnProps = {
10
10
  /** Menu entries for the alternative actions */
11
11
  items: DropdownMenuEntry[];
12
12
  /** Visual treatment, shared by both segments */
13
- variant?: 'primary' | 'secondary';
13
+ variant?: 'primary' | 'secondary' | 'neutral';
14
14
  /** Component size */
15
15
  size?: 'default' | 'compact';
16
16
  /** Disables both segments */
@@ -49,6 +49,7 @@ const SplitButton = React.forwardRef(
49
49
  {
50
50
  className: `${baseClass}__trigger`,
51
51
  icon: "keyboard_arrow_down",
52
+ tooltip: false,
52
53
  ariaLabel: menuLabel,
53
54
  variant,
54
55
  size,
@@ -101,9 +101,10 @@ const TimePicker = React.forwardRef(
101
101
  }
102
102
  }, []);
103
103
  useEffect(() => {
104
+ if (!isOpen) return;
104
105
  document.addEventListener("mousedown", handleClickOutside);
105
106
  return () => document.removeEventListener("mousedown", handleClickOutside);
106
- }, [handleClickOutside]);
107
+ }, [isOpen, handleClickOutside]);
107
108
  const handleKeyDown = (e) => {
108
109
  if (disabled) return;
109
110
  switch (e.key) {
@@ -62,6 +62,23 @@
62
62
  color: var(--color-action-primary-text);
63
63
  }
64
64
 
65
+ /* ============================================
66
+ NEUTRAL VARIANT
67
+ Active items fill grey instead of the action
68
+ teal — for sets where the selection should
69
+ not carry the action colour's weight.
70
+ ============================================ */
71
+
72
+ .ds-toggle-group--neutral .ds-toggle-group__item--active {
73
+ background-color: var(--color-action-neutral-bg);
74
+ color: var(--color-action-neutral-text);
75
+ }
76
+
77
+ .ds-toggle-group--neutral .ds-toggle-group__item--active:hover {
78
+ background-color: var(--color-action-neutral-bg-hover);
79
+ color: var(--color-action-neutral-text);
80
+ }
81
+
65
82
  /* ============================================
66
83
  COMPACT
67
84
  ============================================ */
@@ -17,6 +17,8 @@ type ToggleGroupOwnProps = {
17
17
  multiple?: boolean;
18
18
  /** Component size */
19
19
  size?: 'default' | 'compact';
20
+ /** Visual treatment of active items — teal by default, `neutral` fills them grey */
21
+ variant?: 'primary' | 'neutral';
20
22
  /** Whether the whole group is disabled */
21
23
  disabled?: boolean;
22
24
  /** Called with the next selection — a string when single, an array when `multiple` */
@@ -8,6 +8,7 @@ const ToggleGroup = React.forwardRef(
8
8
  value = [],
9
9
  multiple = false,
10
10
  size = "default",
11
+ variant = "primary",
11
12
  disabled = false,
12
13
  onValueChange,
13
14
  onChange,
@@ -19,6 +20,7 @@ const ToggleGroup = React.forwardRef(
19
20
  const classes = [
20
21
  baseClass,
21
22
  `${baseClass}--${size}`,
23
+ `${baseClass}--${variant}`,
22
24
  disabled ? `${baseClass}--disabled` : "",
23
25
  className
24
26
  ].filter(Boolean).join(" ");
@@ -13,7 +13,11 @@
13
13
  ============================================ */
14
14
 
15
15
  .ds-tool-call {
16
+ /* Two colour channels per status: -text for the status word (tuned for
17
+ text contrast) and -icon for the indicator glyph, which needs a hue
18
+ that survives a plain surface — see the status token comment. */
16
19
  --ds-tool-call-color: var(--color-text-secondary);
20
+ --ds-tool-call-icon-color: var(--color-icon-secondary);
17
21
 
18
22
  display: flex;
19
23
  flex-direction: column;
@@ -40,6 +44,9 @@
40
44
  padding: var(--padding-sm) var(--padding-lg);
41
45
  background: none;
42
46
  border: none;
47
+ /* Buttons default to the UA's buttontext, not the inherited colour, so
48
+ anything in the header that inherits would read black in both themes. */
49
+ color: var(--color-text-primary);
43
50
  text-align: left;
44
51
  cursor: pointer;
45
52
  transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
@@ -68,7 +75,7 @@
68
75
  align-items: center;
69
76
  justify-content: center;
70
77
  flex: none;
71
- color: var(--ds-tool-call-color);
78
+ color: var(--ds-tool-call-icon-color);
72
79
  }
73
80
 
74
81
  .ds-tool-call__indicator .material-symbols-rounded {
@@ -147,18 +154,22 @@
147
154
 
148
155
  .ds-tool-call--pending {
149
156
  --ds-tool-call-color: var(--color-status-warning-text);
157
+ --ds-tool-call-icon-color: var(--color-status-warning-icon);
150
158
  }
151
159
 
152
160
  .ds-tool-call--running {
153
161
  --ds-tool-call-color: var(--color-status-info-text);
162
+ --ds-tool-call-icon-color: var(--color-status-info-icon);
154
163
  }
155
164
 
156
165
  .ds-tool-call--success {
157
166
  --ds-tool-call-color: var(--color-status-positive-text);
167
+ --ds-tool-call-icon-color: var(--color-status-positive-icon);
158
168
  }
159
169
 
160
170
  .ds-tool-call--error {
161
171
  --ds-tool-call-color: var(--color-status-error-text);
172
+ --ds-tool-call-icon-color: var(--color-status-error-icon);
162
173
  }
163
174
 
164
175
  /* ============================================
@@ -215,7 +226,9 @@
215
226
  inherited by default: a consumer stylesheet that sets a colour on
216
227
  .material-symbols-rounded itself (a common global default) would
217
228
  otherwise override inheritance and detach the icon from the state
218
- colours this component sets on its parents. */
219
- .ds-tool-call .material-symbols-rounded {
229
+ colours this component sets on its parents. The chevron is excluded —
230
+ it is the one icon that carries its own colour rule, which this
231
+ higher-specificity rule would otherwise silently override. */
232
+ .ds-tool-call .material-symbols-rounded:not(.ds-tool-call__chevron) {
220
233
  color: inherit;
221
234
  }
@@ -17,8 +17,14 @@
17
17
  z-index: 50;
18
18
  padding: var(--padding-xs) var(--padding-sm); /* 6px 8px */
19
19
  border-radius: var(--radius-xs); /* 4px */
20
- background-color: var(--color-bg-container-inverse);
21
- color: var(--color-text-on-inverse);
20
+ /* The floating surface, same recipe as Popover: page background (the
21
+ container fill is semi-transparent), hairline border, floating shadow.
22
+ Same-polarity in both themes — light bubble on light, dark on dark —
23
+ softer than the old inverse fill. */
24
+ border: var(--border-xs) solid var(--color-bg-container-border);
25
+ background-color: var(--color-bg-page-primary);
26
+ color: var(--color-text-primary);
27
+ box-shadow: var(--shadow-floating);
22
28
  font-family: var(--font-paragraph-sm-family);
23
29
  font-size: var(--font-paragraph-sm-size);
24
30
  font-weight: var(--font-paragraph-sm-weight);
@@ -85,38 +91,49 @@
85
91
  ARROW
86
92
  ============================================ */
87
93
 
94
+ /* The arrow is a rotated square in the panel's fill. It overlaps the panel
95
+ by a pixel more than before so its opaque fill covers the panel border
96
+ behind it, and draws the hairline itself on its two outward edges only. */
88
97
  .ds-tooltip__arrow {
89
98
  position: absolute;
90
99
  width: 8px;
91
100
  height: 8px;
92
- background-color: var(--color-bg-container-inverse);
101
+ background-color: var(--color-bg-page-primary);
93
102
  transform: rotate(45deg);
94
103
  }
95
104
 
96
105
  /* Arrow — top position (arrow points down) */
97
106
  .ds-tooltip__panel--top .ds-tooltip__arrow {
98
- bottom: -4px;
107
+ bottom: -5px;
99
108
  left: 50%;
100
109
  margin-left: -4px;
110
+ border-right: var(--border-xs) solid var(--color-bg-container-border);
111
+ border-bottom: var(--border-xs) solid var(--color-bg-container-border);
101
112
  }
102
113
 
103
114
  /* Arrow — bottom position (arrow points up) */
104
115
  .ds-tooltip__panel--bottom .ds-tooltip__arrow {
105
- top: -4px;
116
+ top: -5px;
106
117
  left: 50%;
107
118
  margin-left: -4px;
119
+ border-top: var(--border-xs) solid var(--color-bg-container-border);
120
+ border-left: var(--border-xs) solid var(--color-bg-container-border);
108
121
  }
109
122
 
110
123
  /* Arrow — left position (arrow points right) */
111
124
  .ds-tooltip__panel--left .ds-tooltip__arrow {
112
- right: -4px;
125
+ right: -5px;
113
126
  top: 50%;
114
127
  margin-top: -4px;
128
+ border-top: var(--border-xs) solid var(--color-bg-container-border);
129
+ border-right: var(--border-xs) solid var(--color-bg-container-border);
115
130
  }
116
131
 
117
132
  /* Arrow — right position (arrow points left) */
118
133
  .ds-tooltip__panel--right .ds-tooltip__arrow {
119
- left: -4px;
134
+ left: -5px;
120
135
  top: 50%;
121
136
  margin-top: -4px;
137
+ border-bottom: var(--border-xs) solid var(--color-bg-container-border);
138
+ border-left: var(--border-xs) solid var(--color-bg-container-border);
122
139
  }
@@ -16,6 +16,7 @@ export interface TooltipProps {
16
16
  /**
17
17
  * Tooltip component for contextual text labels.
18
18
  * Appears on hover or focus with a short delay.
19
- * Uses inverted colours (dark background in light theme).
19
+ * Rides the system's floating surface — page background, hairline border,
20
+ * floating shadow — so the bubble stays light in light theme and dark in dark.
20
21
  */
21
22
  export declare const Tooltip: ({ children, content, position, showDelay, hideDelay, className, }: TooltipProps) => import("react").JSX.Element;
@@ -24,6 +24,12 @@ const Tooltip = ({
24
24
  clearTimeout(showTimeoutRef.current);
25
25
  hideTimeoutRef.current = setTimeout(() => setVisible(false), hideDelay);
26
26
  };
27
+ useEffect(() => {
28
+ return () => {
29
+ clearTimeout(showTimeoutRef.current);
30
+ clearTimeout(hideTimeoutRef.current);
31
+ };
32
+ }, []);
27
33
  useEffect(() => {
28
34
  if (!visible) return;
29
35
  const onKeyDown = (e) => {
@@ -0,0 +1,30 @@
1
+ /* ============================================
2
+ USAGE CARD COMPONENT
3
+ An agent's budgets at a glance — meter rows
4
+ with reset captions, in the chart card chrome
5
+ ============================================ */
6
+
7
+ /* Base — the card chrome itself comes from ds-chart */
8
+
9
+ .ds-usage-card__items {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--gap-lg);
13
+ }
14
+
15
+ .ds-usage-card__item {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: var(--gap-xxs);
19
+ }
20
+
21
+ /* Reset caption — sits under the bar's trailing edge */
22
+
23
+ .ds-usage-card__reset {
24
+ align-self: flex-end;
25
+ font-family: var(--font-caption-family);
26
+ font-size: var(--font-caption-size);
27
+ font-weight: var(--font-caption-weight);
28
+ line-height: var(--font-caption-line-height);
29
+ color: var(--color-text-secondary);
30
+ }
@@ -0,0 +1,48 @@
1
+ import { default as React } from 'react';
2
+ /** One budgeted quantity in the card — a meter row with an optional reset caption. */
3
+ export interface UsageItem {
4
+ /** What is being budgeted, e.g. the context window or a weekly limit */
5
+ label: string;
6
+ /** Current level */
7
+ value: number;
8
+ /** Upper bound of the budget; defaults to 100 */
9
+ max?: number;
10
+ /** Readout override replacing the default percentage, e.g. a token count */
11
+ valueText?: string;
12
+ /** Caption under the bar's trailing edge, e.g. when the limit resets */
13
+ resetLabel?: string;
14
+ /** Status override; left unset, the fill recolours through the card's thresholds */
15
+ variant?: 'info' | 'positive' | 'warning' | 'error' | 'neutral';
16
+ }
17
+ /** Props owned by UsageCard itself — everything else falls through to the root node. */
18
+ type UsageCardOwnProps = {
19
+ /** The budgets to show, one meter row each */
20
+ items: UsageItem[];
21
+ /** Card title in the shared chart-chrome header */
22
+ title?: string;
23
+ /** Supporting line under the title */
24
+ subtitle?: string;
25
+ /** Fractions of an item's max where its fill recolours to warning and then error */
26
+ thresholds?: {
27
+ warning?: number;
28
+ error?: number;
29
+ };
30
+ /** Chrome off (no border, padding, or fill) for use inside a panel that supplies the surface */
31
+ bare?: boolean;
32
+ /** Additional CSS classes */
33
+ className?: string;
34
+ };
35
+ export interface UsageCardProps extends UsageCardOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof UsageCardOwnProps> {
36
+ }
37
+ /**
38
+ * An agent's budgets at a glance — the context window plus any number of
39
+ * plan or rate limits, one Meter row each, with reset captions. Wears the
40
+ * chart family's card chrome and takes `bare` for use inside a panel.
41
+ *
42
+ * Each fill recolours as its level crosses the card's thresholds (info
43
+ * until warning, then warning, then error), mirroring Gauge's threshold
44
+ * idea; an item's own `variant` overrides the derivation. Purely
45
+ * presentational (no 'use client'), so it renders from a Server Component.
46
+ */
47
+ export declare const UsageCard: React.ForwardRefExoticComponent<UsageCardProps & React.RefAttributes<HTMLDivElement>>;
48
+ export {};
@@ -0,0 +1,54 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Meter } from "../Meter/Meter.js";
4
+ import "../Chart/Chart.css";
5
+ import "./UsageCard.css";
6
+ const DEFAULT_THRESHOLDS = { warning: 0.8, error: 0.95 };
7
+ const UsageCard = React.forwardRef(
8
+ ({
9
+ items,
10
+ title,
11
+ subtitle,
12
+ thresholds,
13
+ bare = false,
14
+ className = "",
15
+ ...rest
16
+ }, ref) => {
17
+ const baseClass = "ds-usage-card";
18
+ const limits = { ...DEFAULT_THRESHOLDS, ...thresholds };
19
+ const variantFor = (item) => {
20
+ if (item.variant) return item.variant;
21
+ const max = item.max ?? 100;
22
+ const fraction = max > 0 ? Math.max(0, Math.min(1, item.value / max)) : 0;
23
+ if (fraction >= limits.error) return "error";
24
+ if (fraction >= limits.warning) return "warning";
25
+ return "info";
26
+ };
27
+ const classes = [baseClass, "ds-chart", bare && "ds-chart--bare", className].filter(Boolean).join(" ");
28
+ return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
29
+ (title || subtitle) && /* @__PURE__ */ jsx("div", { className: "ds-chart__header", children: /* @__PURE__ */ jsxs("div", { className: "ds-chart__header-text", children: [
30
+ title && /* @__PURE__ */ jsx("span", { className: "ds-chart__title", children: title }),
31
+ subtitle && /* @__PURE__ */ jsx("span", { className: "ds-chart__subtitle", children: subtitle })
32
+ ] }) }),
33
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__items`, children: items.map((item, index) => /* @__PURE__ */ jsxs("div", { className: `${baseClass}__item`, children: [
34
+ /* @__PURE__ */ jsx(
35
+ Meter,
36
+ {
37
+ label: item.label,
38
+ value: item.value,
39
+ max: item.max ?? 100,
40
+ valueText: item.valueText,
41
+ showValue: true,
42
+ variant: variantFor(item),
43
+ size: "compact"
44
+ }
45
+ ),
46
+ item.resetLabel && /* @__PURE__ */ jsx("span", { className: `${baseClass}__reset`, children: item.resetLabel })
47
+ ] }, `${item.label}-${index}`)) })
48
+ ] });
49
+ }
50
+ );
51
+ UsageCard.displayName = "UsageCard";
52
+ export {
53
+ UsageCard
54
+ };
@@ -18,8 +18,12 @@
18
18
  * deliberately false so consumers can render them from a React
19
19
  * Server Component.
20
20
  * folder set only when the implementation lives in a shared folder
21
- * rather than one named after the component — the nine chart
22
- * components all live in Chart/. Omitted everywhere else.
21
+ * rather than one named after the component — the recharts
22
+ * wrapper set lives in Chart/. Omitted everywhere else.
23
+ * recharts true when the component's module imports recharts, so it
24
+ * exports from the charts barrel rather than the main one
25
+ * (recharts is an optional peer). Held to the actual imports
26
+ * by scripts/generate-library-barrel.mjs on every build.
23
27
  *
24
28
  * `docOnlyHelpers` names the internal documentation helpers that are
25
29
  * deliberately excluded from the public count.
@@ -47,6 +51,8 @@ export interface ComponentMeta {
47
51
  client: boolean;
48
52
  /** Folder under src/components when it differs from `name` (shared folders like Chart) */
49
53
  folder?: string;
54
+ /** True when the module imports recharts and so exports from the charts barrel, not the main one */
55
+ recharts?: boolean;
50
56
  }
51
57
  export interface ComponentCategoryMeta {
52
58
  /** Category id — the value each component's `category` field carries, and the website route segment under /components */