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.
- package/README.md +2 -2
- package/components/Chart/BarChart.js +1 -1
- package/components/Chart/ComboChart.js +1 -1
- package/components/Chart/Treemap.js +83 -6
- package/components/ColorPicker/ColorPicker.js +1 -1
- package/components/Composer/Composer.css +45 -3
- package/components/Composer/Composer.d.ts +14 -6
- package/components/Composer/Composer.js +25 -5
- package/components/DatePicker/DatePicker.css +1 -1
- package/components/EventCalendar/EventCalendar.css +2 -2
- package/components/FileInput/FileInput.css +1 -1
- package/components/Globe/Globe.d.ts +5 -3
- package/components/Globe/Globe.js +18 -2
- package/components/Inspector/Inspector.css +108 -3
- package/components/Inspector/InspectorColorPicker.d.ts +43 -0
- package/components/Inspector/InspectorColorPicker.js +51 -0
- package/components/Instructions/Instructions.css +4 -4
- package/components/Prose/Prose.css +27 -0
- package/components/Sparkline/Sparkline.css +2 -2
- package/components/Spinner/Spinner.css +1 -1
- package/components/Timeline/Timeline.css +1 -1
- package/components/WorldMap/land.d.ts +1 -1
- package/components/registry.json +10 -1
- package/components/registry.json.d.ts +10 -1
- package/components/registry.json.js +1 -1
- package/index.d.ts +1 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/tokens/motion.d.ts +2 -0
- package/tokens/motion.js +2 -0
- package/tokens/presets/contrast.css +2 -4
- package/tokens/presets/coral.css +2 -4
- package/tokens/presets/forest.css +2 -4
- package/tokens/presets/gold.css +4 -4
- package/tokens/presets/mono.css +0 -2
- package/tokens/presets/pink.css +4 -4
- package/tokens/presets/terminal.css +5 -4
- package/tokens/presets/violet.css +2 -4
- package/tokens/presets/warm.css +3 -4
- package/tokens/presets/zest.css +2 -4
- package/tokens/registry.json +1 -0
- package/tokens/registry.json.d.ts +1 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +8 -5
- package/tokens/tokens-light.css +4 -2
- 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 -->
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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
|
|
228
|
-
the
|
|
229
|
-
|
|
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
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
cell height.
|
|
8
8
|
|
|
9
9
|
Event accents use the core accent roles —
|
|
10
|
-
never the action
|
|
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
|
|
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-
|
|
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
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
|
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 {};
|