rift-ds 1.18.0 → 1.19.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 (50) hide show
  1. package/behaviors/useAnchoredPosition.d.ts +3 -1
  2. package/behaviors/useAnchoredPosition.js +1 -0
  3. package/behaviors/useTopLayer.d.ts +35 -0
  4. package/behaviors/useTopLayer.js +22 -0
  5. package/chat/ChatLauncher.js +23 -0
  6. package/chat/ChatWidget.css +8 -3
  7. package/components/AiButton/AiButton.css +11 -0
  8. package/components/AiButton/AiButton.d.ts +2 -0
  9. package/components/AiButton/AiButton.js +3 -1
  10. package/components/AppSidebar/AppSidebar.css +59 -9
  11. package/components/AppSidebar/AppSidebar.d.ts +2 -2
  12. package/components/AppSidebar/AppSidebar.js +15 -9
  13. package/components/Chart/BarChart.js +1 -0
  14. package/components/Chart/Chart.css +6 -0
  15. package/components/Chart/ComboChart.js +1 -0
  16. package/components/Chart/StackedBarChart.js +1 -0
  17. package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +13 -1
  18. package/components/Composer/Composer.css +72 -0
  19. package/components/Composer/Composer.d.ts +5 -1
  20. package/components/Composer/Composer.js +8 -1
  21. package/components/DataTable/DataTable.css +7 -3
  22. package/components/Popover/Popover.d.ts +2 -0
  23. package/components/Popover/Popover.js +2 -1
  24. package/components/SegmentedControl/SegmentedControl.css +3 -1
  25. package/components/SegmentedControl/SegmentedControl.js +2 -0
  26. package/components/SidebarPanel/SidebarPanel.css +10 -2
  27. package/components/SidebarPanel/SidebarPanel.d.ts +1 -1
  28. package/components/SidebarPanel/SidebarPanel.js +5 -2
  29. package/components/SiteHeader/SiteHeader.css +63 -5
  30. package/components/SiteHeader/SiteHeader.d.ts +11 -3
  31. package/components/SiteHeader/SiteHeader.js +51 -16
  32. package/components/SourceChip/SourceChip.css +5 -1
  33. package/components/SourceChip/SourceChip.js +3 -0
  34. package/components/TableToolbar/TableToolbar.css +130 -1
  35. package/components/TableToolbar/TableToolbar.d.ts +52 -0
  36. package/components/TableToolbar/TableToolbar.js +232 -32
  37. package/components/ThemeToggle/ThemeToggle.css +26 -0
  38. package/components/ThemeToggle/ThemeToggle.d.ts +11 -1
  39. package/components/ThemeToggle/ThemeToggle.js +36 -5
  40. package/components/Toast/Toast.css +4 -0
  41. package/components/Toast/Toast.d.ts +9 -0
  42. package/components/Toast/Toast.js +9 -2
  43. package/components/Tooltip/Tooltip.css +9 -4
  44. package/components/Tooltip/Tooltip.d.ts +4 -2
  45. package/components/Tooltip/Tooltip.js +16 -4
  46. package/index.js +2 -1
  47. package/package.json +1 -1
  48. package/tokens/motion.d.ts +2 -0
  49. package/tokens/motion.js +2 -0
  50. package/tokens/tokens-light.css +4 -3
@@ -1,9 +1,11 @@
1
1
  "use client";
2
2
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
- import React, { useState, useEffect, useRef, useLayoutEffect, useId, useSyncExternalStore } from "react";
3
+ import React, { useState, useEffect, useRef, useLayoutEffect, useId, useSyncExternalStore, useCallback } from "react";
4
4
  import { createPortal } from "react-dom";
5
+ import { AiButton } from "../AiButton/AiButton.js";
5
6
  import { Button } from "../Button/Button.js";
6
7
  import { Chip } from "../Chip/Chip.js";
8
+ import { Composer } from "../Composer/Composer.js";
7
9
  import { Dropdown } from "../Dropdown/Dropdown.js";
8
10
  import { CircularButton } from "../CircularButton/CircularButton.js";
9
11
  import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
@@ -13,14 +15,17 @@ import { Popover } from "../Popover/Popover.js";
13
15
  import { SegmentedControl } from "../SegmentedControl/SegmentedControl.js";
14
16
  import { SplitButton } from "../SplitButton/SplitButton.js";
15
17
  import { Tabs } from "../Tabs/Tabs.js";
18
+ import { useOptionalToast, ToastProvider, useToast } from "../Toast/Toast.js";
16
19
  import { ToggleSwitch } from "../ToggleSwitch/ToggleSwitch.js";
