@robr0/design-system 0.10.0 → 0.11.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 +7 -6
- 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/AppSidebar/AppSidebar.css +9 -0
- 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.js +6 -5
- package/components/Chart/LineChart.js +4 -3
- package/components/Chart/PieChart.js +2 -15
- package/components/Chart/RadarChart.js +5 -4
- package/components/Chart/RadialChart.js +2 -15
- package/components/Chart/ScatterChart.js +3 -13
- package/components/Chart/StackedBarChart.js +2 -15
- package/components/Chart/Treemap.js +3 -17
- package/components/Chart/palette.d.ts +1 -0
- package/components/Chart/palette.js +19 -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/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/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/Popover/Popover.css +3 -1
- package/components/Popover/Popover.js +2 -1
- package/components/SourceChip/SourceChip.css +9 -0
- package/components/Stat/Stat.css +9 -0
- 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 +37 -0
- package/components/registry.json.d.ts +37 -0
- package/components/registry.json.js +2 -2
- package/index.d.ts +4 -0
- package/index.js +8 -0
- package/package.json +5 -1
- package/tokens/motion.d.ts +23 -0
- package/tokens/motion.js +14 -0
- package/tokens/registry.json +17 -0
- package/tokens/registry.json.d.ts +17 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +11 -0
- package/tokens/tokens-light.css +11 -0
- package/tokens/tokens-typography.css +20 -0
|
@@ -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;
|
|
@@ -29,6 +29,17 @@ function formatDate(date) {
|
|
|
29
29
|
const d = String(date.getDate()).padStart(2, "0");
|
|
30
30
|
return `${y}-${m}-${d}`;
|
|
31
31
|
}
|
|
32
|
+
const visuallyHidden = {
|
|
33
|
+
position: "absolute",
|
|
34
|
+
width: 1,
|
|
35
|
+
height: 1,
|
|
36
|
+
padding: 0,
|
|
37
|
+
margin: -1,
|
|
38
|
+
overflow: "hidden",
|
|
39
|
+
clip: "rect(0 0 0 0)",
|
|
40
|
+
whiteSpace: "nowrap",
|
|
41
|
+
border: 0
|
|
42
|
+
};
|
|
32
43
|
function isSameDay(a, b) {
|
|
33
44
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
34
45
|
}
|
|
@@ -126,6 +137,11 @@ const DatePicker = React.forwardRef(
|
|
|
126
137
|
}
|
|
127
138
|
)
|
|
128
139
|
] }),
|
|
140
|
+
/* @__PURE__ */ jsxs("span", { "aria-live": "polite", style: visuallyHidden, children: [
|
|
141
|
+
MONTHS[viewMonth],
|
|
142
|
+
" ",
|
|
143
|
+
viewYear
|
|
144
|
+
] }),
|
|
129
145
|
/* @__PURE__ */ jsxs(
|
|
130
146
|
"div",
|
|
131
147
|
{
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
align-items: center;
|
|
12
12
|
justify-content: center;
|
|
13
13
|
opacity: 0;
|
|
14
|
+
visibility: hidden; /* keeps panel controls out of the tab order when closed */
|
|
14
15
|
pointer-events: none;
|
|
15
|
-
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
16
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard), visibility var(--motion-duration-base) var(--motion-ease-standard);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
.ds-dialog--open {
|
|
19
20
|
opacity: 1;
|
|
21
|
+
visibility: visible;
|
|
20
22
|
pointer-events: auto;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -6,6 +6,7 @@ import "./Dialog.css";
|
|
|
6
6
|
import "../../fonts/material-symbols.css";
|
|
7
7
|
const emptySubscribe = () => () => {
|
|
8
8
|
};
|
|
9
|
+
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
9
10
|
const Dialog = React.forwardRef(
|
|
10
11
|
({
|
|
11
12
|
open,
|
|
@@ -43,7 +44,9 @@ const Dialog = React.forwardRef(
|
|
|
43
44
|
if (!open) return;
|
|
44
45
|
const panel = panelRef.current;
|
|
45
46
|
if (panel) {
|
|
46
|
-
const first = panel.querySelector(
|
|
47
|
+
const first = panel.querySelector(
|
|
48
|
+
"button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])"
|
|
49
|
+
) ?? panel.querySelector(FOCUSABLE);
|
|
47
50
|
(first ?? panel).focus();
|
|
48
51
|
}
|
|
49
52
|
const handleKeyDown = (e) => {
|
|
@@ -52,9 +55,7 @@ const Dialog = React.forwardRef(
|
|
|
52
55
|
return;
|
|
53
56
|
}
|
|
54
57
|
if (e.key === "Tab" && panel) {
|
|
55
|
-
const focusable = panel.querySelectorAll(
|
|
56
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
57
|
-
);
|
|
58
|
+
const focusable = panel.querySelectorAll(FOCUSABLE);
|
|
58
59
|
if (focusable.length === 0) return;
|
|
59
60
|
const first = focusable[0];
|
|
60
61
|
const last = focusable[focusable.length - 1];
|
|
@@ -179,3 +179,12 @@
|
|
|
179
179
|
outline: 2px solid var(--color-action-primary-bg);
|
|
180
180
|
outline-offset: -2px;
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
184
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
185
|
+
.material-symbols-rounded itself (a common global default) would
|
|
186
|
+
otherwise override inheritance and detach the icon from the state
|
|
187
|
+
colours this component sets on its parents. */
|
|
188
|
+
.ds-document-chip .material-symbols-rounded {
|
|
189
|
+
color: inherit;
|
|
190
|
+
}
|
|
@@ -100,6 +100,21 @@ const Dropdown = React.forwardRef(
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
break;
|
|
103
|
+
case "Home":
|
|
104
|
+
if (isOpen) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const first = flatOptions.findIndex((opt) => !opt.disabled);
|
|
107
|
+
if (first >= 0) setFocusedIndex(first);
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case "End":
|
|
111
|
+
if (isOpen) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
let last = flatOptions.length - 1;
|
|
114
|
+
while (last >= 0 && flatOptions[last].disabled) last--;
|
|
115
|
+
if (last >= 0) setFocusedIndex(last);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
103
118
|
case "Escape":
|
|
104
119
|
setIsOpen(false);
|
|
105
120
|
break;
|
|
@@ -110,10 +125,11 @@ const Dropdown = React.forwardRef(
|
|
|
110
125
|
};
|
|
111
126
|
useEffect(() => {
|
|
112
127
|
if (isOpen && focusedIndex >= 0 && listRef.current) {
|
|
113
|
-
const focusedEl =
|
|
128
|
+
const focusedEl = document.getElementById(`${inputId}-option-${focusedIndex}`);
|
|
114
129
|
focusedEl?.scrollIntoView({ block: "nearest" });
|
|
115
130
|
}
|
|
116
|
-
}, [focusedIndex, isOpen]);
|
|
131
|
+
}, [focusedIndex, isOpen, inputId]);
|
|
132
|
+
const activeOptionId = isOpen && focusedIndex >= 0 && flatOptions[focusedIndex] ? `${inputId}-option-${focusedIndex}` : void 0;
|
|
117
133
|
return /* @__PURE__ */ jsxs(
|
|
118
134
|
Field,
|
|
119
135
|
{
|
|
@@ -137,7 +153,8 @@ const Dropdown = React.forwardRef(
|
|
|
137
153
|
"aria-haspopup": "listbox",
|
|
138
154
|
"aria-labelledby": label ? `${inputId}-label` : void 0,
|
|
139
155
|
"aria-label": ariaLabel || rest["aria-label"] || (!label ? placeholder : void 0),
|
|
140
|
-
"aria-controls": `${inputId}-listbox
|
|
156
|
+
"aria-controls": isOpen ? `${inputId}-listbox` : void 0,
|
|
157
|
+
"aria-activedescendant": activeOptionId,
|
|
141
158
|
"aria-describedby": helperText ? `${inputId}-helper` : rest["aria-describedby"],
|
|
142
159
|
"aria-invalid": error || void 0,
|
|
143
160
|
tabIndex: disabled ? -1 : 0,
|
|
@@ -178,6 +195,7 @@ const Dropdown = React.forwardRef(
|
|
|
178
195
|
return /* @__PURE__ */ jsxs(
|
|
179
196
|
"li",
|
|
180
197
|
{
|
|
198
|
+
id: `${inputId}-option-${myIndex}`,
|
|
181
199
|
className: [
|
|
182
200
|
`${baseClass}__option`,
|
|
183
201
|
option.value === value ? `${baseClass}__option--selected` : "",
|
|
@@ -210,6 +228,7 @@ const Dropdown = React.forwardRef(
|
|
|
210
228
|
})() : options.map((option, index) => /* @__PURE__ */ jsxs(
|
|
211
229
|
"li",
|
|
212
230
|
{
|
|
231
|
+
id: `${inputId}-option-${index}`,
|
|
213
232
|
className: [
|
|
214
233
|
`${baseClass}__option`,
|
|
215
234
|
option.value === value ? `${baseClass}__option--selected` : "",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import React, { useState, useRef, useCallback, useEffect } from "react";
|
|
3
|
+
import React, { useState, useRef, useId, useCallback, useEffect } from "react";
|
|
4
|
+
import { MOTION_HOVER_HIDE_DELAY_MS } from "../../tokens/motion.js";
|
|
4
5
|
import "./DropdownMenu.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
|
entries: item.children,
|
|
69
71
|
baseClass,
|
|
72
|
+
itemIdBase: `${itemId}-sub`,
|
|
70
73
|
size,
|
|
71
74
|
isSubmenu: true,
|
|
72
75
|
onItemActivate: onActivate
|
|
@@ -79,6 +82,7 @@ const SubMenuItem = ({ item, baseClass, focused, size, onActivate }) => {
|
|
|
79
82
|
const MenuPanel = ({
|
|
80
83
|
entries,
|
|
81
84
|
baseClass,
|
|
85
|
+
itemIdBase,
|
|
82
86
|
size,
|
|
83
87
|
isSubmenu = false,
|
|
84
88
|
onItemActivate
|
|
@@ -88,14 +92,15 @@ const MenuPanel = ({
|
|
|
88
92
|
isSubmenu ? `${baseClass}__submenu` : "",
|
|
89
93
|
size === "compact" ? `${baseClass}__panel--compact` : ""
|
|
90
94
|
].filter(Boolean).join(" ");
|
|
91
|
-
const renderEntry = (entry, index) => {
|
|
95
|
+
const renderEntry = (entry, index, idBase) => {
|
|
96
|
+
const entryId = `${idBase}-${index}`;
|
|
92
97
|
if (entry.type === "separator") {
|
|
93
98
|
return /* @__PURE__ */ jsx("li", { className: `${baseClass}__separator`, role: "separator" }, `sep-${index}`);
|
|
94
99
|
}
|
|
95
100
|
if (entry.type === "group") {
|
|
96
101
|
return /* @__PURE__ */ jsxs("li", { className: `${baseClass}__group`, role: "group", "aria-label": entry.label, children: [
|
|
97
102
|
/* @__PURE__ */ jsx("span", { className: `${baseClass}__group-label`, children: entry.label }),
|
|
98
|
-
/* @__PURE__ */ jsx("ul", { className: `${baseClass}__group-list`, role: "group", children: entry.items.map((subEntry, subIndex) => renderEntry(subEntry, subIndex)) })
|
|
103
|
+
/* @__PURE__ */ jsx("ul", { className: `${baseClass}__group-list`, role: "group", children: entry.items.map((subEntry, subIndex) => renderEntry(subEntry, subIndex, entryId)) })
|
|
99
104
|
] }, `group-${index}`);
|
|
100
105
|
}
|
|
101
106
|
if (entry.children && entry.children.length > 0) {
|
|
@@ -104,6 +109,7 @@ const MenuPanel = ({
|
|
|
104
109
|
{
|
|
105
110
|
item: entry,
|
|
106
111
|
baseClass,
|
|
112
|
+
itemId: entryId,
|
|
107
113
|
focused: false,
|
|
108
114
|
size,
|
|
109
115
|
onActivate: onItemActivate
|
|
@@ -119,6 +125,7 @@ const MenuPanel = ({
|
|
|
119
125
|
return /* @__PURE__ */ jsxs(
|
|
120
126
|
"li",
|
|
121
127
|
{
|
|
128
|
+
id: entryId,
|
|
122
129
|
className: itemClasses,
|
|
123
130
|
role: "menuitem",
|
|
124
131
|
"aria-disabled": entry.disabled || void 0,
|
|
@@ -140,7 +147,7 @@ const MenuPanel = ({
|
|
|
140
147
|
`item-${index}`
|
|
141
148
|
);
|
|
142
149
|
};
|
|
143
|
-
return /* @__PURE__ */ jsx("ul", { className: panelClasses, role: "menu", children: entries.map((entry, index) => renderEntry(entry, index)) });
|
|
150
|
+
return /* @__PURE__ */ jsx("ul", { className: panelClasses, role: "menu", children: entries.map((entry, index) => renderEntry(entry, index, itemIdBase)) });
|
|
144
151
|
};
|
|
145
152
|
const DropdownMenu = ({
|
|
146
153
|
trigger,
|
|
@@ -153,9 +160,12 @@ const DropdownMenu = ({
|
|
|
153
160
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
154
161
|
const rootRef = useRef(null);
|
|
155
162
|
const panelRef = useRef(null);
|
|
163
|
+
const generatedId = useId();
|
|
156
164
|
const baseClass = "ds-dropdown-menu";
|
|
157
165
|
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
158
166
|
const flatItems = collectItems(items);
|
|
167
|
+
const menuId = `${generatedId}-menu`;
|
|
168
|
+
const activeDescendantId = isOpen && focusedIndex >= 0 ? `${generatedId}-item-${focusedIndex}` : void 0;
|
|
159
169
|
const handleToggle = () => {
|
|
160
170
|
setIsOpen((prev) => {
|
|
161
171
|
if (!prev) setFocusedIndex(-1);
|
|
@@ -214,6 +224,26 @@ const DropdownMenu = ({
|
|
|
214
224
|
});
|
|
215
225
|
}
|
|
216
226
|
break;
|
|
227
|
+
case "Home":
|
|
228
|
+
if (isOpen) {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
setFocusedIndex((prev) => {
|
|
231
|
+
let next = 0;
|
|
232
|
+
while (next < flatItems.length && flatItems[next].disabled) next++;
|
|
233
|
+
return next < flatItems.length ? next : prev;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case "End":
|
|
238
|
+
if (isOpen) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
setFocusedIndex((prev) => {
|
|
241
|
+
let next = flatItems.length - 1;
|
|
242
|
+
while (next >= 0 && flatItems[next].disabled) next--;
|
|
243
|
+
return next >= 0 ? next : prev;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
217
247
|
case "Escape":
|
|
218
248
|
case "Tab":
|
|
219
249
|
handleClose();
|
|
@@ -248,6 +278,7 @@ const DropdownMenu = ({
|
|
|
248
278
|
{
|
|
249
279
|
item: entry,
|
|
250
280
|
baseClass,
|
|
281
|
+
itemId: `${generatedId}-item-${myIndex}`,
|
|
251
282
|
focused: isFocused,
|
|
252
283
|
size,
|
|
253
284
|
onActivate: handleClose
|
|
@@ -264,6 +295,7 @@ const DropdownMenu = ({
|
|
|
264
295
|
return /* @__PURE__ */ jsxs(
|
|
265
296
|
"li",
|
|
266
297
|
{
|
|
298
|
+
id: `${generatedId}-item-${myIndex}`,
|
|
267
299
|
className: itemClasses,
|
|
268
300
|
role: "menuitem",
|
|
269
301
|
"aria-disabled": entry.disabled || void 0,
|
|
@@ -295,6 +327,8 @@ const DropdownMenu = ({
|
|
|
295
327
|
/* @__PURE__ */ jsx("div", { className: `${baseClass}__trigger`, children: React.isValidElement(trigger) ? React.cloneElement(trigger, {
|
|
296
328
|
"aria-haspopup": "menu",
|
|
297
329
|
"aria-expanded": isOpen,
|
|
330
|
+
"aria-controls": isOpen ? menuId : void 0,
|
|
331
|
+
"aria-activedescendant": activeDescendantId,
|
|
298
332
|
onClick: handleToggle,
|
|
299
333
|
onKeyDown: handleKeyDown
|
|
300
334
|
}) : /* @__PURE__ */ jsx(
|
|
@@ -303,6 +337,8 @@ const DropdownMenu = ({
|
|
|
303
337
|
type: "button",
|
|
304
338
|
"aria-haspopup": "menu",
|
|
305
339
|
"aria-expanded": isOpen,
|
|
340
|
+
"aria-controls": isOpen ? menuId : void 0,
|
|
341
|
+
"aria-activedescendant": activeDescendantId,
|
|
306
342
|
onClick: handleToggle,
|
|
307
343
|
onKeyDown: handleKeyDown,
|
|
308
344
|
children: trigger
|
|
@@ -311,6 +347,7 @@ const DropdownMenu = ({
|
|
|
311
347
|
isOpen && /* @__PURE__ */ jsx(
|
|
312
348
|
"ul",
|
|
313
349
|
{
|
|
350
|
+
id: menuId,
|
|
314
351
|
className: panelClasses,
|
|
315
352
|
role: "menu",
|
|
316
353
|
ref: (el) => {
|
|
@@ -113,3 +113,12 @@
|
|
|
113
113
|
line-height: var(--font-paragraph-em-line-height);
|
|
114
114
|
letter-spacing: var(--font-paragraph-em-letter-spacing);
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
118
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
119
|
+
.material-symbols-rounded itself (a common global default) would
|
|
120
|
+
otherwise override inheritance and detach the icon from the state
|
|
121
|
+
colours this component sets on its parents. */
|
|
122
|
+
.ds-empty-state .material-symbols-rounded {
|
|
123
|
+
color: inherit;
|
|
124
|
+
}
|
|
@@ -270,3 +270,12 @@
|
|
|
270
270
|
.ds-file-input--compact .ds-file-input__file {
|
|
271
271
|
padding: var(--padding-xs) var(--padding-sm); /* 6px 8px */
|
|
272
272
|
}
|
|
273
|
+
|
|
274
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
275
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
276
|
+
.material-symbols-rounded itself (a common global default) would
|
|
277
|
+
otherwise override inheritance and detach the icon from the state
|
|
278
|
+
colours this component sets on its parents. */
|
|
279
|
+
.ds-file-input .material-symbols-rounded {
|
|
280
|
+
color: inherit;
|
|
281
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
GLOBE COMPONENT
|
|
3
|
+
An orthographic globe: graticule, markers and
|
|
4
|
+
great-circle arcs. Every colour is a token; the
|
|
5
|
+
stroke widths are SVG geometry in viewBox units,
|
|
6
|
+
like Sparkline's, and scale with the drawing.
|
|
7
|
+
============================================ */
|
|
8
|
+
|
|
9
|
+
/* Base */
|
|
10
|
+
|
|
11
|
+
.ds-globe {
|
|
12
|
+
position: relative;
|
|
13
|
+
display: block;
|
|
14
|
+
width: 100%;
|
|
15
|
+
aspect-ratio: 1;
|
|
16
|
+
color: var(--color-text-primary);
|
|
17
|
+
border-radius: var(--radius-md);
|
|
18
|
+
outline: none;
|
|
19
|
+
touch-action: none;
|
|
20
|
+
user-select: none;
|
|
21
|
+
-webkit-user-select: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ds-globe:focus-visible {
|
|
25
|
+
outline: var(--border-md) solid var(--color-action-primary-bg);
|
|
26
|
+
outline-offset: var(--padding-xxs);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ds-globe--interactive {
|
|
30
|
+
cursor: grab;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ds-globe--dragging {
|
|
34
|
+
cursor: grabbing;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.ds-globe__svg {
|
|
38
|
+
display: block;
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
41
|
+
overflow: visible;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Sphere */
|
|
45
|
+
|
|
46
|
+
.ds-globe__limb {
|
|
47
|
+
fill: none;
|
|
48
|
+
stroke: var(--color-text-tertiary);
|
|
49
|
+
stroke-width: 1;
|
|
50
|
+
opacity: 0.6;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ds-globe__graticule {
|
|
54
|
+
fill: none;
|
|
55
|
+
stroke: var(--color-divider);
|
|
56
|
+
stroke-width: 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ds-globe__graticule-back {
|
|
60
|
+
fill: none;
|
|
61
|
+
stroke: var(--color-divider);
|
|
62
|
+
stroke-width: 1;
|
|
63
|
+
stroke-dasharray: 2 4;
|
|
64
|
+
opacity: 0.5;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Arcs — the default gradient runs along the cobalt-to-violet end of the
|
|
68
|
+
chart series; a per-arc `color` replaces it with a flat stroke. */
|
|
69
|
+
|
|
70
|
+
.ds-globe__arc-stop-start {
|
|
71
|
+
stop-color: var(--color-chart-series-7);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ds-globe__arc-stop-end {
|
|
75
|
+
stop-color: var(--color-chart-series-5);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ds-globe__arc {
|
|
79
|
+
fill: none;
|
|
80
|
+
stroke-width: 1.5;
|
|
81
|
+
stroke-linecap: round;
|
|
82
|
+
stroke-linejoin: round;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ds-globe__arc-back {
|
|
86
|
+
fill: none;
|
|
87
|
+
stroke-width: 1;
|
|
88
|
+
stroke-linecap: round;
|
|
89
|
+
opacity: 0.15;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Markers */
|
|
93
|
+
|
|
94
|
+
.ds-globe__point {
|
|
95
|
+
color: var(--color-text-secondary);
|
|
96
|
+
transition: color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.ds-globe__point--back {
|
|
100
|
+
opacity: 0.3;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ds-globe__point--active,
|
|
104
|
+
.ds-globe__point:hover {
|
|
105
|
+
color: var(--color-text-primary);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.ds-globe__hit {
|
|
109
|
+
fill: transparent;
|
|
110
|
+
stroke: none;
|
|
111
|
+
pointer-events: all;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.ds-globe--interactive .ds-globe__point {
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ds-globe__glyph {
|
|
119
|
+
fill: none;
|
|
120
|
+
stroke: currentColor;
|
|
121
|
+
stroke-width: 1.5;
|
|
122
|
+
stroke-linecap: round;
|
|
123
|
+
transform-box: fill-box;
|
|
124
|
+
transform-origin: center;
|
|
125
|
+
transition: transform var(--motion-duration-fast) var(--motion-ease-emphasized);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ds-globe__point--active .ds-globe__glyph {
|
|
129
|
+
transform: scale(1.4);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ds-globe__label {
|
|
133
|
+
font-family: var(--font-family-code);
|
|
134
|
+
font-size: var(--font-caption-size);
|
|
135
|
+
fill: var(--color-text-tertiary);
|
|
136
|
+
pointer-events: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ds-globe__point--active .ds-globe__label {
|
|
140
|
+
fill: var(--color-text-primary);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Callout overlay — positioned at the marker, opening away from the centre. */
|
|
144
|
+
|
|
145
|
+
.ds-globe__callout {
|
|
146
|
+
position: absolute;
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
padding: 0 var(--padding-md);
|
|
149
|
+
transform: translate(0, -100%);
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.ds-globe__callout[data-side='left'] {
|
|
154
|
+
transform: translate(-100%, -100%);
|
|
155
|
+
}
|