@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
@@ -196,6 +196,43 @@
196
196
  background-color: var(--color-action-passive-bg-active);
197
197
  }
198
198
 
199
+ /* ============================================
200
+ NEUTRAL VARIANT (SOLID GREY)
201
+ Filled like primary but in the neutral greys —
202
+ unlike tertiary, the background is always shown.
203
+ ============================================ */
204
+
205
+ .ds-circular-button--neutral {
206
+ background-color: var(--color-action-neutral-bg);
207
+ border: none;
208
+ }
209
+
210
+ .ds-circular-button--neutral .ds-circular-button__icon {
211
+ color: var(--color-action-neutral-text);
212
+ }
213
+
214
+ .ds-circular-button--neutral.ds-circular-button--hover {
215
+ background-color: var(--color-action-neutral-bg-hover);
216
+ }
217
+
218
+ .ds-circular-button--neutral.ds-circular-button--active {
219
+ background-color: var(--color-action-neutral-bg-active);
220
+ }
221
+
222
+ .ds-circular-button--neutral.ds-circular-button--disabled {
223
+ opacity: 0.4;
224
+ cursor: not-allowed;
225
+ }
226
+
227
+ /* Interactive hover/active */
228
+ .ds-circular-button--neutral:not(.ds-circular-button--disabled):hover {
229
+ background-color: var(--color-action-neutral-bg-hover);
230
+ }
231
+
232
+ .ds-circular-button--neutral:not(.ds-circular-button--disabled):active {
233
+ background-color: var(--color-action-neutral-bg-active);
234
+ }
235
+
199
236
  /* ============================================
200
237
  LOADING
201
238
  Keeps the variant's full-colour appearance (no disabled dim);
@@ -207,6 +244,15 @@
207
244
  pointer-events: none;
208
245
  }
209
246
 
247
+ /* The spinner is a box, not a glyph: left inline, it sits on the text
248
+ baseline and the strut's descender pushes it ~1px above the button's
249
+ centre. Flex centres it geometrically. */
250
+ .ds-circular-button--loading .ds-circular-button__icon {
251
+ display: inline-flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ }
255
+
210
256
  /* ============================================
211
257
  FOCUS
212
258
  ============================================ */
@@ -4,7 +4,7 @@ type CircularButtonOwnProps = {
4
4
  /** Material Symbol icon name */
5
5
  icon: string;
6
6
  /** Visual treatment */
7
- variant?: 'primary' | 'secondary' | 'tertiary';
7
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'neutral';
8
8
  /**
9
9
  * Legacy alias for `variant`.
10
10
  *
@@ -34,6 +34,17 @@ type CircularButtonOwnProps = {
34
34
  loading?: boolean;
35
35
  /** Accessible label — required, because the button has no visible text */
36
36
  ariaLabel: string;
37
+ /**
38
+ * The hover/focus tooltip. An icon-only control names itself: by default
39
+ * the button wears a Tooltip carrying `ariaLabel`. Pass a string to show
40
+ * different wording, or `false` to opt out — for a host that labels the
41
+ * control another way, or one that owns the button's box directly
42
+ * (SplitButton's trigger stretches to its sibling segment, which the
43
+ * tooltip wrapper would block).
44
+ */
45
+ tooltip?: string | false;
46
+ /** Which side the tooltip opens on. */
47
+ tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
37
48
  /** Optional href — renders as <a> instead of <button> */
38
49
  href?: string;
39
50
  /** Optional target attribute for links */
@@ -48,7 +59,7 @@ export interface CircularButtonProps extends CircularButtonOwnProps, Omit<React.
48
59
  /**
49
60
  * Circular icon button component.
50
61
  * A round button containing a single icon, available in
51
- * primary, secondary and tertiary variants with default and compact sizes.
62
+ * primary, secondary, tertiary and neutral variants with default and compact sizes.
52
63
  *
53
64
  * Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
54
65
  * whichever element it renders, and spreads unrecognised props onto it.
@@ -1,6 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Spinner } from "../Spinner/Spinner.js";
4
+ import { Tooltip } from "../Tooltip/Tooltip.js";
4
5
  import "./CircularButton.css";
5
6
  import "../../fonts/material-symbols.css";
6
7
  const CircularButton = React.forwardRef(
@@ -13,6 +14,8 @@ const CircularButton = React.forwardRef(
13
14
  size = "default",
14
15
  loading,
15
16
  ariaLabel,
17
+ tooltip,
18
+ tooltipPosition = "top",
16
19
  href,
17
20
  target,
18
21
  rel,
@@ -33,22 +36,19 @@ const CircularButton = React.forwardRef(
33
36
  className
34
37
  ].filter(Boolean).join(" ");
35
38
  const children = isLoading ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Spinner, { size: size === "compact" ? "sm" : "md", variant: "inherit" }) }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon });
36
- if (href && !isDisabled && !isLoading) {
37
- return /* @__PURE__ */ jsx(
38
- "a",
39
- {
40
- ...rest,
41
- ref,
42
- className: classes,
43
- href,
44
- target,
45
- rel,
46
- "aria-label": ariaLabel,
47
- children
48
- }
49
- );
50
- }
51
- return /* @__PURE__ */ jsx(
39
+ const control = href && !isDisabled && !isLoading ? /* @__PURE__ */ jsx(
40
+ "a",
41
+ {
42
+ ...rest,
43
+ ref,
44
+ className: classes,
45
+ href,
46
+ target,
47
+ rel,
48
+ "aria-label": ariaLabel,
49
+ children
50
+ }
51
+ ) : /* @__PURE__ */ jsx(
52
52
  "button",
53
53
  {
54
54
  ...rest,
@@ -61,6 +61,9 @@ const CircularButton = React.forwardRef(
61
61
  children
62
62
  }
63
63
  );
64
+ const tooltipContent = tooltip === false ? null : tooltip ?? ariaLabel;
65
+ if (!tooltipContent) return control;
66
+ return /* @__PURE__ */ jsx(Tooltip, { content: tooltipContent, position: tooltipPosition, children: control });
64
67
  }
65
68
  );
