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.
- package/bin/templates/blocks/app/dashboard/page.module.css +4 -3
- package/bin/templates/blocks/app/dashboard/page.tsx +1 -1
- package/bin/templates/blocks/components/blocks/AppShell.module.css +50 -0
- package/bin/templates/blocks/components/blocks/AppShell.tsx +26 -7
- package/chat/ChatLauncher.d.ts +11 -1
- package/chat/ChatLauncher.js +7 -1
- package/chat/ChatThreads.d.ts +3 -1
- package/chat/ChatThreads.js +3 -1
- package/chat/ChatWidget.css +21 -0
- package/components/AppLayout/AppLayout.css +90 -2
- package/components/AppLayout/AppLayout.d.ts +10 -2
- package/components/AppLayout/AppLayout.js +20 -2
- package/components/AppSidebar/AppSidebar.css +104 -38
- package/components/AppSidebar/AppSidebar.d.ts +15 -4
- package/components/AppSidebar/AppSidebar.js +8 -2
- package/components/DataTable/DataTable.js +29 -9
- package/components/DataTable/FilterEditor.js +1 -2
- package/components/DataTable/filterModel.js +4 -3
- package/components/DataTable/views.d.ts +9 -0
- package/components/DataTable/views.js +10 -1
- package/components/SegmentedControl/SegmentedControl.css +69 -0
- package/components/SegmentedControl/SegmentedControl.d.ts +4 -3
- package/components/SegmentedControl/SegmentedControl.js +90 -48
- package/components/Stat/Stat.css +57 -0
- package/components/Stat/Stat.d.ts +8 -2
- package/components/Stat/Stat.js +4 -2
- package/components/TableToolbar/TableToolbar.css +6 -0
- package/components/TableToolbar/TableToolbar.js +52 -16
- package/components/ThemeToggle/ThemeToggle.css +2 -72
- package/components/ThemeToggle/ThemeToggle.d.ts +1 -1
- package/components/ThemeToggle/ThemeToggle.js +4 -40
- package/components/registry.json +1 -1
- package/components/registry.json.d.ts +1 -1
- package/components/registry.json.js +1 -1
- package/package.json +1 -1
- package/tokens/registry.json +2 -0
- package/tokens/registry.json.d.ts +2 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +7 -0
- 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
|
|
3
|
-
|
|
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-
|
|
85
|
+
gap: var(--gap-300);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
.panels {
|
|
@@ -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
|
-
<
|
|
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
|
|
108
|
+
<CommandPaletteTrigger className={styles.search} />
|
|
90
109
|
<div className={styles.topBarActions}>
|
|
91
|
-
<CommandPaletteTrigger
|
|
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
|
|
95
|
-
<ThemeToggle
|
|
113
|
+
<AiButton />
|
|
114
|
+
<ThemeToggle showLabels={false} />
|
|
96
115
|
</div>
|
|
97
116
|
</header>
|
|
98
117
|
|
package/chat/ChatLauncher.d.ts
CHANGED
|
@@ -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;
|
package/chat/ChatLauncher.js
CHANGED
|
@@ -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",
|
package/chat/ChatThreads.d.ts
CHANGED
|
@@ -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;
|
package/chat/ChatThreads.js
CHANGED
|
@@ -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
|
}
|
package/chat/ChatWidget.css
CHANGED
|
@@ -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-
|
|
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
|
|
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:
|
|
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
|
);
|