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
@@ -223,11 +223,19 @@
223
223
 
224
224
  /* On a filled row the pill steps back to the page ground, or it would
225
225
  share the row's fill and dissolve into it. */
226
- .ds-sidebar-panel__row--active .ds-sidebar-panel__badge,
227
- .ds-sidebar-panel__row:hover .ds-sidebar-panel__badge {
226
+ .ds-sidebar-panel__row--active .ds-sidebar-panel__badge:not(.ds-sidebar-panel__badge--count),
227
+ .ds-sidebar-panel__row:hover .ds-sidebar-panel__badge:not(.ds-sidebar-panel__badge--count) {
228
228
  background-color: var(--color-bg-page-primary);
229
229
  }
230
230
 
231
+ /* A count: a solid pill on the count badge pair, the "how many" mark the
232
+ rail beside it, BottomNav and TableToolbar share, which reads on any
233
+ row fill. A word such as "New" keeps the quiet pill above. */
234
+ .ds-sidebar-panel__badge--count {
235
+ color: var(--color-count-badge-text);
236
+ background-color: var(--color-count-badge-bg);
237
+ }
238
+
231
239
  .ds-sidebar-panel__chevron {
232
240
  --icon-size: var(--icon-size-500);
233
241
  flex-shrink: 0;
@@ -9,7 +9,7 @@ export interface SidebarPanelItem {
9
9
  href?: string;
10
10
  /** Click handler — also fires on a link item, so a consumer can route client-side */
11
11
  onClick?: () => void;
12
- /** Trailing count pill, e.g. unread items */
12
+ /** Trailing pill, e.g. unread items. A count takes the count badge colours; a word stays a quiet pill */
13
13
  badge?: string | number;
14
14
  /** Group heading this item sits under; consecutive items sharing a group render beneath one heading */
15
15
  group?: string;
@@ -4,6 +4,9 @@ import React, { useId, useState, useCallback } from "react";
4
4
  import { useLinkComponent } from "../../behaviors/LinkProvider.js";
5
5
  import "./SidebarPanel.css";
6
6
  import "../../fonts/material-symbols.css";
7
+ function isCount(badge) {
8
+ return typeof badge === "number" || /^\d+\+?$/.test(badge.trim());
9
+ }
7
10
  function groupRuns(items) {
8
11
  const runs = [];
9
12
  for (const item of items) {
@@ -56,7 +59,7 @@ const SidebarPanel = React.forwardRef(
56
59
  ].filter(Boolean).join(" ");
57
60
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [
58
61
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: item.label }),
59
- item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__badge`, children: item.badge })
62
+ item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__badge${isCount(item.badge) ? ` ${baseClass}__badge--count` : ""}`, children: item.badge })
60
63
  ] });
