rift-ds 1.18.0 → 1.19.1

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 (55) 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/Attachment/AttachmentViewer.js +2 -0
  14. package/components/BentoTile/BentoTile.js +2 -0
  15. package/components/Chart/BarChart.js +1 -0
  16. package/components/Chart/Chart.css +6 -0
  17. package/components/Chart/ComboChart.js +1 -0
  18. package/components/Chart/StackedBarChart.js +1 -0
  19. package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +13 -1
  20. package/components/Composer/Composer.css +72 -0
  21. package/components/Composer/Composer.d.ts +5 -1
  22. package/components/Composer/Composer.js +8 -1
  23. package/components/DataTable/DataTable.css +7 -3
  24. package/components/Popover/Popover.d.ts +2 -0
  25. package/components/Popover/Popover.js +2 -1
  26. package/components/SegmentedControl/SegmentedControl.css +9 -1
  27. package/components/SegmentedControl/SegmentedControl.js +2 -0
  28. package/components/SidebarPanel/SidebarPanel.css +10 -2
  29. package/components/SidebarPanel/SidebarPanel.d.ts +1 -1
  30. package/components/SidebarPanel/SidebarPanel.js +5 -2
  31. package/components/SiteHeader/SiteHeader.css +63 -5
  32. package/components/SiteHeader/SiteHeader.d.ts +11 -3
  33. package/components/SiteHeader/SiteHeader.js +51 -16
  34. package/components/SourceChip/SourceChip.css +5 -1
  35. package/components/SourceChip/SourceChip.js +3 -0
  36. package/components/TableToolbar/TableToolbar.css +132 -2
  37. package/components/TableToolbar/TableToolbar.d.ts +52 -0
  38. package/components/TableToolbar/TableToolbar.js +239 -39
  39. package/components/ThemeToggle/ThemeToggle.css +26 -0
  40. package/components/ThemeToggle/ThemeToggle.d.ts +11 -1
  41. package/components/ThemeToggle/ThemeToggle.js +36 -5
  42. package/components/Toast/Toast.css +4 -0
  43. package/components/Toast/Toast.d.ts +9 -0
  44. package/components/Toast/Toast.js +9 -2
  45. package/components/Tooltip/Tooltip.css +9 -4
  46. package/components/Tooltip/Tooltip.d.ts +4 -2
  47. package/components/Tooltip/Tooltip.js +16 -4
  48. package/index.js +2 -1
  49. package/package.json +1 -1
  50. package/tokens/motion.d.ts +2 -0
  51. package/tokens/motion.js +2 -0
  52. package/tokens/presets/mono.css +14 -0
  53. package/tokens/presets/zest.css +2 -0
  54. package/tokens/tokens-dark.css +1 -1
  55. package/tokens/tokens-light.css +19 -17
@@ -14,6 +14,7 @@ const Popover = React.forwardRef(
14
14
  size = "default",
15
15
  surface = "panel",
16
16
  position = "bottom",
17
+ align = "start",
17
18
  trigger = "click",
18
19
  open: controlledOpen,
19
20
  defaultOpen = false,
@@ -77,7 +78,7 @@ const Popover = React.forwardRef(
77
78
  anchorRef: triggerRef,
78
79
  panelRef,
79
80
  side: position,
80
- align: "start",
81
+ align,
81
82
  gapProperty: "--gap-050"
82
83
  });
83
84
  const baseClass = "ds-popover";
@@ -247,7 +247,9 @@
247
247
  ============================================ */
248
248
 
249
249
  .ds-segmented-control__label--tip {
250
- /* Out of flow at rest; the behaviour makes it fixed once it shows. */
250
+ /* Out of flow at rest; the behaviour makes it fixed once it shows, and
251
+ lifts it into the top layer (src/behaviors/useTopLayer), above every
252
+ z-index on the page. */
251
253
  position: absolute;
252
254
  z-index: var(--layer-floating);
253
255
  padding: var(--padding-150) var(--padding-200);
@@ -270,6 +272,12 @@
270
272
  transform var(--motion-duration-fast) var(--motion-ease-standard);
271
273
  }
272
274
 
