rift-ds 1.16.0 → 1.18.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 (40) hide show
  1. package/bin/templates/blocks/app/dashboard/page.module.css +4 -3
  2. package/bin/templates/blocks/app/dashboard/page.tsx +1 -1
  3. package/bin/templates/blocks/components/blocks/AppShell.module.css +50 -0
  4. package/bin/templates/blocks/components/blocks/AppShell.tsx +26 -7
  5. package/chat/ChatLauncher.d.ts +11 -1
  6. package/chat/ChatLauncher.js +7 -1
  7. package/chat/ChatThreads.d.ts +3 -1
  8. package/chat/ChatThreads.js +3 -1
  9. package/chat/ChatWidget.css +21 -0
  10. package/components/AppLayout/AppLayout.css +90 -2
  11. package/components/AppLayout/AppLayout.d.ts +10 -2
  12. package/components/AppLayout/AppLayout.js +20 -2
  13. package/components/AppSidebar/AppSidebar.css +104 -38
  14. package/components/AppSidebar/AppSidebar.d.ts +15 -4
  15. package/components/AppSidebar/AppSidebar.js +8 -2
  16. package/components/DataTable/DataTable.js +29 -9
  17. package/components/DataTable/FilterEditor.js +1 -2
  18. package/components/DataTable/filterModel.js +4 -3
  19. package/components/DataTable/views.d.ts +9 -0
  20. package/components/DataTable/views.js +10 -1
  21. package/components/SegmentedControl/SegmentedControl.css +69 -0
  22. package/components/SegmentedControl/SegmentedControl.d.ts +4 -3
  23. package/components/SegmentedControl/SegmentedControl.js +90 -48
  24. package/components/Stat/Stat.css +57 -0
  25. package/components/Stat/Stat.d.ts +8 -2
  26. package/components/Stat/Stat.js +4 -2
  27. package/components/TableToolbar/TableToolbar.css +6 -0
  28. package/components/TableToolbar/TableToolbar.js +52 -16
  29. package/components/ThemeToggle/ThemeToggle.css +2 -72
  30. package/components/ThemeToggle/ThemeToggle.d.ts +1 -1
  31. package/components/ThemeToggle/ThemeToggle.js +4 -40
  32. package/components/registry.json +1 -1
  33. package/components/registry.json.d.ts +1 -1
  34. package/components/registry.json.js +1 -1
  35. package/package.json +1 -1
  36. package/tokens/registry.json +2 -0
  37. package/tokens/registry.json.d.ts +2 -0
  38. package/tokens/registry.json.js +1 -1
  39. package/tokens/tokens-dark.css +7 -0
  40. package/tokens/tokens-light.css +7 -0