20
+ import { Tooltip } from "../Tooltip/Tooltip.js";
17
21
  import { FilterEditor } from "../DataTable/FilterEditor.js";
18
22
  import { startDrag } from "../DataTable/dragGesture.js";
19
23
  import { parseDate, absoluteDate } from "../DataTable/columnTypes.js";
20
24
  import "../../behaviors/visually-hidden.css";
21
25
  import { DEFAULT_FILTER_LABELS, isComplete, VALUELESS, OPERATORS, valueText, newFilterId } from "../DataTable/filterModel.js";
22
26
  import { useTableContext } from "../DataTable/tableContext.js";
23
- import { BREAKPOINT_PHONE_PX, mediaBelow } from "../../tokens/breakpoints.js";
27
+ import { MOTION_ACTION_DISMISS_MS } from "../../tokens/motion.js";
28
+ import { BREAKPOINT_PHONE_PX, BREAKPOINT_SHEET_PX, mediaBelow } from "../../tokens/breakpoints.js";
24
29
  import "./TableToolbar.css";
25
30
  import { mirrorClass } from "../../behaviors/direction.js";
26
31
  const DEFAULT_LABELS = {
@@ -67,6 +72,14 @@ const DEFAULT_LABELS = {
67
72
  movedSort: (column, rank, count) => `${column}, sort ${rank} of ${count}`,
68
73
  selected: (count) => `${count} selected`,
69
74
  allSelected: (count) => `All ${count} selected`,
75
+ command: "Describe",
76
+ commandClose: "Close",
77
+ commandPlaceholder: "Tell the table what to show",
78
+ commandSend: "Apply",
79
+ commandApplied: "The table has been updated.",
80
+ commandFailed: "That did not work. The table has not changed.",
81
+ commandUndo: "Undo",
82
+ commandDismiss: "Dismiss",
70
83
  moreBulkActions: "More actions",
71
84
  selectAll: (count) => `Select all ${count}`,
72
85
  clearSelection: "Clear selection",
@@ -127,6 +140,134 @@ function ToolbarPanel({
127
140
  )
128
141
  ] });
129
142
  }