66
69
  CircularButton.displayName = "CircularButton";
@@ -14,13 +14,20 @@
14
14
  opacity: 0;
15
15
  visibility: hidden; /* also removes the input from the tab order */
16
16
  pointer-events: none;
17
- transition: opacity var(--motion-duration-fast) var(--motion-ease-standard), visibility var(--motion-duration-fast) var(--motion-ease-standard);
17
+ /* Visibility waits out the fade only on close (duration token as delay);
18
+ on open it must flip instantly — the open effect focuses the input a
19
+ frame later, and focus() on a still-hidden element is a no-op, which
20
+ left the palette opening without its caret. */
21
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard),
22
+ visibility 0s linear var(--motion-duration-fast);
18
23
  }
19
24
 
20
25
  .ds-command-palette--open {
21
26
  opacity: 1;
22
27
  visibility: visible;
23
28
  pointer-events: auto;
29
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard),
30
+ visibility 0s;
24
31
  }
25
32
 
26
33
  /* ============================================
@@ -70,6 +77,13 @@
70
77
  padding: var(--padding-sm-md) var(--padding-md); /* 12px 16px */
71
78
  border-bottom: var(--border-xs) solid var(--color-divider);
72
79
  flex-shrink: 0;
80
+ transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
81
+ }
82
+
83
+ /* The focused field marks the whole row with the active input border, the
84
+ same signal a focused Input gives its own edge. */
85
+ .ds-command-palette__search:focus-within {
86
+ border-bottom-color: var(--color-input-border-selected);
73
87
  }
74
88
 
75
89
  .ds-command-palette__search-icon {
@@ -135,6 +149,17 @@
135
149
  min-height: 0;
136
150
  overflow-y: auto;
137
151
  padding: var(--padding-xxs);
152
+ /* Quiet scrollbar, ChatThread's recipe: thin and token-coloured instead
153
+ of the 15px native chrome rail a classic-scrollbar platform would draw
154
+ inside the panel. The stable gutter matters here because filtering
155
+ toggles overflow — without it every keystroke that crosses the
156
+ threshold reflows the rows sideways. */
157
+ scrollbar-width: thin;
158
+ scrollbar-gutter: stable;
159
+ scrollbar-color: var(--color-divider) transparent;
160
+ /* The scroll lock lets wheel and touch through only inside this list, so
161
+ hitting its ends must not chain the leftover delta to the page. */
162
+ overscroll-behavior: contain;
138
163
  }
139
164
 
140
165
  .ds-command-palette__status {
@@ -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(" ");
@@ -4,14 +4,20 @@
4
4
  day, coloured by the contribution colour ramp
5
5
  ============================================ */
6
6
 
7
+ /* Card chrome, header, and padding come from the shared chart styles
8
+ (Chart.css); this file owns the grid, legend, and cell ramp. */
9
+
7
10
  .ds-contribution-graph {
8
11
  --ds-contribution-cell: 12px;
9
12
  --ds-contribution-gap: 3px;
10
13
 
14
+ min-width: 0;
15
+ }
16
+
17
+ .ds-contribution-graph__content {
11
18
  display: flex;
12
19
  flex-direction: column;
13
20
  gap: var(--gap-sm);
14
- width: 100%;
15
21
  min-width: 0;
16
22
  }
17
23
 
@@ -73,6 +79,27 @@
73
79
  width: 100%;
74
80
  height: auto;
75
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));
76
103
  }