61
64
  return item.href ? /* @__PURE__ */ jsx(
62
65
  Link,
@@ -104,7 +107,7 @@ const SidebarPanel = React.forwardRef(
104
107
  },
105
108
  children: [
106
109
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: item.label }),
107
- item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__badge`, children: item.badge }),
110
+ item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__badge${isCount(item.badge) ? ` ${baseClass}__badge--count` : ""}`, children: item.badge }),
108
111
  /* @__PURE__ */ jsx(
109
112
  "span",
110
113
  {
@@ -159,11 +159,31 @@
159
159
  rotate: 180deg;
160
160
  }
161
161
 
162
- /* The menu button only exists once the bar folds. */
162
+ /* The menu button only exists once the bar folds. It wears the track
163
+ recipe the CommandPaletteTrigger, the ThemePicker's trigger and the
164
+ ThemeToggle's cycle circle wear (container fill, hairline border, the
165
+ track's curve), so the circles at the bar's end read as one set, and it
166
+ hovers as they do: the fill steps up, the glyph holds its colour. */
163
167
  .ds-site-header__menu-button {
164
168
  display: none;
165
169
  }
166
170
 
171
+ .ds-site-header .ds-site-header__menu-button {
172
+ background-color: var(--color-bg-container-primary);
173
+ border: var(--border-025) solid var(--color-bg-container-border);
174
+ border-radius: calc(var(--radius-pill) + var(--padding-100));
175
+ }
176
+
177
+ .ds-site-header .ds-site-header__menu-button:not(.ds-circular-button--disabled):hover {
178
+ background-color: var(--color-bg-container-secondary);
179
+ }
180
+
181
+ .ds-site-header .ds-site-header__menu-button .ds-circular-button__icon,
182
+ .ds-site-header .ds-site-header__menu-button:not(.ds-circular-button--disabled):hover .ds-circular-button__icon,
183
+ .ds-site-header .ds-site-header__menu-button:not(.ds-circular-button--disabled):active .ds-circular-button__icon {
184
+ color: var(--color-action-icon-default);
185
+ }
186
+
167
187
  /* While the drawer is open its close button stands in this exact spot, on
168
188
  the top layer; the button underneath would only show through the scrim. */
169
189
  .ds-site-header__menu-button--hidden {
@@ -188,6 +208,16 @@
188
208
  pointer-events: auto;
189
209
  }
190
210
 
211
+ /* Condensed: `condensedActions` take the place of `actions`. Whichever set
212
+ is not showing leaves the row but stays measurable (hidden, out of flow,
213
+ inert), as the folded pills do. */
214
+ .ds-site-header--condensed .ds-site-header__actions:not(.ds-site-header__actions--condensed),
215
+ .ds-site-header:not(.ds-site-header--condensed) .ds-site-header__actions--condensed {
216
+ position: absolute;
217
+ visibility: hidden;
218
+ pointer-events: none;
219
+ }
220
+
191
221
  /* Folded: the pills and the wide actions leave the row but stay measurable
192
222
  (hidden, out of flow, inert), and the menu button takes their place. */
193
223
  .ds-site-header--folded .ds-site-header__nav,
@@ -207,8 +237,8 @@
207
237
 
208
238
  /* Before the bar has measured itself (the first frame, which a
209
239
  server-rendered page shows before its script runs), it folds by CSS below
210
- the `initialFold` step, so a phone never sees the pills spill past the
211
- actions. The first measurement replaces this in the same commit as
240
+ the `initialFold` step, with `condensedActions` in place of `actions`, so
241
+ a phone never sees the pills spill past the actions. The first measurement replaces this in the same commit as
212
242
  hydration. */
213
243
  @media (max-width: 959px) {
214
244
  .ds-site-header--prefold-tablet .ds-site-header__nav,
@@ -221,6 +251,18 @@
221
251
  .ds-site-header--prefold-tablet .ds-site-header__menu-button {
222
252
  display: inline-flex;
223
253
  }
254
+
255
+ .ds-site-header--prefold-tablet .ds-site-header__actions:not(.ds-site-header__actions--condensed) {
256
+ position: absolute;
257
+ visibility: hidden;
258
+ pointer-events: none;
259
+ }
260
+
261
+ .ds-site-header.ds-site-header--prefold-tablet .ds-site-header__actions--condensed {
262
+ position: static;
263
+ visibility: visible;
264
+ pointer-events: auto;
265
+ }
224
266
  }
225
267
 
226
268
  @media (max-width: 1279px) {
@@ -234,6 +276,18 @@
234
276
  .ds-site-header--prefold-mid .ds-site-header__menu-button {
235
277
  display: inline-flex;
236
278
  }
279
+
280
+ .ds-site-header--prefold-mid .ds-site-header__actions:not(.ds-site-header__actions--condensed) {
281
+ position: absolute;
282
+ visibility: hidden;
283
+ pointer-events: none;
284
+ }
285
+
286
+ .ds-site-header.ds-site-header--prefold-mid .ds-site-header__actions--condensed {
287
+ position: static;
288
+ visibility: visible;
289
+ pointer-events: auto;
290
+ }
237
291
  }
238
292
 
239
293
  /* The drawer: its brand row lands on the bar's own brand. SiteHeader
@@ -261,8 +315,9 @@
261
315
  position: absolute;
262
316
  inset-block-start: var(--ds-site-header-close-top, var(--padding-500));
263
317
  inset-inline-start: var(--ds-site-header-close-start, auto);
264
- width: var(--control-height-default);
265
- height: var(--control-height-default);
318
+ /* The menu button's compact box, so the close lands exactly on it. */
319
+ width: var(--control-height-compact);
320
+ height: var(--control-height-compact);
266
321
  /* ds-allow(color): on-scrim furniture, as Lightbox's: the scrim is dark in both themes, so the glyph is white in both. */
267
322
  color: #ffffff;
268
323
  opacity: 0;
@@ -273,6 +328,9 @@
273
328
  turning into another. */
274
329
  .ds-site-header__drawer .ds-drawer__close .material-symbols-rounded {
275
330
  --icon-size: var(--icon-size-600);
331
+ /* The glyph takes the close's on-scrim white, not CircularButton's
332
+ icon colour, which is dark in light mode and vanished on the scrim. */
333
+ color: inherit;
276
334
  }
277
335
 
278
336
  .ds-site-header__drawer .ds-drawer__panel[data-state="open"] .ds-drawer__close {
@@ -46,11 +46,19 @@ type SiteHeaderOwnProps = {
46
46
  sections: SiteHeaderSection[];
47
47
  /** The path of the page being viewed, used to mark the current section, row and drawer item. */
48
48
  currentHref?: string;
49
- /** Controls at the bar's end that stay at every width, such as a search button. */
49
+ /** Controls at the bar's end that stay at every width, such as a search button; `condensedActions`, when given, stand in for them on the condensed step. */
50
50
  actions?: React.ReactNode;
51
+ /**
52
+ * The same controls drawn narrower, such as an icon-only CommandPaletteTrigger and a
53
+ * `cycle` ThemeToggle. When the full row stops fitting, these take the place of
54
+ * `actions` (the header adds `ds-site-header--condensed`), and the pills fold to the
55
+ * menu button only when the condensed row does not fit either. Without it the
56
+ * header folds as soon as the full row stops fitting.
57
+ */
58
+ condensedActions?: React.ReactNode;
51
59
  /**
52
60
  * Controls at the bar's end that show only while the pills fit, such as a
53
- * theme toggle. Once the bar folds to its menu button they leave it; put
61
+ * Get started button. Once the bar folds to its menu button they leave it; put
54
62
  * their drawer twin in `drawerFooter`.
55
63
  */
56
64
  wideActions?: React.ReactNode;
@@ -113,5 +121,5 @@ export interface SiteHeaderProps extends SiteHeaderOwnProps, Omit<React.Componen
113
121
  * same sections. The forwarded ref and any unrecognised props target the
114
122
  * `<header>`.
115
123
  */
116
- export declare function SiteHeader({ brand, sections, currentHref, actions, wideActions, drawerFooter, drawerOpen: drawerOpenProp, defaultDrawerOpen, onDrawerOpenChange, initialFold, sticky, tintAfter, linkComponent, navLabel, drawerOpenLabel, drawerTitle, drawerCloseLabel, className, ref, ...rest }: SiteHeaderProps): React.JSX.Element;
124
+ export declare function SiteHeader({ brand, sections, currentHref, actions, condensedActions, wideActions, drawerFooter, drawerOpen: drawerOpenProp, defaultDrawerOpen, onDrawerOpenChange, initialFold, sticky, tintAfter, linkComponent, navLabel, drawerOpenLabel, drawerTitle, drawerCloseLabel, className, ref, ...rest }: SiteHeaderProps): React.JSX.Element;
117
125
  export {};
@@ -19,6 +19,7 @@ function SiteHeader({
19
19
  sections,
20
20
  currentHref,
21
21
  actions,
22
+ condensedActions,
22
23
  wideActions,
23
24
  drawerFooter,
24
25
  drawerOpen: drawerOpenProp,
@@ -45,12 +46,15 @@ function SiteHeader({
45
46
  const navRef = useRef(null);
46
47
  const wideRef = useRef(null);
47
48
  const actionsRef = useRef(null);
49
+ const condensedActionsRef = useRef(null);
48
50
  const panelRef = useRef(null);
49
51
  const menuButtonRef = useRef(null);
50
52
  const closeTimer = useRef(void 0);
51
53
  const returningFocus = useRef(false);
54
+ const lockedGutter = useRef(0);
52
55
  const [openId, setOpenId] = useState(null);
53
56
  const [shownId, setShownId] = useState(null);
57
+ const [condensed, setCondensed] = useState(false);
54
58
  const [folded, setFolded] = useState(false);
55
59
  const [measured, setMeasured] = useState(false);
56
60
  const [drawerOffset, setDrawerOffset] = useState({});
@@ -59,6 +63,7 @@ function SiteHeader({
59
63
  const drawerOpen = drawerOpenProp ?? drawerOpenState;
60
64
  const setDrawerOpen = useCallback(
61
65
  (next) => {
66
+ lockedGutter.current = next ? window.innerWidth - document.documentElement.clientWidth : 0;
62
67
  if (drawerOpenProp === void 0) setDrawerOpenState(next);
63
68
  onDrawerOpenChange?.(next);
64
69
  },
@@ -73,23 +78,33 @@ function SiteHeader({
73
78
  const bar = barRef.current;
74
79
  if (!bar) return;
75
80
  const measure = () => {
76
- const style = getComputedStyle(bar);
77
- const gap = parseFloat(style.columnGap) || 0;
81
+ const gap = parseFloat(getComputedStyle(bar).columnGap) || 0;
78
82
  const navStart = navRef.current ? parseFloat(getComputedStyle(navRef.current).marginInlineStart) || 0 : 0;
79
- const parts = [brandRef.current, navRef.current, wideRef.current, actionsRef.current];
80
- const widths = parts.map((el) => el ? el.scrollWidth : 0);
81
- const present = widths.filter((w) => w > 0).length;
82
- const needed = widths.reduce((sum, w) => sum + w, 0) + navStart + gap * Math.max(present - 1, 0);
83
- setFolded(needed > bar.clientWidth);
83
+ const rowWidth = (actionsEl) => {
84
+ const parts = [brandRef.current, navRef.current, wideRef.current, actionsEl];
85
+ const widths = parts.map((el) => el ? el.scrollWidth : 0);
86
+ const present = widths.filter((w) => w > 0).length;
87
+ return widths.reduce((sum, w) => sum + w, 0) + navStart + gap * Math.max(present - 1, 0);
88
+ };
89
+ const room = bar.clientWidth - lockedGutter.current;
90
+ const full = rowWidth(actionsRef.current);
91
+ const tight = condensedActionsRef.current ? rowWidth(condensedActionsRef.current) : full;
92
+ setCondensed(condensedActionsRef.current != null && full > room);
93
+ setFolded(tight > room);
84
94
  setMeasured(true);
85
95
  };
86
96
  measure();
87
97
  const observer = new ResizeObserver(measure);
88
- [bar, brandRef.current, navRef.current, wideRef.current, actionsRef.current].forEach(
89
- (el) => el && observer.observe(el)
90
- );
98
+ [
99
+ bar,
100
+ brandRef.current,
101
+ navRef.current,
102
+ wideRef.current,
103
+ actionsRef.current,
104
+ condensedActionsRef.current
105
+ ].forEach((el) => el && observer.observe(el));
91
106
  return () => observer.disconnect();
92
- }, [sections, actions, wideActions, brand]);
107
+ }, [sections, actions, condensedActions, wideActions, brand]);
93
108
  useEffect(() => {
94
109
  if (!sticky) return;
95
110
  let frame = 0;
@@ -108,12 +123,11 @@ function SiteHeader({
108
123
  };
109
124
  }, [sticky, tintAfter]);
110
125
  useEffect(() => () => clearTimeout(closeTimer.current), []);
111
- useLayoutEffect(() => {
112
- if (!drawerOpen) return;
126
+ const placeDrawer = useCallback(() => {
113
127
  const bar = barRef.current;
114
128
  if (!bar) return;
115
129
  const rtl = isRtl(bar);
116
- const start = (rect) => rtl ? window.innerWidth - rect.right : rect.left;
130
+ const start = (rect) => rtl ? document.documentElement.clientWidth - rect.right : rect.left;
117
131
  const barRect = bar.getBoundingClientRect();
118
132
  const offsets = {
119
133
  "--ds-site-header-height": `${bar.offsetHeight}px`,
@@ -126,7 +140,17 @@ function SiteHeader({
126
140
  offsets["--ds-site-header-close-start"] = `${start(button)}px`;
127
141
  }
128
142
  setDrawerOffset(offsets);
129
- }, [drawerOpen]);
143
+ }, []);
144
+ useLayoutEffect(() => {
145
+ if (!drawerOpen) {
146
+ lockedGutter.current = 0;
147
+ return;
148
+ }
149
+ placeDrawer();
150
+ }, [drawerOpen, placeDrawer]);
151
+ useEffect(() => {
152
+ if (drawerOpen) placeDrawer();
153
+ }, [drawerOpen, placeDrawer]);
130
154
  const openSection = useCallback((id) => {
131
155
  clearTimeout(closeTimer.current);
132
156
  setOpenId(id);
@@ -173,6 +197,7 @@ function SiteHeader({
173
197
  const classes = [
174
198
  baseClass,
175
199
  sticky && `${baseClass}--sticky`,
200
+ condensed && `${baseClass}--condensed`,
176
201
  folded && `${baseClass}--folded`,
177
202
  !measured && initialFold !== "never" && `${baseClass}--prefold-${initialFold}`,
178
203
  className
@@ -247,7 +272,16 @@ function SiteHeader({
247
272
  );
248
273
  }) }),
249
274
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__end`, children: [
250
- actions && /* @__PURE__ */ jsx("div", { ref: actionsRef, className: `${baseClass}__actions`, children: actions }),
275
+ actions && /* @__PURE__ */ jsx("div", { ref: actionsRef, className: `${baseClass}__actions`, inert: condensed, children: actions }),
276
+ condensedActions && /* @__PURE__ */ jsx(
277
+ "div",
278
+ {
279
+ ref: condensedActionsRef,
280
+ className: `${baseClass}__actions ${baseClass}__actions--condensed`,
281
+ inert: !condensed,
282
+ children: condensedActions
283
+ }
284
+ ),
251
285
  wideActions && /* @__PURE__ */ jsx("div", { ref: wideRef, className: `${baseClass}__wide-actions`, inert: folded, children: wideActions }),
252
286
  /* @__PURE__ */ jsx(
253
287
  CircularButton,
@@ -255,6 +289,7 @@ function SiteHeader({
255
289
  ref: menuButtonRef,
256
290
  className: [`${baseClass}__menu-button`, drawerOpen && `${baseClass}__menu-button--hidden`].filter(Boolean).join(" "),
257
291
  icon: "menu",
292
+ size: "compact",
258
293
  variant: "secondary",
259
294
  tone: "neutral",
260
295
  ariaLabel: drawerOpenLabel,
@@ -188,7 +188,11 @@ button.ds-source-chip {
188
188
  }
189
189
 
190
190
  .ds-source-chip__preview {
191
+ /* A manual popover in the top layer while visible
192
+ (src/behaviors/useTopLayer); the floating layer only ranks it in a
193
+ browser without the Popover API. */
191
194
  z-index: var(--layer-floating);
195
+ overflow: visible;
192
196
  width: max-content;
193
197
  max-width: min(320px, calc(100vw - var(--padding-400)));
194
198
  padding: var(--padding-300) var(--padding-400);
@@ -216,7 +220,7 @@ button.ds-source-chip {
216
220
  display: none;
217
221
  opacity: 0;
218
222
  transform: translateY(4px);
219
- transition-property: opacity, transform, display;
223
+ transition-property: opacity, transform, display, overlay;
220
224
  transition-duration: var(--motion-duration-fast);
221
225
  transition-timing-function: var(--motion-ease-standard);
222
226
  transition-behavior: allow-discrete;
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useRef, useId, useEffect } from "react";
4
4
  import { useAnchoredPosition } from "../../behaviors/useAnchoredPosition.js";
5
+ import { useTopLayer } from "../../behaviors/useTopLayer.js";
5
6
  import { useLinkComponent } from "../../behaviors/LinkProvider.js";
6
7
  import { useLayer } from "../../behaviors/useLayer.js";
7
8
  import "./SourceChip.css";
@@ -61,6 +62,7 @@ const SourceChip = React.forwardRef(({ title, index, icon = "language", href, ex
61
62
  if (closeOpenPreview === closeThis) closeOpenPreview = null;
62
63
  };
63
64
  }, [visible]);
65
+ useTopLayer({ open: visible, panelRef });
64
66
  useAnchoredPosition({
65
67
  open: visible,
66
68
  anchorRef: hostRef,
@@ -135,6 +137,7 @@ const SourceChip = React.forwardRef(({ title, index, icon = "language", href, ex
135
137
  ref: panelRef,
136
138
  id: panelId,
137
139
  role: "tooltip",
140
+ popover: "manual",
138
141
  className: `${baseClass}__preview${visible ? ` ${baseClass}__preview--visible` : ""}`,
139
142
  children: [
140
143
  (logo || source || meta || href) && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__preview-top`, children: [
@@ -1,7 +1,7 @@
1
1
  /* ============================================
2
2
  TABLE TOOLBAR
3
3
  The control bar over a table: search, filter chips, the More panel's columns,
4
- the density switch, the count and actions, in one row that wraps.
4
+ the density switch, the count, the actions and the AI command, in one line.
5
5
  ============================================ */
6
6
 
7
7
  .ds-table-toolbar {
@@ -63,6 +63,17 @@
63
63
  flex-shrink: 0;
64
64
  }
65
65
 
66
+ /* Folded, the start cluster holds only the AI command, at its own size. */
67
+ .ds-table-toolbar--narrow .ds-table-toolbar__start {
68
+ flex: 0 0 auto;
69
+ }
70
+
71
+ /* Folded, search moves into the end cluster and the start cluster is empty:
72
+ out of the row, so its gap cannot push the controls off the table's edge. */
73
+ .ds-table-toolbar__start:empty {
74
+ display: none;
75
+ }
76
+
66
77
  /* The search field flexes between a floor and a ceiling: the line wraps
67
78
  against its basis, so chips take the room it would otherwise hold, and it
68
79
  grows into room nothing else uses. */
@@ -371,8 +382,68 @@
371
382
  min-width: 0;
372
383
  }
373
384
 
385
+ /* A row that takes over the bar: the bar's controls fade out, then the row
386
+ comes in. Coming back is the reverse. The bar stays laid out throughout, so the
387
+ row never changes height. */
388
+ .ds-table-toolbar__start > *,
389
+ .ds-table-toolbar__end {
390
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard) var(--motion-duration-fast);
391
+ }
392
+
374
393
  .ds-table-toolbar__main[data-covered] {
375
394
  visibility: hidden;
395
+ transition: visibility 0s linear calc(var(--motion-duration-fast) + var(--motion-duration-base));
396
+ }
397
+
398
+ .ds-table-toolbar__main[data-covered] .ds-table-toolbar__start > *,
399
+ .ds-table-toolbar__main[data-covered] .ds-table-toolbar__end {
400
+ opacity: 0;
401
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
402
+ }
403
+
404
+ /* The AI button collapses to the circle the row's close button takes over,
405
+ clipped from its far end. Only while it does: a clip at rest would make the
406
+ button a stacking context and paint its glow over its fill. */
407
+ .ds-table-toolbar__start > .ds-table-toolbar__command {
408
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard) var(--motion-duration-fast);
409
+ }
410
+
411
+ .ds-table-toolbar__main[data-covered='command'] .ds-table-toolbar__start > .ds-table-toolbar__command {
412
+ --ds-table-toolbar-circle: var(--control-height-default);
413
+ /* ds-allow(physical): inset() has no logical form; the rtl rule below mirrors it */
414
+ --ds-table-toolbar-collapsed: inset(0 calc(100% - var(--ds-table-toolbar-circle)) 0 0 round var(--radius-pill));
415
+ opacity: 0;
416
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard) var(--motion-duration-base);
417
+ animation: ds-table-toolbar-collapse var(--motion-duration-base) var(--motion-ease-standard) forwards;
418
+ }
419
+
420
+ .ds-table-toolbar__main[data-covered='command'] .ds-table-toolbar__start > .ds-table-toolbar__command.ds-ai-button--compact {
421
+ --ds-table-toolbar-circle: var(--control-height-compact);
422
+ }
423
+
424
+ .ds-table-toolbar__main[data-covered='command'] .ds-table-toolbar__start > .ds-table-toolbar__command:dir(rtl) {
425
+ /* ds-allow(physical): the mirror of the rule above */
426
+ --ds-table-toolbar-collapsed: inset(0 0 0 calc(100% - var(--ds-table-toolbar-circle)) round var(--radius-pill));
427
+ }
428
+
429
+ @keyframes ds-table-toolbar-collapse {
430
+ from {
431
+ clip-path: inset(calc(var(--gap-400) * -1) calc(var(--gap-400) * -1) round var(--radius-pill));
432
+ }
433
+ to {
434
+ clip-path: var(--ds-table-toolbar-collapsed);
435
+ }
436
+ }
437
+
438
+ /* The bulk bar fades in as the bar finishes fading out. */
439
+ .ds-table-toolbar__bulk:not(.ds-table-toolbar__bulk--floating) {
440
+ animation: ds-table-toolbar-row-in var(--motion-duration-base) var(--motion-ease-entrance) var(--motion-duration-fast) backwards;
441
+ }
442
+
443
+ @keyframes ds-table-toolbar-row-in {
444
+ from {
445
+ opacity: 0;
446
+ }
376
447
  }
377
448
 
378
449
  .ds-table-toolbar__bulk {
@@ -498,3 +569,61 @@
498
569
  display: inline-flex;
499
570
  flex-shrink: 0;
500
571
  }
572
+
573
+ /* The AI command's row: it covers the bar as the bulk bar does, the composer
574
+ taking all of it after the close button. Shut, it stays laid out and out of
575
+ sight. Opening, the close button arrives as the AI button finishes collapsing
576
+ and the composer opens out from beside it; closing, both leave first. */
577
+ .ds-table-toolbar__command-row {
578
+ position: absolute;
579
+ inset: 0;
580
+ z-index: 1;
581
+ display: flex;
582
+ align-items: center;
583
+ gap: var(--gap-200);
584
+ visibility: hidden;
585
+ pointer-events: none;
586
+ transition: visibility 0s linear var(--motion-duration-fast);
587
+ }
588
+
589
+ .ds-table-toolbar__command-row[data-open] {
590
+ visibility: visible;
591
+ pointer-events: auto;
592
+ transition: visibility 0s;
593
+ }
594
+
595
+ .ds-table-toolbar__command-row .ds-composer {
596
+ flex: 1;
597
+ min-width: 0;
598
+ opacity: 0;
599
+ /* ds-allow(physical): inset() has no logical form; the rtl rule below mirrors it */
600
+ clip-path: inset(0 100% 0 0 round var(--radius-300));
601
+ transition:
602
+ opacity var(--motion-duration-fast) var(--motion-ease-standard),
603
+ clip-path var(--motion-duration-fast) var(--motion-ease-standard),
604
+ border-color var(--motion-duration-fast) var(--motion-ease-standard);
605
+ }
606
+
607
+ .ds-table-toolbar__command-row .ds-composer:dir(rtl) {
608
+ /* ds-allow(physical): the mirror of the rule above */
609
+ clip-path: inset(0 0 0 100% round var(--radius-300));
610
+ }
611
+
612
+ .ds-table-toolbar__command-row[data-open] .ds-composer {
613
+ opacity: 1;
614
+ clip-path: inset(0 0 0 0 round var(--radius-300));
615
+ transition:
616
+ opacity var(--motion-duration-base) var(--motion-ease-entrance) var(--motion-duration-base),
617
+ clip-path var(--motion-duration-base) var(--motion-ease-entrance) var(--motion-duration-base),
618
+ border-color var(--motion-duration-fast) var(--motion-ease-standard);
619
+ }
620
+
621
+ .ds-table-toolbar__command-close {
622
+ opacity: 0;
623
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
624
+ }
625
+
626
+ .ds-table-toolbar__command-row[data-open] .ds-table-toolbar__command-close {
627
+ opacity: 1;
628
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard) var(--motion-duration-base);
629
+ }
@@ -1,6 +1,8 @@
1
1
  import { default as React } from 'react';
2
2
  import { FilterKind } from '../DataTable/filterModel';
3
3
  import { DataTableFilter as Filter, DataTableFilterLabels as FilterLabels, DataTableSort as Sort, DataTableView } from '../DataTable/DataTable';
4
+ import { ToolbarColumn } from '../DataTable/tableContext';
5
+ import { TableView } from '../DataTable/views';
4
6
  /** One column as a TableToolbar over some other table is told about it. */
5
7
  export interface TableToolbarColumn {
6
8
  /** The column's key. */
@@ -156,6 +158,22 @@ export interface TableToolbarLabels {
156
158
  selected: (count: number) => string;
157
159
  /** The bulk bar's count once every matching row is selected, given how many. */
158
160
  allSelected: (count: number) => string;
161
+ /** The command button's label. */
162
+ command: string;
163
+ /** Closes the command's row and brings the bar back. */
164
+ commandClose: string;
165
+ /** The command composer's placeholder. */
166
+ commandPlaceholder: string;
167
+ /** The command composer's send button. */
168
+ commandSend: string;
169
+ /** Shown when a command changes the table without a message of its own. */
170
+ commandApplied: string;
171
+ /** Shown when a command fails without a message of its own. */
172
+ commandFailed: string;
173
+ /** Puts the table back as it was before the last command. */
174
+ commandUndo: string;
175
+ /** Closes a command's toast in the bar's own stack; inside a ToastProvider, its `dismissLabel` applies instead. */
176
+ commandDismiss: string;
159
177
  /** The bulk bar's menu of the actions that do not fit. */
160
178
  moreBulkActions: string;
161
179
  /** The bulk bar's button that selects every matching row, given how many match. */
@@ -260,6 +278,17 @@ type TableToolbarOwnProps = {
260
278
  secondaryActions?: TableToolbarAction[];
261
279
  /** Any other controls at the bar's end, after the actions. */
262
280
  actions?: React.ReactNode;
281
+ /**
282
+ * The AI command: a button at the bar's start that turns the whole row into a
283
+ * one-off composer, as selecting rows turns it into the bulk bar, whose
284
+ * text your handler turns into a whole table setup. It reaches everything a
285
+ * view holds (filters, sorts, search, columns, pinning, grouping, density),
286
+ * so it can do in a sentence what the bar's other controls do one at a time.
287
+ * The library calls no model; your handler does. A change or an error lands in
288
+ * a toast, in the surrounding ToastProvider's stack if there is one, else in the
289
+ * bar's own at the bottom-right. Shows only inside a DataTable.
290
+ */
291
+ command?: TableToolbarCommand;
263
292
  /** The bar's words, merged over their English defaults. */
264
293
  labels?: Partial<TableToolbarLabels>;
265
294
  /** The filters' words, merged over their English defaults (inside a DataTable, over the table's). */
@@ -267,6 +296,29 @@ type TableToolbarOwnProps = {
267
296
  /** Additional CSS classes */
268
297
  className?: string;
269
298
  };
299
+ /** What a TableToolbar's command handler is given. */
300
+ export interface TableToolbarCommandRequest {
301
+ /** What the reader typed. */
302
+ text: string;
303
+ /** The table's setup right now, so a command can refine it. */
304
+ view: TableView;
305
+ /** The columns, as the bar knows them: keys, labels, filter kinds and choices. */
306
+ columns: ToolbarColumn[];
307
+ /** Aborts when the table leaves the page while the command runs. */
308
+ signal: AbortSignal;
309
+ }
310
+ /** What a TableToolbar's command handler answers with. */
311
+ export interface TableToolbarCommandResult {
312
+ /** The whole setup to apply; applying replaces the setup, and leaving it out changes nothing. */
313
+ view?: TableView;
314
+ /** What was done or found. A toast carries it, with an Undo when the setup changed; a setup that changed with no message takes the `commandApplied` label. */
315
+ message?: string;
316
+ }
317
+ /** The AI command in a TableToolbar. */
318
+ export interface TableToolbarCommand {
319
+ /** Turns the reader's text into a setup. A rejection shows as an error toast, using the error's message, and leaves the table alone. */
320
+ onSubmit: (request: TableToolbarCommandRequest) => Promise<TableToolbarCommandResult>;
321
+ }
270
322
  export interface TableToolbarProps extends TableToolbarOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof TableToolbarOwnProps> {
271
323
  }
272
324
  /**