143
+ function CommandRow({ command, table, labels, size, open, onClose, state, onStateChange }) {
144
+ const { toast, dismiss } = useToast();
145
+ const [text, setText] = useState("");
146
+ const row = useRef(null);
147
+ const lastToast = useRef(null);
148
+ const controller = useRef(null);
149
+ const latestTable = useRef(table);
150
+ useEffect(() => {
151
+ latestTable.current = table;
152
+ }, [table]);
153
+ useEffect(() => {
154
+ if (open) row.current?.querySelector("textarea")?.focus();
155
+ }, [open]);
156
+ useEffect(() => () => controller.current?.abort(), []);
157
+ const report = useCallback(
158
+ (data) => {
159
+ lastToast.current = toast(data);
160
+ },
161
+ [toast]
162
+ );
163
+ const run = useCallback(
164
+ async (typed) => {
165
+ const trimmed = typed.trim();
166
+ if (!trimmed || state === "working") return;
167
+ const before = JSON.parse(JSON.stringify(table.view));
168
+ onClose();
169
+ setText("");
170
+ onStateChange("working");
171
+ if (lastToast.current) dismiss(lastToast.current);
172
+ lastToast.current = null;
173
+ const running = new AbortController();
174
+ controller.current = running;
175
+ try {
176
+ const result = await command.onSubmit({
177
+ text: trimmed,
178
+ view: before,
179
+ columns: table.columns,
180
+ signal: running.signal
181
+ });
182
+ if (running.signal.aborted) return;
183
+ const changed = Boolean(result.view) && JSON.stringify(result.view) !== JSON.stringify(before);
184
+ if (changed) {
185
+ latestTable.current.applyView(result.view);
186
+ const id = `table-command-${Date.now()}`;
187
+ report({
188
+ id,
189
+ variant: "positive",
190
+ description: result.message || labels.commandApplied,
191
+ duration: MOTION_ACTION_DISMISS_MS,
192
+ action: /* @__PURE__ */ jsx(
193
+ Button,
194
+ {
195
+ variant: "secondary",
196
+ tone: "neutral",
197
+ size: "compact",
198
+ iconLeft: "refresh",
199
+ label: labels.commandUndo,
200
+ onClick: () => {
201
+ latestTable.current.applyView(before);
202
+ dismiss(id);
203
+ }
204
+ }
205
+ )
206
+ });
207
+ } else if (result.message) {
208
+ report({ variant: "info", description: result.message });
209
+ }
210
+ onStateChange("done");
211
+ } catch (error) {
212
+ if (running.signal.aborted) return;
213
+ setText(trimmed);
214
+ report({
215
+ variant: "error",
216
+ description: error instanceof Error && error.message ? error.message : labels.commandFailed,
217
+ duration: 0
218
+ });
219
+ onStateChange("error");
220
+ }
221
+ },
222
+ [command, table, state, onClose, onStateChange, labels.commandApplied, labels.commandFailed, labels.commandUndo, report, dismiss]
223
+ );
224
+ return /* @__PURE__ */ jsxs(
225
+ "div",
226
+ {
227
+ className: "ds-table-toolbar__command-row",
228
+ "data-open": open || void 0,
229
+ inert: !open || void 0,
230
+ ref: row,
231
+ onKeyDown: (event) => {
232
+ if (event.key !== "Escape") return;
233
+ event.preventDefault();
234
+ onClose();
235
+ },
236
+ children: [
237
+ /* @__PURE__ */ jsx(
238
+ CircularButton,
239
+ {
240
+ className: "ds-table-toolbar__command-close",
241
+ icon: "close",
242
+ variant: "tertiary",
243
+ size,
244
+ ariaLabel: labels.commandClose,
245
+ tooltip: false,
246
+ onClick: onClose
247
+ }
248
+ ),
249
+ /* @__PURE__ */ jsx(
250
+ Composer,
251
+ {
252
+ inline: true,
253
+ size,
254
+ value: text,
255
+ onValueChange: setText,
256
+ onSubmit: run,
257
+ placeholder: labels.commandPlaceholder,
258
+ sendLabel: labels.commandSend,
259
+ attachButton: false
260
+ }
261
+ )
262
+ ]
263
+ }
264
+ );
265
+ }
266
+ function ToolbarCommand(props) {
267
+ const host = useOptionalToast();
268
+ if (host) return /* @__PURE__ */ jsx(CommandRow, { ...props });
269
+ return /* @__PURE__ */ jsx(ToastProvider, { position: "bottom-right", dismissLabel: props.labels.commandDismiss, children: /* @__PURE__ */ jsx(CommandRow, { ...props }) });
270
+ }
130
271
  function useBound(value, defaultValue, onChange, table) {
131
272
  const [own, setOwn] = useState(defaultValue);
132
273
  const current = value !== void 0 ? value : table ? table.value : own;
@@ -181,6 +322,7 @@ const TableToolbar = React.forwardRef(
181
322
  primaryAction,
182
323
  secondaryActions = [],
183
324
  actions,
325
+ command,
184
326
  labels: labelsProp,
185
327
  filterLabels: filterLabelsProp,
186
328
  className = "",
@@ -366,10 +508,14 @@ const TableToolbar = React.forwardRef(
366
508
  const activeFilters = filters.filter(isComplete).length;
367
509
  const rootRef = useRef(null);
368
510
  const [narrow, setNarrow] = useState(false);
511
+ const [crowded, setCrowded] = useState(false);
369
512
  useEffect(() => {
370
513
  const node = rootRef.current;
371
514
  if (!node) return;
372
- const observer = new ResizeObserver(([entry]) => setNarrow(entry.contentRect.width < BREAKPOINT_PHONE_PX));
515
+ const observer = new ResizeObserver(([entry]) => {
516
+ setNarrow(entry.contentRect.width < BREAKPOINT_PHONE_PX);
517
+ setCrowded(entry.contentRect.width < BREAKPOINT_SHEET_PX);
518
+ });
373
519
  observer.observe(node);
374
520
  return () => observer.disconnect();
375
521
  }, []);
@@ -459,6 +605,29 @@ const TableToolbar = React.forwardRef(
459
605
  const bulkMode = selectedCount > 0;
460
606
  const phoneScreen = useSyncExternalStore(subscribePhone, isPhone, () => false);
461
607
  const floating = phoneScreen && bulkMode;
608
+ const [commandOpen, setCommandOpen] = useState(false);
609
+ const [commandState, setCommandState] = useState("idle");
610
+ const commandButton = useRef(null);
611
+ const commandShown = commandOpen && Boolean(command && table) && !(bulkMode && !floating);
612
+ const closeCommand = () => {
613
+ setCommandOpen(false);
614
+ requestAnimationFrame(() => commandButton.current?.focus());
615
+ };
616
+ const covered = commandShown || bulkMode && !floating;
617
+ const commandButtonDrawn = /* @__PURE__ */ jsx(
618
+ AiButton,
619
+ {
620
+ ref: commandButton,
621
+ className: "ds-table-toolbar__command",
622
+ iconOnly: narrow,
623
+ size,
624
+ label: labels.command,
625
+ state: commandState,
626
+ "aria-expanded": commandShown,
627
+ onClick: () => setCommandOpen(true)
628
+ }
629
+ );
630
+ const commandTrigger = narrow ? /* @__PURE__ */ jsx(Tooltip, { content: labels.command, children: commandButtonDrawn }) : commandButtonDrawn;
462
631
  const matching = table?.matchingIds ?? [];
463
632
  const matchingCount = table?.matchingCount ?? matching.length;
464
633
  const allMatching = table?.allMatching ?? (matching.length > 0 && matching.every((id) => table.selectedIds.includes(id)));
@@ -1070,7 +1239,30 @@ const TableToolbar = React.forwardRef(
1070
1239
  }
1071
1240
  )
1072
1241
  ] });