@@ -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
@@ -78,21 +95,23 @@ export function AppShell({ activeKey, breadcrumb, title, actions, children }: Ap
78
95
  expanded={expanded}
79
96
  onExpandedChange={setExpanded}
80
97
  logoText={PRODUCT_NAME}
81
- floating
98
+ layout="floating"
82
99
  />
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
 
@@ -44,6 +44,16 @@ export interface ChatLauncherProps {
44
44
  widget?: Omit<ChatWidgetProps, 'fullscreenEnabled' | 'placementEnabled' | 'compact' | 'phone'>;
45
45
  /** Rendered inside the panel after the widget: for a host's extra chrome. */
46
46
  children?: ReactNode;
47
+ /**
48
+ * How the docked panel sits on the page. `raised` (the default) is a glass
49
+ * card with the floating shadow, inset by `--ds-chat-inset`. `inset` is
50
+ * flat: the app surface colour, no shadow, border or blur, and the
51
+ * `--ds-shell-inset` gap, so it matches AppLayout's `inset` content
52
+ * surface beside it. The overlay below the dock threshold takes the same
53
+ * gap but keeps its shadow, since it still covers the page; the floating
54
+ * card and the takeover keep their own look.
55
+ */
56
+ surface?: 'raised' | 'inset';
47
57
  }
48
58
  /**
49
59
  * The site-wide panel. Mount it once, after the page content, so it sits
@@ -68,4 +78,4 @@ export interface ChatLauncherProps {
68
78
  * touch the host's body). The panel's geometry reads `--ds-chat-width`,
69
79
  * `--ds-chat-inset` and `--ds-chat-float-height`, defaulted on `:root`.
70
80
  */
71
- export declare function ChatLauncher({ label, icon, summary, hidden, panelId, hostAttribute, panelLabel, aiButtonLabels, widget, children, }: ChatLauncherProps): import("react").JSX.Element | null;
81
+ export declare function ChatLauncher({ label, icon, summary, hidden, panelId, hostAttribute, panelLabel, aiButtonLabels, widget, children, surface, }: ChatLauncherProps): import("react").JSX.Element | null;
@@ -44,7 +44,8 @@ function ChatLauncher({
44
44
  panelLabel = "Chat",
45
45
  aiButtonLabels,
46
46
  widget,
47
- children
47
+ children,
48
+ surface = "raised"
48
49
  }) {
49
50
  const { open, setOpen, panelPhase, view, side, floating, returnFocusRef, send } = useChatWidget();
50
51
  const launchSuggestion = (id) => {
@@ -270,6 +271,10 @@ function ChatLauncher({
270
271
  side === "left" ? "ds-chat-launcher__panel--left" : "",
271
272
  isFloating ? "ds-chat-launcher__panel--floating" : "",
272
273
  isFull ? "ds-chat-launcher__panel--full" : "",
274
+ // Inset: the dock and the overlay both sit at the shell's gap; only the
275
+ // dock goes flat, since the overlay still covers the page.
276
+ surface === "inset" && !isFloating && !isFull ? "ds-chat-launcher__panel--inset" : "",
277
+ surface === "inset" && docked && !isFloating && !isFull ? "ds-chat-launcher__panel--flat" : "",
273
278
  panelPhase === "closing" ? "ds-chat-launcher__panel--closing" : ""
274
279
  ].filter(Boolean).join(" ");
275
280
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -332,6 +337,7 @@ function ChatLauncher({
332
337
  className: [
333
338
  "ds-chat-launcher__dock-handle",
334
339
  side === "left" ? "ds-chat-launcher__dock-handle--left" : "",
340
+ surface === "inset" ? "ds-chat-launcher__dock-handle--inset" : "",
335
341
  resizing ? "ds-chat-launcher__dock-handle--resizing" : ""
336
342
  ].filter(Boolean).join(" "),
337
343
  "aria-hidden": "true",
@@ -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
  }
@@ -995,6 +995,22 @@ html[data-chat-resizing] .ds-chat-launcher__panel {
995
995
  left: var(--ds-chat-inset); /* ds-allow(physical): the left dock is the screen's left edge */
996
996
  }
997
997
 
998
+ /* The inset surface (ChatLauncher's surface="inset"). Docked and as an
999
+ overlay, the panel is held off the edges by the shell's gap rather than
1000
+ the chat's own, so it lines up with AppLayout's inset content surface.
1001
+ Docked, it is also flat: a plane on the app surface colour with no
1002
+ shadow, border or blur. The overlay keeps its shadow over the page. */
1003
+ .ds-chat-launcher__panel--inset {
1004
+ --ds-chat-inset: var(--ds-shell-inset, 10px); /* AppSidebar.css declares the gap; the fallback mirrors it */
1005
+ }
1006
+
1007
+ .ds-chat-launcher__panel--flat {
1008
+ background: var(--color-bg-app-surface);
1009
+ border-color: transparent;
1010
+ box-shadow: none;
1011
+ backdrop-filter: none;
1012
+ }
1013
+
998
1014
  /* Fullscreen view: a true viewport takeover. The stronger blur keeps the
999
1015
  transcript readable while the page's colours still shine through. The
1000
1016
  border fades rather than vanishing, so the geometry never snaps mid-glide. */
@@ -1047,6 +1063,11 @@ html[data-chat-resizing] .ds-chat-launcher__panel {
1047
1063
  left: calc(var(--ds-chat-inset) + var(--ds-chat-width) - 10px); /* ds-allow(physical): the dock grip tracks the docked card's screen edge */
1048
1064
  }
1049
1065
 
1066
+ /* The inset surface's dock sits at the shell's gap, so its grip does too. */
1067
+ .ds-chat-launcher__dock-handle--inset {
1068
+ --ds-chat-inset: var(--ds-shell-inset, 10px); /* AppSidebar.css declares the gap; the fallback mirrors it */
1069
+ }
1070
+
1050
1071
  .ds-chat-launcher__dock-handle::after {
1051
1072
  content: "";
1052
1073
  width: 4px;
@@ -19,7 +19,7 @@
19
19
  /* The rail's width plus, while it shows, the side panel's. */
20
20
  margin-inline-start: calc(var(--ds-app-layout-rail, 64px) + var(--ds-app-layout-panel, 0px));
21
21
  padding: var(--padding-1000) var(--padding-500);
22
- transition: margin-left var(--motion-duration-slow) var(--motion-ease-emphasized);
22
+ transition: margin-inline-start var(--motion-duration-slow) var(--motion-ease-emphasized);
23
23
  min-width: 0;
24
24
  }
25
25
 
@@ -47,6 +47,72 @@
47
47
  gap: var(--gap-1000);
48
48
  }
49
49
 
50
+ /* ============================================
51
+ FLOATING LAYOUT — the rail floats as a glass
52
+ card, so the main column clears its inset on
53
+ both sides of it
54
+ ============================================ */
55
+ .ds-app-layout--floating .ds-app-layout__main {
56
+ margin-inline-start: calc(
57
+ var(--ds-app-layout-rail, 64px) + var(--ds-app-layout-panel, 0px) +
58
+ var(--ds-sidebar-float-inset, 20px) * 2
59
+ );
60
+ }
61
+
62
+ /* ============================================
63
+ INSET LAYOUT — the rail flush on the shell
64
+ colour, the content full width in a surface
65
+ inset from the other three edges. The page
66
+ holds the viewport and the surface scrolls.
67
+ The rail's own item inset is the gap on its
68
+ side, so the selected item sits evenly
69
+ between the two edges.
70
+ ============================================ */
71
+ .ds-app-layout--inset {
72
+ height: 100dvh;
73
+ min-height: 0;
74
+ background-color: var(--color-bg-app-shell);
75
+ }
76
+
77
+ .ds-app-layout--inset .ds-app-layout__main {
78
+ padding: var(--ds-shell-inset);
79
+ padding-inline-start: 0;
80
+ min-height: 0;
81
+ }
82
+
83
+ .ds-app-layout__surface {
84
+ flex: 1;
85
+ min-width: 0;
86
+ display: flex;
87
+ overflow: hidden;
88
+ background-color: var(--color-bg-app-surface);
89
+ border-radius: var(--radius-600);
90
+ }
91
+
92
+ .ds-app-layout__scroller {
93
+ flex: 1;
94
+ min-width: 0;
95
+ overflow-y: auto;
96
+ padding: var(--padding-1000) var(--padding-1000);
97
+ /* The scrollbar stays invisible until the surface scrolls (data-scrolling
98
+ is set by the scroll handler and cleared once it settles). */
99
+ scrollbar-width: thin;
100
+ scrollbar-color: transparent transparent;
101
+ }
102
+
103
+ .ds-app-layout__scroller[data-scrolling] {
104
+ scrollbar-color: var(--color-bg-container-border) transparent;
105
+ }
106
+
107
+ /* Full width in the surface, and a size container: content inside can
108
+ restack by the room the surface gives it (an open chat panel or an
109
+ expanded rail narrows it without the viewport changing) with
110
+ @container ds-app-layout-content queries. */
111
+ .ds-app-layout--inset .ds-app-layout__content {
112
+ max-width: none;
113
+ container: ds-app-layout-content / inline-size;
114
+ }
115
+
50
116
  /* ============================================
51
117
  RESPONSIVE
52
118
  ============================================ */
@@ -56,7 +122,29 @@
56
122
  padding: var(--padding-500) var(--padding-400);
57
123
  }
58
124
 
59
- .ds-app-layout--expanded .ds-app-layout__main {
125
+ .ds-app-layout--expanded .ds-app-layout__main,
126
+ .ds-app-layout--floating .ds-app-layout__main {
60
127
  margin-inline-start: 0;
61
128
  }
129
+
130
+ /* Phones: the rail is a drawer, so the inset surface runs edge to edge
131
+ and the page scrolls normally. */
132
+ .ds-app-layout--inset {
133
+ height: auto;
134
+ min-height: 100vh;
135
+ }
136
+
137
+ .ds-app-layout--inset .ds-app-layout__main {
138
+ padding: 0;
139
+ }
140
+
141
+ .ds-app-layout__surface {
142
+ overflow: visible;
143
+ border-radius: 0;
144
+ }
145
+
146
+ .ds-app-layout__scroller {
147
+ overflow-y: visible;
148
+ padding: var(--padding-500) var(--padding-400);
149
+ }
62
150
  }
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { AppSidebarSection, AppSidebarProfile } from '../AppSidebar/AppSidebar';
2
+ import { AppSidebarSection, AppSidebarProfile, AppSidebarLayout } from '../AppSidebar/AppSidebar';
3
3
  import { LinkComponent } from '../../behaviors/LinkProvider';
4
4
  /** Props owned by AppLayout itself — everything else falls through to the outer wrapper `<div>`. */
5
5
  type AppLayoutOwnProps = {
@@ -23,6 +23,14 @@ type AppLayoutOwnProps = {
23
23
  logo?: React.ReactNode;
24
24
  /** Page content — centred in the main area */
25
25
  children: React.ReactNode;
26
+ /**
27
+ * How the rail and the content meet the page. `flush` (the default) runs
28
+ * the rail edge to edge with the content centred beside it. `floating`
29
+ * floats the rail as a glass card, the content still centred. `inset` runs
30
+ * the rail flush on the app shell colour and sets the content full width
31
+ * in a surface inset from the other edges, which scrolls inside itself.
32
+ */
33
+ layout?: AppSidebarLayout;
26
34
  /**
27
35
  * Colour scheme: 'dark' pins the layout to the dark theme (the historical
28
36
  * behaviour and the default); 'inherit' drops the pin so the layout
@@ -42,7 +50,7 @@ export interface AppLayoutProps extends AppLayoutOwnProps, Omit<React.ComponentP
42
50
  }
43
51
  /**
44
52
  * AppLayout — full-page template with a collapsible sidebar
45
- * and a centred content area.
53
+ * and a content area, centred or set in an inset surface (see `layout`).
46
54
  *
47
55
  * The forwarded ref and any unrecognised props target the outer wrapper
48
56
  * `<div>`. With `theme="inherit"` a `data-theme` passed by the consumer
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsxs, jsx } from "react/jsx-runtime";
3
- import React, { useState, useCallback } from "react";
3
+ import React, { useState, useCallback, useRef, useEffect } from "react";
4
4
  import { AppSidebar } from "../AppSidebar/AppSidebar.js";
5
+ import { MOTION_SCROLL_SETTLE_MS } from "../../tokens/motion.js";
5
6
  import "./AppLayout.css";
6
7
  const AppLayout = React.forwardRef(
7
8
  ({
@@ -15,6 +16,7 @@ const AppLayout = React.forwardRef(
15
16
  logoText,
16
17
  logo,
17
18
  children,
19
+ layout = "flush",
18
20
  theme = "dark",
19
21
  linkComponent,
20
22
  className = "",
@@ -25,9 +27,20 @@ const AppLayout = React.forwardRef(
25
27
  const handleExpandedChange = useCallback((next) => {
26
28
  setExpanded(next);
27
29
  }, []);
30
+ const scrollTimer = useRef(void 0);
31
+ useEffect(() => () => clearTimeout(scrollTimer.current), []);
32
+ const handleSurfaceScroll = useCallback((event) => {
33
+ const scroller = event.currentTarget;
34
+ scroller.dataset.scrolling = "";
35
+ clearTimeout(scrollTimer.current);
36
+ scrollTimer.current = setTimeout(() => {
37
+ delete scroller.dataset.scrolling;
38
+ }, MOTION_SCROLL_SETTLE_MS);
39
+ }, []);
28
40
  const classes = [
29
41
  baseClass,
30
42
  expanded ? `${baseClass}--expanded` : "",
43
+ layout !== "flush" ? `${baseClass}--${layout}` : "",
31
44
  className
32
45
  ].filter(Boolean).join(" ");
33
46
  return /* @__PURE__ */ jsxs(
@@ -51,10 +64,15 @@ const AppLayout = React.forwardRef(
51
64
  onExpandedChange: handleExpandedChange,
52
65
  logoText,
53
66
  logo,
67
+ layout,
54
68
  linkComponent
55
69
  }
56
70
  ),
57
- /* @__PURE__ */ jsx("main", { className: `${baseClass}__main`, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children }) })
71
+ /* @__PURE__ */ jsx("main", { className: `${baseClass}__main`, children: layout === "inset" ? (
72
+ // The surface clips to its rounded corners and the scroller
73
+ // inside it scrolls, so the scrollbar never squares a corner.
74
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__surface`, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__scroller`, onScroll: handleSurfaceScroll, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children }) }) })
75
+ ) : /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children }) })
58
76
  ]
59
77
  }
60
78
  );