77
104
 
78
105
  .ds-contribution-graph__cell--empty {
@@ -9,6 +9,12 @@ export interface ContributionDay {
9
9
  export interface ContributionGraphProps {
10
10
  /** One entry per day, ordered oldest to newest */
11
11
  days: ContributionDay[];
12
+ /** Chart title, in the shared chart header. */
13
+ title?: string;
14
+ /** Description text below the title. */
15
+ subtitle?: string;
16
+ /** Strip the card chrome (border, padding, fill) when the chart sits inside another panel that supplies the surface */
17
+ bare?: boolean;
12
18
  /** Summary line shown under the grid, e.g. "496 contributions in the last year" */
13
19
  caption?: string;
14
20
  /** Show month labels above the grid */
@@ -22,5 +28,7 @@ export interface ContributionGraphProps {
22
28
  * ContributionGraph renders a GitHub-style contribution heatmap:
23
29
  * one cell per day, arranged in weekday rows and week columns,
24
30
  * coloured by activity level via the --color-chart-contribution-* ramp.
31
+ * It wears the chart family's card chrome (title, subtitle, padding) and
32
+ * takes `bare` to drop it inside a panel that supplies the surface.
25
33
  */
26
- export declare const ContributionGraph: ({ days, caption, showMonthLabels, showLegend, className, }: ContributionGraphProps) => import("react").JSX.Element;
34
+ export declare const ContributionGraph: ({ days, title, subtitle, bare, caption, showMonthLabels, showLegend, className, }: ContributionGraphProps) => import("react").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
+ import "../Chart/Chart.css";
2
3
  import "./ContributionGraph.css";
3
4
  const MONTH_LABELS = [
4
5
  "Jan",
@@ -62,6 +63,9 @@ const buildMonthLabels = (weeks) => {
62
63
  };
63
64
  const ContributionGraph = ({
64
65
  days,
66
+ title,
67
+ subtitle,
68
+ bare = false,
65
69
  caption,
66
70
  showMonthLabels = true,
67
71
  showLegend = true,
@@ -69,53 +73,73 @@ const ContributionGraph = ({
69
73
  }) => {
70
74
  const weeks = buildWeeks(days);
71
75
  const monthLabels = buildMonthLabels(weeks);
72
- const classes = ["ds-contribution-graph", className].filter(Boolean).join(" ");
76
+ const chartClass = "ds-chart";
77
+ const classes = [
78
+ chartClass,
79
+ bare && `${chartClass}--bare`,
80
+ "ds-contribution-graph",
81
+ className
82
+ ].filter(Boolean).join(" ");
73
83
  const columnTemplate = `repeat(${weeks.length}, minmax(var(--ds-contribution-cell), 1fr))`;
74
84
  return /* @__PURE__ */ jsxs("div", { className: classes, children: [
75
- /* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__scroll", children: [
76
- showMonthLabels && /* @__PURE__ */ jsx(
77
- "div",
78
- {
79
- className: "ds-contribution-graph__months",
80
- style: { gridTemplateColumns: columnTemplate },
81
- "aria-hidden": "true",
82
- children: monthLabels.map(({ weekIndex, label }) => /* @__PURE__ */ jsx(
83
- "span",
84
- {
85
- className: "ds-contribution-graph__month-label",
86
- style: { gridColumnStart: weekIndex + 1 },
87
- children: label
88
- },
89
- `${label}-${weekIndex}`
90
- ))
91
- }
92
- ),
93
- /* @__PURE__ */ jsx(
94
- "div",
95
- {
96
- className: "ds-contribution-graph__grid",
97
- style: { gridTemplateColumns: columnTemplate },
98
- role: "img",
99
- "aria-label": caption ?? "Contribution activity",
100
- children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__week", children: week.map(
101
- (day, dayIndex) => day ? /* @__PURE__ */ jsx(
85
+ (title || subtitle) && /* @__PURE__ */ jsx("div", { className: `${chartClass}__header`, children: /* @__PURE__ */ jsxs("div", { className: `${chartClass}__header-text`, children: [
86
+ title && /* @__PURE__ */ jsx("h3", { className: `${chartClass}__title`, children: title }),
87
+ subtitle && /* @__PURE__ */ jsx("p", { className: `${chartClass}__subtitle`, children: subtitle })
88
+ ] }) }),
89
+ /* @__PURE__ */ jsxs("div", { className: `${chartClass}__body ds-contribution-graph__content`, children: [
90
+ /* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__scroll", children: [
91
+ showMonthLabels && /* @__PURE__ */ jsx(
92
+ "div",
93
+ {
94
+ className: "ds-contribution-graph__months",
95
+ style: { gridTemplateColumns: columnTemplate },
96
+ "aria-hidden": "true",
97
+ children: monthLabels.map(({ weekIndex, label }) => /* @__PURE__ */ jsx(
98
+ "span",
99
+ {
100
+ className: "ds-contribution-graph__month-label",
101
+ style: { gridColumnStart: weekIndex + 1 },
102
+ children: label
103
+ },
104
+ `${label}-${weekIndex}`
105
+ ))
106
+ }
107
+ ),
108
+ /* @__PURE__ */ jsx(
109
+ "div",
110
+ {
111
+ className: "ds-contribution-graph__grid",
112
+ style: { gridTemplateColumns: columnTemplate },
113
+ role: "img",
114
+ "aria-label": caption ?? "Contribution activity",
115
+ children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx(
102
116
  "div",
103
117
  {
104
- className: `ds-contribution-graph__cell ds-contribution-graph__cell--level-${day.level}`,
105
- title: formatTooltip(day)
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
+ )
106
130
  },
107
- day.date
108
- ) : /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__cell ds-contribution-graph__cell--empty" }, `pad-${dayIndex}`)
109
- ) }, weekIndex))
110
- }
111
- )
112
- ] }),
113
- (caption || showLegend) && /* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__footer", children: [
114
- caption && /* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__caption", children: caption }),
115
- showLegend && /* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__legend", "aria-hidden": "true", children: [
116
- /* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__legend-label", children: "Less" }),
117
- [0, 1, 2, 3, 4].map((level) => /* @__PURE__ */ jsx("div", { className: `ds-contribution-graph__cell ds-contribution-graph__cell--level-${level}` }, level)),
118
- /* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__legend-label", children: "More" })
131
+ weekIndex
132
+ ))
133
+ }
134
+ )
135
+ ] }),
136
+ (caption || showLegend) && /* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__footer", children: [
137
+ caption && /* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__caption", children: caption }),
138
+ showLegend && /* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__legend", "aria-hidden": "true", children: [
139
+ /* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__legend-label", children: "Less" }),
140
+ [0, 1, 2, 3, 4].map((level) => /* @__PURE__ */ jsx("div", { className: `ds-contribution-graph__cell ds-contribution-graph__cell--level-${level}` }, level)),
141
+ /* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__legend-label", children: "More" })
142
+ ] })
119
143
  ] })
120
144
  ] })
121
145
  ] });
@@ -13,6 +13,11 @@
13
13
  gap: var(--gap-md); /* 16px */
14
14
  padding: var(--padding-xl); /* 32px */
15
15
  width: 100%;
16
+ /* width: 100% plus own padding — border-box locally (as ContactCard,
17
+ Checkbox, Composer, SelectionCard and Textarea do), so a consumer
18
+ without a global reset doesn't get a horizontal scrollbar wherever
19
+ this is embedded. */
20
+ box-sizing: border-box;
16
21
  }
17
22
 
18
23
  /* ============================================
@@ -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,41 +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 */
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. */
13
14
 
14
- .ds-funnel-chart {
15
- display: flex;
16
- align-items: center;
17
- gap: var(--gap-xxs);
18
- }
19
-
20
- /* Parts */
21
-
22
- .ds-funnel-chart__stage {
23
- flex: 1;
24
- min-width: 0;
25
- height: calc(var(--funnel-stage-size) * 1%);
26
- border-radius: var(--radius-sm);
27
- background-color: var(--funnel-stage-color);
28
- display: grid;
29
- place-items: center;
30
- }
31
-
32
- .ds-funnel-chart__pct {
33
- background-color: var(--color-bg-container-primary);
34
- color: var(--color-text-primary);
35
- border-radius: var(--radius-full);
36
- padding: var(--padding-xxxs) var(--padding-xs);
37
- font-family: var(--font-caption-family);
38
- font-size: var(--font-caption-size);
39
- font-weight: var(--font-paragraph-sm-em-weight);
40
- line-height: var(--font-caption-line-height);
15
+ .ds-funnel-chart .recharts-label-list text {
16
+ font-family: var(--font-family-primary);
41
17
  }