rift-ds 1.2.0 → 1.3.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 (46) hide show
  1. package/README.md +2 -2
  2. package/components/Chart/BarChart.js +1 -1
  3. package/components/Chart/ComboChart.js +1 -1
  4. package/components/Chart/Treemap.js +83 -6
  5. package/components/ColorPicker/ColorPicker.js +1 -1
  6. package/components/Composer/Composer.css +45 -3
  7. package/components/Composer/Composer.d.ts +14 -6
  8. package/components/Composer/Composer.js +25 -5
  9. package/components/DatePicker/DatePicker.css +1 -1
  10. package/components/EventCalendar/EventCalendar.css +2 -2
  11. package/components/FileInput/FileInput.css +1 -1
  12. package/components/Globe/Globe.d.ts +5 -3
  13. package/components/Globe/Globe.js +18 -2
  14. package/components/Inspector/Inspector.css +108 -3
  15. package/components/Inspector/InspectorColorPicker.d.ts +43 -0
  16. package/components/Inspector/InspectorColorPicker.js +51 -0
  17. package/components/Instructions/Instructions.css +4 -4
  18. package/components/Prose/Prose.css +27 -0
  19. package/components/Sparkline/Sparkline.css +2 -2
  20. package/components/Spinner/Spinner.css +1 -1
  21. package/components/Timeline/Timeline.css +1 -1
  22. package/components/WorldMap/land.d.ts +1 -1
  23. package/components/registry.json +10 -1
  24. package/components/registry.json.d.ts +10 -1
  25. package/components/registry.json.js +1 -1
  26. package/index.d.ts +1 -0
  27. package/index.js +2 -0
  28. package/package.json +1 -1
  29. package/tokens/motion.d.ts +2 -0
  30. package/tokens/motion.js +2 -0
  31. package/tokens/presets/contrast.css +2 -4
  32. package/tokens/presets/coral.css +2 -4
  33. package/tokens/presets/forest.css +2 -4
  34. package/tokens/presets/gold.css +4 -4
  35. package/tokens/presets/mono.css +0 -2
  36. package/tokens/presets/pink.css +4 -4
  37. package/tokens/presets/terminal.css +5 -4
  38. package/tokens/presets/violet.css +2 -4
  39. package/tokens/presets/warm.css +3 -4
  40. package/tokens/presets/zest.css +2 -4
  41. package/tokens/registry.json +1 -0
  42. package/tokens/registry.json.d.ts +1 -0
  43. package/tokens/registry.json.js +1 -1
  44. package/tokens/tokens-dark.css +8 -5
  45. package/tokens/tokens-light.css +4 -2
  46. package/tokens/tokens-motion.css +2 -0
package/README.md CHANGED
@@ -73,13 +73,13 @@ import 'rift-ds/tokens/presets/presets.css';
73
73
 
74
74
  ## Components
75
75
 
76
- <!-- component-count -->142<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
76
+ <!-- component-count -->143<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
77
77
 
78
78
  <details>
79
79
  <summary>The full list</summary>
80
80
 
81
81
  <!-- component-list:start -->
82
- Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · World map
82
+ Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · World map
83
83
  <!-- component-list:end -->
84
84
 
85
85
  </details>
