@robr0/design-system 0.10.0 → 0.12.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 +9 -8
- package/charts.d.ts +1 -0
- package/charts.js +2 -0
- package/components/AgentPlan/AgentPlan.css +14 -3
- package/components/AiButton/AiButton.css +1 -1
- package/components/Alert/Alert.css +9 -0
- package/components/AlertDialog/AlertDialog.css +3 -1
- package/components/AlertDialog/AlertDialog.js +2 -3
- package/components/AppLayout/AppLayout.d.ts +7 -1
- package/components/AppLayout/AppLayout.js +2 -1
- package/components/AppSidebar/AppSidebar.css +113 -40
- package/components/AppSidebar/AppSidebar.d.ts +14 -1
- package/components/AppSidebar/AppSidebar.js +26 -17
- package/components/Badge/Badge.css +4 -5
- package/components/Card/Card.css +7 -2
- package/components/CardStack/CardStack.css +106 -0
- package/components/CardStack/CardStack.d.ts +44 -0
- package/components/CardStack/CardStack.js +140 -0
- package/components/Carousel/Carousel.css +9 -0
- package/components/Carousel/Carousel.js +37 -4
- package/components/Chart/AreaChart.d.ts +5 -1
- package/components/Chart/AreaChart.js +11 -9
- package/components/Chart/BarChart.d.ts +3 -1
- package/components/Chart/BarChart.js +3 -3
- package/components/Chart/Chart.css +45 -0
- package/components/Chart/ComboChart.d.ts +40 -0
- package/components/Chart/ComboChart.js +142 -0
- package/components/Chart/LineChart.d.ts +3 -1
- package/components/Chart/LineChart.js +7 -6
- package/components/Chart/PieChart.d.ts +3 -1
- package/components/Chart/PieChart.js +5 -18
- package/components/Chart/RadarChart.d.ts +3 -1
- package/components/Chart/RadarChart.js +8 -7
- package/components/Chart/RadialChart.d.ts +7 -1
- package/components/Chart/RadialChart.js +53 -58
- package/components/Chart/ScatterChart.d.ts +3 -1
- package/components/Chart/ScatterChart.js +6 -16
- package/components/Chart/StackedBarChart.d.ts +3 -1
- package/components/Chart/StackedBarChart.js +5 -18
- package/components/Chart/Treemap.d.ts +3 -1
- package/components/Chart/Treemap.js +6 -20
- package/components/Chart/palette.d.ts +1 -0
- package/components/Chart/palette.js +17 -0
- package/components/ChatMarker/ChatMarker.css +9 -0
- package/components/ChatMessage/ChatMessage.css +16 -0
- package/components/ChatThread/ChatThread.js +2 -2
- package/components/Chip/Chip.css +9 -0
- package/components/CodeBlock/CodeBlock.css +10 -1
- package/components/CodeBlock/CodeBlock.js +2 -1
- package/components/ColorPicker/ColorPicker.js +10 -0
- package/components/Combobox/Combobox.css +9 -0
- package/components/CommandPalette/CommandPalette.css +9 -0
- package/components/CommandPalette/CommandPalette.js +31 -5
- package/components/Composer/Composer.css +118 -0
- package/components/Composer/Composer.d.ts +7 -0
- package/components/Composer/Composer.js +7 -1
- package/components/ContactCard/ContactCard.css +9 -0
- package/components/ContextMenu/ContextMenu.js +39 -3
- package/components/DatePicker/DatePicker.js +16 -0
- package/components/Dialog/Dialog.css +3 -1
- package/components/Dialog/Dialog.js +5 -4
- package/components/DocumentChip/DocumentChip.css +9 -0
- package/components/Dropdown/Dropdown.js +22 -3
- package/components/DropdownMenu/DropdownMenu.js +43 -6
- package/components/EmptyState/EmptyState.css +9 -0
- package/components/FileInput/FileInput.css +9 -0
- package/components/FunnelChart/FunnelChart.css +41 -0
- package/components/FunnelChart/FunnelChart.d.ts +40 -0
- package/components/FunnelChart/FunnelChart.js +48 -0
- package/components/Globe/Globe.css +155 -0
- package/components/Globe/Globe.d.ts +96 -0
- package/components/Globe/Globe.js +415 -0
- package/components/InterruptCard/InterruptCard.css +27 -17
- package/components/InterruptCard/InterruptCard.d.ts +1 -1
- package/components/InterruptCard/InterruptCard.js +1 -1
- package/components/LegendTile/LegendTile.css +60 -0
- package/components/LegendTile/LegendTile.d.ts +30 -0
- package/components/LegendTile/LegendTile.js +21 -0
- package/components/MapCallout/MapCallout.css +46 -0
- package/components/MapCallout/MapCallout.d.ts +29 -0
- package/components/MapCallout/MapCallout.js +17 -0
- package/components/MapLegend/MapLegend.css +88 -0
- package/components/MapLegend/MapLegend.d.ts +39 -0
- package/components/MapLegend/MapLegend.js +68 -0
- package/components/MessageActions/MessageActions.css +9 -0
- package/components/MessageCard/MessageCard.css +68 -16
- package/components/MessageCard/MessageCard.d.ts +1 -1
- package/components/MessageCard/MessageCard.js +6 -4
- package/components/ModelPicker/ModelPicker.css +4 -1
- package/components/ModelPicker/ModelPicker.js +43 -1
- package/components/NotificationCenter/NotificationCenter.css +9 -0
- package/components/NotificationCenter/NotificationCenter.js +26 -10
- package/components/NumberInput/NumberInput.css +9 -0
- package/components/Panel/Panel.css +26 -0
- package/components/Panel/Panel.d.ts +22 -0
- package/components/Panel/Panel.js +19 -0
- package/components/Popover/Popover.css +3 -1
- package/components/Popover/Popover.js +2 -1
- package/components/SourceChip/SourceChip.css +9 -0
- package/components/Stat/Stat.css +44 -2
- package/components/Stat/Stat.d.ts +3 -1
- package/components/Stat/Stat.js +7 -1
- package/components/Stepper/Stepper.css +9 -0
- package/components/TagInput/TagInput.css +9 -0
- package/components/TimePicker/TimePicker.js +4 -1
- package/components/Toast/Toast.css +32 -11
- package/components/Toast/Toast.js +5 -4
- package/components/ToggleGroup/ToggleGroup.css +9 -0
- package/components/ToolCall/ToolCall.css +18 -6
- package/components/Tooltip/Tooltip.js +20 -16
- package/components/registry.json +70 -0
- package/components/registry.json.d.ts +70 -0
- package/components/registry.json.js +2 -2
- package/index.d.ts +7 -0
- package/index.js +14 -0
- package/package.json +5 -1
- package/tokens/motion.d.ts +23 -0
- package/tokens/motion.js +14 -0
- package/tokens/registry.json +21 -1
- package/tokens/registry.json.d.ts +21 -1
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +14 -0
- package/tokens/tokens-light.css +22 -0
- package/tokens/tokens-primitives.css +2 -0
- package/tokens/tokens-typography.css +32 -5
|
@@ -23,6 +23,8 @@ export interface TreemapProps {
|
|
|
23
23
|
summaryItems?: ChartSummaryItem[];
|
|
24
24
|
/** Chart area height in pixels */
|
|
25
25
|
height?: number;
|
|
26
|
+
/** Strip the card chrome (border, padding, fill) when the chart sits inside another panel that supplies the surface */
|
|
27
|
+
bare?: boolean;
|
|
26
28
|
/** Additional CSS classes on the wrapper */
|
|
27
29
|
className?: string;
|
|
28
30
|
}
|
|
@@ -30,4 +32,4 @@ export interface TreemapProps {
|
|
|
30
32
|
* Treemap chart built on Recharts with design-system tokens.
|
|
31
33
|
* Displays hierarchical data as nested coloured rectangles sized by value.
|
|
32
34
|
*/
|
|
33
|
-
export declare const Treemap: ({ data, dataKey, title, subtitle, summaryItems, height, className, }: TreemapProps) => import("react").JSX.Element;
|
|
35
|
+
export declare const Treemap: ({ data, dataKey, title, subtitle, summaryItems, height, bare, className, }: TreemapProps) => import("react").JSX.Element;
|
|
@@ -1,31 +1,16 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback } from "react";
|
|
3
3
|
import { ResponsiveContainer, Treemap as Treemap$1, Tooltip } from "recharts";
|
|
4
|
+
import { getChartSeriesColors } from "./palette.js";
|
|
4
5
|
import "./Chart.css";
|
|
5
|
-
const PALETTE = [
|
|
6
|
-
"#118AB2",
|
|
7
|
-
// teal-07
|
|
8
|
-
"#06D6A0",
|
|
9
|
-
// green-07
|
|
10
|
-
"#EF476F",
|
|
11
|
-
// red-07
|
|
12
|
-
"#FFD166",
|
|
13
|
-
// yellow-07
|
|
14
|
-
"#9E47EF",
|
|
15
|
-
// purple-07
|
|
16
|
-
"#1E47B0",
|
|
17
|
-
// blue-07
|
|
18
|
-
"#EF8247"
|
|
19
|
-
// orange
|
|
20
|
-
];
|
|
21
6
|
function getCSSVar(name, fallback) {
|
|
22
|
-
|
|
23
|
-
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
7
|
+
return `var(${name}, ${fallback})`;
|
|
24
8
|
}
|
|
25
9
|
function TreemapContent(props) {
|
|
26
10
|
const { x, y, width, height, name, index, color } = props;
|
|
27
11
|
if (width < 4 || height < 4) return null;
|
|
28
|
-
const
|
|
12
|
+
const seriesColors = getChartSeriesColors();
|
|
13
|
+
const fill = color || seriesColors[index % seriesColors.length];
|
|
29
14
|
const textColor = getCSSVar("--color-text-primary", "#050505");
|
|
30
15
|
const showLabel = width > 50 && height > 28;
|
|
31
16
|
return /* @__PURE__ */ jsxs("g", { children: [
|
|
@@ -72,10 +57,11 @@ const Treemap = ({
|
|
|
72
57
|
subtitle,
|
|
73
58
|
summaryItems,
|
|
74
59
|
height = 350,
|
|
60
|
+
bare = false,
|
|
75
61
|
className = ""
|
|
76
62
|
}) => {
|
|
77
63
|
const baseClass = "ds-chart";
|
|
78
|
-
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
64
|
+
const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
|
|
79
65
|
const renderTooltip = useCallback(
|
|
80
66
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
81
67
|
(props) => /* @__PURE__ */ jsx(TreemapTooltip, { ...props }),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getChartSeriesColors(): string[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const SERIES_FALLBACKS = [
|
|
2
|
+
"#0E6E8F",
|
|
3
|
+
"#06D6A0",
|
|
4
|
+
"#FFD166",
|
|
5
|
+
"#EF476F",
|
|
6
|
+
"#9E47EF",
|
|
7
|
+
"#EF8247",
|
|
8
|
+
"#1E47B0"
|
|
9
|
+
];
|
|
10
|
+
function getChartSeriesColors() {
|
|
11
|
+
return SERIES_FALLBACKS.map(
|
|
12
|
+
(fallback, i) => `var(--color-chart-series-${i + 1}, ${fallback})`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
getChartSeriesColors
|
|
17
|
+
};
|
|
@@ -50,3 +50,12 @@
|
|
|
50
50
|
.ds-chat-marker__icon .material-symbols-rounded {
|
|
51
51
|
--icon-size: var(--icon-size-sm);
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
55
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
56
|
+
.material-symbols-rounded itself (a common global default) would
|
|
57
|
+
otherwise override inheritance and detach the icon from the state
|
|
58
|
+
colours this component sets on its parents. */
|
|
59
|
+
.ds-chat-marker .material-symbols-rounded {
|
|
60
|
+
color: inherit;
|
|
61
|
+
}
|
|
@@ -110,6 +110,22 @@
|
|
|
110
110
|
color: var(--color-text-primary);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/* Card furniture inside a turn steps clear of the content around it — at
|
|
114
|
+
the body's default rhythm an adjacent text line sits close enough to read
|
|
115
|
+
as a caption rather than a neighbouring block. Margins, not gap, so a
|
|
116
|
+
card standing alone adds nothing. */
|
|
117
|
+
.ds-chat-message__content > .ds-message-card:not(:first-child),
|
|
118
|
+
.ds-chat-message__content > .ds-interrupt-card:not(:first-child),
|
|
119
|
+
.ds-chat-message__content > .ds-tool-call:not(:first-child) {
|
|
120
|
+
margin-top: var(--gap-sm-md);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.ds-chat-message__content > .ds-message-card:not(:last-child),
|
|
124
|
+
.ds-chat-message__content > .ds-interrupt-card:not(:last-child),
|
|
125
|
+
.ds-chat-message__content > .ds-tool-call:not(:last-child) {
|
|
126
|
+
margin-bottom: var(--gap-sm-md);
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
/* Bubble — capped width so a short line never drags a surface across the
|
|
114
130
|
whole row; the fallback is the sanctioned consumer-override hook */
|
|
115
131
|
.ds-chat-message--bubble .ds-chat-message__content {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
3
|
import React, { useRef, useEffect, useState, useLayoutEffect } from "react";
|
|
4
|
+
import { MOTION_SCROLL_SETTLE_MS } from "../../tokens/motion.js";
|
|
4
5
|
import "./ChatThread.css";
|
|
5
6
|
import "../../fonts/material-symbols.css";
|
|
6
|
-
const SCROLLBAR_SETTLE_MS = 600;
|
|
7
7
|
const JUMP_THRESHOLD_PX = 24;
|
|
8
8
|
const ChatThread = React.forwardRef(
|
|
9
9
|
({
|
|
@@ -129,7 +129,7 @@ const ChatThread = React.forwardRef(
|
|
|
129
129
|
window.clearTimeout(settleTimer.current);
|
|
130
130
|
settleTimer.current = window.setTimeout(
|
|
131
131
|
() => setScrolling(false),
|
|
132
|
-
|
|
132
|
+
MOTION_SCROLL_SETTLE_MS
|
|
133
133
|
);
|
|
134
134
|
};
|
|
135
135
|
const jumpToBottom = () => {
|
package/components/Chip/Chip.css
CHANGED
|
@@ -220,3 +220,12 @@
|
|
|
220
220
|
outline: 2px solid var(--color-action-primary-bg);
|
|
221
221
|
outline-offset: 2px;
|
|
222
222
|
}
|
|
223
|
+
|
|
224
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
225
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
226
|
+
.material-symbols-rounded itself (a common global default) would
|
|
227
|
+
otherwise override inheritance and detach the icon from the state
|
|
228
|
+
colours this component sets on its parents. */
|
|
229
|
+
.ds-chip .material-symbols-rounded {
|
|
230
|
+
color: inherit;
|
|
231
|
+
}
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
font-weight: var(--font-paragraph-sm-em-weight);
|
|
96
96
|
line-height: var(--font-paragraph-sm-line-height);
|
|
97
97
|
text-transform: uppercase;
|
|
98
|
-
letter-spacing:
|
|
98
|
+
letter-spacing: var(--font-overline-letter-spacing);
|
|
99
99
|
color: var(--color-text-tertiary);
|
|
100
100
|
flex-shrink: 0;
|
|
101
101
|
}
|
|
@@ -178,3 +178,12 @@
|
|
|
178
178
|
color: var(--color-text-primary);
|
|
179
179
|
white-space: pre;
|
|
180
180
|
}
|
|
181
|
+
|
|
182
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
183
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
184
|
+
.material-symbols-rounded itself (a common global default) would
|
|
185
|
+
otherwise override inheritance and detach the icon from the state
|
|
186
|
+
colours this component sets on its parents. */
|
|
187
|
+
.ds-code-block .material-symbols-rounded {
|
|
188
|
+
color: inherit;
|
|
189
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useId } from "react";
|
|
4
|
+
import { MOTION_FEEDBACK_RESET_MS } from "../../tokens/motion.js";
|
|
4
5
|
import "./CodeBlock.css";
|
|
5
6
|
import "../../fonts/material-symbols.css";
|
|
6
7
|
const CodeBlock = ({
|
|
@@ -44,7 +45,7 @@ const CodeBlock = ({
|
|
|
44
45
|
fallback();
|
|
45
46
|
}
|
|
46
47
|
setCopied(true);
|
|
47
|
-
window.setTimeout(() => setCopied(false),
|
|
48
|
+
window.setTimeout(() => setCopied(false), MOTION_FEEDBACK_RESET_MS);
|
|
48
49
|
};
|
|
49
50
|
return /* @__PURE__ */ jsxs("div", { className: classes, style, children: [
|
|
50
51
|
hasHeader && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
|
|
@@ -170,6 +170,16 @@ const ColorPicker = React.forwardRef(
|
|
|
170
170
|
e.preventDefault();
|
|
171
171
|
commit({ h, s, v: clamp(v - step, 0, 100), a });
|
|
172
172
|
break;
|
|
173
|
+
// The slider pattern requires Home/End; they act on the horizontal
|
|
174
|
+
// (saturation) axis, matching the arrow keys above.
|
|
175
|
+
case "Home":
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
commit({ h, s: 0, v, a });
|
|
178
|
+
break;
|
|
179
|
+
case "End":
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
commit({ h, s: 100, v, a });
|
|
182
|
+
break;
|
|
173
183
|
}
|
|
174
184
|
};
|
|
175
185
|
const displayHex = formatHex(hsva, showAlpha);
|
|
@@ -373,3 +373,12 @@
|
|
|
373
373
|
animation: none;
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
|
+
|
|
377
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
378
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
379
|
+
.material-symbols-rounded itself (a common global default) would
|
|
380
|
+
otherwise override inheritance and detach the icon from the state
|
|
381
|
+
colours this component sets on its parents. */
|
|
382
|
+
.ds-combobox .material-symbols-rounded {
|
|
383
|
+
color: inherit;
|
|
384
|
+
}
|
|
@@ -294,3 +294,12 @@
|
|
|
294
294
|
transition: none;
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
|
+
|
|
298
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
299
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
300
|
+
.material-symbols-rounded itself (a common global default) would
|
|
301
|
+
otherwise override inheritance and detach the icon from the state
|
|
302
|
+
colours this component sets on its parents. */
|
|
303
|
+
.ds-command-palette .material-symbols-rounded {
|
|
304
|
+
color: inherit;
|
|
305
|
+
}
|
|
@@ -7,6 +7,7 @@ import "./CommandPalette.css";
|
|
|
7
7
|
import "../../fonts/material-symbols.css";
|
|
8
8
|
const emptySubscribe = () => () => {
|
|
9
9
|
};
|
|
10
|
+
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
10
11
|
const CommandPalette = ({
|
|
11
12
|
open,
|
|
12
13
|
onOpenChange,
|
|
@@ -25,6 +26,7 @@ const CommandPalette = ({
|
|
|
25
26
|
const [rawActiveIndex, setActiveIndex] = useState(0);
|
|
26
27
|
const [prevOpen, setPrevOpen] = useState(open);
|
|
27
28
|
const inputRef = useRef(null);
|
|
29
|
+
const panelRef = useRef(null);
|
|
28
30
|
const listRef = useRef(null);
|
|
29
31
|
const previousFocusRef = useRef(null);
|
|
30
32
|
const generatedId = useId();
|
|
@@ -87,6 +89,33 @@ const CommandPalette = ({
|
|
|
87
89
|
document.body.style.overflow = "";
|
|
88
90
|
};
|
|
89
91
|
}, [open]);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!open) return;
|
|
94
|
+
const handleTrapKeyDown = (e) => {
|
|
95
|
+
if (e.key === "Escape") {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
onOpenChange(false);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (e.key === "Tab") {
|
|
101
|
+
const panel = panelRef.current;
|
|
102
|
+
if (!panel) return;
|
|
103
|
+
const focusable = panel.querySelectorAll(FOCUSABLE);
|
|
104
|
+
if (focusable.length === 0) return;
|
|
105
|
+
const first = focusable[0];
|
|
106
|
+
const last = focusable[focusable.length - 1];
|
|
107
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
last.focus();
|
|
110
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
first.focus();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
document.addEventListener("keydown", handleTrapKeyDown);
|
|
117
|
+
return () => document.removeEventListener("keydown", handleTrapKeyDown);
|
|
118
|
+
}, [open, onOpenChange]);
|
|
90
119
|
const runCommand = (command) => {
|
|
91
120
|
if (command.disabled) return;
|
|
92
121
|
command.onSelect?.();
|
|
@@ -119,10 +148,6 @@ const CommandPalette = ({
|
|
|
119
148
|
e.preventDefault();
|
|
120
149
|
if (flatCommands[activeIndex]) runCommand(flatCommands[activeIndex]);
|
|
121
150
|
break;
|
|
122
|
-
case "Escape":
|
|
123
|
-
e.preventDefault();
|
|
124
|
-
onOpenChange(false);
|
|
125
|
-
break;
|
|
126
151
|
}
|
|
127
152
|
};
|
|
128
153
|
useEffect(() => {
|
|
@@ -148,6 +173,7 @@ const CommandPalette = ({
|
|
|
148
173
|
"div",
|
|
149
174
|
{
|
|
150
175
|
className: `${baseClass}__panel`,
|
|
176
|
+
ref: panelRef,
|
|
151
177
|
role: "dialog",
|
|
152
178
|
"aria-modal": "true",
|
|
153
179
|
"aria-label": "Command palette",
|
|
@@ -186,7 +212,7 @@ const CommandPalette = ({
|
|
|
186
212
|
] }),
|
|
187
213
|
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__list`, role: "listbox", id: listboxId, ref: listRef, children: [
|
|
188
214
|
loading && /* @__PURE__ */ jsx("p", { className: `${baseClass}__status`, children: "Loading…" }),
|
|
189
|
-
!loading && filteredGroups.length === 0 && /* @__PURE__ */ jsx("p", { className: `${baseClass}__status`, children: emptyMessage }),
|
|
215
|
+
!loading && filteredGroups.length === 0 && /* @__PURE__ */ jsx("p", { className: `${baseClass}__status`, "aria-live": "polite", children: emptyMessage }),
|
|
190
216
|
!loading && filteredGroups.map((group) => /* @__PURE__ */ jsxs(
|
|
191
217
|
"div",
|
|
192
218
|
{
|
|
@@ -3,7 +3,20 @@
|
|
|
3
3
|
The chat input shell: attachments, auto-growing textarea, send/stop
|
|
4
4
|
============================================ */
|
|
5
5
|
|
|
6
|
+
/* The focus treatment animates a registered custom property, exactly as
|
|
7
|
+
AiButton does: a conic gradient's angle cannot tween otherwise. Browsers
|
|
8
|
+
without @property render the ring at its initial angle, static — a
|
|
9
|
+
correct, quieter fallback. Registered under its own name so Composer
|
|
10
|
+
never depends on AiButton's stylesheet being loaded. */
|
|
11
|
+
@property --ds-composer-glow-angle {
|
|
12
|
+
syntax: '<angle>';
|
|
13
|
+
initial-value: 0deg;
|
|
14
|
+
inherits: false;
|
|
15
|
+
}
|
|
16
|
+
|
|
6
17
|
.ds-composer {
|
|
18
|
+
/* Anchors the focus ring and glow pseudo-elements */
|
|
19
|
+
position: relative;
|
|
7
20
|
display: flex;
|
|
8
21
|
flex-direction: column;
|
|
9
22
|
width: 100%;
|
|
@@ -36,6 +49,105 @@
|
|
|
36
49
|
border-color: var(--color-input-border-selected);
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
/* ============================================
|
|
53
|
+
AI GLOW — opt-in via the `aiGlow` prop
|
|
54
|
+
While focused, the plain selected border stands down and the AI gradient
|
|
55
|
+
ring takes its place — the same "a model answers here" signal AiButton
|
|
56
|
+
carries, for composers whose messages are answered by a model.
|
|
57
|
+
============================================ */
|
|
58
|
+
|
|
59
|
+
.ds-composer--ai-glow:focus-within {
|
|
60
|
+
border-color: transparent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* The gradient ring: AiButton's treatment on the composer shell — a conic
|
|
64
|
+
gradient painted across the whole box, masked to the border band (two
|
|
65
|
+
fills, exclude-composited, inset by the border width). The gradient
|
|
66
|
+
repeats back to its start colour so the seam at 360° is invisible while
|
|
67
|
+
it turns. Negative inset lays the pseudo over the shell's own border
|
|
68
|
+
box, so the ring replaces the border exactly. */
|
|
69
|
+
.ds-composer--ai-glow::before {
|
|
70
|
+
content: '';
|
|
71
|
+
position: absolute;
|
|
72
|
+
inset: calc(-1 * var(--border-xs));
|
|
73
|
+
border-radius: inherit;
|
|
74
|
+
/* One step up from the shell's --border-xs border: the outer edge stays on
|
|
75
|
+
the border box and the extra pixel grows inward, so the thicker band
|
|
76
|
+
reads clearly on low-density screens without changing the geometry. */
|
|
77
|
+
padding: var(--border-md);
|
|
78
|
+
background: conic-gradient(
|
|
79
|
+
from var(--ds-composer-glow-angle),
|
|
80
|
+
var(--color-ai-gradient-start),
|
|
81
|
+
var(--color-ai-gradient-mid),
|
|
82
|
+
var(--color-ai-gradient-end),
|
|
83
|
+
var(--color-ai-gradient-mid),
|
|
84
|
+
var(--color-ai-gradient-start)
|
|
85
|
+
);
|
|
86
|
+
/* Standard properties only — the build pipeline prefixes per its browser
|
|
87
|
+
targets, and hand-written -webkit- duplicates confuse its merging. */
|
|
88
|
+
mask:
|
|
89
|
+
linear-gradient(#fff 0 0) content-box,
|
|
90
|
+
linear-gradient(#fff 0 0);
|
|
91
|
+
mask-composite: exclude;
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
opacity: 0;
|
|
94
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
95
|
+
/* ds-allow(motion): ambient signature rotation, matched to AiButton's —
|
|
96
|
+
deliberately far slower than the interaction-scale motion tokens */
|
|
97
|
+
animation: ds-composer-glow-turn 7s linear infinite paused;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* The glow: the same gradient, blurred behind the opaque shell at low
|
|
101
|
+
opacity, so it reads as a halo around the border. A colour effect, not
|
|
102
|
+
an elevation shadow — depth stays token-owned. */
|
|
103
|
+
.ds-composer--ai-glow::after {
|
|
104
|
+
content: '';
|
|
105
|
+
position: absolute;
|
|
106
|
+
inset: calc(-1 * var(--border-xs));
|
|
107
|
+
border-radius: inherit;
|
|
108
|
+
background: conic-gradient(
|
|
109
|
+
from var(--ds-composer-glow-angle),
|
|
110
|
+
var(--color-ai-gradient-start),
|
|
111
|
+
var(--color-ai-gradient-mid),
|
|
112
|
+
var(--color-ai-gradient-end),
|
|
113
|
+
var(--color-ai-gradient-mid),
|
|
114
|
+
var(--color-ai-gradient-start)
|
|
115
|
+
);
|
|
116
|
+
/* The glow's spread; a length with no token family, matched to AiButton. */
|
|
117
|
+
--ds-composer-glow: 14px;
|
|
118
|
+
filter: blur(var(--ds-composer-glow));
|
|
119
|
+
opacity: 0;
|
|
120
|
+
z-index: -1;
|
|
121
|
+
pointer-events: none;
|
|
122
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
123
|
+
/* ds-allow(motion): same ambient rotation as the ring, kept in phase */
|
|
124
|
+
animation: ds-composer-glow-turn 7s linear infinite paused;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ds-composer--ai-glow:focus-within::before {
|
|
128
|
+
opacity: 1;
|
|
129
|
+
animation-play-state: running;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ds-composer--ai-glow:focus-within::after {
|
|
133
|
+
opacity: 0.3;
|
|
134
|
+
animation-play-state: running;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@keyframes ds-composer-glow-turn {
|
|
138
|
+
to {
|
|
139
|
+
--ds-composer-glow-angle: 360deg;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Reduced motion: the ring and glow stay, the turning stops. */
|
|
144
|
+
@media (prefers-reduced-motion: reduce) {
|
|
145
|
+
.ds-composer--ai-glow::before,
|
|
146
|
+
.ds-composer--ai-glow::after {
|
|
147
|
+
animation: none;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
39
151
|
/* ============================================
|
|
40
152
|
ATTACHMENTS ROW
|
|
41
153
|
============================================ */
|
|
@@ -175,6 +287,12 @@
|
|
|
175
287
|
border-color: var(--color-input-border-disabled);
|
|
176
288
|
}
|
|
177
289
|
|
|
290
|
+
.ds-composer--disabled:focus-within::before,
|
|
291
|
+
.ds-composer--disabled:focus-within::after {
|
|
292
|
+
opacity: 0;
|
|
293
|
+
animation-play-state: paused;
|
|
294
|
+
}
|
|
295
|
+
|
|
178
296
|
.ds-composer--disabled .ds-composer__textarea {
|
|
179
297
|
cursor: not-allowed;
|
|
180
298
|
color: var(--color-input-text-disabled);
|
|
@@ -28,6 +28,13 @@ type ComposerOwnProps = {
|
|
|
28
28
|
onStop?: () => void;
|
|
29
29
|
/** Growth cap in text rows before the textarea scrolls internally. */
|
|
30
30
|
maxRows?: number;
|
|
31
|
+
/**
|
|
32
|
+
* While focused, the shell wears AiButton's slowly rotating gradient ring
|
|
33
|
+
* and glow in place of the plain selected border — the system's "a model
|
|
34
|
+
* answers here" signal, for composers whose messages are answered by one.
|
|
35
|
+
* Off by default.
|
|
36
|
+
*/
|
|
37
|
+
aiGlow?: boolean;
|
|
31
38
|
/**
|
|
32
39
|
* Attachment row rendered above the textarea (DocumentChips). Fully
|
|
33
40
|
* controlled by the caller — Composer never owns the list.
|
|
@@ -12,6 +12,7 @@ const Composer = React.forwardRef(
|
|
|
12
12
|
streaming = false,
|
|
13
13
|
onStop,
|
|
14
14
|
maxRows = 8,
|
|
15
|
+
aiGlow = false,
|
|
15
16
|
attachments,
|
|
16
17
|
actions,
|
|
17
18
|
trailingActions,
|
|
@@ -73,7 +74,12 @@ const Composer = React.forwardRef(
|
|
|
73
74
|
if (target.closest('button, a, textarea, input, select, [role="button"]')) return;
|
|
74
75
|
textareaRef.current?.focus();
|
|
75
76
|
};
|
|
76
|
-
const classes = [
|
|
77
|
+
const classes = [
|
|
78
|
+
baseClass,
|
|
79
|
+
aiGlow ? `${baseClass}--ai-glow` : "",
|
|
80
|
+
disabled ? `${baseClass}--disabled` : "",
|
|
81
|
+
className
|
|
82
|
+
].filter(Boolean).join(" ");
|
|
77
83
|
const ariaLabel = rest["aria-label"] ?? (rest["aria-labelledby"] !== void 0 ? void 0 : "Message");
|
|
78
84
|
return /* @__PURE__ */ jsxs(
|
|
79
85
|
"div",
|
|
@@ -121,3 +121,12 @@
|
|
|
121
121
|
color: var(--color-text-tertiary);
|
|
122
122
|
flex-shrink: 0;
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
126
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
127
|
+
.material-symbols-rounded itself (a common global default) would
|
|
128
|
+
otherwise override inheritance and detach the icon from the state
|
|
129
|
+
colours this component sets on its parents. */
|
|
130
|
+
.ds-contact-card .material-symbols-rounded {
|
|
131
|
+
color: inherit;
|
|
132
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import React, { useState, useRef, useCallback, useEffect, useLayoutEffect } from "react";
|
|
3
|
+
import React, { useState, useRef, useId, useCallback, useEffect, useLayoutEffect } from "react";
|
|
4
|
+
import { MOTION_HOVER_HIDE_DELAY_MS } from "../../tokens/motion.js";
|
|
4
5
|
import "./ContextMenu.css";
|
|
5
6
|
import "../../fonts/material-symbols.css";
|
|
6
7
|
function collectItems(entries) {
|
|
@@ -15,7 +16,7 @@ function collectItems(entries) {
|
|
|
15
16
|
}
|
|
16
17
|
return result;
|
|
17
18
|
}
|
|
18
|
-
const SubMenuItem = ({ item, baseClass, focused, size, onActivate }) => {
|
|
19
|
+
const SubMenuItem = ({ item, baseClass, itemId, focused, size, onActivate }) => {
|
|
19
20
|
const [subOpen, setSubOpen] = useState(false);
|
|
20
21
|
const timeoutRef = useRef(null);
|
|
21
22
|
const handleMouseEnter = () => {
|
|
@@ -23,7 +24,7 @@ const SubMenuItem = ({ item, baseClass, focused, size, onActivate }) => {
|
|
|
23
24
|
setSubOpen(true);
|
|
24
25
|
};
|
|
25
26
|
const handleMouseLeave = () => {
|
|
26
|
-
timeoutRef.current = setTimeout(() => setSubOpen(false),
|
|
27
|
+
timeoutRef.current = setTimeout(() => setSubOpen(false), MOTION_HOVER_HIDE_DELAY_MS);
|
|
27
28
|
};
|
|
28
29
|
useEffect(() => {
|
|
29
30
|
return () => {
|
|
@@ -45,6 +46,7 @@ const SubMenuItem = ({ item, baseClass, focused, size, onActivate }) => {
|
|
|
45
46
|
return /* @__PURE__ */ jsxs(
|
|
46
47
|
"li",
|
|
47
48
|
{
|
|
49
|
+
id: itemId,
|
|
48
50
|
className: classes,
|
|
49
51
|
role: "menuitem",
|
|
50
52
|
"aria-haspopup": "menu",
|
|
@@ -67,6 +69,7 @@ const SubMenuItem = ({ item, baseClass, focused, size, onActivate }) => {
|
|
|
67
69
|
{
|
|
68
70
|
entry,
|
|
69
71
|
baseClass,
|
|
72
|
+
itemId: `${itemId}-sub-${index}`,
|
|
70
73
|
size,
|
|
71
74
|
onActivate
|
|
72
75
|
},
|
|
@@ -79,6 +82,7 @@ const SubMenuItem = ({ item, baseClass, focused, size, onActivate }) => {
|
|
|
79
82
|
const SubEntry = ({
|
|
80
83
|
entry,
|
|
81
84
|
baseClass,
|
|
85
|
+
itemId,
|
|
82
86
|
size,
|
|
83
87
|
onActivate
|
|
84
88
|
}) => {
|
|
@@ -93,6 +97,7 @@ const SubEntry = ({
|
|
|
93
97
|
{
|
|
94
98
|
entry: subEntry,
|
|
95
99
|
baseClass,
|
|
100
|
+
itemId: `${itemId}-sub-${subIndex}`,
|
|
96
101
|
size,
|
|
97
102
|
onActivate
|
|
98
103
|
},
|
|
@@ -106,6 +111,7 @@ const SubEntry = ({
|
|
|
106
111
|
{
|
|
107
112
|
item: entry,
|
|
108
113
|
baseClass,
|
|
114
|
+
itemId,
|
|
109
115
|
focused: false,
|
|
110
116
|
size,
|
|
111
117
|
onActivate
|
|
@@ -120,6 +126,7 @@ const SubEntry = ({
|
|
|
120
126
|
return /* @__PURE__ */ jsxs(
|
|
121
127
|
"li",
|
|
122
128
|
{
|
|
129
|
+
id: itemId,
|
|
123
130
|
className: itemClasses,
|
|
124
131
|
role: "menuitem",
|
|
125
132
|
"aria-disabled": entry.disabled || void 0,
|
|
@@ -154,6 +161,8 @@ const ContextMenu = React.forwardRef(
|
|
|
154
161
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
155
162
|
const rootRef = useRef(null);
|
|
156
163
|
const panelRef = useRef(null);
|
|
164
|
+
const previousFocusRef = useRef(null);
|
|
165
|
+
const generatedId = useId();
|
|
157
166
|
const baseClass = "ds-context-menu";
|
|
158
167
|
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
159
168
|
const isOpen = position !== null;
|
|
@@ -166,6 +175,11 @@ const ContextMenu = React.forwardRef(
|
|
|
166
175
|
const handleClose = useCallback(() => {
|
|
167
176
|
setPosition(null);
|
|
168
177
|
setFocusedIndex(-1);
|
|
178
|
+
const previous = previousFocusRef.current;
|
|
179
|
+
previousFocusRef.current = null;
|
|
180
|
+
if (previous && document.contains(previous)) {
|
|
181
|
+
previous.focus();
|
|
182
|
+
}
|
|
169
183
|
}, []);
|
|
170
184
|
const handleContextMenu = (e) => {
|
|
171
185
|
onContextMenu?.(e);
|
|
@@ -177,6 +191,9 @@ const ContextMenu = React.forwardRef(
|
|
|
177
191
|
x = rect.left + rect.width / 2;
|
|
178
192
|
y = rect.top + rect.height / 2;
|
|
179
193
|
}
|
|
194
|
+
if (!isOpen) {
|
|
195
|
+
previousFocusRef.current = document.activeElement;
|
|
196
|
+
}
|
|
180
197
|
setPosition({ x, y });
|
|
181
198
|
setFocusedIndex(-1);
|
|
182
199
|
};
|
|
@@ -248,6 +265,22 @@ const ContextMenu = React.forwardRef(
|
|
|
248
265
|
return next >= 0 ? next : prev;
|
|
249
266
|
});
|
|
250
267
|
break;
|
|
268
|
+
case "Home":
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
setFocusedIndex((prev) => {
|
|
271
|
+
let next = 0;
|
|
272
|
+
while (next < flatItems.length && flatItems[next].disabled) next++;
|
|
273
|
+
return next < flatItems.length ? next : prev;
|
|
274
|
+
});
|
|
275
|
+
break;
|
|
276
|
+
case "End":
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
setFocusedIndex((prev) => {
|
|
279
|
+
let next = flatItems.length - 1;
|
|
280
|
+
while (next >= 0 && flatItems[next].disabled) next--;
|
|
281
|
+
return next >= 0 ? next : prev;
|
|
282
|
+
});
|
|
283
|
+
break;
|
|
251
284
|
case "Escape":
|
|
252
285
|
case "Tab":
|
|
253
286
|
e.preventDefault();
|
|
@@ -276,6 +309,7 @@ const ContextMenu = React.forwardRef(
|
|
|
276
309
|
{
|
|
277
310
|
item: entry,
|
|
278
311
|
baseClass,
|
|
312
|
+
itemId: `${generatedId}-item-${myIndex}`,
|
|
279
313
|
focused: isFocused,
|
|
280
314
|
size,
|
|
281
315
|
onActivate: handleClose
|
|
@@ -292,6 +326,7 @@ const ContextMenu = React.forwardRef(
|
|
|
292
326
|
return /* @__PURE__ */ jsxs(
|
|
293
327
|
"li",
|
|
294
328
|
{
|
|
329
|
+
id: `${generatedId}-item-${myIndex}`,
|
|
295
330
|
className: itemClasses,
|
|
296
331
|
role: "menuitem",
|
|
297
332
|
"aria-disabled": entry.disabled || void 0,
|
|
@@ -333,6 +368,7 @@ const ContextMenu = React.forwardRef(
|
|
|
333
368
|
className: panelClasses,
|
|
334
369
|
role: "menu",
|
|
335
370
|
"aria-label": ariaLabel,
|
|
371
|
+
"aria-activedescendant": focusedIndex >= 0 ? `${generatedId}-item-${focusedIndex}` : void 0,
|
|
336
372
|
tabIndex: -1,
|
|
337
373
|
ref: (el) => {
|
|
338
374
|
panelRef.current = el;
|