1073
- const groupControl = groupable && groupableColumns.length > 0 && /* @__PURE__ */ jsxs("span", { className: "ds-table-toolbar__badge-host", children: [
1242
+ const groupOffered = groupable && groupableColumns.length > 0;
1243
+ const groupPanel = /* @__PURE__ */ jsxs("div", { className: "ds-table-toolbar__menu", children: [
1244
+ /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-heading", "aria-hidden": "true", children: labels.groupBy }),
1245
+ /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-row", children: /* @__PURE__ */ jsx(
1246
+ Dropdown,
1247
+ {
1248
+ className: "ds-table-toolbar__menu-field",
1249
+ size,
1250
+ "aria-label": labels.groupBy,
1251
+ options: [
1252
+ { value: "", label: labels.noGrouping },
1253
+ ...groupableColumns.map((col) => ({ value: col.key, label: col.label }))
1254
+ ],
1255
+ value: groupedBy ?? "",
1256
+ onValueChange: (next) => setGrouping(next ? [String(next)] : [])
1257
+ }
1258
+ ) }),
1259
+ table && groupedBy && menuSeparator,
1260
+ table && groupedBy && /* @__PURE__ */ jsxs("div", { className: "ds-table-toolbar__menu-actions", children: [
1261
+ menuItem(labels.collapseAll, "unfold_less", () => table.setAllGroupsOpen(false)),
1262
+ menuItem(labels.expandAll, "unfold_more", () => table.setAllGroupsOpen(true))
1263
+ ] })
1264
+ ] });
1265
+ const groupControl = groupOffered && /* @__PURE__ */ jsxs("span", { className: "ds-table-toolbar__badge-host", children: [
1074
1266
  /* @__PURE__ */ jsx(
1075
1267
  ToolbarPanel,
1076
1268
  {
@@ -1079,28 +1271,7 @@ const TableToolbar = React.forwardRef(
1079
1271
  size,
1080
1272
  ariaLabel: labels.group(Boolean(groupedBy)),
1081
1273
  position: "bottom",
1082
- content: /* @__PURE__ */ jsxs("div", { className: "ds-table-toolbar__menu", children: [
1083
- /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-heading", "aria-hidden": "true", children: labels.groupBy }),
1084
- /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-row", children: /* @__PURE__ */ jsx(
1085
- Dropdown,
1086
- {
1087
- className: "ds-table-toolbar__menu-field",
1088
- size,
1089
- "aria-label": labels.groupBy,
1090
- options: [
1091
- { value: "", label: labels.noGrouping },
1092
- ...groupableColumns.map((col) => ({ value: col.key, label: col.label }))
1093
- ],
1094
- value: groupedBy ?? "",
1095
- onValueChange: (next) => setGrouping(next ? [String(next)] : [])
1096
- }
1097
- ) }),
1098
- table && groupedBy && menuSeparator,
1099
- table && groupedBy && /* @__PURE__ */ jsxs("div", { className: "ds-table-toolbar__menu-actions", children: [
1100
- menuItem(labels.collapseAll, "unfold_less", () => table.setAllGroupsOpen(false)),
1101
- menuItem(labels.expandAll, "unfold_more", () => table.setAllGroupsOpen(true))
1102
- ] })
1103
- ] }),
1274
+ content: groupPanel,
1104
1275
  children: /* @__PURE__ */ jsx(
1105
1276
  CircularButton,
1106
1277
  {
@@ -1144,7 +1315,9 @@ const TableToolbar = React.forwardRef(
1144
1315
  );
1145
1316
  const foldedActions = narrow ? [...secondaryActions, ...primaryAction?.menu ?? []] : [];
1146
1317
  const foldedDensity = narrow ? density : false;
1147
- const moreControl = (foldedDensity || foldedActions.length > 0) && /* @__PURE__ */ jsx(
1318
+ const foldedGroup = crowded && groupOffered;
1319
+ const foldedColumns = crowded ? columnsList : false;
1320
+ const morePanel = /* @__PURE__ */ jsx(
1148
1321
  ToolbarPanel,
1149
1322
  {
1150
1323
  sheet: phoneScreen,
@@ -1157,10 +1330,19 @@ const TableToolbar = React.forwardRef(
1157
1330
  /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-heading", "aria-hidden": "true", children: labels.actionsSection }),
1158
1331
  /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-actions", children: foldedActions.map((action) => /* @__PURE__ */ jsx("span", { children: menuItem(action.label, action.icon, action.onSelect, action.disabled) }, action.id)) })
1159
1332
  ] }),
1160
- foldedActions.length > 0 && foldedDensity && menuSeparator,
1333
+ foldedActions.length > 0 && (foldedGroup || foldedDensity) && menuSeparator,
1334
+ foldedGroup && groupPanel,
1335
+ foldedGroup && foldedDensity && menuSeparator,
1161
1336
  foldedDensity && /* @__PURE__ */ jsxs(Fragment, { children: [
1162
1337
  /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-heading", "aria-hidden": "true", children: labels.density }),
1163
1338
  /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-row", children: foldedDensity })
1339
+ ] }),
1340
+ foldedColumns && (foldedActions.length > 0 || foldedGroup || foldedDensity) && menuSeparator,
1341
+ foldedColumns && /* @__PURE__ */ jsxs(Fragment, { children: [
1342
+ /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-heading", "aria-hidden": "true", children: labels.columns }),
1343
+ foldedColumns,
1344
+ menuSeparator,
1345
+ /* @__PURE__ */ jsx("div", { className: "ds-table-toolbar__menu-actions", children: showAllColumns })
1164
1346
  ] })
1165
1347
  ] }),
1166
1348
  children: /* @__PURE__ */ jsx(
@@ -1176,6 +1358,10 @@ const TableToolbar = React.forwardRef(
1176
1358
  )
1177
1359
  }
1178
1360
  );
1361
+ const moreControl = (foldedDensity || foldedActions.length > 0 || foldedGroup || foldedColumns) && /* @__PURE__ */ jsxs("span", { className: "ds-table-toolbar__badge-host", children: [
1362
+ morePanel,
1363
+ foldedGroup && groupedBy && /* @__PURE__ */ jsx("span", { className: "ds-table-toolbar__badge", "aria-hidden": "true", children: "1" })
1364
+ ] });
1179
1365
  const showFilters = filterable && filterableColumns.length > 0;
1180
1366
  return /* @__PURE__ */ jsxs(
1181
1367
  "div",
@@ -1203,14 +1389,28 @@ const TableToolbar = React.forwardRef(
1203
1389
  }
1204
1390
  )
1205
1391
  ] }),