@@ -39,7 +39,7 @@ const BarChart = ({
39
39
  const subtitleId = useId();
40
40
  const baseClass = "ds-chart";
41
41
  const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
42
- const resolvedBarColor = barColor || getCSSVar("--color-action-primary-bg", "#0E6E8F");
42
+ const resolvedBarColor = barColor || getCSSVar("--color-chart-series-1", "#0E6E8F");
43
43
  const textSecondary = getCSSVar("--color-text-secondary", "#303030");
44
44
  const gridColor = getCSSVar("--color-divider", "rgba(214, 214, 214, 0.8)");
45
45
  const cursorColor = getCSSVar("--color-bg-container-secondary", "#F1F1F1");
@@ -45,7 +45,7 @@ const ComboChart = ({
45
45
  const baseClass = "ds-chart";
46
46
  const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
47
47
  const seriesColors = getChartSeriesColors();
48
- const resolvedBarColor = barColor || getCSSVar("--color-action-primary-bg", "#0E6E8F");
48
+ const resolvedBarColor = barColor || getCSSVar("--color-chart-series-1", "#0E6E8F");
49
49
  const resolvedLineColor = lineColor || seriesColors[1];
50
50
  const textSecondary = getCSSVar("--color-text-secondary", "#303030");
51
51
  const gridColor = getCSSVar("--color-divider", "rgba(214, 214, 214, 0.8)");
@@ -1,22 +1,88 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useId, useCallback } from "react";
3
+ import { useId, useCallback, useState, useEffect, useRef } from "react";
3
4
  import { ResponsiveContainer, Treemap as Treemap$1, Tooltip } from "recharts";
4
5
  import { getChartSeriesColors } from "./palette.js";
5
6
  import "./Chart.css";
6
7
  function getCSSVar(name, fallback) {
7
8
  return `var(${name}, ${fallback})`;
8
9
  }
10
+ let colorProbe;
11
+ function toRgb(value) {
12
+ const input = value.trim();
13
+ if (!input || input === "none") return null;
14
+ if (colorProbe === void 0) {
15
+ colorProbe = document.createElement("canvas").getContext("2d");
16
+ }
17
+ if (!colorProbe) return null;
18
+ colorProbe.fillStyle = "#000000";
19
+ colorProbe.fillStyle = input;
20
+ const out = String(colorProbe.fillStyle);
21
+ if (out.startsWith("#")) {
22
+ return [1, 3, 5].map((i) => parseInt(out.slice(i, i + 2), 16));
23
+ }
24
+ const m = out.match(/rgba?\(([^)]+)\)/);
25
+ if (!m) return null;
26
+ const [r, g, b] = m[1].split(/[\s,/]+/).map(Number);
27
+ return [r, g, b];
28
+ }
29
+ const luminance = ([r, g, b]) => {
30
+ const lin = (c) => {
31
+ const s = c / 255;
32
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
33
+ };
34
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
35
+ };
36
+ const contrast = (a, b) => {
37
+ const [hi, lo] = [luminance(a), luminance(b)].sort((p, q) => q - p);
38
+ return (hi + 0.05) / (lo + 0.05);
39
+ };
40
+ const AA_TEXT = 4.5;
41
+ function pickLabelStyle(rect) {
42
+ const style = getComputedStyle(rect);
43
+ const fill = toRgb(style.fill);
44
+ const primary = toRgb(style.getPropertyValue("--color-text-primary"));
45
+ const onInverse = toRgb(style.getPropertyValue("--color-text-on-inverse"));
46
+ if (!fill || !primary || !onInverse) return { tone: "primary", halo: false };
47
+ const primaryRatio = contrast(primary, fill);
48
+ const onInverseRatio = contrast(onInverse, fill);
49
+ const tone = primaryRatio >= onInverseRatio ? "primary" : "on-inverse";
50
+ return { tone, halo: Math.max(primaryRatio, onInverseRatio) < AA_TEXT };
51
+ }
52
+ function useThemeVersion() {
53
+ const [version, setVersion] = useState(0);
54
+ useEffect(() => {
55
+ const observer = new MutationObserver(() => setVersion((v) => v + 1));
56
+ observer.observe(document.documentElement, {
57
+ attributes: true,
58
+ attributeFilter: ["data-theme", "data-brand", "style", "class"]
59
+ });
60
+ return () => observer.disconnect();
61
+ }, []);
62
+ return version;
63
+ }
9
64
  function TreemapContent(props) {
10
- const { x, y, width, height, name, index, color } = props;
11
- if (width < 4 || height < 4) return null;
65
+ const { x, y, width, height, name, index, color, themeVersion } = props;
66
+ const rectRef = useRef(null);
67
+ const [label, setLabel] = useState({ tone: "primary", halo: false });
12
68
  const seriesColors = getChartSeriesColors();
13
69
  const fill = color || seriesColors[index % seriesColors.length];
14
- const textColor = getCSSVar("--color-text-primary", "#050505");
70
+ const visible = width >= 4 && height >= 4;
15
71
  const showLabel = width > 50 && height > 28;
72
+ useEffect(() => {
73
+ if (!showLabel || !rectRef.current) return;
74
+ setLabel(pickLabelStyle(rectRef.current));
75
+ }, [fill, showLabel, themeVersion]);
76
+ if (!visible) return null;
77
+ const primaryText = getCSSVar("--color-text-primary", "#050505");
78
+ const onInverseText = getCSSVar("--color-text-on-inverse", "#F1F1F1");
79
+ const textColor = label.tone === "primary" ? primaryText : onInverseText;
80
+ const haloColor = label.tone === "primary" ? onInverseText : primaryText;
16
81
  return /* @__PURE__ */ jsxs("g", { children: [
17
82
  /* @__PURE__ */ jsx(
18
83
  "rect",
19
84
  {
85
+ ref: rectRef,
20
86
  x,
21
87
  y,
22
88
  width,
@@ -33,7 +99,17 @@ function TreemapContent(props) {
33
99
  y: y + height / 2,
34
100
  textAnchor: "middle",
35
101
  dominantBaseline: "central",
36
- style: { fill: textColor, fontSize: 12, fontWeight: 600 },
102
+ style: {
103
+ fill: textColor,
104
+ fontSize: 12,
105
+ fontWeight: 600,
106
+ ...label.halo && {
107
+ stroke: haloColor,
108
+ strokeWidth: 3,
109
+ strokeLinejoin: "round",
110
+ paintOrder: "stroke"
111
+ }
112
+ },
37
113
  children: name
38
114
  }
39
115
  )
@@ -62,6 +138,7 @@ const Treemap = ({
62
138
  }) => {
63
139
  const titleId = useId();
64
140
  const subtitleId = useId();
141
+ const themeVersion = useThemeVersion();
65
142
  const baseClass = "ds-chart";
66
143
  const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
67
144
  const renderTooltip = useCallback(
@@ -93,7 +170,7 @@ const Treemap = ({
93
170
  data,
94
171
  dataKey,
95
172
  aspectRatio: 4 / 3,
96
- content: /* @__PURE__ */ jsx(TreemapContent, {}),
173
+ content: /* @__PURE__ */ jsx(TreemapContent, { themeVersion }),
97
174
  children: /* @__PURE__ */ jsx(Tooltip, { content: renderTooltip })
98
175
  }
99
176
  ) }) })
@@ -243,7 +243,7 @@ const ColorPicker = React.forwardRef(
243
243
  "aria-hidden": "true"
244
244
  }
245
245
  ),
246
- showText && /* @__PURE__ */ jsx("span", { className: `${baseClass}__trigger-text`, children: displayHex })
246
+ showText && /* @__PURE__ */ jsx("span", { id: `${inputId}-value`, className: `${baseClass}__trigger-text`, children: displayHex })
247
247
  ]
248
248
  }
249
249
  ),
@@ -224,9 +224,10 @@
224
224
 
225
225
  /* ============================================
226
226
  CONTEXT CHIP
227
- A full-width, non-interactive note at the very top of the shell — what
228
- the model is currently looking at. Composer owns the chrome; the caller
229
- passes the text.
227
+ A full-width, non-interactive note — what the model is currently looking
228
+ at. At the very top of the shell by default, or lifted above it as its
229
+ own bar (contextPlacement="above", styled at the end of this section).
230
+ Composer owns the chrome; the caller passes the text.
230
231
  ============================================ */
231
232
 
232
233
  .ds-composer__context {
@@ -267,6 +268,47 @@
267
268
  --icon-size: var(--icon-size-500);
268
269
  }
269
270
 
271
+ /* ---------- contextPlacement="above" ----------
272
+ The chip leaves the shell for its own bar, full width, a small gap clear
273
+ of the shell's top edge, and the layout aligns on one optical rail: the
274
+ bar's --padding-200 ring plus the --padding-400 pill inset (24px). The
275
+ typed text, the attachments, the chip's icon ink and a ModelPicker
276
+ trigger's label all start on it. The inside placement keeps its own
277
+ geometry above, untouched. */
278
+
279
+ /* The bar and the shell travel as one block, so a host's gap never lands
280
+ between them */
281
+ .ds-composer-group {
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: var(--gap-200);
285
+ width: 100%;
286
+ }
287
+
288
+ .ds-composer__context--above {
289
+ margin: 0;
290
+ /* Outside the shell, so the shell's custom properties do not reach it:
291
+ the send button's raw 40px, restated */
292
+ height: 40px;
293
+ /* The rail, plus the shell border this bar does not sit inside */
294
+ padding: 0 var(--padding-400) 0
295
+ calc(var(--padding-200) + var(--padding-400) + var(--border-025));
296
+ }
297
+
298
+ /* Optical alignment: a glyph's ink starts inside its box, so the box is
299
+ pulled back by that side bearing to land the ink on the rail. The
300
+ default fits the portrait document glyphs a context note usually
301
+ carries (ink starts 4/24 into the box); the fallback is a consumer hook
302
+ for a glyph with a different bearing. */
303
+ .ds-composer__context--above .ds-composer__context-icon {
304
+ margin-left: calc(-1 * var(--ds-composer-context-icon-bearing, var(--icon-size-500) * 4 / 24));
305
+ }
306
+
307
+ .ds-composer--context-above .ds-composer__attachments,
308
+ .ds-composer--context-above .ds-composer__content {
309
+ padding-inline: calc(var(--padding-200) + var(--padding-400));
310
+ }
311
+
270
312
  /* The text owns the truncation: one line, ellipsized only when the note
271
313
  actually outgrows the shell — an ellipsis is a truncation mark, not
272
314
  punctuation. The icon beside it never shrinks. */
@@ -38,14 +38,22 @@ type ComposerOwnProps = {
38
38
  */
39
39
  aiGlow?: boolean;
40
40
  /**
41
- * Contextual note rendered as a full-width, non-interactive chip at the
42
- * very top of the shell, above any attachments — the "what the model is
43
- * looking at" line a chat host pins over the message ("Looking at
44
- * “Page name”"). One line: a note too long for the shell truncates with
45
- * an ellipsis. Composer owns the chip's chrome; the caller passes the
46
- * text.
41
+ * Contextual note rendered as a full-width, non-interactive chip — the
42
+ * "what the model is looking at" line a chat host pins over the message
43
+ * ("Looking at “Page name”"). `contextPlacement` decides where it sits.
44
+ * One line: a note too long for the shell truncates with an ellipsis.
45
+ * Composer owns the chip's chrome; the caller passes the text.
47
46
  */
48
47
  context?: React.ReactNode;
48
+ /**
49
+ * Where the `context` chip sits. `inside` (the default) pins it at the very
50
+ * top of the shell, above any attachments. `above` lifts it out of the
51
+ * shell into its own bar a small gap above it, so it reads as what the
52
+ * model can see rather than part of the message being typed. Either way
53
+ * its icon starts on the shell's text rail, and a click on it focuses the
54
+ * textarea.
55
+ */
56
+ contextPlacement?: 'inside' | 'above';
49
57
  /**
50
58
  * Icon at the left of the context chip — Material Symbol name (string,
51
59
  * e.g. `visibility`, `article`) or custom element (ReactNode). Decorative
@@ -16,6 +16,7 @@ const Composer = React.forwardRef(
16
16
  aiGlow = false,
17
17
  context,
18
18
  contextIcon,
19
+ contextPlacement = "inside",
19
20
  attachments,
20
21
  actions,
21
22
  trailingActions,
@@ -82,20 +83,32 @@ const Composer = React.forwardRef(
82
83
  aiGlow ? `${baseClass}--ai-glow` : "",
83
84
  streaming ? `${baseClass}--streaming` : "",
84
85
  disabled ? `${baseClass}--disabled` : "",
86
+ // The above placement moves the text onto the chip's rail; the shell
87
+ // needs to know, since the chip is no longer inside it.
88
+ context && contextPlacement === "above" ? `${baseClass}--context-above` : "",
85
89
  className
86
90
  ].filter(Boolean).join(" ");
87
91
  const ariaLabel = rest["aria-label"] ?? (rest["aria-labelledby"] !== void 0 ? void 0 : "Message");
88
- return /* @__PURE__ */ jsxs(
92
+ const contextAbove = contextPlacement === "above";
93
+ const contextChip = context ? /* @__PURE__ */ jsxs(
94
+ "div",
95
+ {
96
+ className: `${baseClass}__context${contextAbove ? ` ${baseClass}__context--above` : ""}`,
97
+ onClick: contextAbove ? handleShellClick : void 0,
98
+ children: [
99
+ contextIcon && /* @__PURE__ */ jsx("span", { className: `${baseClass}__context-icon`, "aria-hidden": "true", children: typeof contextIcon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: contextIcon }) : contextIcon }),
100
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__context-text`, children: context })
101
+ ]
102
+ }
103
+ ) : null;
104
+ const shell = /* @__PURE__ */ jsxs(
89
105
  "div",
90
106
  {
91
107
  className: classes,
92
108
  style: { "--ds-composer-max-rows": maxRows },
93
109
  onClick: handleShellClick,
94
110
  children: [
95
- context && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__context`, children: [
96
- contextIcon && /* @__PURE__ */ jsx("span", { className: `${baseClass}__context-icon`, "aria-hidden": "true", children: typeof contextIcon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: contextIcon }) : contextIcon }),
97
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__context-text`, children: context })
98
- ] }),
111
+ !contextAbove && contextChip,
99
112
  attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
100
113
  /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, ref: contentRef, children: /* @__PURE__ */ jsx(
101
114
  "textarea",
@@ -138,6 +151,13 @@ const Composer = React.forwardRef(
138
151
  ]
139
152
  }
140
153
  );
154
+ if (contextAbove && contextChip) {
155
+ return /* @__PURE__ */ jsxs("div", { className: `${baseClass}-group`, children: [
156
+ contextChip,
157
+ shell
158
+ ] });
159
+ }
160
+ return shell;
141
161
  }
142
162
  );
143
163
  Composer.displayName = "Composer";
@@ -138,7 +138,7 @@
138
138
 
139
139
  /* Today indicator */
140
140
  .ds-date-picker__cell--today {
141
- border: var(--border-025) solid var(--color-action-primary-bg);
141
+ border: var(--border-025) solid var(--color-bg-page-inverse);
142
142
  }
143
143
 
144
144
  /* Selected */
@@ -7,7 +7,7 @@
7
7
  cell height.
8
8
 
9
9
  Event accents use the core accent roles —
10
- never the action teal, which stays reserved
10
+ never the action colour, which stays reserved
11
11
  for actions.
12
12
  ============================================ */
13
13
 
@@ -190,7 +190,7 @@ button.ds-event-calendar__day:focus-visible {
190
190
  letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
191
191
  }
192
192
 
193
- /* Selected — the teal selection chip (the selected item of a mutually
193
+ /* Selected — the action-colour selection chip (the selected item of a mutually
194
194
  exclusive set, DatePicker's convention). Declared after the today block
195
195
  so a selected today reads as selected. */
196
196
  .ds-event-calendar__cell--selected .ds-event-calendar__day {
@@ -178,7 +178,7 @@
178
178
  display: block;
179
179
  height: 100%;
180
180
  border-radius: var(--radius-pill);
181
- background-color: var(--color-action-primary-bg);
181
+ background-color: var(--color-core-accent-mint);
182
182
  transition: width var(--motion-duration-base) var(--motion-ease-standard);
183
183
  }
184
184
 
@@ -54,9 +54,11 @@ type GlobeOwnProps = {
54
54
  /** Fires whenever the view changes — drag, keys, or the auto-rotation. */
55
55
  onRotationChange?: (rotation: GlobeRotation) => void;
56
56
  /**
57
- * Spin slowly on its own, in degrees per second. Pauses while the pointer
58
- * is over the globe or it has focus, and never runs under
59
- * `prefers-reduced-motion`. `0` switches it off.
57
+ * Spin slowly on its own, in degrees per second at the shipped motion
58
+ * tempo. The speed follows `--motion-duration-orbit`, so a theme that
59
+ * stretches or shortens its durations slows or quickens the spin with
60
+ * them. Pauses while the pointer is over the globe or it has focus, and
61
+ * never runs under `prefers-reduced-motion`. `0` switches it off.
60
62
  */
61
63
  autoRotate?: number;
62
64
  /** Drag to rotate, and rotate with the arrow keys (or W, A, S, D) when focused. */
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsxs, jsx } from "react/jsx-runtime";
3
3
  import React, { useId, useState, useRef, useCallback, useSyncExternalStore, useEffect } from "react";
4
+ import { MOTION_ORBIT_REFERENCE_MS } from "../../tokens/motion.js";
4
5
  import "./Globe.css";
5
6
  const VIEW = 480;
6
7
  const CENTER = VIEW / 2;
@@ -11,6 +12,15 @@ const DRAG_DEGREES_PER_RADIUS = 90;
11
12
  const KEY_STEP = 10;
12
13
  const MAX_LAT = 80;
13
14
  const MARKER = 5;
15
+ const ORBIT_READ_INTERVAL_MS = 250;
16
+ const readOrbitTempo = (node) => {
17
+ if (!node) return 1;
18
+ const raw = getComputedStyle(node).getPropertyValue("--motion-duration-orbit").trim();
19
+ const m = raw.match(/^([\d.]+)(ms|s)$/);
20
+ if (!m) return 1;
21
+ const ms = parseFloat(m[1]) * (m[2] === "s" ? 1e3 : 1);
22
+ return ms > 0 ? MOTION_ORBIT_REFERENCE_MS / Math.max(ms, MOTION_ORBIT_REFERENCE_MS / 20) : 1;
23
+ };
14
24
  const DEG = Math.PI / 180;
15
25
  const toVec = (lat, lng) => {
16
26
  const φ = lat * DEG;
@@ -148,22 +158,28 @@ const Globe = React.forwardRef(
148
158
  getReducedMotion,
149
159
  getServerReducedMotion
150
160
  );
161
+ const rootRef = useRef(null);
151
162
  const spinning = autoRotate !== 0 && !paused && !reducedMotion;
152
163
  useEffect(() => {
153
164
  if (!spinning) return;
154
165
  let frame = 0;
155
166
  let last = performance.now();
167
+ let tempo = readOrbitTempo(rootRef.current);
168
+ let lastRead = last;
156
169
  const tick = (now) => {
157
170
  const dt = (now - last) / 1e3;
158
171
  last = now;
172
+ if (now - lastRead >= ORBIT_READ_INTERVAL_MS) {
173
+ tempo = readOrbitTempo(rootRef.current);
174
+ lastRead = now;
175
+ }
159
176
  const [lng, lat] = rotationRef.current;
160
- commit([lng - autoRotate * dt, lat]);
177
+ commit([lng - autoRotate * tempo * dt, lat]);
161
178
  frame = requestAnimationFrame(tick);
162
179
  };
163
180
  frame = requestAnimationFrame(tick);
164
181
  return () => cancelAnimationFrame(frame);
165
182
  }, [spinning, autoRotate, commit]);
166
- const rootRef = useRef(null);
167
183
  const drag = useRef(null);
168
184
  const [dragging, setDragging] = useState(false);
169
185
  const setRef = (node) => {
@@ -4,7 +4,8 @@
4
4
  rails: each control is one bar with its name
5
5
  inside on the left and its state on the right.
6
6
  The form controls (Slider, Input, ToggleSwitch,
7
- SegmentedControl, Dropdown) keep their label
7
+ SegmentedControl, Dropdown, ColorPicker) keep
8
+ their label
8
9
  above; these are their counterparts for a
9
10
  dense panel.
10
11
 
@@ -33,7 +34,8 @@
33
34
  default 8px + 24px line + 8px = 40px, 20px sides, paragraph-emphasis
34
35
  compact 6px + 20px line + 6px = 32px, 12px sides, paragraph-sm-emphasis */
35
36
  .ds-inspector-bar,
36
- .ds-inspector-dropdown {
37
+ .ds-inspector-dropdown,
38
+ .ds-inspector-color-picker {
37
39
  --ds-inspector-height: calc(2 * var(--padding-200) + var(--font-paragraph-emphasis-line-height));
38
40
  --ds-inspector-inset: var(--padding-500);
39
41
  --ds-inspector-font-family: var(--font-paragraph-emphasis-family);
@@ -45,7 +47,8 @@
45
47
  }
46
48
 
47
49
  .ds-inspector-bar--compact,
48
- .ds-inspector-dropdown--compact {
50
+ .ds-inspector-dropdown--compact,
51
+ .ds-inspector-color-picker--compact {
49
52
  --ds-inspector-height: calc(2 * var(--padding-150) + var(--font-paragraph-sm-emphasis-line-height));
50
53
  --ds-inspector-inset: var(--padding-300);
51
54
  --ds-inspector-font-family: var(--font-paragraph-sm-emphasis-family);
@@ -424,6 +427,108 @@
424
427
  color: var(--color-icon-secondary);
425
428
  }
426
429
 
430
+ /* ============================================
431
+ COLOR PICKER — the library ColorPicker, dressed as a bar
432
+ ============================================ */
433
+
434
+ .ds-inspector-color-picker {
435
+ position: relative;
436
+ min-width: 0;
437
+ }
438
+
439
+ /* The name rides over the trigger and lets clicks through, so the whole
440
+ bar opens the panel. The reading sits at the far end, so no room needs
441
+ reserving for the name the way the dropdown's left-aligned value does. */
442
+ .ds-inspector-color-picker__label {
443
+ position: absolute;
444
+ top: calc(var(--ds-inspector-height) / 2);
445
+ left: var(--ds-inspector-inset);
446
+ z-index: 1;
447
+ transform: translateY(-50%);
448
+ white-space: nowrap;
449
+ font-family: var(--ds-inspector-font-family);
450
+ font-size: var(--ds-inspector-font-size);
451
+ font-weight: var(--ds-inspector-label-weight);
452
+ line-height: var(--ds-inspector-line-height);
453
+ letter-spacing: var(--ds-inspector-letter-spacing);
454
+ color: var(--color-text-primary);
455
+ pointer-events: none;
456
+ }
457
+
458
+ .ds-inspector-color-picker .ds-colorpicker {
459
+ display: flex;
460
+ align-items: stretch;
461
+ width: 100%;
462
+ }
463
+
464
+ /* The swatch is a circle one line tall, so its end inset is whatever
465
+ centres it in the bar: concentric with the pill's end cap. */
466
+ .ds-inspector-color-picker .ds-colorpicker__trigger {
467
+ justify-content: flex-end;
468
+ gap: var(--gap-300);
469
+ width: 100%;
470
+ box-sizing: border-box;
471
+ height: var(--ds-inspector-height);
472
+ padding: 0 calc((var(--ds-inspector-height) - var(--ds-inspector-line-height)) / 2) 0
473
+ var(--ds-inspector-inset);
474
+ border-color: transparent;
475
+ border-radius: var(--radius-pill);
476
+ background: var(--color-action-passive-bg-hover);
477
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
478
+ }
479
+
480
+ .ds-inspector-color-picker .ds-colorpicker__trigger:hover:not(:disabled),
481
+ .ds-inspector-color-picker .ds-colorpicker--open .ds-colorpicker__trigger {
482
+ border-color: transparent;
483
+ background: var(--color-action-neutral-bg-hover);
484
+ }
485
+
486
+ .ds-inspector-color-picker .ds-colorpicker__trigger:active:not(:disabled) {
487
+ background: var(--color-action-neutral-bg-active);
488
+ }
489
+
490
+ /* The trigger's selected-border focus becomes the family's ring. */
491
+ .ds-inspector-color-picker .ds-colorpicker__trigger:focus-visible {
492
+ border-color: transparent;
493
+ outline: 2px solid var(--color-action-primary-bg);
494
+ outline-offset: 2px;
495
+ }
496
+
497
+ /* The reading comes before the swatch, so the colour sits at the bar's end
498
+ where the switch's track and the dropdown's chevron sit. */
499
+ .ds-inspector-color-picker .ds-colorpicker__trigger-text {
500
+ order: -1;
501
+ padding-right: 0;
502
+ font-family: var(--ds-inspector-font-family);
503
+ font-size: var(--ds-inspector-font-size);
504
+ font-weight: var(--ds-inspector-value-weight);
505
+ line-height: var(--ds-inspector-line-height);
506
+ letter-spacing: var(--ds-inspector-letter-spacing);
507
+ color: var(--color-text-secondary);
508
+ }
509
+
510
+ .ds-inspector-color-picker .ds-colorpicker__trigger-swatch {
511
+ width: var(--ds-inspector-line-height);
512
+ height: var(--ds-inspector-line-height);
513
+ border-radius: var(--radius-pill);
514
+ }
515
+
516
+ /* The panel opens under the bar and spans it. */
517
+ .ds-inspector-color-picker .ds-colorpicker__panel {
518
+ right: 0;
519
+ width: auto;
520
+ }
521
+
522
+ /* Disabled dims the whole bar, name included, rather than the trigger alone. */
523
+ .ds-inspector-color-picker--disabled {
524
+ opacity: 0.4;
525
+ cursor: not-allowed;
526
+ }
527
+
528
+ .ds-inspector-color-picker--disabled .ds-colorpicker__trigger {
529
+ opacity: 1;
530
+ }
531
+
427
532
  /* ============================================
428
533
  SECTION — a collapsible, titled run of controls
429
534
  ============================================ */
@@ -0,0 +1,43 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by InspectorColorPicker itself — everything else falls through to the wrapper. */
3
+ type InspectorColorPickerOwnProps = {
4
+ /** The setting's name, drawn inside the bar on the left. Also the start of the trigger's accessible name. */
5
+ label: string;
6
+ /** Current colour as a hex string — 3, 6 or 8 digit, with or without `#`. */
7
+ value?: string;
8
+ /** Initial colour for uncontrolled use. */
9
+ defaultValue?: string;
10
+ /**
11
+ * Called with the colour as an uppercase hex string (`#RRGGBB`, or
12
+ * `#RRGGBBAA` when `showAlpha` and alpha is below 100%). Fires live while
13
+ * dragging, as in ColorPicker.
14
+ */
15
+ onValueChange?: (value: string) => void;
16
+ /** Add an alpha (opacity) slider to the panel and emit 8-digit hex when alpha is below 100%. */
17
+ showAlpha?: boolean;
18
+ /** When set, a hidden input carries the current hex under this name, so the picker joins native form submission. */
19
+ name?: string;
20
+ /** Whether the picker is disabled. */
21
+ disabled?: boolean;
22
+ /** Component size, matching Button: `default` is 40px tall, `compact` 32px. */
23
+ size?: 'default' | 'compact';
24
+ /** Additional CSS classes, applied to the wrapper. */
25
+ className?: string;
26
+ };
27
+ export interface InspectorColorPickerProps extends InspectorColorPickerOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof InspectorColorPickerOwnProps | 'onChange' | 'defaultValue'> {
28
+ }
29
+ /**
30
+ * A one-row colour setting for inspector panels: the name on the left, the
31
+ * hex reading and a round swatch at the bar's end. It is the library
32
+ * ColorPicker with its trigger dressed as an inspector bar, so the panel,
33
+ * its saturation area, hue and alpha sliders and hex field are
34
+ * ColorPicker's own. The form counterpart, with a label above and helper
35
+ * text below, is ColorPicker itself.
36
+ *
37
+ * The whole bar is the trigger: the name is an overlay that lets clicks
38
+ * through, and the trigger is named by the name followed by the hex reading
39
+ * ("Tint colour #163300"). Forwards a ref to the wrapper and spreads
40
+ * unrecognised props onto it.
41
+ */
42
+ export declare const InspectorColorPicker: React.ForwardRefExoticComponent<InspectorColorPickerProps & React.RefAttributes<HTMLDivElement>>;
43
+ export {};