@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.
Files changed (93) hide show
  1. package/README.md +7 -6
  2. package/components/AgentPlan/AgentPlan.css +14 -3
  3. package/components/AiButton/AiButton.css +1 -1
  4. package/components/Alert/Alert.css +9 -0
  5. package/components/AlertDialog/AlertDialog.css +3 -1
  6. package/components/AlertDialog/AlertDialog.js +2 -3
  7. package/components/AppSidebar/AppSidebar.css +9 -0
  8. package/components/Card/Card.css +7 -2
  9. package/components/CardStack/CardStack.css +106 -0
  10. package/components/CardStack/CardStack.d.ts +44 -0
  11. package/components/CardStack/CardStack.js +140 -0
  12. package/components/Carousel/Carousel.css +9 -0
  13. package/components/Carousel/Carousel.js +37 -4
  14. package/components/Chart/AreaChart.js +6 -5
  15. package/components/Chart/LineChart.js +4 -3
  16. package/components/Chart/PieChart.js +2 -15
  17. package/components/Chart/RadarChart.js +5 -4
  18. package/components/Chart/RadialChart.js +2 -15
  19. package/components/Chart/ScatterChart.js +3 -13
  20. package/components/Chart/StackedBarChart.js +2 -15
  21. package/components/Chart/Treemap.js +3 -17
  22. package/components/Chart/palette.d.ts +1 -0
  23. package/components/Chart/palette.js +19 -0
  24. package/components/ChatMarker/ChatMarker.css +9 -0
  25. package/components/ChatMessage/ChatMessage.css +16 -0
  26. package/components/ChatThread/ChatThread.js +2 -2
  27. package/components/Chip/Chip.css +9 -0
  28. package/components/CodeBlock/CodeBlock.css +10 -1
  29. package/components/CodeBlock/CodeBlock.js +2 -1
  30. package/components/ColorPicker/ColorPicker.js +10 -0
  31. package/components/Combobox/Combobox.css +9 -0
  32. package/components/CommandPalette/CommandPalette.css +9 -0
  33. package/components/CommandPalette/CommandPalette.js +31 -5
  34. package/components/Composer/Composer.css +118 -0
  35. package/components/Composer/Composer.d.ts +7 -0
  36. package/components/Composer/Composer.js +7 -1
  37. package/components/ContactCard/ContactCard.css +9 -0
  38. package/components/ContextMenu/ContextMenu.js +39 -3
  39. package/components/DatePicker/DatePicker.js +16 -0
  40. package/components/Dialog/Dialog.css +3 -1
  41. package/components/Dialog/Dialog.js +5 -4
  42. package/components/DocumentChip/DocumentChip.css +9 -0
  43. package/components/Dropdown/Dropdown.js +22 -3
  44. package/components/DropdownMenu/DropdownMenu.js +43 -6
  45. package/components/EmptyState/EmptyState.css +9 -0
  46. package/components/FileInput/FileInput.css +9 -0
  47. package/components/Globe/Globe.css +155 -0
  48. package/components/Globe/Globe.d.ts +96 -0
  49. package/components/Globe/Globe.js +415 -0
  50. package/components/InterruptCard/InterruptCard.css +27 -17
  51. package/components/InterruptCard/InterruptCard.d.ts +1 -1
  52. package/components/InterruptCard/InterruptCard.js +1 -1
  53. package/components/MapCallout/MapCallout.css +46 -0
  54. package/components/MapCallout/MapCallout.d.ts +29 -0
  55. package/components/MapCallout/MapCallout.js +17 -0
  56. package/components/MapLegend/MapLegend.css +88 -0
  57. package/components/MapLegend/MapLegend.d.ts +39 -0
  58. package/components/MapLegend/MapLegend.js +68 -0
  59. package/components/MessageActions/MessageActions.css +9 -0
  60. package/components/MessageCard/MessageCard.css +68 -16
  61. package/components/MessageCard/MessageCard.d.ts +1 -1
  62. package/components/MessageCard/MessageCard.js +6 -4
  63. package/components/ModelPicker/ModelPicker.css +4 -1
  64. package/components/ModelPicker/ModelPicker.js +43 -1
  65. package/components/NotificationCenter/NotificationCenter.css +9 -0
  66. package/components/NotificationCenter/NotificationCenter.js +26 -10
  67. package/components/NumberInput/NumberInput.css +9 -0
  68. package/components/Popover/Popover.css +3 -1
  69. package/components/Popover/Popover.js +2 -1
  70. package/components/SourceChip/SourceChip.css +9 -0
  71. package/components/Stat/Stat.css +9 -0
  72. package/components/Stepper/Stepper.css +9 -0
  73. package/components/TagInput/TagInput.css +9 -0
  74. package/components/TimePicker/TimePicker.js +4 -1
  75. package/components/Toast/Toast.css +32 -11
  76. package/components/Toast/Toast.js +5 -4
  77. package/components/ToggleGroup/ToggleGroup.css +9 -0
  78. package/components/ToolCall/ToolCall.css +18 -6
  79. package/components/Tooltip/Tooltip.js +20 -16
  80. package/components/registry.json +37 -0
  81. package/components/registry.json.d.ts +37 -0
  82. package/components/registry.json.js +2 -2
  83. package/index.d.ts +4 -0
  84. package/index.js +8 -0
  85. package/package.json +5 -1
  86. package/tokens/motion.d.ts +23 -0
  87. package/tokens/motion.js +14 -0
  88. package/tokens/registry.json +17 -0
  89. package/tokens/registry.json.d.ts +17 -0
  90. package/tokens/registry.json.js +1 -1
  91. package/tokens/tokens-dark.css +11 -0
  92. package/tokens/tokens-light.css +11 -0
  93. 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 = [baseClass, disabled ? `${baseClass}--disabled` : "", className].filter(Boolean).join(" ");
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), 150);
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("button, [href], input, select, textarea") ?? panel.querySelector('[tabindex]:not([tabindex="-1"])');
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 = listRef.current.children[focusedIndex];
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), 150);
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
+ }