1392
+ command && table && /* @__PURE__ */ jsx(
1393
+ ToolbarCommand,
1394
+ {
1395
+ command,
1396
+ table,
1397
+ labels,
1398
+ size,
1399
+ open: commandShown,
1400
+ onClose: closeCommand,
1401
+ state: commandState,
1402
+ onStateChange: setCommandState
1403
+ }
1404
+ ),
1206
1405
  /* @__PURE__ */ jsxs(
1207
1406
  "div",
1208
1407
  {
1209
1408
  className: "ds-table-toolbar__main",
1210
- inert: bulkMode && !floating || void 0,
1211
- "data-covered": bulkMode && !floating || void 0,
1409
+ inert: covered || void 0,
1410
+ "data-covered": commandShown ? "command" : covered ? "bulk" : void 0,
1212
1411
  children: [
1213
1412
  /* @__PURE__ */ jsxs("div", { className: "ds-table-toolbar__start", ref: startRef, children: [
1413
+ command && table && commandTrigger,
1214
1414
  searchable && !narrow && searchField,
1215
1415
  showFilters && !narrow && /* @__PURE__ */ jsx("span", { className: "ds-table-toolbar__filter-button", children: addFilterMenu(filterIcon) }),
1216
1416
  showFilters && !narrow && chips,
@@ -1268,8 +1468,8 @@ const TableToolbar = React.forwardRef(
1268
1468
  ),
1269
1469
  showFilters && narrow && filterPanel,
1270
1470
  sortControl,
1271
- groupControl,
1272
- columnsControl,
1471
+ !crowded && groupControl,
1472
+ !crowded && columnsControl,
1273
1473
  !narrow && densityIcons,
1274
1474
  moreControl,
1275
1475
  (primaryAction || !narrow && secondaryActions.length > 0) && /* @__PURE__ */ jsxs("span", { className: "ds-table-toolbar__actions", children: [
@@ -10,3 +10,29 @@
10
10
  .ds-theme-toggle {
11
11
  flex-shrink: 0;
12
12
  }
13
+
14
+ /* The cycle form: one circle in the track recipe the strip, the
15
+ CommandPaletteTrigger and the ThemePicker's trigger wear (container fill,
16
+ hairline border, the track's curve), so it reads as one of that family in
17
+ a row; it hovers like the picker's trigger, the fill stepping up. */
18
+ .ds-theme-toggle--cycle {
19
+ display: inline-flex;
20
+ }
21
+
22
+ .ds-theme-toggle--cycle .ds-circular-button {
23
+ background-color: var(--color-bg-container-primary);
24
+ border: var(--border-025) solid var(--color-bg-container-border);
25
+ border-radius: calc(var(--radius-pill) + var(--padding-100));
26
+ }
27
+
28
+ .ds-theme-toggle--cycle .ds-circular-button:not(.ds-circular-button--disabled):hover {
29
+ background-color: var(--color-bg-container-secondary);
30
+ }
31
+
32
+ /* The glyph holds the icon colour in every state, as the family's glyphs
33
+ do: only the fill steps on hover. */
34
+ .ds-theme-toggle--cycle .ds-circular-button .ds-circular-button__icon,
35
+ .ds-theme-toggle--cycle .ds-circular-button:not(.ds-circular-button--disabled):hover .ds-circular-button__icon,
36
+ .ds-theme-toggle--cycle .ds-circular-button:not(.ds-circular-button--disabled):active .ds-circular-button__icon {
37
+ color: var(--color-action-icon-default);
38
+ }
@@ -17,6 +17,15 @@ type ThemeToggleOwnProps = {
17
17
  size?: 'default' | 'compact';
18
18
  /** Show each segment's label beside its icon. Set `false` for an icon-only strip: each label becomes the segment's tooltip on hover and keyboard focus, and stays its accessible name. */
19
19
  showLabels?: boolean;
20
+ /**
21
+ * Draw one circle that shows the current setting and steps to the next on
22
+ * each press (System, Light, Dark), in place of the strip, for a row short
23
+ * of room, such as a condensed SiteHeader's `condensedActions`. Each label
24
+ * becomes the circle's tooltip and its accessible name comes from `cycleLabel`.
25
+ */
26
+ cycle?: boolean;
27
+ /** The cycle circle's accessible name, from the current setting's label and the next one's. Default: the control's `aria-label`, then both, as in "Colour mode: Light. Switch to Dark". */
28
+ cycleLabel?: (current: string, next: string) => string;
20
29
  /** The localStorage key the choice is kept under. Match the key given to `colorModeScript`. Default `theme`. */
21
30
  storageKey?: string;
22
31
  /** Label of the setting that follows the operating system. */
@@ -36,7 +45,8 @@ export interface ThemeToggleProps extends ThemeToggleOwnProps, Omit<React.Compon
36
45
  * choice survives a reload. Pair it with `colorModeScript` in the document
37
46
  * head, which applies the stored choice before first paint.
38
47
  *
39
- * Forwards a ref to the strip and spreads unrecognised props onto it.
48
+ * Forwards a ref to the strip (in the `cycle` form, the circle's wrapper)
49
+ * and spreads unrecognised props onto it.
40
50
  */
41
51
  export declare const ThemeToggle: React.ForwardRefExoticComponent<ThemeToggleProps & React.RefAttributes<HTMLDivElement>>;
42
52
  export {};
@@ -2,6 +2,7 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import React, { useEffect } from "react";
4
4
  import { SegmentedControl } from "../SegmentedControl/SegmentedControl.js";
5
+ import { CircularButton } from "../CircularButton/CircularButton.js";
5
6
  import { DEFAULT_COLOR_MODE_STORAGE_KEY } from "./colorMode.js";
6
7
  import { useColorMode } from "./useColorMode.js";
7
8
  import "./ThemeToggle.css";
@@ -12,12 +13,14 @@ const ThemeToggle = React.forwardRef(
12
13
  onValueChange,
13
14
  size = "default",
14
15
  showLabels = true,
16
+ cycle = false,
15
17
  storageKey,
16
18
  systemLabel = "System",
17
19
  lightLabel = "Light",
18
20
  darkLabel = "Dark",
19
21
  className = "",
20
22
  "aria-label": ariaLabel = "Colour mode",
23
+ cycleLabel = (current, next) => `${ariaLabel}: ${current}. Switch to ${next}`,
21
24
  ...rest
22
25
  }, ref) => {
23
26
  const [storedSetting, setSetting] = useColorMode({ storageKey });
@@ -36,6 +39,38 @@ const ThemeToggle = React.forwardRef(
36
39
  { value: "light", label: lightLabel, icon: "light_mode" },
37
40
  { value: "dark", label: darkLabel, icon: "dark_mode" }
38
41
  ];
42
+ const pick = (next) => {
43
+ if (value === void 0) setSetting(next);
44
+ onValueChange?.(next);
45
+ };
46
+ if (cycle) {
47
+ const index = Math.max(
48
+ segments.findIndex((segment) => segment.value === setting),
49
+ 0
50
+ );
51
+ const current = segments[index];
52
+ const next = segments[(index + 1) % segments.length];
53
+ return /* @__PURE__ */ jsx(
54
+ "div",
55
+ {
56
+ ...rest,
57
+ ref,
58
+ className: ["ds-theme-toggle", "ds-theme-toggle--cycle", className].filter(Boolean).join(" "),
59
+ children: /* @__PURE__ */ jsx(
60
+ CircularButton,
61
+ {
62
+ icon: current.icon,
63
+ size,
64
+ variant: "secondary",
65
+ tone: "neutral",
66
+ ariaLabel: cycleLabel(current.label, next.label),
67
+ tooltip: current.label,
68
+ onClick: () => pick(next.value)
69
+ }
70
+ )
71
+ }
72
+ );
73
+ }
39
74
  return /* @__PURE__ */ jsx(
40
75
  SegmentedControl,
41
76
  {
@@ -45,11 +80,7 @@ const ThemeToggle = React.forwardRef(
45
80
  className: ["ds-theme-toggle", !showLabels && "ds-theme-toggle--icons", className].filter(Boolean).join(" "),
46
81
  segments,
47
82
  value: setting,
48
- onValueChange: (picked) => {
49
- const next = picked;
50
- if (value === void 0) setSetting(next);
51
- onValueChange?.(next);
52
- },
83
+ onValueChange: (picked) => pick(picked),
53
84
  size,
54
85
  showLabels
55
86
  }
@@ -361,3 +361,7 @@
361
361
  .ds-toast .material-symbols-rounded {
362
362
  color: inherit;
363
363
  }
364
+
365
+ .ds-toast__action {
366
+ margin-block-start: var(--gap-200);
367
+ }
@@ -14,6 +14,8 @@ export interface ToastData {
14
14
  dismissible?: boolean;
15
15
  /** Custom icon override — Material Symbol name (string) or custom element (ReactNode) */
16
16
  icon?: string | React.ReactNode;
17
+ /** A control under the text, such as an Undo. One whose control must not leave on a timer sets `duration` to 0. */
18
+ action?: React.ReactNode;
17
19
  }
18
20
  export interface ToastProviderProps {
19
21
  /** Children that can access the toast context */
@@ -40,6 +42,11 @@ export interface ToastContextValue {
40
42
  * Must be used within a ToastProvider.
41
43
  */
42
44
  export declare const useToast: () => ToastContextValue;
45
+ /**
46
+ * The toast API when a ToastProvider surrounds the caller, else null: for a
47
+ * component that joins the host's stack if there is one and brings its own if not.
48
+ */
49
+ export declare const useOptionalToast: () => ToastContextValue | null;
43
50
  /**
44
51
  * ToastProvider wraps your application and provides the
45
52
  * toast context. Renders a fixed container that stacks
@@ -61,6 +68,8 @@ type ToastOwnProps = {
61
68
  dismissible?: boolean;
62
69
  /** Custom icon override — Material Symbol name (string) or custom element (ReactNode) */
63
70
  icon?: string | React.ReactNode;
71
+ /** A control under the text, such as an Undo. */
72
+ action?: React.ReactNode;
64
73
  /** Fires when the dismiss button is pressed. The button renders only when this is given (and `dismissible` is not false); the host owns removing the toast */
65
74
  onDismiss?: () => void;
66
75
  /** Accessible name of the dismiss button */
@@ -16,6 +16,7 @@ const useToast = () => {
16
16
  }
17
17
  return context;
18
18
  };
19
+ const useOptionalToast = () => useContext(ToastContext);
19
20
  const defaultIcons = {
20
21
  info: "info",
21
22
  positive: "check_circle",
@@ -31,6 +32,7 @@ const ToastItem = ({
31
32
  variant,
32
33
  dismissible,
33
34
  icon,
35
+ action,
34
36
  duration,
35
37
  dismissLabel,
36
38
  onDismiss
@@ -81,7 +83,8 @@ const ToastItem = ({
81
83
  ),
82
84
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__content`, children: [
83
85
  title && /* @__PURE__ */ jsx("p", { className: `${baseClass}__title`, children: title }),
84
- description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, children: description })
86
+ description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, children: description }),
87
+ action && /* @__PURE__ */ jsx("div", { className: `${baseClass}__action`, children: action })
85
88
  ] }),
86
89
  dismissible && /* @__PURE__ */ jsx(
87
90
  "button",
@@ -164,6 +167,7 @@ const ToastProvider = ({
164
167
  variant: t.variant || "info",
165
168
  dismissible: t.dismissible ?? true,
166
169
  icon: t.icon,
170
+ action: t.action,
167
171
  duration: t.duration ?? MOTION_AUTO_DISMISS_MS,
168
172
  dismissLabel,
169
173
  onDismiss: dismiss
@@ -183,6 +187,7 @@ const Toast = React.forwardRef(
183
187
  variant = "info",
184
188
  dismissible = true,
185
189
  icon,
190
+ action,
186
191
  onDismiss,
187
192
  dismissLabel = "Dismiss notification",
188
193
  className = "",
@@ -202,7 +207,8 @@ const Toast = React.forwardRef(
202
207
  ),
203
208
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__content`, children: [
204
209
  title && /* @__PURE__ */ jsx("p", { className: `${baseClass}__title`, children: title }),
205
- description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, children: description })
210
+ description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, children: description }),
211
+ action && /* @__PURE__ */ jsx("div", { className: `${baseClass}__action`, children: action })
206
212
  ] }),
207
213
  dismissible && onDismiss && /* @__PURE__ */ jsx(
208
214
  "button",
@@ -221,5 +227,6 @@ Toast.displayName = "Toast";
221
227
  export {
222
228
  Toast,
223
229
  ToastProvider,
230
+ useOptionalToast,
224
231
  useToast
225
232
  };
@@ -16,11 +16,15 @@
16
16
  /* Placed by the shared anchored-position behaviour
17
17
  (src/behaviors/useAnchoredPosition.ts), which makes the panel fixed and
18
18
  sets where it sits, so a scroll container around the trigger (a bordered
19
- table's wrapper, a rail) cannot clip it. The panel keeps its place in the
20
- DOM, so it stacks inside the trigger's own context at the floating layer.
21
- The side it lands on, after any flip, is its data-side. */
19
+ table's wrapper, a rail) cannot clip it. While visible the panel is a
20
+ manual popover in the top layer (src/behaviors/useTopLayer), above every
21
+ z-index on the page; the floating layer only ranks it in a browser
22
+ without the Popover API. The side it lands on, after any flip, is its
23
+ data-side. */
22
24
  .ds-tooltip__panel {
23
25
  z-index: var(--layer-floating);
26
+ /* Over the popover default, so the arrow outside the box still draws. */
27
+ overflow: visible;
24
28
  padding: var(--padding-150) var(--padding-200); /* 6px 8px */
25
29
  border-radius: var(--radius-100); /* 4px */
26
30
  /* The floating surface, same recipe as Popover: page background (the
@@ -43,7 +47,8 @@
43
47
  browsers without those skip the fades but show and hide correctly. */
44
48
  display: none;
45
49
  opacity: 0;
46
- transition-property: opacity, transform, display;
50
+ /* overlay holds the panel in the top layer through the exit fade. */
51
+ transition-property: opacity, transform, display, overlay;
47
52
  transition-duration: var(--motion-duration-fast);
48
53
  transition-timing-function: var(--motion-ease-standard);
49
54
  transition-behavior: allow-discrete;
@@ -24,8 +24,10 @@ export interface TooltipProps extends TooltipOwnProps, Omit<React.ComponentProps
24
24
  * Rides the system's floating surface — page background, hairline border,
25
25
  * floating shadow — so the bubble stays light in light theme and dark in dark.
26
26
  *
27
- * The panel stays in the trigger's DOM position (so it keeps the trigger's
28
- * stacking context and any theme scoped above it) but is `position: fixed`,
27
+ * The panel stays in the trigger's DOM position (so it keeps any theme
28
+ * scoped above it) but paints in the browser's top layer while visible
29
+ * (src/behaviors/useTopLayer), above every z-index on the page, and is
30
+ * `position: fixed`,
29
31
  * placed from the trigger's rectangle each time it shows and again on scroll
30
32
  * and resize while visible. That is what lets a tooltip inside a scroll
31
33
  * container — a bordered table's wrapper, a rail — show whole instead of