rift-ds 1.17.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 (59) 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/bin/templates/blocks/app/dashboard/page.module.css +4 -3
  6. package/bin/templates/blocks/app/dashboard/page.tsx +1 -1
  7. package/bin/templates/blocks/components/blocks/AppShell.module.css +50 -0
  8. package/bin/templates/blocks/components/blocks/AppShell.tsx +25 -6
  9. package/chat/ChatLauncher.js +23 -0
  10. package/chat/ChatThreads.d.ts +3 -1
  11. package/chat/ChatThreads.js +3 -1
  12. package/chat/ChatWidget.css +8 -3
  13. package/components/AiButton/AiButton.css +11 -0
  14. package/components/AiButton/AiButton.d.ts +2 -0
  15. package/components/AiButton/AiButton.js +3 -1
  16. package/components/AppSidebar/AppSidebar.css +142 -52
  17. package/components/AppSidebar/AppSidebar.d.ts +2 -2
  18. package/components/AppSidebar/AppSidebar.js +16 -9
  19. package/components/Chart/BarChart.js +1 -0
  20. package/components/Chart/Chart.css +6 -0
  21. package/components/Chart/ComboChart.js +1 -0
  22. package/components/Chart/StackedBarChart.js +1 -0
  23. package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +13 -1
  24. package/components/Composer/Composer.css +72 -0
  25. package/components/Composer/Composer.d.ts +5 -1
  26. package/components/Composer/Composer.js +8 -1
  27. package/components/DataTable/DataTable.css +7 -3
  28. package/components/Popover/Popover.d.ts +2 -0
  29. package/components/Popover/Popover.js +2 -1
  30. package/components/SegmentedControl/SegmentedControl.css +3 -1
  31. package/components/SegmentedControl/SegmentedControl.js +2 -0
  32. package/components/SidebarPanel/SidebarPanel.css +10 -2
  33. package/components/SidebarPanel/SidebarPanel.d.ts +1 -1
  34. package/components/SidebarPanel/SidebarPanel.js +5 -2
  35. package/components/SiteHeader/SiteHeader.css +63 -5
  36. package/components/SiteHeader/SiteHeader.d.ts +11 -3
  37. package/components/SiteHeader/SiteHeader.js +51 -16
  38. package/components/SourceChip/SourceChip.css +5 -1
  39. package/components/SourceChip/SourceChip.js +3 -0
  40. package/components/Stat/Stat.css +57 -0
  41. package/components/Stat/Stat.d.ts +8 -2
  42. package/components/Stat/Stat.js +4 -2
  43. package/components/TableToolbar/TableToolbar.css +130 -1
  44. package/components/TableToolbar/TableToolbar.d.ts +52 -0
  45. package/components/TableToolbar/TableToolbar.js +232 -32
  46. package/components/ThemeToggle/ThemeToggle.css +26 -0
  47. package/components/ThemeToggle/ThemeToggle.d.ts +11 -1
  48. package/components/ThemeToggle/ThemeToggle.js +36 -5
  49. package/components/Toast/Toast.css +4 -0
  50. package/components/Toast/Toast.d.ts +9 -0
  51. package/components/Toast/Toast.js +9 -2
  52. package/components/Tooltip/Tooltip.css +9 -4
  53. package/components/Tooltip/Tooltip.d.ts +4 -2
  54. package/components/Tooltip/Tooltip.js +16 -4
  55. package/index.js +2 -1
  56. package/package.json +1 -1
  57. package/tokens/motion.d.ts +2 -0
  58. package/tokens/motion.js +2 -0
  59. 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
+ };
@@ -1,6 +1,7 @@
1
1
  /* The dashboard's grid. Four stats across on a desktop, two from the
2
- laptop width down to a phone; the two panels sit side by side until the
3
- tablet width, then stack. */
2
+ laptop width down to a phone, where each stat drops its delta under its
3
+ label once its tile is narrow (deltaPlacement="auto"); the two panels sit
4
+ side by side until the tablet width, then stack. */
4
5
 
5
6
  .stats {
6
7
  display: grid;
@@ -81,7 +82,7 @@
81
82
 
82
83
  @media (max-width: 600px) {
83
84
  .stats {
84
- gap: var(--gap-400);
85
+ gap: var(--gap-300);
85
86
  }
86
87
 
87
88
  .panels {
@@ -85,7 +85,7 @@ export default function DashboardPage() {
85
85
  value={stat.value}
86
86
  delta={stat.delta}
87
87
  trend={stat.trend}
88
- deltaPlacement="inline"
88
+ deltaPlacement="auto"
89
89
  />
90
90
  </Panel>
91
91
  ))}
@@ -29,6 +29,7 @@
29
29
  }