275
+ /* The tip sits on the page surface, not the selected segment's fill, so it
276
+ keeps its own ink when the active-label rules above would recolour it. */
277
+ .ds-segmented-control .ds-segmented-control__segment .ds-segmented-control__label--tip {
278
+ color: var(--color-text-primary);
279
+ }
280
+
273
281
  /* --motion-duration-slow matches MOTION_HOVER_SHOW_DELAY_MS, the hover-intent
274
282
  delay Tooltip uses in JS, so the label does not flash while the pointer
275
283
  passes through. Hiding takes no delay, like a tooltip leaving. */
@@ -6,6 +6,7 @@ import "../../behaviors/visually-hidden.css";
6
6
  import "../../fonts/material-symbols.css";
7
7
  import { mirrorClass, logicalArrowKey } from "../../behaviors/direction.js";
8
8
  import { useAnchoredPosition } from "../../behaviors/useAnchoredPosition.js";
9
+ import { useTopLayer } from "../../behaviors/useTopLayer.js";
9
10
  const SegmentedControl = React.forwardRef(
10
11
  ({
11
12
  segments,
@@ -197,6 +198,7 @@ const SegmentedControl = React.forwardRef(
197
198
  tipLabelRef.current = label;
198
199
  setTip([...strip.querySelectorAll(`.${baseClass}__segment`)].indexOf(segment));
199
200
  };
201
+ useTopLayer({ open: tip !== null, panelRef: tipLabelRef, target: tip });
200
202
  useAnchoredPosition({
201
203
  open: tip !== null,
202
204
  target: tip,
@@ -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. */
@@ -109,6 +120,7 @@
109
120
  chip's editor) holds its pressed fill until the panel closes, so the
110
121
  bar says which control the open panel belongs to. */
111
122
  .ds-table-toolbar .ds-circular-button--tertiary[aria-expanded='true'],
123
+ .ds-table-toolbar .ds-circular-button--secondary-neutral[aria-expanded='true'],
112
124
  .ds-table-toolbar .ds-chip[aria-expanded='true'],
113
125
  .ds-table-toolbar .ds-button[aria-expanded='true'] {
114
126
  background-color: var(--color-action-passive-bg-active);
@@ -291,7 +303,7 @@
291
303
 
292
304
  /* A count on an icon's top-end corner, drawn as BottomNav's count badge:
293
305
  a solid pill on the count badge pair, so it reads at a glance on the
294
- filled icon buttons in both modes. */
306
+ outlined icon buttons in both modes. */
295
307
  .ds-table-toolbar__badge {
296
308
  position: absolute;
297
309
  inset-block-start: calc(-1 * var(--padding-100));
@@ -371,8 +383,68 @@
371
383
  min-width: 0;
372
384
  }
373
385
 
386
+ /* A row that takes over the bar: the bar's controls fade out, then the row
387
+ comes in. Coming back is the reverse. The bar stays laid out throughout, so the
388
+ row never changes height. */
389
+ .ds-table-toolbar__start > *,
390
+ .ds-table-toolbar__end {
391
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard) var(--motion-duration-fast);
392
+ }
393
+
374
394
  .ds-table-toolbar__main[data-covered] {
375
395
  visibility: hidden;
396
+ transition: visibility 0s linear calc(var(--motion-duration-fast) + var(--motion-duration-base));
397
+ }
398
+
399
+ .ds-table-toolbar__main[data-covered] .ds-table-toolbar__start > *,
400
+ .ds-table-toolbar__main[data-covered] .ds-table-toolbar__end {
401
+ opacity: 0;
402
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
403
+ }
404
+
405
+ /* The AI button collapses to the circle the row's close button takes over,
406
+ clipped from its far end. Only while it does: a clip at rest would make the
407
+ button a stacking context and paint its glow over its fill. */
408
+ .ds-table-toolbar__start > .ds-table-toolbar__command {
409
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard) var(--motion-duration-fast);
410
+ }
411
+
412
+ .ds-table-toolbar__main[data-covered='command'] .ds-table-toolbar__start > .ds-table-toolbar__command {
413
+ --ds-table-toolbar-circle: var(--control-height-default);
414
+ /* ds-allow(physical): inset() has no logical form; the rtl rule below mirrors it */
415
+ --ds-table-toolbar-collapsed: inset(0 calc(100% - var(--ds-table-toolbar-circle)) 0 0 round var(--radius-pill));
416
+ opacity: 0;
417
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard) var(--motion-duration-base);
418
+ animation: ds-table-toolbar-collapse var(--motion-duration-base) var(--motion-ease-standard) forwards;
419
+ }
420
+
421
+ .ds-table-toolbar__main[data-covered='command'] .ds-table-toolbar__start > .ds-table-toolbar__command.ds-ai-button--compact {
422
+ --ds-table-toolbar-circle: var(--control-height-compact);
423
+ }
424
+
425
+ .ds-table-toolbar__main[data-covered='command'] .ds-table-toolbar__start > .ds-table-toolbar__command:dir(rtl) {
426
+ /* ds-allow(physical): the mirror of the rule above */
427
+ --ds-table-toolbar-collapsed: inset(0 0 0 calc(100% - var(--ds-table-toolbar-circle)) round var(--radius-pill));
428
+ }
429
+
430
+ @keyframes ds-table-toolbar-collapse {
431
+ from {
432
+ clip-path: inset(calc(var(--gap-400) * -1) calc(var(--gap-400) * -1) round var(--radius-pill));
433
+ }
434
+ to {
435
+ clip-path: var(--ds-table-toolbar-collapsed);
436
+ }
437
+ }
438
+
439
+ /* The bulk bar fades in as the bar finishes fading out. */
440
+ .ds-table-toolbar__bulk:not(.ds-table-toolbar__bulk--floating) {
441
+ animation: ds-table-toolbar-row-in var(--motion-duration-base) var(--motion-ease-entrance) var(--motion-duration-fast) backwards;
442
+ }
443
+
444
+ @keyframes ds-table-toolbar-row-in {
445
+ from {
446
+ opacity: 0;
447
+ }
376
448
  }
377
449
 
378
450
  .ds-table-toolbar__bulk {
@@ -498,3 +570,61 @@
498
570
  display: inline-flex;
499
571
  flex-shrink: 0;
500
572
  }
573
+
574
+ /* The AI command's row: it covers the bar as the bulk bar does, the composer
575
+ taking all of it after the close button. Shut, it stays laid out and out of
576
+ sight. Opening, the close button arrives as the AI button finishes collapsing
577
+ and the composer opens out from beside it; closing, both leave first. */
578
+ .ds-table-toolbar__command-row {
579
+ position: absolute;
580
+ inset: 0;
581
+ z-index: 1;
582
+ display: flex;
583
+ align-items: center;
584
+ gap: var(--gap-200);
585
+ visibility: hidden;
586
+ pointer-events: none;
587
+ transition: visibility 0s linear var(--motion-duration-fast);
588
+ }
589
+
590
+ .ds-table-toolbar__command-row[data-open] {
591
+ visibility: visible;
592
+ pointer-events: auto;
593
+ transition: visibility 0s;
594
+ }
595
+
596
+ .ds-table-toolbar__command-row .ds-composer {
597
+ flex: 1;
598
+ min-width: 0;
599
+ opacity: 0;
600
+ /* ds-allow(physical): inset() has no logical form; the rtl rule below mirrors it */
601
+ clip-path: inset(0 100% 0 0 round var(--radius-300));
602
+ transition:
603
+ opacity var(--motion-duration-fast) var(--motion-ease-standard),
604
+ clip-path var(--motion-duration-fast) var(--motion-ease-standard),
605
+ border-color var(--motion-duration-fast) var(--motion-ease-standard);
606
+ }
607
+
608
+ .ds-table-toolbar__command-row .ds-composer:dir(rtl) {
609
+ /* ds-allow(physical): the mirror of the rule above */
610
+ clip-path: inset(0 0 0 100% round var(--radius-300));
611
+ }
612
+
613
+ .ds-table-toolbar__command-row[data-open] .ds-composer {
614
+ opacity: 1;
615
+ clip-path: inset(0 0 0 0 round var(--radius-300));
616
+ transition:
617
+ opacity var(--motion-duration-base) var(--motion-ease-entrance) var(--motion-duration-base),
618
+ clip-path var(--motion-duration-base) var(--motion-ease-entrance) var(--motion-duration-base),
619
+ border-color var(--motion-duration-fast) var(--motion-ease-standard);
620
+ }
621
+
622
+ .ds-table-toolbar__command-close {
623
+ opacity: 0;
624
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
625
+ }
626
+
627
+ .ds-table-toolbar__command-row[data-open] .ds-table-toolbar__command-close {
628
+ opacity: 1;
629
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard) var(--motion-duration-base);
630
+ }