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
@@ -4,7 +4,9 @@
4
4
  * hover cards, tooltips).
5
5
  *
6
6
  * The panel stays where it is in the DOM, so it keeps the trigger's stacking
7
- * context and any theme scoped above it, but is `position: fixed`, placed
7
+ * context (a tooltip excepted, which paints in the top layer through
8
+ * src/behaviors/useTopLayer) and any theme scoped above it, but is
9
+ * `position: fixed`, placed
8
10
  * from the anchor's rectangle. A fixed panel is not clipped by an ancestor's
9
11
  * `overflow`, which is what lets a menu inside a table's scroll wrapper, a
10
12
  * rail or a card show whole. Portals would escape the same way but would
@@ -2,6 +2,7 @@
2
2
  import { useRef, useLayoutEffect } from "react";
3
3
  import { isRtl } from "./direction.js";
4
4
  function fixedContainingBlock(element) {
5
+ if (element.matches(":popover-open")) return null;
5
6
  for (let node = element.parentElement; node; node = node.parentElement) {
6
7
  const style = getComputedStyle(node);
7
8
  const read = (property) => style.getPropertyValue(property).trim();
@@ -0,0 +1,35 @@
1
+ import { RefObject } from 'react';
2
+ /**
3
+ * Lifts a panel into the browser's top layer while `open`, so it paints
4
+ * above everything on the page: every z-index, every stacking context, every
5
+ * `overflow` clip. Tooltips always sit here; no layer token can rank above
6
+ * the top layer, so nothing a page or a host does can cover one.
7
+ *
8
+ * The panel renders with `popover="manual"` and this hook calls
9
+ * `showPopover()` / `hidePopover()`. A panel that must stay in the
10
+ * accessibility tree at rest (SegmentedControl's segment labels, which are
11
+ * the segments' names) cannot carry the attribute, since a closed popover is
12
+ * `display: none`; the hook adds it for as long as the panel is shown and
13
+ * takes it off again. A manual popover has no light dismiss
14
+ * and closes no other popover, so dismissal stays with the shared layer
15
+ * stack (src/behaviors/useLayer). The panel keeps its place in the DOM, so
16
+ * it still inherits the custom properties of any theme scoped above it; only
17
+ * where it paints changes.
18
+ *
19
+ * Call it before useAnchoredPosition in the component: layout effects run in
20
+ * order, and the anchored hook must see the panel already in the top layer,
21
+ * where the viewport is its containing block whatever its ancestors
22
+ * transform (`fixedContainingBlock` returns null for an open popover).
23
+ *
24
+ * The panel's CSS adds `overlay` to its transition list with
25
+ * `allow-discrete`, so the exit fade still plays in the top layer, and sets
26
+ * `overflow: visible` over the popover default so an arrow outside the box
27
+ * still draws. A browser without the Popover API leaves the panel where it
28
+ * was, stacking at its layer token.
29
+ */
30
+ export declare function useTopLayer({ open, panelRef, target, }: {
31
+ open: boolean;
32
+ panelRef: RefObject<HTMLElement | null>;
33
+ /** Changes when the ref is pointed at a different panel, as useAnchoredPosition's `target`. */
34
+ target?: unknown;
35
+ }): void;
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { useLayoutEffect } from "react";
3
+ function useTopLayer({
4
+ open,
5
+ panelRef,
6
+ target
7
+ }) {
8
+ useLayoutEffect(() => {
9
+ const panel = panelRef.current;
10
+ if (!open || !panel || typeof panel.showPopover !== "function") return;
11
+ const added = !panel.hasAttribute("popover");
12
+ if (added) panel.setAttribute("popover", "manual");
13
+ if (!panel.matches(":popover-open")) panel.showPopover();
14
+ return () => {
15
+ if (panel.isConnected && panel.matches(":popover-open")) panel.hidePopover();
16
+ if (added) panel.removeAttribute("popover");
17
+ };
18
+ }, [open, panelRef, target]);
19
+ }
20
+ export {
21
+ useTopLayer
22
+ };
@@ -82,6 +82,29 @@ function ChatLauncher({
82
82
  }, [showPanel, isDocked, side, takeover, hostAttribute]);
83
83
  useScrollLock(modal && !takeover);
84
84
  const panelRef = useRef(null);
85
+ useLayoutEffect(() => {
86
+ const panel = panelRef.current;
87
+ const vv = window.visualViewport;
88
+ if (!showPanel || !takeover || !panel || !vv) return;
89
+ let frame = 0;
90
+ const sync = () => {
91
+ cancelAnimationFrame(frame);
92
+ frame = requestAnimationFrame(() => {
93
+ panel.style.setProperty("--ds-chat-viewport-height", `${vv.height}px`);
94
+ panel.style.setProperty("--ds-chat-viewport-top", `${vv.offsetTop}px`);
95
+ });
96
+ };
97
+ sync();
98
+ vv.addEventListener("resize", sync);
99
+ vv.addEventListener("scroll", sync);
100
+ return () => {
101
+ cancelAnimationFrame(frame);
102
+ vv.removeEventListener("resize", sync);
103
+ vv.removeEventListener("scroll", sync);
104
+ panel.style.removeProperty("--ds-chat-viewport-height");
105
+ panel.style.removeProperty("--ds-chat-viewport-top");
106
+ };
107
+ }, [showPanel, takeover]);
85
108
  const wasOpen = useRef(false);
86
109
  useEffect(() => {
87
110
  if (wasOpen.current && !open) {
@@ -1207,13 +1207,18 @@ html[data-chat-resizing] .ds-chat-launcher__panel--floating .ds-chat-header {
1207
1207
  way the browser handles any page with a field at the bottom. The launcher
1208
1208
  reads the same threshold as TAKEOVER_QUERY; this rule covers the first
1209
1209
  render, before a media query has been read, so the two must stay in step.
1210
- No transition: there is no card form to glide from. */
1210
+ No transition: there is no card form to glide from. Once open, the
1211
+ launcher writes the visual viewport's height and pan here, so the
1212
+ composer ends at the keyboard and the header stays on screen; 100dvh is
1213
+ the first paint's height and the fallback without visualViewport. */
1211
1214
  @media (max-width: 719px) {
1212
1215
  .ds-chat-launcher__panel {
1213
1216
  position: relative;
1214
- top: auto;
1217
+ /* A relative offset, not a transform: a transform would become the
1218
+ containing block of every fixed popup inside the panel. */
1219
+ top: var(--ds-chat-viewport-top, 0px);
1215
1220
  left: auto;
1216
- height: 100dvh;
1221
+ height: var(--ds-chat-viewport-height, 100dvh);
1217
1222
  width: 100%;
1218
1223
  max-width: none;
1219
1224
  border: none;
@@ -279,6 +279,7 @@
279
279
  focus outline is its signal, as on Button, and a widened ring inside it
280
280
  would read as two concentric strokes. */
281
281
  .ds-ai-button:hover::before,
282
+ .ds-ai-button[aria-expanded='true']::before,
282
283
  .ds-ai-button--busy::before,
283
284
  .ds-ai-button--done::before,
284
285
  .ds-ai-button--error::before {
@@ -287,6 +288,7 @@
287
288
  }
288
289
 
289
290
  .ds-ai-button:hover::after,
291
+ .ds-ai-button[aria-expanded='true']::after,
290
292
  .ds-ai-button--busy::after,
291
293
  .ds-ai-button--done::after,
292
294
  .ds-ai-button--error::after {
@@ -339,6 +341,15 @@
339
341
  --icon-size: var(--icon-size-500);
340
342
  }
341
343
 
344
+ .ds-ai-button--icon-only {
345
+ width: var(--control-height-default);
346
+ padding: 0;
347
+ }
348
+
349
+ .ds-ai-button--compact.ds-ai-button--icon-only {
350
+ width: var(--control-height-compact);
351
+ }
352
+
342
353
  /* Reduced motion: the ring and glow stay, the turning stops. */
343
354
  /* ds-allow(motion): parks a looping animation, which the collapsed duration would strobe rather than stop (the ring's turn and the busy spin), and drops the status icon's entrance. */
344
355
  @media (prefers-reduced-motion: reduce) {
@@ -21,6 +21,8 @@ type AiButtonOwnProps = {
21
21
  label?: string;
22
22
  /** Leading icon — Material Symbol name (string) or custom element (ReactNode) */
23
23
  icon?: string | React.ReactNode;
24
+ /** Draws the button as a circle holding only its icon, for a tight bar. The label stays as its accessible name. */
25
+ iconOnly?: boolean;
24
26
  /** Button size */
25
27
  size?: 'default' | 'compact';
26
28
  /** Whether the button is disabled */
@@ -26,6 +26,7 @@ const AiButton = React.forwardRef(
26
26
  ({
27
27
  label = "Ask AI",
28
28
  icon = "auto_awesome",
29
+ iconOnly = false,
29
30
  size = "default",
30
31
  disabled,
31
32
  state: status,
@@ -164,6 +165,7 @@ const AiButton = React.forwardRef(
164
165
  }
165
166
  };
166
167
  const classes = [
168
+ iconOnly ? `${baseClass}--icon-only` : "",
167
169
  baseClass,
168
170
  `${baseClass}--${size}`,
169
171
  disabled && `${baseClass}--disabled`,
@@ -183,7 +185,7 @@ const AiButton = React.forwardRef(
183
185
  },
184
186
  shownStatus
185
187
  ) }),
186
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__text`, children: label })
188
+ /* @__PURE__ */ jsx("span", { className: iconOnly ? "ds-visually-hidden" : `${baseClass}__text`, children: label })
187
189
  ] });
188
190
  const control = href && !disabled ? /* @__PURE__ */ jsx(
189
191
  Link,
@@ -268,6 +268,9 @@
268
268
  opacity: 1;
269
269
  }
270
270
 
271
+ /* The trailing pill holds a beat off the row's end, so it sits as far in
272
+ from the rail's edge as the icon does at the start. A word is a quiet
273
+ pill; a count takes the count badge pair (the --count rule below). */
271
274
  .ds-app-sidebar__btn-badge {
272
275
  font-family: var(--font-caption-family);
273
276
  font-size: var(--font-caption-size);
@@ -277,6 +280,7 @@
277
280
  background-color: var(--color-bg-container-tertiary);
278
281
  border-radius: var(--radius-pill);
279
282
  padding: var(--padding-050) var(--padding-200);
283
+ margin-inline-end: var(--padding-100);
280
284
  min-width: 0;
281
285
  flex-shrink: 1;
282
286
  overflow: hidden;
@@ -288,6 +292,61 @@
288
292
  opacity: 1;
289
293
  }
290
294
 
295
+ /* On a filled row the quiet pill steps back to the page ground, or it
296
+ would share the row's fill and dissolve into it. */
297
+ .ds-app-sidebar__btn--active .ds-app-sidebar__btn-badge:not(.ds-app-sidebar__btn-badge--count),
298
+ .ds-app-sidebar__btn:hover .ds-app-sidebar__btn-badge:not(.ds-app-sidebar__btn-badge--count),
299
+ .ds-app-sidebar__sub-btn--active .ds-app-sidebar__btn-badge:not(.ds-app-sidebar__btn-badge--count),
300
+ .ds-app-sidebar__sub-btn:hover .ds-app-sidebar__btn-badge:not(.ds-app-sidebar__btn-badge--count) {
301
+ background-color: var(--color-bg-page-primary);
302
+ }
303
+
304
+ /* A count: a solid pill on the count badge pair, the "how many" mark
305
+ BottomNav and TableToolbar share, which reads on any row fill. */
306
+ .ds-app-sidebar__btn-badge--count {
307
+ color: var(--color-count-badge-text);
308
+ background-color: var(--color-count-badge-bg);
309
+ }
310
+
311
+ /* The icon's seat: the same box as the icon, there to anchor the corner
312
+ copy of the count. */
313
+ .ds-app-sidebar__btn-seat {
314
+ position: relative;
315
+ display: inline-flex;
316
+ flex-shrink: 0;
317
+ }
318
+
319
+ /* The collapsed rail's count rides the icon's top-end corner, BottomNav's
320
+ badge in size and pair. Its end edge sits on the row's padding edge, so
321
+ it stays inside the collapsed row's clip, and it grows back over the
322
+ icon; it fades out as the trailing pill fades in. */
323
+ .ds-app-sidebar__btn-corner-badge {
324
+ position: absolute;
325
+ top: calc(var(--padding-100) * -1);
326
+ inset-inline-end: calc(var(--padding-200) * -1);
327
+ box-sizing: border-box;
328
+ min-width: 16px;
329
+ height: 16px;
330
+ padding: 0 var(--padding-100);
331
+ border-radius: var(--radius-pill);
332
+ background-color: var(--color-count-badge-bg);
333
+ color: var(--color-count-badge-text);
334
+ font-family: var(--font-caption-family);
335
+ font-size: var(--font-caption-size);
336
+ font-weight: var(--font-paragraph-sm-emphasis-weight);
337
+ line-height: var(--font-caption-line-height);
338
+ text-align: center;
339
+ white-space: nowrap;
340
+ pointer-events: none;
341
+ opacity: 1;
342
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
343
+ }
344
+
345
+ .ds-app-sidebar--expanded .ds-app-sidebar__btn-corner-badge {
346
+ opacity: 0;
347
+ transition-duration: var(--motion-duration-fast);
348
+ }
349
+
291
350
  .ds-app-sidebar__btn-chevron {
292
351
  --icon-size: var(--icon-size-600);
293
352
  color: var(--color-icon-secondary);
@@ -400,15 +459,6 @@
400
459
  flex-shrink: 0;
401
460
  }
402
461
 
403
- /* On a filled row the pill steps back to the page ground, or it would
404
- share the row's fill and dissolve into it. */
405
- .ds-app-sidebar__btn--active .ds-app-sidebar__btn-badge,
406
- .ds-app-sidebar__btn:hover .ds-app-sidebar__btn-badge,
407
- .ds-app-sidebar__sub-btn--active .ds-app-sidebar__btn-badge,
408
- .ds-app-sidebar__sub-btn:hover .ds-app-sidebar__btn-badge {
409
- background-color: var(--color-bg-page-primary);
410
- }
411
-
412
462
  .ds-app-sidebar__sub-chevron {
413
463
  --icon-size: var(--icon-size-500);
414
464
  flex-shrink: 0;
@@ -9,7 +9,7 @@ export interface AppSidebarSubItem {
9
9
  onClick?: () => void;
10
10
  /** Renders the sub-item as a real link to this URL instead of a button. Ignored when the sub-item has children, whose row opens them */
11
11
  href?: string;
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 entry sits under when it is listed in the panel (see `subNav`); ignored in the accordion */
15
15
  group?: string;
@@ -42,7 +42,7 @@ export interface AppSidebarItem {
42
42
  onClick?: () => void;
43
43
  /** Renders the item as a real link to this URL instead of a button. Ignored when the item has children, whose row is the accordion toggle */
44
44
  href?: string;
45
- /** Trailing count pill, e.g. unread items; hidden while the rail is collapsed */
45
+ /** Trailing pill, e.g. unread items. A count takes the count badge colours and, while the rail is collapsed, rides the icon's corner; a word stays a quiet pill, hidden while collapsed */
46
46
  badge?: string | number;
47
47
  /** Sub-items: an accordion under the row, or the side panel's contents when `subNav="panel"` */
48
48
  children?: AppSidebarSubItem[];
@@ -14,6 +14,9 @@ import "../../behaviors/direction.css";
14
14
  import "../../fonts/material-symbols.css";
15
15
  import { mirrorClass } from "../../behaviors/direction.js";
16
16
  import { distinctPixelInks } from "../PixelAvatar/pixelInks.js";
17
+ function isCount(badge) {
18
+ return typeof badge === "number" || /^\d+\+?$/.test(badge.trim());
19
+ }
17
20
  function DefaultLogo() {
18
21
  return /* @__PURE__ */ jsxs(
19
22
  "svg",
@@ -315,17 +318,20 @@ const AppSidebar = React.forwardRef(function AppSidebar2({
315
318
  ].filter(Boolean).join(" ");
316
319
  const itemContent = /* @__PURE__ */ jsxs(Fragment, { children: [
317
320
  /* @__PURE__ */ jsxs("span", { className: `${baseClass}__btn-left`, children: [
318
- item.pixelAvatar ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-icon ${baseClass}__btn-icon--avatar`, children: /* @__PURE__ */ jsx(PixelAvatar, { name: avatarSeed(item), ink: avatarInks.get(item.key) }) }) : /* @__PURE__ */ jsx(
319
- "span",
320
- {
321
- className: `${baseClass}__btn-icon${typeof item.icon === "string" ? ` material-symbols-rounded ${mirrorClass(item.icon)}` : ""}`,
322
- children: item.icon
323
- }
324
- ),
321
+ /* @__PURE__ */ jsxs("span", { className: `${baseClass}__btn-seat`, children: [
322
+ item.pixelAvatar ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-icon ${baseClass}__btn-icon--avatar`, children: /* @__PURE__ */ jsx(PixelAvatar, { name: avatarSeed(item), ink: avatarInks.get(item.key) }) }) : /* @__PURE__ */ jsx(
323
+ "span",
324
+ {
325
+ className: `${baseClass}__btn-icon${typeof item.icon === "string" ? ` material-symbols-rounded ${mirrorClass(item.icon)}` : ""}`,
326
+ children: item.icon
327
+ }
328
+ ),
329
+ item.badge !== void 0 && isCount(item.badge) && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-corner-badge`, "aria-hidden": "true", children: item.badge })
330
+ ] }),
325
331
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-label`, children: item.label })
326
332
  ] }),
327
333
  isPanelMode && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-caption`, "aria-hidden": "true", children: item.label }),
328
- item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-badge`, children: item.badge }),
334
+ item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-badge${isCount(item.badge) ? ` ${baseClass}__btn-badge--count` : ""}`, children: item.badge }),
329
335
  hasChildren && /* @__PURE__ */ jsx(
330
336
  "span",
331
337
  {
@@ -391,7 +397,7 @@ const AppSidebar = React.forwardRef(function AppSidebar2({
391
397
  ].filter(Boolean).join(" ");
392
398
  const subLabel = /* @__PURE__ */ jsxs(Fragment, { children: [
393
399
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__sub-label`, children: sub.label }),
394
- sub.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-badge`, children: sub.badge }),
400
+ sub.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-badge${isCount(sub.badge) ? ` ${baseClass}__btn-badge--count` : ""}`, children: sub.badge }),
395
401
  subHasChildren && /* @__PURE__ */ jsx(
396
402
  "span",
397
403
  {
@@ -121,6 +121,7 @@ const BarChart = React.forwardRef(({
121
121
  Bar,
122
122
  {
123
123
  ...animation,
124
+ className: `${baseClass}__bar`,
124
125
  dataKey: yKey,
125
126
  fill: resolvedBarColor,
126
127
  radius: [3, 3, 0, 0],
@@ -100,6 +100,12 @@
100
100
  position: relative;
101
101
  }
102
102
 
103
+ /* A bar is a large filled mass, so it takes a wash of its series colour, the
104
+ way an area fill does; thin marks (lines, dots, swatches) keep the full token. */
105
+ .ds-chart__bar .recharts-rectangle {
106
+ fill-opacity: 0.45;
107
+ }
108
+
103
109
  /* ============================================
104
110
  BARE MODE — the chart sits inside another
105
111
  panel that supplies the surface, so its own
@@ -138,6 +138,7 @@ const ComboChart = React.forwardRef(({
138
138
  Bar,
139
139
  {
140
140
  ...animation,
141
+ className: `${baseClass}__bar`,
141
142
  yAxisId: "bar",
142
143
  dataKey: barKey,
143
144
  name: resolvedBarLabel,
@@ -106,6 +106,7 @@ const StackedBarChart = React.forwardRef(({
106
106
  Bar,
107
107
  {
108
108
  ...animation,
109
+ className: `${baseClass}__bar`,
109
110
  dataKey: s.dataKey,
110
111
  name: s.label,
111
112
  fill: s.color || defaultColors[i % defaultColors.length],
@@ -70,12 +70,24 @@
70
70
  padding: 0 var(--padding-200);
71
71
  }
72
72
 
73
+ /* The compact field's keycaps step down to caption type with no block
74
+ padding, so they sit inside the shorter row with air around them
75
+ instead of filling it. */
76
+ .ds-command-palette-trigger--compact .ds-kbd--compact {
77
+ padding-block: 0;
78
+ font-size: var(--font-caption-size);
79
+ line-height: var(--font-caption-line-height);
80
+ }
81
+
73
82
  /* Folded to the icon: a circle the row height across, hovering like the
74
- theme picker's trigger rather than like a field. */
83
+ theme picker's trigger rather than like a field. With no prompt beside it
84
+ the glyph is the control, so it takes the icon colour the family's other
85
+ circles wear rather than the field's placeholder tone. */
75
86
  .ds-command-palette-trigger--icon-only {
76
87
  justify-content: center;
77
88
  width: var(--control-height-default);
78
89
  padding: 0;
90
+ color: var(--color-action-icon-default);
79
91
  }
80
92
 
81
93
  .ds-command-palette-trigger--compact.ds-command-palette-trigger--icon-only {
@@ -633,3 +633,75 @@
633
633
  .ds-composer--disabled.ds-composer--mentions .ds-composer__textarea {
634
634
  color: transparent;
635
635
  }
636
+
637
+ /* ============================================
638
+ COMPACT: the same shell at the compact density
639
+ ============================================ */
640
+ .ds-composer--compact {
641
+ --ds-composer-send-size: var(--control-height-compact);
642
+ --ds-composer-text-inset: var(--padding-300);
643
+ /* Concentric on this density's tighter ring. */
644
+ --ds-composer-radius: calc(
645
+ min(var(--radius-pill), var(--ds-composer-send-size) / 2) + var(--padding-100) + var(--border-025)
646
+ );
647
+ }
648
+
649
+ .ds-composer--compact .ds-composer__content {
650
+ padding-block: var(--padding-200) var(--padding-100);
651
+ height: calc(var(--ds-composer-text-height, var(--font-paragraph-sm-line-height)) + var(--padding-200) + var(--padding-100));
652
+ }
653
+
654
+ .ds-composer--compact .ds-composer__textarea {
655
+ font-size: var(--font-paragraph-sm-size);
656
+ line-height: var(--font-paragraph-sm-line-height);
657
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
658
+ max-height: calc(var(--ds-composer-max-rows, 8) * var(--font-paragraph-sm-line-height));
659
+ }
660
+
661
+ .ds-composer--compact .ds-composer__footer {
662
+ padding: var(--padding-100);
663
+ }
664
+
665
+ /* ============================================
666
+ INLINE: one row at the control height, the send button on the text's line.
667
+ The shell is an input here, so it does not float, and the button sits
668
+ inside the height, concentric on a thinner ring.
669
+ ============================================ */
670
+ .ds-composer--inline {
671
+ --ds-composer-control: var(--control-height-default);
672
+ --ds-composer-line: var(--font-paragraph-line-height);
673
+ --ds-composer-send-size: calc(var(--ds-composer-control) - var(--padding-050) * 2 - var(--border-025) * 2);
674
+ --ds-composer-radius: calc(
675
+ min(var(--radius-pill), var(--ds-composer-send-size) / 2) + var(--padding-050) + var(--border-025)
676
+ );
677
+ flex-direction: row;
678
+ align-items: center;
679
+ box-shadow: none;
680
+ }
681
+
682
+ .ds-composer--inline.ds-composer--compact {
683
+ --ds-composer-control: var(--control-height-compact);
684
+ --ds-composer-line: var(--font-paragraph-sm-line-height);
685
+ }
686
+
687
+ .ds-composer--inline .ds-composer__content,
688
+ .ds-composer--inline.ds-composer--compact .ds-composer__content {
689
+ --ds-composer-text-block: calc((var(--ds-composer-control) - var(--ds-composer-line)) / 2 - var(--border-025));
690
+ flex: 1;
691
+ min-width: 0;
692
+ width: auto;
693
+ padding-block: var(--ds-composer-text-block);
694
+ padding-inline: var(--ds-composer-text-inset) var(--padding-100);
695
+ height: calc(var(--ds-composer-text-height, var(--ds-composer-line)) + var(--ds-composer-text-block) * 2);
696
+ }
697
+
698
+ .ds-composer--inline .ds-composer__footer,
699
+ .ds-composer--inline.ds-composer--compact .ds-composer__footer {
700
+ flex-shrink: 0;
701
+ padding: var(--padding-050);
702
+ }
703
+
704
+ .ds-composer--inline .ds-composer__footer .ds-circular-button {
705
+ width: var(--ds-composer-send-size);
706
+ height: var(--ds-composer-send-size);
707
+ }
@@ -32,7 +32,11 @@ type ComposerOwnProps = {
32
32
  streaming?: boolean;
33
33
  /** Fires when the stop button is pressed while `streaming`. */
34
34
  onStop?: () => void;
35
- /** Growth cap in text rows before the textarea scrolls internally. */
35
+ /** The density, matching a table's or a bar's: `compact` tightens the padding and takes the smaller text and buttons. */
36
+ size?: 'default' | 'compact';
37
+ /** One row with the send button on the text's line, the shell exactly `size`'s control height, so it lines up with an Input or a table's toolbar. For a popover, a sheet or a tight bar. `maxRows` defaults to 1. */
38
+ inline?: boolean;
39
+ /** Growth cap in text rows before the textarea scrolls internally. Defaults to 8, or 1 when `inline`. */
36
40
  maxRows?: number;
37
41
  /**
38
42
  * While focused, the shell wears AiButton's slowly rotating gradient ring
@@ -34,7 +34,9 @@ const Composer = React.forwardRef(
34
34
  onSubmit,
35
35
  streaming = false,
36
36
  onStop,
37
- maxRows = 8,
37
+ size = "default",
38
+ inline = false,
39
+ maxRows = inline ? 1 : 8,
38
40
  aiGlow = false,
39
41
  context,
40
42
  contextIcon,
@@ -362,6 +364,8 @@ const Composer = React.forwardRef(
362
364
  const classes = [
363
365
  baseClass,
364
366
  aiGlow ? `${baseClass}--ai-glow` : "",
367
+ size === "compact" ? `${baseClass}--compact` : "",
368
+ inline ? `${baseClass}--inline` : "",
365
369
  streaming ? `${baseClass}--streaming` : "",
366
370
  disabled ? `${baseClass}--disabled` : "",
367
371
  dragging ? `${baseClass}--dragging` : "",
@@ -469,6 +473,7 @@ const Composer = React.forwardRef(
469
473
  {
470
474
  icon: "add",
471
475
  variant: "tertiary",
476
+ size,
472
477
  ariaLabel: attachLabel,
473
478
  disabled,
474
479
  onClick: () => fileInputRef.current?.click()
@@ -483,6 +488,7 @@ const Composer = React.forwardRef(
483
488
  {
484
489
  icon: "stop",
485
490
  variant: "primary",
491
+ size,
486
492
  ariaLabel: stopLabel,
487
493
  disabled,
488
494
  onClick: onStop
@@ -492,6 +498,7 @@ const Composer = React.forwardRef(
492
498
  {
493
499
  icon: "arrow_upward",
494
500
  variant: "primary",
501
+ size,
495
502
  ariaLabel: sendLabel,
496
503
  disabled: !canSend,
497
504
  onClick: submit
@@ -848,7 +848,10 @@
848
848
  read, from a visually hidden thead.
849
849
  ============================================ */
850
850
 
851
+ /* The wrapper qualifier outranks Table's phone rule, which holds a
852
+ column table at max-content so it scrolls: cards fit the frame. */
851
853
  .ds-data-table__grid--stacked,
854
+ .ds-table__wrapper .ds-data-table__grid--stacked,
852
855
  .ds-data-table__grid--stacked > tbody {
853
856
  display: block;
854
857
  width: 100%;
@@ -901,8 +904,10 @@
901
904
  width: auto;
902
905
  }
903
906
 
907
+ /* The actions close the title line, at its end, with or without a row header beside them. */
904
908
  .ds-data-table__grid--stacked .ds-data-table__actions {
905
909
  order: 1;
910
+ margin-inline-start: auto;
906
911
  }
907
912
 
908
913
  /* A row has no drag on a card, and no filler column. */
@@ -922,11 +927,10 @@
922
927
  gap: var(--gap-200);
923
928
  }
924
929
 
930
+ /* A long header wraps in its quarter rather than losing its end. */
925
931
  .ds-data-table__stack-label {
926
932
  min-width: 0;
927
- overflow: hidden;
928
- text-overflow: ellipsis;
929
- white-space: nowrap;
933
+ overflow-wrap: break-word;
930
934
  font-family: var(--font-caption-family);
931
935
  font-size: var(--font-caption-size);
932
936
  font-weight: var(--font-caption-weight);
@@ -19,6 +19,8 @@ type PopoverOwnProps = {
19
19
  surface?: 'panel' | 'menu';
20
20
  /** Preferred position. `left` and `right` are screen sides; `start` and `end` follow the reading direction, so `end` opens to the right in English and to the left in Arabic. */
21
21
  position?: 'top' | 'bottom' | 'left' | 'right' | 'start' | 'end';
22
+ /** Which edge of the trigger the panel lines up with, on the top and bottom sides. `start` and `end` follow the reading direction. Defaults to `start`. */
23
+ align?: 'start' | 'end';
22
24
  /** Trigger mode */
23
25
  trigger?: 'click' | 'hover';
24
26
  /** Whether the popover is open (controlled mode) */
@@ -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);
@@ -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,