30
30
 
31
31
  .content {
32
+ position: relative;
32
33
  width: 100%;
33
34
  max-width: 1440px;
34
35
  min-width: 0;
@@ -40,12 +41,61 @@
40
41
 
41
42
  /* ---------------------------------------------------------- top bar */
42
43
 
44
+ /* The bar holds one inset off the top of the page and the content runs
45
+ under it. Once the page scrolls (data-stuck), the page colour fills the
46
+ strip above it, fading out across the bar, and the blur behind it
47
+ dissolves a few rows below. */
43
48
  .topBar {
49
+ position: sticky;
50
+ top: var(--shell-inset);
51
+ z-index: var(--layer-sticky);
44
52
  display: flex;
45
53
  align-items: center;
46
54
  justify-content: space-between;
47
55
  gap: var(--gap-200);
48
56
  min-height: var(--control-height-default);
57
+ /* Room between the bar and the page header under it. */
58
+ margin-bottom: var(--gap-400);
59
+ }
60
+
61
+ .topBar::before {
62
+ content: '';
63
+ position: absolute;
64
+ top: calc(-1 * var(--shell-inset));
65
+ bottom: 0;
66
+ inset-inline: calc(-1 * var(--padding-400));
67
+ z-index: -1;
68
+ background: linear-gradient(to bottom, var(--color-bg-page-primary) var(--shell-inset), transparent);
69
+ pointer-events: none;
70
+ opacity: 0;
71
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
72
+ }
73
+
74
+ .topBar[data-stuck]::before {
75
+ opacity: 1;
76
+ }
77
+
78
+ .topBarBlur:global(.ds-progressive-blur) {
79
+ position: absolute;
80
+ top: calc(-1 * var(--shell-inset));
81
+ bottom: calc(-1 * var(--gap-1000));
82
+ inset-inline: calc(-1 * var(--padding-400));
83
+ height: auto;
84
+ z-index: -1;
85
+ opacity: 0;
86
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
87
+ }
88
+
89
+ .topBarBlur:global(.ds-progressive-blur--tinted) {
90
+ opacity: 1;
91
+ }
92
+
93
+ /* A one-pixel marker at the bar's resting place. Absolute, so the
94
+ column's gap skips it; it keeps its static place at the column's top. */
95
+ .topBarMarker {
96
+ position: absolute;
97
+ width: 1px;
98
+ height: 1px;
49
99
  }
50
100
 
51
101
  .search {
@@ -7,13 +7,17 @@
7
7
  * set. This file is yours: rename the product, change the navigation, and
8
8
  * grow it with your app.
9
9
  *
10
+ * The top bar sticks to the top of the page and the content scrolls under
11
+ * it: bare while the page is at the top, then a fill above it and the
12
+ * library's ProgressiveBlur behind it fade in once the page scrolls.
13
+ *
10
14
  * Responsive by construction: above 768px the sidebar floats beside the
11
15
  * page and the content moves over by its width; below it the sidebar
12
16
  * becomes a drawer behind its own menu button, the search field folds to
13
17
  * its icon, and the content runs edge to edge at a tighter gutter.
14
18
  */
15
19
 
16
- import { useState, type ReactNode } from 'react';
20
+ import { useEffect, useRef, useState, type ReactNode } from 'react';
17
21
  import { AiButton } from 'rift-ds/components/AiButton/AiButton';
18
22
  import {
19
23
  AppSidebar,
@@ -22,6 +26,7 @@ import {
22
26
  } from 'rift-ds/components/AppSidebar/AppSidebar';
23
27
  import { Breadcrumb, type BreadcrumbItem } from 'rift-ds/components/Breadcrumb/Breadcrumb';
24
28
  import { CommandPaletteTrigger } from 'rift-ds/components/CommandPaletteTrigger/CommandPaletteTrigger';
29
+ import { ProgressiveBlur } from 'rift-ds/components/ProgressiveBlur/ProgressiveBlur';
25
30
  import { ThemeToggle } from 'rift-ds/components/ThemeToggle/ThemeToggle';
26
31
  import styles from './AppShell.module.css';
27
32
 
@@ -69,6 +74,18 @@ export interface AppShellProps {
69
74
  export function AppShell({ activeKey, breadcrumb, title, actions, children }: AppShellProps) {
70
75
  const [expanded, setExpanded] = useState(true);
71
76
 
77
+ // The top bar is stuck once the marker at its resting place scrolls out
78
+ // of view; the fill and the blur behind the bar follow.
79
+ const markerRef = useRef<HTMLDivElement>(null);
80
+ const [stuck, setStuck] = useState(false);
81
+ useEffect(() => {
82
+ const marker = markerRef.current;
83
+ if (!marker) return;
84
+ const observer = new IntersectionObserver(([entry]) => setStuck(!entry.isIntersecting));
85
+ observer.observe(marker);
86
+ return () => observer.disconnect();
87
+ }, []);
88
+
72
89
  return (
73
90
  <div className={styles.shell}>
74
91
  <AppSidebar
@@ -83,16 +100,18 @@ export function AppShell({ activeKey, breadcrumb, title, actions, children }: Ap
83
100
 
84
101
  <div className={`${styles.main} ${expanded ? styles.mainExpanded : ''}`}>
85
102
  <div className={styles.content}>
86
- <header className={styles.topBar}>
103
+ <div ref={markerRef} className={styles.topBarMarker} aria-hidden="true" />
104
+ <header className={styles.topBar} data-stuck={stuck ? '' : undefined}>
105
+ <ProgressiveBlur className={styles.topBarBlur} tinted={stuck} />
87
106
  {/* Wire both to your command palette: CommandPalette from
88
107
  rift-ds, opened from here and from Cmd+K. */}
89
- <CommandPaletteTrigger size="compact" className={styles.search} />
108
+ <CommandPaletteTrigger className={styles.search} />
90
109
  <div className={styles.topBarActions}>
91
- <CommandPaletteTrigger size="compact" iconOnly className={styles.searchIcon} />
110
+ <CommandPaletteTrigger iconOnly className={styles.searchIcon} />
92
111
  {/* The AI entry. Point it at your chat: `npx rift-ds init chat`
93
112
  scaffolds one, and ChatLauncher opens it. */}
94
- <AiButton size="compact" />
95
- <ThemeToggle size="compact" showLabels={false} />
113
+ <AiButton />
114
+ <ThemeToggle showLabels={false} />
96
115
  </div>
97
116
  </header>
98
117
 
@@ -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) {
@@ -11,6 +11,8 @@ export interface ChatThreadsProps {
11
11
  logoText?: string;
12
12
  /** The signed-in person, shown on the inline rail. */
13
13
  profile?: ThreadPanelProfile;
14
+ /** Rendered in the rail's footer in both seats (above the profile row on the inline rail; the sheet has none): a theme toggle, a link out. Fades out while the inline rail is collapsed. */
15
+ footerSlot?: ReactNode;
14
16
  /** Whether the inline rail can collapse to its icon strip. */
15
17
  collapsible?: boolean;
16
18
  /**
@@ -38,4 +40,4 @@ export interface ChatThreadsProps {
38
40
  * thread lifecycle (`useThreads`). The widget seats it inline on a wide
39
41
  * host and as a slide-over sheet on a narrow one, and tells it which.
40
42
  */
41
- export declare function ChatThreads({ overlay, close, logo, logoText, profile, collapsible, defaultExpanded, historyLabel, threadPanelLabels, actionLabels, }: ChatThreadsProps): import("react").JSX.Element;
43
+ export declare function ChatThreads({ overlay, close, logo, logoText, profile, footerSlot, collapsible, defaultExpanded, historyLabel, threadPanelLabels, actionLabels, }: ChatThreadsProps): import("react").JSX.Element;
@@ -9,6 +9,7 @@ function ChatThreads({
9
9
  logo,
10
10
  logoText,
11
11
  profile,
12
+ footerSlot,
12
13
  collapsible = true,
13
14
  defaultExpanded = false,
14
15
  historyLabel = "Chat history",
@@ -65,7 +66,8 @@ function ChatThreads({
65
66
  logoText: overlay ? logoText : void 0,
66
67
  expanded: overlay || !collapsible ? true : expanded,
67
68
  onExpandedChange: overlay || !collapsible ? void 0 : setExpanded,
68
- profile: overlay ? void 0 : profile
69
+ profile: overlay ? void 0 : profile,
70
+ footerSlot
69
71
  }
70
72
  );
71
73
  }
@@ -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,