@happyvertical/smrt-svelte 0.51.39 → 0.52.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/AGENTS.md +3 -0
- package/README.md +167 -1
- package/agents/workspace.md +24 -0
- package/dist/browser-ai/adapters/stt/browser-speech.d.ts +11 -0
- package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/browser-speech.js +20 -4
- package/dist/browser-ai/adapters/stt/browser-speech.test.js +113 -0
- package/dist/browser-ai/adapters/stt/dictation-source.d.ts +3 -0
- package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/dictation-source.js +38 -0
- package/dist/browser-ai/adapters/stt/dictation-source.test.js +52 -0
- package/dist/browser-ai/adapters/stt/index.d.ts +2 -1
- package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/index.js +2 -1
- package/dist/components/forms/Form.svelte +118 -8
- package/dist/components/forms/Form.svelte.d.ts +18 -2
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormScope.svelte +202 -0
- package/dist/components/forms/FormScope.svelte.d.ts +22 -0
- package/dist/components/forms/FormScope.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/Form.ui-controls.test.js +119 -0
- package/dist/components/forms/__tests__/Form.webmcp.test.js +5 -3
- package/dist/components/forms/__tests__/FormScope.test.js +39 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte +30 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte +84 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts +14 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/index.d.ts +1 -0
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +1 -0
- package/dist/components/workspace/README.md +11 -0
- package/dist/components/workspace/__tests__/AdminShell.responsive.test.js +586 -0
- package/dist/components/workspace/__tests__/AdminShell.test.js +85 -0
- package/dist/components/workspace/__tests__/TenantNav.test.js +49 -0
- package/dist/components/workspace/__tests__/admin-shell-header-overflow.test.js +25 -0
- package/dist/components/workspace/__tests__/admin-shell-mobile.test.js +217 -0
- package/dist/components/workspace/__tests__/admin-shell-page-contracts.test.js +30 -0
- package/dist/components/workspace/__tests__/admin-shell-responsive-state.test.js +184 -0
- package/dist/components/workspace/__tests__/admin-shell-theme-hooks.test.js +36 -0
- package/dist/components/workspace/__tests__/shell-chrome-components.test.js +108 -0
- package/dist/components/workspace/admin-shell/AdminShell.svelte +998 -40
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +42 -1
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte +170 -0
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte +93 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte +88 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts +14 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte +42 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/TenantNav.svelte +46 -1
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts +37 -0
- package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/mobile-shell-dom.js +150 -0
- package/dist/components/workspace/admin-shell/mobile-shell.d.ts +191 -0
- package/dist/components/workspace/admin-shell/mobile-shell.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/mobile-shell.js +165 -0
- package/dist/components/workspace/admin-shell/settings.d.ts +27 -2
- package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/settings.js +84 -2
- package/dist/components/workspace/admin-shell/state.svelte.d.ts +55 -1
- package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/state.svelte.js +149 -6
- package/dist/components/workspace/admin-shell/types.d.ts +97 -0
- package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/types.js +13 -0
- package/dist/components/workspace/index.d.ts +11 -4
- package/dist/components/workspace/index.d.ts.map +1 -1
- package/dist/components/workspace/index.js +12 -3
- package/dist/i18n/strings.workspace.d.ts +10 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -1
- package/dist/i18n/strings.workspace.js +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/web/__tests__/assistant-dock.integration.svelte.test.js +2 -2
- package/dist/web/__tests__/list-sort-surface.fixture.svelte +41 -0
- package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts +15 -0
- package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/list-sort-surface.svelte.test.js +59 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte +69 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts +16 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/surfaces.fixture.svelte +26 -0
- package/dist/web/__tests__/surfaces.fixture.svelte.d.ts +18 -0
- package/dist/web/__tests__/surfaces.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/surfaces.svelte.test.js +140 -0
- package/dist/web/__tests__/webmcp-ui.test.js +18 -0
- package/dist/web/index.d.ts +4 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +4 -1
- package/dist/web/link-surface.svelte.d.ts +14 -0
- package/dist/web/link-surface.svelte.d.ts.map +1 -0
- package/dist/web/link-surface.svelte.js +49 -0
- package/dist/web/list-surface.svelte.d.ts +80 -0
- package/dist/web/list-surface.svelte.d.ts.map +1 -0
- package/dist/web/list-surface.svelte.js +309 -0
- package/dist/web/step-surface.svelte.d.ts +17 -0
- package/dist/web/step-surface.svelte.d.ts.map +1 -0
- package/dist/web/step-surface.svelte.js +56 -0
- package/dist/web/webmcp-ui-context.d.ts +7 -0
- package/dist/web/webmcp-ui-context.d.ts.map +1 -1
- package/dist/web/webmcp-ui-context.js +10 -0
- package/dist/web/webmcp-ui.d.ts.map +1 -1
- package/dist/web/webmcp-ui.js +16 -3
- package/package.json +16 -16
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
import type { ShellState as ModuleShellState } from './state.svelte.js';
|
|
3
3
|
import type { PanelEdge as ModulePanelEdge } from './types.js';
|
|
4
4
|
|
|
5
|
+
function expandedSize(shell: ModuleShellState, edge: ModulePanelEdge): string {
|
|
6
|
+
const resized = shell.panelSize(edge);
|
|
7
|
+
return resized !== null
|
|
8
|
+
? `${resized}px`
|
|
9
|
+
: shell.config.panels[edge].expandedSize;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
function trackFor(shell: ModuleShellState, edge: ModulePanelEdge): string {
|
|
6
13
|
const state = shell.panels[edge];
|
|
7
14
|
const config = shell.config.panels[edge];
|
|
8
15
|
if (state === 'hidden') return '0rem';
|
|
9
16
|
if (state === 'collapsed') return config.collapsedSize;
|
|
10
|
-
if (
|
|
11
|
-
return
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function expandedSize(shell: ModuleShellState, edge: ModulePanelEdge): string {
|
|
15
|
-
return shell.config.panels[edge].expandedSize;
|
|
17
|
+
if (shell.presentationFor(edge) === 'overlay') return config.collapsedSize;
|
|
18
|
+
return expandedSize(shell, edge);
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
function collapsedSize(shell: ModuleShellState, edge: ModulePanelEdge): string {
|
|
@@ -37,6 +40,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
37
40
|
</script>
|
|
38
41
|
|
|
39
42
|
<script lang="ts">
|
|
43
|
+
import { swipeDismiss } from '@happyvertical/smrt-ui/feedback';
|
|
40
44
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
41
45
|
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
42
46
|
import { onMount, untrack } from 'svelte';
|
|
@@ -48,12 +52,22 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
48
52
|
formatHotkeyBinding,
|
|
49
53
|
shellActionFromKeyboardEvent,
|
|
50
54
|
} from './hotkeys.js';
|
|
51
|
-
import {
|
|
55
|
+
import { type BottomBarMode, bottomBarMode } from './mobile-shell.js';
|
|
56
|
+
import {
|
|
57
|
+
installKeyboardWatcher,
|
|
58
|
+
installScrollChrome,
|
|
59
|
+
installShellOverlayMedia,
|
|
60
|
+
installShellViewport,
|
|
61
|
+
watchFormActionBar,
|
|
62
|
+
} from './mobile-shell-dom.js';
|
|
63
|
+
import { clampPanelSize, resolveHotkey } from './settings.js';
|
|
52
64
|
import { createShellState, type ShellState } from './state.svelte.js';
|
|
53
65
|
import type {
|
|
66
|
+
AdminShellPhoneOptions,
|
|
54
67
|
AdminShellProps,
|
|
55
68
|
PanelEdge,
|
|
56
69
|
ShellFocusTool,
|
|
70
|
+
ShellViewport,
|
|
57
71
|
} from './types.js';
|
|
58
72
|
|
|
59
73
|
interface Props extends AdminShellProps {
|
|
@@ -87,6 +101,39 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
87
101
|
bottomRightCorner?: Snippet;
|
|
88
102
|
/** Content for the keyboard shortcuts overlay. */
|
|
89
103
|
shortcutsOverlay?: Snippet;
|
|
104
|
+
/**
|
|
105
|
+
* A full-width header row above every edge (`#smrt-admin-shell-header`,
|
|
106
|
+
* 3.5rem; override `--smrt-admin-shell-header-size`). Shown at every
|
|
107
|
+
* width except on phones when `phoneTopBar` is given.
|
|
108
|
+
*/
|
|
109
|
+
header?: Snippet<[{ viewport: ShellViewport }]>;
|
|
110
|
+
/**
|
|
111
|
+
* Phone-only context bar over the top of the main region. Hides while
|
|
112
|
+
* scrolling down and returns on the first scroll up (see
|
|
113
|
+
* `phone.hideOnScroll`); pinned while `pinChrome` or a drawer is open.
|
|
114
|
+
*/
|
|
115
|
+
phoneTopBar?: Snippet<[{ hidden: boolean }]>;
|
|
116
|
+
/**
|
|
117
|
+
* Phone-only bottom bar row. Replaced by a form's action bar
|
|
118
|
+
* (`[data-form-action-bar]` inside main) and hidden while the on-screen
|
|
119
|
+
* keyboard is open.
|
|
120
|
+
*/
|
|
121
|
+
phoneBottomBar?: Snippet;
|
|
122
|
+
/**
|
|
123
|
+
* Host surfaces laid over the shell above the phone bottom bar (phone
|
|
124
|
+
* sheets, status strips). The layer ignores pointer events; its direct
|
|
125
|
+
* children receive them.
|
|
126
|
+
*/
|
|
127
|
+
overlays?: Snippet<[{ viewport: ShellViewport; bottomBar: BottomBarMode }]>;
|
|
128
|
+
/** Phone behavior switches (scrim, swipe to close, hide on scroll). */
|
|
129
|
+
phone?: AdminShellPhoneOptions;
|
|
130
|
+
/** Keep the phone top bar shown (e.g. while a banner is visible). */
|
|
131
|
+
pinChrome?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Current location. When it changes on a phone, open side drawers and
|
|
134
|
+
* sheets close and the phone top bar comes back.
|
|
135
|
+
*/
|
|
136
|
+
path?: string;
|
|
90
137
|
children: Snippet;
|
|
91
138
|
}
|
|
92
139
|
|
|
@@ -112,6 +159,13 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
112
159
|
bottomLeftCorner,
|
|
113
160
|
bottomRightCorner,
|
|
114
161
|
shortcutsOverlay,
|
|
162
|
+
header,
|
|
163
|
+
phoneTopBar,
|
|
164
|
+
phoneBottomBar,
|
|
165
|
+
overlays,
|
|
166
|
+
phone,
|
|
167
|
+
pinChrome = false,
|
|
168
|
+
path,
|
|
115
169
|
children,
|
|
116
170
|
}: Props = $props();
|
|
117
171
|
|
|
@@ -138,8 +192,162 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
138
192
|
}
|
|
139
193
|
const shortcutEdges: PanelEdge[] = ['top', 'left', 'bottom', 'right'];
|
|
140
194
|
|
|
195
|
+
const SIDE_EDGES = ['left', 'right'] as const;
|
|
196
|
+
type SideEdge = (typeof SIDE_EDGES)[number];
|
|
197
|
+
|
|
198
|
+
let mainElement: HTMLElement | undefined = $state();
|
|
199
|
+
let keyboardOpen = $state(false);
|
|
200
|
+
let formActions = $state(false);
|
|
201
|
+
let chromeHidden = $state(false);
|
|
202
|
+
let resizing = $state<SideEdge | null>(null);
|
|
203
|
+
const sideElements = $state<Partial<Record<SideEdge, HTMLElement>>>({});
|
|
204
|
+
let scrollChrome: ReturnType<typeof installScrollChrome> | null = null;
|
|
205
|
+
|
|
206
|
+
const viewport = $derived(shell.viewport);
|
|
207
|
+
const isPhone = $derived(viewport === 'phone');
|
|
208
|
+
const showPhoneTop = $derived(isPhone && Boolean(phoneTopBar));
|
|
209
|
+
const showHeader = $derived(Boolean(header) && !showPhoneTop);
|
|
210
|
+
const bottomBar = $derived<BottomBarMode>(
|
|
211
|
+
isPhone && phoneBottomBar
|
|
212
|
+
? bottomBarMode({ formActions, keyboardOpen })
|
|
213
|
+
: 'none',
|
|
214
|
+
);
|
|
215
|
+
const phoneSurfaceOpen = $derived(
|
|
216
|
+
isPhone &&
|
|
217
|
+
SIDE_EDGES.some(
|
|
218
|
+
(edge) => panelState(edge) === 'expanded' && shell.isEdgeShown(edge),
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
const chromeStyle = $derived(
|
|
222
|
+
[
|
|
223
|
+
`--smrt-admin-shell-header-track: ${showHeader ? 'var(--smrt-admin-shell-header-size)' : '0rem'}`,
|
|
224
|
+
`--smrt-admin-shell-phone-bar-track: ${bottomBar === 'nav' ? 'var(--smrt-admin-shell-phone-bar-size)' : '0rem'}`,
|
|
225
|
+
].join('; '),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Tablet/desktop: the side edge that is open as an overlay (its
|
|
230
|
+
* `overlayMedia` matches), laid over the page above a scrim.
|
|
231
|
+
*/
|
|
232
|
+
const overlayEdge = $derived<SideEdge | null>(
|
|
233
|
+
isPhone
|
|
234
|
+
? null
|
|
235
|
+
: (SIDE_EDGES.find(
|
|
236
|
+
(edge) =>
|
|
237
|
+
panelState(edge) === 'expanded' &&
|
|
238
|
+
shell.isEdgeShown(edge) &&
|
|
239
|
+
shell.presentationFor(edge) === 'overlay',
|
|
240
|
+
) ?? null),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// An overlay takes focus when it opens (unless something inside already
|
|
244
|
+
// has it) and hands it back to whatever opened it when it closes.
|
|
245
|
+
let overlayReturnFocus: HTMLElement | null = null;
|
|
246
|
+
let lastOverlayEdge: SideEdge | null = null;
|
|
247
|
+
/**
|
|
248
|
+
* An overlay that was just closed and is sliding back out: it keeps its
|
|
249
|
+
* expanded look (inert, above a fading scrim) until the animation ends.
|
|
250
|
+
*/
|
|
251
|
+
let closingEdge = $state<SideEdge | null>(null);
|
|
252
|
+
let closingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
253
|
+
|
|
254
|
+
function endClosing(): void {
|
|
255
|
+
if (closingTimer) clearTimeout(closingTimer);
|
|
256
|
+
closingTimer = null;
|
|
257
|
+
closingEdge = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function prefersReducedMotion(): boolean {
|
|
261
|
+
return (
|
|
262
|
+
typeof window !== 'undefined' &&
|
|
263
|
+
typeof window.matchMedia === 'function' &&
|
|
264
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** An edge shown open: expanded, or an overlay still sliding out. */
|
|
269
|
+
function shownOpen(edge: PanelEdge): boolean {
|
|
270
|
+
return edgeExpanded(edge) || closingEdge === edge;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
$effect(() => {
|
|
274
|
+
const edge = overlayEdge;
|
|
275
|
+
untrack(() => {
|
|
276
|
+
if (edge === lastOverlayEdge) return;
|
|
277
|
+
const previous = lastOverlayEdge;
|
|
278
|
+
lastOverlayEdge = edge;
|
|
279
|
+
if (typeof document === 'undefined') return;
|
|
280
|
+
if (edge) {
|
|
281
|
+
endClosing();
|
|
282
|
+
const active = document.activeElement;
|
|
283
|
+
overlayReturnFocus =
|
|
284
|
+
active instanceof HTMLElement && active !== document.body
|
|
285
|
+
? active
|
|
286
|
+
: null;
|
|
287
|
+
// Effects run after the DOM update, so the panel is already shown.
|
|
288
|
+
const panel = sideElements[edge];
|
|
289
|
+
if (panel && !panel.contains(document.activeElement)) {
|
|
290
|
+
panel.focus({ preventScroll: true });
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const panel = previous ? sideElements[previous] : undefined;
|
|
295
|
+
const active = document.activeElement;
|
|
296
|
+
const lost =
|
|
297
|
+
!active || active === document.body || Boolean(panel?.contains(active));
|
|
298
|
+
if (lost && overlayReturnFocus?.isConnected) {
|
|
299
|
+
overlayReturnFocus.focus({ preventScroll: true });
|
|
300
|
+
}
|
|
301
|
+
overlayReturnFocus = null;
|
|
302
|
+
// Closed (not just docked by a wider window): slide back out.
|
|
303
|
+
if (
|
|
304
|
+
previous &&
|
|
305
|
+
panelState(previous) !== 'expanded' &&
|
|
306
|
+
shell.presentationFor(previous) === 'overlay' &&
|
|
307
|
+
!isPhone &&
|
|
308
|
+
!prefersReducedMotion()
|
|
309
|
+
) {
|
|
310
|
+
endClosing();
|
|
311
|
+
closingEdge = previous;
|
|
312
|
+
// Fallback when animationend never fires (hidden tab, no CSS).
|
|
313
|
+
closingTimer = setTimeout(endClosing, 600);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
function onEdgeAnimationEnd(event: AnimationEvent, edge: SideEdge): void {
|
|
319
|
+
if (closingEdge === edge && event.target === event.currentTarget) {
|
|
320
|
+
endClosing();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function collapsePhoneSurfaces(): void {
|
|
325
|
+
for (const edge of SIDE_EDGES) {
|
|
326
|
+
if (panelState(edge) === 'expanded' && shell.isEdgeShown(edge)) {
|
|
327
|
+
shell.collapsePanel(edge);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
141
332
|
onMount(() => {
|
|
142
|
-
|
|
333
|
+
const offViewport = installShellViewport(shell);
|
|
334
|
+
const offOverlayMedia = installShellOverlayMedia(shell);
|
|
335
|
+
void shell.hydrate().then(() => shell.closeOverlaidEdges());
|
|
336
|
+
const offKeyboard = phoneBottomBar
|
|
337
|
+
? installKeyboardWatcher((open) => (keyboardOpen = open))
|
|
338
|
+
: () => {};
|
|
339
|
+
const offFormActions =
|
|
340
|
+
phoneBottomBar && mainElement
|
|
341
|
+
? watchFormActionBar(mainElement, (present) => (formActions = present))
|
|
342
|
+
: () => {};
|
|
343
|
+
scrollChrome =
|
|
344
|
+
phoneTopBar && phone?.hideOnScroll !== false && mainElement
|
|
345
|
+
? installScrollChrome(mainElement, {
|
|
346
|
+
onChange: (hidden) => (chromeHidden = hidden),
|
|
347
|
+
isPinned: () =>
|
|
348
|
+
pinChrome || phoneSurfaceOpen || shell.viewport !== 'phone',
|
|
349
|
+
})
|
|
350
|
+
: null;
|
|
143
351
|
|
|
144
352
|
function handleKeydown(event: KeyboardEvent): void {
|
|
145
353
|
if (event.key === 'Escape') {
|
|
@@ -167,9 +375,148 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
167
375
|
}
|
|
168
376
|
|
|
169
377
|
window.addEventListener('keydown', handleKeydown);
|
|
170
|
-
return () =>
|
|
378
|
+
return () => {
|
|
379
|
+
window.removeEventListener('keydown', handleKeydown);
|
|
380
|
+
offViewport();
|
|
381
|
+
offOverlayMedia();
|
|
382
|
+
if (closingTimer) clearTimeout(closingTimer);
|
|
383
|
+
offKeyboard();
|
|
384
|
+
offFormActions();
|
|
385
|
+
scrollChrome?.destroy();
|
|
386
|
+
scrollChrome = null;
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Pinning (a banner appears, a drawer opens, leaving phone) shows the bar.
|
|
391
|
+
$effect(() => {
|
|
392
|
+
if (pinChrome || phoneSurfaceOpen || !isPhone) chromeHidden = false;
|
|
171
393
|
});
|
|
172
394
|
|
|
395
|
+
// Navigating on a phone closes drawers/sheets and brings the top bar back.
|
|
396
|
+
let lastPath: string | undefined;
|
|
397
|
+
$effect(() => {
|
|
398
|
+
const current = path;
|
|
399
|
+
untrack(() => {
|
|
400
|
+
if (lastPath !== undefined && current !== lastPath) {
|
|
401
|
+
if (shell.viewport === 'phone') collapsePhoneSurfaces();
|
|
402
|
+
scrollChrome?.reset();
|
|
403
|
+
chromeHidden = false;
|
|
404
|
+
}
|
|
405
|
+
lastPath = current;
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ---- Resizable side edges ------------------------------------------------
|
|
410
|
+
|
|
411
|
+
function remInPx(): number {
|
|
412
|
+
if (typeof window === 'undefined') return 16;
|
|
413
|
+
const size = Number.parseFloat(
|
|
414
|
+
window.getComputedStyle(document.documentElement).fontSize,
|
|
415
|
+
);
|
|
416
|
+
return Number.isFinite(size) && size > 0 ? size : 16;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function cssLengthToPx(length: string): number | null {
|
|
420
|
+
const match = /^\s*(-?\d*\.?\d+)(px|rem|em)?\s*$/.exec(length);
|
|
421
|
+
if (!match) return null;
|
|
422
|
+
const value = Number.parseFloat(match[1]);
|
|
423
|
+
return match[2] === 'rem' || match[2] === 'em' ? value * remInPx() : value;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The edge's expanded width in px: the resized width, else the configured
|
|
428
|
+
* `expandedSize` (measured when it is not a plain px/rem length).
|
|
429
|
+
*/
|
|
430
|
+
function currentSize(edge: SideEdge): number {
|
|
431
|
+
const limits = shell.resizeLimits(edge);
|
|
432
|
+
const resized = shell.panelSize(edge);
|
|
433
|
+
if (resized !== null) return resized;
|
|
434
|
+
const width =
|
|
435
|
+
cssLengthToPx(shell.config.panels[edge].expandedSize) ??
|
|
436
|
+
sideElements[edge]?.getBoundingClientRect().width ??
|
|
437
|
+
0;
|
|
438
|
+
return limits ? clampPanelSize(width, limits) : Math.round(width);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function canResize(edge: SideEdge): boolean {
|
|
442
|
+
return (
|
|
443
|
+
!isPhone &&
|
|
444
|
+
edgeExpanded(edge) &&
|
|
445
|
+
shell.presentationFor(edge) === 'push' &&
|
|
446
|
+
shell.resizeLimits(edge) !== null
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function onResizeKeydown(event: KeyboardEvent, edge: SideEdge): void {
|
|
451
|
+
const limits = shell.resizeLimits(edge);
|
|
452
|
+
if (!limits) return;
|
|
453
|
+
const step = limits.step * (event.shiftKey ? 4 : 1);
|
|
454
|
+
const grow = edge === 'left' ? 'ArrowRight' : 'ArrowLeft';
|
|
455
|
+
const shrink = edge === 'left' ? 'ArrowLeft' : 'ArrowRight';
|
|
456
|
+
let next: number | null;
|
|
457
|
+
if (event.key === grow) next = currentSize(edge) + step;
|
|
458
|
+
else if (event.key === shrink) next = currentSize(edge) - step;
|
|
459
|
+
else if (event.key === 'Home') next = limits.min;
|
|
460
|
+
else if (event.key === 'End') next = limits.max;
|
|
461
|
+
else if (event.key === 'Enter') next = null;
|
|
462
|
+
else return;
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
shell.setPanelSize(edge, next);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Tears down the drag in progress, if any (see onResizePointerdown). */
|
|
468
|
+
let stopActiveResize: ((commit: boolean) => void) | null = null;
|
|
469
|
+
|
|
470
|
+
// Unmounting mid-drag must not leave the handle's listeners driving a shell
|
|
471
|
+
// state that outlives this component, nor a stale `resizing` flag.
|
|
472
|
+
$effect(() => () => stopActiveResize?.(false));
|
|
473
|
+
|
|
474
|
+
function onResizePointerdown(event: PointerEvent, edge: SideEdge): void {
|
|
475
|
+
if (event.button !== 0 || !shell.resizeLimits(edge)) return;
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
stopActiveResize?.(true);
|
|
478
|
+
const handle = event.currentTarget as HTMLElement;
|
|
479
|
+
const pointerId = event.pointerId;
|
|
480
|
+
if (pointerId !== undefined) handle.setPointerCapture?.(pointerId);
|
|
481
|
+
const startX = event.clientX;
|
|
482
|
+
const startSize = currentSize(edge);
|
|
483
|
+
let latest = startSize;
|
|
484
|
+
resizing = edge;
|
|
485
|
+
|
|
486
|
+
function onMove(move: PointerEvent): void {
|
|
487
|
+
const dx = move.clientX - startX;
|
|
488
|
+
latest = startSize + (edge === 'left' ? dx : -dx);
|
|
489
|
+
shell.setPanelSize(edge, latest, { persist: false });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function stop(commit: boolean): void {
|
|
493
|
+
if (stopActiveResize !== stop) return;
|
|
494
|
+
stopActiveResize = null;
|
|
495
|
+
handle.removeEventListener('pointermove', onMove);
|
|
496
|
+
handle.removeEventListener('pointerup', onEnd);
|
|
497
|
+
handle.removeEventListener('pointercancel', onEnd);
|
|
498
|
+
handle.removeEventListener('lostpointercapture', onEnd);
|
|
499
|
+
if (pointerId !== undefined && handle.hasPointerCapture?.(pointerId)) {
|
|
500
|
+
handle.releasePointerCapture(pointerId);
|
|
501
|
+
}
|
|
502
|
+
resizing = null;
|
|
503
|
+
if (commit && latest !== startSize) shell.setPanelSize(edge, latest);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// pointerup/pointercancel end a drag normally; losing the capture (a
|
|
507
|
+
// system gesture, the element leaving the DOM) ends it too, or the shell
|
|
508
|
+
// would stay in its resizing state with nothing to release it.
|
|
509
|
+
function onEnd(): void {
|
|
510
|
+
stop(true);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
stopActiveResize = stop;
|
|
514
|
+
handle.addEventListener('pointermove', onMove);
|
|
515
|
+
handle.addEventListener('pointerup', onEnd);
|
|
516
|
+
handle.addEventListener('pointercancel', onEnd);
|
|
517
|
+
handle.addEventListener('lostpointercapture', onEnd);
|
|
518
|
+
}
|
|
519
|
+
|
|
173
520
|
const layoutStyle = $derived(buildLayoutStyle(shell));
|
|
174
521
|
|
|
175
522
|
// Top/bottom edges reserve grid tracks for corner snippets. When a corner
|
|
@@ -194,6 +541,18 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
194
541
|
return panelState(edge) === 'expanded';
|
|
195
542
|
}
|
|
196
543
|
|
|
544
|
+
/** Collapsed edge whose panel content stays mounted (`keepMounted`). */
|
|
545
|
+
function keepsContent(edge: PanelEdge): boolean {
|
|
546
|
+
return (
|
|
547
|
+
panelState(edge) === 'collapsed' &&
|
|
548
|
+
shell.config.panels[edge].keepMounted === true
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function panelMounted(edge: PanelEdge): boolean {
|
|
553
|
+
return shownOpen(edge) || keepsContent(edge);
|
|
554
|
+
}
|
|
555
|
+
|
|
197
556
|
function labelFor(edge: PanelEdge): string {
|
|
198
557
|
return shell.config.panels[edge].label;
|
|
199
558
|
}
|
|
@@ -273,6 +632,30 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
273
632
|
</Button>
|
|
274
633
|
{/snippet}
|
|
275
634
|
|
|
635
|
+
{#snippet resizer(edge: SideEdge)}
|
|
636
|
+
{#if canResize(edge)}
|
|
637
|
+
{@const limits = shell.resizeLimits(edge)}
|
|
638
|
+
<!-- A focusable separator is the WAI-ARIA window-splitter widget; Svelte's
|
|
639
|
+
role table lists `separator` as non-interactive only in its static form. -->
|
|
640
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
641
|
+
<div
|
|
642
|
+
class="smrt-admin-shell__resizer smrt-admin-shell__resizer--{edge}"
|
|
643
|
+
role="separator"
|
|
644
|
+
aria-orientation="vertical"
|
|
645
|
+
aria-controls={`smrt-admin-shell-${edge}-panel`}
|
|
646
|
+
aria-label={t(M['ui.admin_shell.resize_panel'], { label: labelFor(edge) })}
|
|
647
|
+
aria-valuemin={limits?.min}
|
|
648
|
+
aria-valuemax={limits?.max}
|
|
649
|
+
aria-valuenow={currentSize(edge)}
|
|
650
|
+
tabindex="0"
|
|
651
|
+
data-testid={`admin-shell-resizer-${edge}`}
|
|
652
|
+
onpointerdown={(event) => onResizePointerdown(event, edge)}
|
|
653
|
+
onkeydown={(event) => onResizeKeydown(event, edge)}
|
|
654
|
+
ondblclick={() => shell.setPanelSize(edge, null)}
|
|
655
|
+
></div>
|
|
656
|
+
{/if}
|
|
657
|
+
{/snippet}
|
|
658
|
+
|
|
276
659
|
{#snippet focusContent(tool: ShellFocusTool | null)}
|
|
277
660
|
{#if focusPanel}
|
|
278
661
|
{@render focusPanel({ tool })}
|
|
@@ -294,8 +677,23 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
294
677
|
data-left-state={panelState('left')}
|
|
295
678
|
data-right-state={panelState('right')}
|
|
296
679
|
data-bottom-state={panelState('bottom')}
|
|
297
|
-
|
|
680
|
+
data-viewport={viewport}
|
|
681
|
+
data-bottom-bar={bottomBar}
|
|
682
|
+
data-phone-top={showPhoneTop ? '' : undefined}
|
|
683
|
+
data-chrome-hidden={chromeHidden ? '' : undefined}
|
|
684
|
+
data-resizing={resizing ?? undefined}
|
|
685
|
+
style={`${layoutStyle}; ${chromeStyle}`}
|
|
298
686
|
>
|
|
687
|
+
{#if showHeader}
|
|
688
|
+
<div
|
|
689
|
+
id="smrt-admin-shell-header"
|
|
690
|
+
class="smrt-admin-shell__header"
|
|
691
|
+
data-testid="admin-shell-header"
|
|
692
|
+
>
|
|
693
|
+
{@render header?.({ viewport })}
|
|
694
|
+
</div>
|
|
695
|
+
{/if}
|
|
696
|
+
|
|
299
697
|
{#if panelState('top') !== 'hidden'}
|
|
300
698
|
<header
|
|
301
699
|
id="smrt-admin-shell-top-panel"
|
|
@@ -330,10 +728,11 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
330
728
|
{@render topRightCorner()}
|
|
331
729
|
</div>
|
|
332
730
|
{/if}
|
|
333
|
-
{#if
|
|
731
|
+
{#if panelMounted('top')}
|
|
334
732
|
<section
|
|
335
733
|
class="smrt-admin-shell__drawer smrt-admin-shell__drawer--top"
|
|
336
734
|
aria-label={labelFor('top')}
|
|
735
|
+
hidden={!edgeExpanded('top')}
|
|
337
736
|
>
|
|
338
737
|
{#if appPanel}
|
|
339
738
|
{@render appPanel()}
|
|
@@ -347,18 +746,36 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
347
746
|
</header>
|
|
348
747
|
{/if}
|
|
349
748
|
|
|
350
|
-
{#if
|
|
749
|
+
{#if shell.isEdgeShown('left')}
|
|
750
|
+
<!-- tabindex -1 only while overlaid, so the panel can take focus on open -->
|
|
751
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
351
752
|
<aside
|
|
352
753
|
id="smrt-admin-shell-left-panel"
|
|
353
754
|
class="smrt-admin-shell__edge smrt-admin-shell__edge--left"
|
|
354
|
-
data-state={panelState('left')}
|
|
355
|
-
data-
|
|
755
|
+
data-state={closingEdge === 'left' ? 'expanded' : panelState('left')}
|
|
756
|
+
data-closing={closingEdge === 'left' ? '' : undefined}
|
|
757
|
+
data-presentation={shell.presentationFor('left')}
|
|
356
758
|
role="navigation"
|
|
357
759
|
aria-label={labelFor('left')}
|
|
760
|
+
tabindex={overlayEdge === 'left' ? -1 : undefined}
|
|
761
|
+
inert={(isPhone && !edgeExpanded('left')) ||
|
|
762
|
+
overlayEdge === 'right' ||
|
|
763
|
+
closingEdge === 'left'}
|
|
764
|
+
onanimationend={(event) => onEdgeAnimationEnd(event, 'left')}
|
|
765
|
+
bind:this={sideElements.left}
|
|
766
|
+
use:swipeDismiss={{
|
|
767
|
+
direction: 'left',
|
|
768
|
+
onDismiss: () => shell.collapsePanel('left'),
|
|
769
|
+
enabled: isPhone && Boolean(phone?.swipeToClose) && edgeExpanded('left'),
|
|
770
|
+
}}
|
|
358
771
|
>
|
|
772
|
+
{@render resizer('left')}
|
|
359
773
|
<div class="smrt-admin-shell__rail">
|
|
360
|
-
{#if
|
|
361
|
-
<div
|
|
774
|
+
{#if shownOpen('left') || (tenantPanel && keepsContent('left'))}
|
|
775
|
+
<div
|
|
776
|
+
class="smrt-admin-shell__tenant-stack"
|
|
777
|
+
hidden={!shownOpen('left')}
|
|
778
|
+
>
|
|
362
779
|
<div class="smrt-admin-shell__tenant-content">
|
|
363
780
|
{#if tenantPanel}
|
|
364
781
|
{@render tenantPanel()}
|
|
@@ -374,27 +791,104 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
374
791
|
</div>
|
|
375
792
|
{/if}
|
|
376
793
|
</div>
|
|
377
|
-
{
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
794
|
+
{/if}
|
|
795
|
+
{#if !shownOpen('left')}
|
|
796
|
+
{#if tenantRail}
|
|
797
|
+
{@render tenantRail()}
|
|
798
|
+
{:else}
|
|
799
|
+
{@render edgeToggle('left')}
|
|
800
|
+
{/if}
|
|
381
801
|
{/if}
|
|
382
802
|
</div>
|
|
383
803
|
</aside>
|
|
384
804
|
{/if}
|
|
385
805
|
|
|
386
|
-
<main
|
|
806
|
+
<main
|
|
807
|
+
id="smrt-admin-shell-main"
|
|
808
|
+
class="smrt-admin-shell__main"
|
|
809
|
+
bind:this={mainElement}
|
|
810
|
+
inert={overlayEdge !== null}
|
|
811
|
+
>
|
|
387
812
|
{@render children()}
|
|
388
813
|
</main>
|
|
389
814
|
|
|
390
|
-
{#if
|
|
815
|
+
{#if showPhoneTop}
|
|
816
|
+
<div class="smrt-admin-shell__phone-top" data-testid="admin-shell-phone-top">
|
|
817
|
+
{@render phoneTopBar?.({ hidden: chromeHidden })}
|
|
818
|
+
</div>
|
|
819
|
+
{/if}
|
|
820
|
+
|
|
821
|
+
{#if isPhone && phone?.scrim && phoneSurfaceOpen}
|
|
822
|
+
<!-- raw-primitive-allow: click-catching backdrop behind a phone drawer -->
|
|
823
|
+
<button
|
|
824
|
+
type="button"
|
|
825
|
+
class="smrt-admin-shell__scrim"
|
|
826
|
+
tabindex="-1"
|
|
827
|
+
aria-label={t(M['ui.admin_shell.close_panel'], {
|
|
828
|
+
label: labelFor(edgeExpanded('left') && shell.isEdgeShown('left') ? 'left' : 'right'),
|
|
829
|
+
})}
|
|
830
|
+
data-testid="admin-shell-scrim"
|
|
831
|
+
onclick={collapsePhoneSurfaces}
|
|
832
|
+
></button>
|
|
833
|
+
{/if}
|
|
834
|
+
|
|
835
|
+
{#if overlayEdge || closingEdge}
|
|
836
|
+
{@const scrimEdge = (overlayEdge ?? closingEdge) as SideEdge}
|
|
837
|
+
<!-- raw-primitive-allow: click-catching backdrop behind an overlaid side panel -->
|
|
838
|
+
<button
|
|
839
|
+
type="button"
|
|
840
|
+
class="smrt-admin-shell__overlay-scrim"
|
|
841
|
+
tabindex="-1"
|
|
842
|
+
aria-label={t(M['ui.admin_shell.close_panel'], { label: labelFor(scrimEdge) })}
|
|
843
|
+
data-testid="admin-shell-overlay-scrim"
|
|
844
|
+
data-closing={overlayEdge ? undefined : ''}
|
|
845
|
+
onclick={() => {
|
|
846
|
+
if (overlayEdge) shell.collapsePanel(scrimEdge);
|
|
847
|
+
}}
|
|
848
|
+
></button>
|
|
849
|
+
{/if}
|
|
850
|
+
|
|
851
|
+
{#if shell.isEdgeShown('right')}
|
|
852
|
+
{@const rightPhone = isPhone ? shell.phonePresentation('right') : undefined}
|
|
853
|
+
<!-- tabindex -1 only while overlaid, so the panel can take focus on open -->
|
|
854
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
391
855
|
<aside
|
|
392
856
|
id="smrt-admin-shell-right-panel"
|
|
393
857
|
class="smrt-admin-shell__edge smrt-admin-shell__edge--right"
|
|
394
|
-
data-state={panelState('right')}
|
|
395
|
-
data-
|
|
858
|
+
data-state={closingEdge === 'right' ? 'expanded' : panelState('right')}
|
|
859
|
+
data-closing={closingEdge === 'right' ? '' : undefined}
|
|
860
|
+
data-presentation={shell.presentationFor('right')}
|
|
861
|
+
data-phone={rightPhone}
|
|
396
862
|
aria-label={labelFor('right')}
|
|
863
|
+
tabindex={overlayEdge === 'right' ? -1 : undefined}
|
|
864
|
+
inert={(isPhone && !edgeExpanded('right')) ||
|
|
865
|
+
overlayEdge === 'left' ||
|
|
866
|
+
closingEdge === 'right'}
|
|
867
|
+
onanimationend={(event) => onEdgeAnimationEnd(event, 'right')}
|
|
868
|
+
bind:this={sideElements.right}
|
|
869
|
+
use:swipeDismiss={{
|
|
870
|
+
direction: 'right',
|
|
871
|
+
onDismiss: () => shell.collapsePanel('right'),
|
|
872
|
+
enabled:
|
|
873
|
+
rightPhone === 'drawer' &&
|
|
874
|
+
Boolean(phone?.swipeToClose) &&
|
|
875
|
+
edgeExpanded('right'),
|
|
876
|
+
}}
|
|
397
877
|
>
|
|
878
|
+
{@render resizer('right')}
|
|
879
|
+
{#if rightPhone === 'sheet'}
|
|
880
|
+
<div
|
|
881
|
+
class="smrt-admin-shell__sheet-grabber"
|
|
882
|
+
aria-hidden="true"
|
|
883
|
+
use:swipeDismiss={{
|
|
884
|
+
direction: 'down',
|
|
885
|
+
onDismiss: () => shell.collapsePanel('right'),
|
|
886
|
+
enabled: edgeExpanded('right'),
|
|
887
|
+
}}
|
|
888
|
+
>
|
|
889
|
+
<span></span>
|
|
890
|
+
</div>
|
|
891
|
+
{/if}
|
|
398
892
|
<div class="smrt-admin-shell__rail">
|
|
399
893
|
{#if focusRail}
|
|
400
894
|
{@render focusRail()}
|
|
@@ -430,8 +924,11 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
430
924
|
{@render edgeToggle('right')}
|
|
431
925
|
{/if}
|
|
432
926
|
</div>
|
|
433
|
-
{#if
|
|
434
|
-
<div
|
|
927
|
+
{#if panelMounted('right')}
|
|
928
|
+
<div
|
|
929
|
+
class="smrt-admin-shell__panel smrt-admin-shell__panel--right"
|
|
930
|
+
hidden={!shownOpen('right')}
|
|
931
|
+
>
|
|
435
932
|
{#key shell.activeFocusToolId}
|
|
436
933
|
{@render focusContent(resolveActiveFocusTool())}
|
|
437
934
|
{/key}
|
|
@@ -468,10 +965,11 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
468
965
|
{@render bottomRightCorner()}
|
|
469
966
|
</div>
|
|
470
967
|
{/if}
|
|
471
|
-
{#if
|
|
968
|
+
{#if panelMounted('bottom')}
|
|
472
969
|
<section
|
|
473
970
|
class="smrt-admin-shell__drawer smrt-admin-shell__drawer--bottom"
|
|
474
971
|
aria-label={labelFor('bottom')}
|
|
972
|
+
hidden={!edgeExpanded('bottom')}
|
|
475
973
|
>
|
|
476
974
|
{#if systemPanel}
|
|
477
975
|
{@render systemPanel()}
|
|
@@ -485,6 +983,18 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
485
983
|
</footer>
|
|
486
984
|
{/if}
|
|
487
985
|
|
|
986
|
+
{#if overlays}
|
|
987
|
+
<div class="smrt-admin-shell__overlays">
|
|
988
|
+
{@render overlays({ viewport, bottomBar })}
|
|
989
|
+
</div>
|
|
990
|
+
{/if}
|
|
991
|
+
|
|
992
|
+
{#if bottomBar === 'nav'}
|
|
993
|
+
<div class="smrt-admin-shell__phone-bar" data-testid="admin-shell-phone-bar">
|
|
994
|
+
{@render phoneBottomBar?.()}
|
|
995
|
+
</div>
|
|
996
|
+
{/if}
|
|
997
|
+
|
|
488
998
|
{#if shortcutsOpen}
|
|
489
999
|
<div
|
|
490
1000
|
class="smrt-admin-shell__shortcuts"
|
|
@@ -537,19 +1047,63 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
537
1047
|
--smrt-admin-shell-right-collapsed: 4.25rem;
|
|
538
1048
|
--smrt-admin-shell-left-expanded: 16rem;
|
|
539
1049
|
--smrt-admin-shell-right-expanded: 20rem;
|
|
1050
|
+
--smrt-admin-shell-header-size: 3.5rem;
|
|
1051
|
+
--smrt-admin-shell-header-track: 0rem;
|
|
1052
|
+
--smrt-admin-shell-phone-top-size: 3.5rem;
|
|
1053
|
+
--smrt-admin-shell-phone-bar-size: calc(3.5rem + env(safe-area-inset-bottom));
|
|
1054
|
+
--smrt-admin-shell-phone-bar-track: 0rem;
|
|
1055
|
+
--smrt-admin-shell-chrome-duration: var(--smrt-duration-short4, 200ms);
|
|
540
1056
|
position: relative;
|
|
541
1057
|
display: grid;
|
|
542
1058
|
grid-template-columns:
|
|
543
1059
|
var(--smrt-admin-shell-left-track) minmax(0, 1fr)
|
|
544
1060
|
var(--smrt-admin-shell-right-track);
|
|
1061
|
+
/* header · top edge · body · bottom edge · phone bottom bar. The extra
|
|
1062
|
+
rows are 0 unless the header / phone bar render. */
|
|
545
1063
|
grid-template-rows:
|
|
1064
|
+
var(--smrt-admin-shell-header-track)
|
|
546
1065
|
var(--smrt-admin-shell-top-track) minmax(0, 1fr)
|
|
547
|
-
var(--smrt-admin-shell-bottom-track)
|
|
1066
|
+
var(--smrt-admin-shell-bottom-track)
|
|
1067
|
+
var(--smrt-admin-shell-phone-bar-track);
|
|
548
1068
|
min-block-size: 100svh;
|
|
549
1069
|
block-size: 100svh;
|
|
550
|
-
|
|
1070
|
+
/* Themeable: an app sets --smrt-admin-shell-background on the shell or an
|
|
1071
|
+
ancestor (a tinted page behind cards, for instance). */
|
|
1072
|
+
background: var(--smrt-admin-shell-background, var(--smrt-color-surface));
|
|
551
1073
|
color: var(--smrt-color-on-surface);
|
|
552
1074
|
overflow: hidden;
|
|
1075
|
+
/* Themed, thin scrollbars for every scroller inside the shell. Override
|
|
1076
|
+
--smrt-admin-shell-scrollbar-track / -thumb / -thumb-hover to retint. */
|
|
1077
|
+
scrollbar-color: var(--smrt-admin-shell-scrollbar-thumb, color-mix(in srgb, var(--smrt-color-on-surface) 26%, transparent))
|
|
1078
|
+
var(--smrt-admin-shell-scrollbar-track, color-mix(in srgb, var(--smrt-color-surface-container-low) 88%, transparent));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.smrt-admin-shell :global(*) {
|
|
1082
|
+
scrollbar-width: thin;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.smrt-admin-shell :global(*)::-webkit-scrollbar {
|
|
1086
|
+
width: 0.7rem;
|
|
1087
|
+
height: 0.7rem;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.smrt-admin-shell :global(*)::-webkit-scrollbar-track {
|
|
1091
|
+
background: var(--smrt-admin-shell-scrollbar-track, color-mix(in srgb, var(--smrt-color-surface-container-low) 88%, transparent));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.smrt-admin-shell :global(*)::-webkit-scrollbar-thumb {
|
|
1095
|
+
background-color: var(--smrt-admin-shell-scrollbar-thumb, color-mix(in srgb, var(--smrt-color-on-surface) 26%, transparent));
|
|
1096
|
+
background-clip: padding-box;
|
|
1097
|
+
border: 2px solid transparent;
|
|
1098
|
+
border-radius: var(--smrt-radius-full, 999px);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.smrt-admin-shell :global(*)::-webkit-scrollbar-thumb:hover {
|
|
1102
|
+
background-color: var(--smrt-admin-shell-scrollbar-thumb-hover, color-mix(in srgb, var(--smrt-color-on-surface) 38%, transparent));
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.smrt-admin-shell :global(*)::-webkit-scrollbar-corner {
|
|
1106
|
+
background: transparent;
|
|
553
1107
|
}
|
|
554
1108
|
|
|
555
1109
|
.smrt-admin-shell__edge {
|
|
@@ -560,9 +1114,30 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
560
1114
|
border-color: var(--smrt-color-outline-variant);
|
|
561
1115
|
}
|
|
562
1116
|
|
|
563
|
-
.smrt-admin-
|
|
1117
|
+
.smrt-admin-shell__header {
|
|
564
1118
|
grid-column: 1 / -1;
|
|
565
1119
|
grid-row: 1;
|
|
1120
|
+
display: flex;
|
|
1121
|
+
align-items: center;
|
|
1122
|
+
gap: var(--smrt-spacing-2);
|
|
1123
|
+
box-sizing: border-box;
|
|
1124
|
+
min-inline-size: 0;
|
|
1125
|
+
min-block-size: 0;
|
|
1126
|
+
padding: 0 var(--smrt-spacing-3) 0 var(--smrt-spacing-2);
|
|
1127
|
+
/* Clip long titles sideways only: menus and popovers opened from the
|
|
1128
|
+
header's actions (a notifications bell, an account menu) must be able
|
|
1129
|
+
to drop below it. `clip` (unlike `hidden`) allows a visible block axis. */
|
|
1130
|
+
overflow-x: clip;
|
|
1131
|
+
overflow-y: visible;
|
|
1132
|
+
border-block-end: 1px solid var(--smrt-color-outline-variant);
|
|
1133
|
+
background: var(--smrt-color-surface-container-low);
|
|
1134
|
+
color: var(--smrt-color-on-surface);
|
|
1135
|
+
z-index: 30;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.smrt-admin-shell__edge--top {
|
|
1139
|
+
grid-column: 1 / -1;
|
|
1140
|
+
grid-row: 2;
|
|
566
1141
|
display: grid;
|
|
567
1142
|
grid-template-columns: var(
|
|
568
1143
|
--edge-columns,
|
|
@@ -574,15 +1149,17 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
574
1149
|
}
|
|
575
1150
|
|
|
576
1151
|
.smrt-admin-shell__edge--left {
|
|
1152
|
+
position: relative;
|
|
577
1153
|
grid-column: 1;
|
|
578
|
-
grid-row:
|
|
1154
|
+
grid-row: 3;
|
|
579
1155
|
border-inline-end: 1px solid var(--smrt-color-outline-variant);
|
|
580
1156
|
z-index: 20;
|
|
581
1157
|
}
|
|
582
1158
|
|
|
583
1159
|
.smrt-admin-shell__edge--right {
|
|
1160
|
+
position: relative;
|
|
584
1161
|
grid-column: 3;
|
|
585
|
-
grid-row:
|
|
1162
|
+
grid-row: 3;
|
|
586
1163
|
border-inline-start: 1px solid var(--smrt-color-outline-variant);
|
|
587
1164
|
z-index: 20;
|
|
588
1165
|
}
|
|
@@ -596,7 +1173,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
596
1173
|
|
|
597
1174
|
.smrt-admin-shell__edge--bottom {
|
|
598
1175
|
grid-column: 1 / -1;
|
|
599
|
-
grid-row:
|
|
1176
|
+
grid-row: 4;
|
|
600
1177
|
display: grid;
|
|
601
1178
|
grid-template-columns: var(
|
|
602
1179
|
--edge-columns,
|
|
@@ -609,7 +1186,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
609
1186
|
|
|
610
1187
|
.smrt-admin-shell__main {
|
|
611
1188
|
grid-column: 2;
|
|
612
|
-
grid-row:
|
|
1189
|
+
grid-row: 3;
|
|
613
1190
|
min-width: 0;
|
|
614
1191
|
min-height: 0;
|
|
615
1192
|
overflow: auto;
|
|
@@ -656,6 +1233,15 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
656
1233
|
padding: var(--smrt-spacing-3);
|
|
657
1234
|
}
|
|
658
1235
|
|
|
1236
|
+
/* A right rail never pads wider than its own track: a rail collapsed to 0
|
|
1237
|
+
(an app that shows nothing there) paints no padding. */
|
|
1238
|
+
.smrt-admin-shell__edge--right .smrt-admin-shell__rail {
|
|
1239
|
+
padding-inline: min(
|
|
1240
|
+
var(--smrt-spacing-3),
|
|
1241
|
+
calc(var(--smrt-admin-shell-right-collapsed) / 4)
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
659
1245
|
.smrt-admin-shell__focus-rail {
|
|
660
1246
|
display: grid;
|
|
661
1247
|
/* Center tracks, not just items: buttons may be wider than the
|
|
@@ -704,6 +1290,15 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
704
1290
|
overflow: hidden;
|
|
705
1291
|
}
|
|
706
1292
|
|
|
1293
|
+
/* A collapsed `keepMounted` edge keeps its panel in the DOM but hidden;
|
|
1294
|
+
the panel classes set `display`, which would otherwise win over the
|
|
1295
|
+
`hidden` attribute. */
|
|
1296
|
+
.smrt-admin-shell__tenant-stack[hidden],
|
|
1297
|
+
.smrt-admin-shell__panel[hidden],
|
|
1298
|
+
.smrt-admin-shell__drawer[hidden] {
|
|
1299
|
+
display: none !important;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
707
1302
|
.smrt-admin-shell__tenant-stack {
|
|
708
1303
|
display: grid;
|
|
709
1304
|
grid-template-rows: minmax(0, 1fr) auto;
|
|
@@ -764,14 +1359,19 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
764
1359
|
}
|
|
765
1360
|
|
|
766
1361
|
.smrt-admin-shell__drawer--top {
|
|
767
|
-
inset-block-start:
|
|
1362
|
+
inset-block-start: calc(
|
|
1363
|
+
var(--smrt-admin-shell-header-track) + var(--smrt-admin-shell-top-track)
|
|
1364
|
+
);
|
|
768
1365
|
inset-inline: var(--smrt-admin-shell-left-track)
|
|
769
1366
|
var(--smrt-admin-shell-right-track);
|
|
770
1367
|
max-block-size: var(--smrt-admin-shell-top-expanded);
|
|
771
1368
|
}
|
|
772
1369
|
|
|
773
1370
|
.smrt-admin-shell__drawer--bottom {
|
|
774
|
-
inset-block-end:
|
|
1371
|
+
inset-block-end: calc(
|
|
1372
|
+
var(--smrt-admin-shell-bottom-track) +
|
|
1373
|
+
var(--smrt-admin-shell-phone-bar-track)
|
|
1374
|
+
);
|
|
775
1375
|
/* The system scope is the application footer. Its expanded drawer must
|
|
776
1376
|
remain footer-wide while it rises above every desktop pane. */
|
|
777
1377
|
inset-inline: 0;
|
|
@@ -848,20 +1448,332 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
848
1448
|
font-family: var(--smrt-font-family-mono);
|
|
849
1449
|
}
|
|
850
1450
|
|
|
1451
|
+
/* ---- Resizable side edges ------------------------------------------ */
|
|
1452
|
+
|
|
1453
|
+
.smrt-admin-shell__resizer {
|
|
1454
|
+
position: absolute;
|
|
1455
|
+
inset-block: 0;
|
|
1456
|
+
z-index: 2;
|
|
1457
|
+
inline-size: 0.75rem;
|
|
1458
|
+
cursor: col-resize;
|
|
1459
|
+
touch-action: none;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.smrt-admin-shell__resizer--left {
|
|
1463
|
+
inset-inline-end: -0.375rem;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
.smrt-admin-shell__resizer--right {
|
|
1467
|
+
inset-inline-start: -0.375rem;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
.smrt-admin-shell__resizer::after {
|
|
1471
|
+
content: '';
|
|
1472
|
+
position: absolute;
|
|
1473
|
+
inset-block: 0;
|
|
1474
|
+
inset-inline-start: calc(50% - 1px);
|
|
1475
|
+
inline-size: 2px;
|
|
1476
|
+
background: var(--smrt-color-primary);
|
|
1477
|
+
opacity: 0;
|
|
1478
|
+
transition: opacity var(--smrt-duration-short2, 100ms)
|
|
1479
|
+
var(--smrt-easing-standard, ease);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.smrt-admin-shell__resizer:hover::after,
|
|
1483
|
+
.smrt-admin-shell__resizer:focus-visible::after,
|
|
1484
|
+
.smrt-admin-shell[data-resizing] .smrt-admin-shell__resizer::after {
|
|
1485
|
+
opacity: 1;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
.smrt-admin-shell__resizer:focus-visible {
|
|
1489
|
+
outline: none;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.smrt-admin-shell[data-resizing] {
|
|
1493
|
+
cursor: col-resize;
|
|
1494
|
+
user-select: none;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/* ---- Phone chrome ----------------------------------------------------- */
|
|
1498
|
+
|
|
1499
|
+
.smrt-admin-shell__phone-top {
|
|
1500
|
+
grid-column: 1 / -1;
|
|
1501
|
+
grid-row: 3;
|
|
1502
|
+
align-self: start;
|
|
1503
|
+
z-index: 10;
|
|
1504
|
+
min-inline-size: 0;
|
|
1505
|
+
transition: transform var(--smrt-admin-shell-chrome-duration)
|
|
1506
|
+
var(--smrt-easing-standard, ease);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.smrt-admin-shell[data-chrome-hidden] .smrt-admin-shell__phone-top {
|
|
1510
|
+
transform: translateY(-100%);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.smrt-admin-shell[data-phone-top] .smrt-admin-shell__main {
|
|
1514
|
+
padding-block-start: var(--smrt-admin-shell-phone-top-size);
|
|
1515
|
+
scroll-padding-block-start: var(--smrt-admin-shell-phone-top-size);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
.smrt-admin-shell__phone-bar {
|
|
1519
|
+
grid-column: 1 / -1;
|
|
1520
|
+
grid-row: 5;
|
|
1521
|
+
z-index: 30;
|
|
1522
|
+
min-inline-size: 0;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
.smrt-admin-shell__overlays {
|
|
1526
|
+
position: relative;
|
|
1527
|
+
grid-column: 1 / -1;
|
|
1528
|
+
grid-row: 1 / 5;
|
|
1529
|
+
z-index: 35;
|
|
1530
|
+
min-inline-size: 0;
|
|
1531
|
+
min-block-size: 0;
|
|
1532
|
+
pointer-events: none;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
.smrt-admin-shell__overlays > :global(*) {
|
|
1536
|
+
pointer-events: auto;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
.smrt-admin-shell__scrim {
|
|
1540
|
+
grid-column: 1 / -1;
|
|
1541
|
+
grid-row: 1 / 5;
|
|
1542
|
+
z-index: 19;
|
|
1543
|
+
margin: 0;
|
|
1544
|
+
padding: 0;
|
|
1545
|
+
border: 0;
|
|
1546
|
+
background: var(--smrt-color-scrim);
|
|
1547
|
+
opacity: 0.36;
|
|
1548
|
+
cursor: pointer;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/* Tablet/desktop overlay: an expanded side edge whose `overlayMedia`
|
|
1552
|
+
matches slides over the page instead of taking a grid track. */
|
|
1553
|
+
.smrt-admin-shell__edge--left[data-presentation='overlay'][data-state='expanded'],
|
|
1554
|
+
.smrt-admin-shell__edge--right[data-presentation='overlay'][data-state='expanded'] {
|
|
1555
|
+
grid-column: 1 / -1;
|
|
1556
|
+
z-index: 25;
|
|
1557
|
+
box-shadow: var(
|
|
1558
|
+
--smrt-elevation-3,
|
|
1559
|
+
0 0.5rem 1.5rem color-mix(in srgb, var(--smrt-color-shadow, #000) 24%, transparent)
|
|
1560
|
+
);
|
|
1561
|
+
outline: none;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
.smrt-admin-shell__edge--left[data-presentation='overlay'][data-state='expanded'] {
|
|
1565
|
+
justify-self: start;
|
|
1566
|
+
inline-size: min(var(--smrt-admin-shell-left-expanded), 100%);
|
|
1567
|
+
animation: smrt-admin-shell-slide-from-left
|
|
1568
|
+
var(--smrt-admin-shell-chrome-duration) ease-out;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
.smrt-admin-shell__edge--right[data-presentation='overlay'][data-state='expanded'] {
|
|
1572
|
+
justify-self: end;
|
|
1573
|
+
inline-size: min(var(--smrt-admin-shell-right-expanded), 100%);
|
|
1574
|
+
animation: smrt-admin-shell-slide-from-right
|
|
1575
|
+
var(--smrt-admin-shell-chrome-duration) ease-out;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.smrt-admin-shell__overlay-scrim {
|
|
1579
|
+
grid-column: 1 / -1;
|
|
1580
|
+
grid-row: 3;
|
|
1581
|
+
z-index: 22;
|
|
1582
|
+
margin: 0;
|
|
1583
|
+
padding: 0;
|
|
1584
|
+
border: 0;
|
|
1585
|
+
background: var(--smrt-color-scrim);
|
|
1586
|
+
opacity: 0.24;
|
|
1587
|
+
cursor: pointer;
|
|
1588
|
+
animation: smrt-admin-shell-scrim-in var(--smrt-admin-shell-chrome-duration)
|
|
1589
|
+
ease-out;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
.smrt-admin-shell__edge--left[data-presentation='overlay'][data-closing] {
|
|
1593
|
+
animation: smrt-admin-shell-slide-to-left
|
|
1594
|
+
var(--smrt-admin-shell-chrome-duration) ease-in forwards;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
.smrt-admin-shell__edge--right[data-presentation='overlay'][data-closing] {
|
|
1598
|
+
animation: smrt-admin-shell-slide-to-right
|
|
1599
|
+
var(--smrt-admin-shell-chrome-duration) ease-in forwards;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
.smrt-admin-shell__overlay-scrim[data-closing] {
|
|
1603
|
+
pointer-events: none;
|
|
1604
|
+
animation: smrt-admin-shell-scrim-out
|
|
1605
|
+
var(--smrt-admin-shell-chrome-duration) ease-in forwards;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
@keyframes smrt-admin-shell-slide-to-right {
|
|
1609
|
+
to {
|
|
1610
|
+
transform: translateX(100%);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
@keyframes smrt-admin-shell-slide-to-left {
|
|
1615
|
+
to {
|
|
1616
|
+
transform: translateX(-100%);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
@keyframes smrt-admin-shell-scrim-out {
|
|
1621
|
+
to {
|
|
1622
|
+
opacity: 0;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
@keyframes smrt-admin-shell-slide-from-right {
|
|
1627
|
+
from {
|
|
1628
|
+
transform: translateX(100%);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
@keyframes smrt-admin-shell-slide-from-left {
|
|
1633
|
+
from {
|
|
1634
|
+
transform: translateX(-100%);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
@keyframes smrt-admin-shell-scrim-in {
|
|
1639
|
+
from {
|
|
1640
|
+
opacity: 0;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1645
|
+
.smrt-admin-shell__edge--left[data-presentation='overlay'][data-state='expanded'],
|
|
1646
|
+
.smrt-admin-shell__edge--right[data-presentation='overlay'][data-state='expanded'],
|
|
1647
|
+
.smrt-admin-shell__edge[data-presentation='overlay'][data-closing],
|
|
1648
|
+
.smrt-admin-shell__overlay-scrim {
|
|
1649
|
+
animation: none;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
.smrt-admin-shell__sheet-grabber {
|
|
1654
|
+
display: grid;
|
|
1655
|
+
place-items: center;
|
|
1656
|
+
block-size: 1.5rem;
|
|
1657
|
+
touch-action: pan-x;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
.smrt-admin-shell__sheet-grabber span {
|
|
1661
|
+
inline-size: 2.5rem;
|
|
1662
|
+
block-size: 0.25rem;
|
|
1663
|
+
border-radius: var(--smrt-radius-full);
|
|
1664
|
+
background: var(--smrt-color-outline-variant);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/* Phone page contracts: breadcrumbs give way to the top bar, and so does
|
|
1668
|
+
the page title (visually hidden: screen readers and the outline keep
|
|
1669
|
+
it); page tabs stick under it and slide away with it; a form's action
|
|
1670
|
+
row is fixed to the bottom and replaces the bottom bar. */
|
|
1671
|
+
.smrt-admin-shell[data-viewport='phone'] :global([data-shell-breadcrumbs]) {
|
|
1672
|
+
display: none;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.smrt-admin-shell[data-phone-top] :global([data-shell-page-title]) {
|
|
1676
|
+
position: absolute;
|
|
1677
|
+
inline-size: 1px;
|
|
1678
|
+
block-size: 1px;
|
|
1679
|
+
margin: -1px;
|
|
1680
|
+
padding: 0;
|
|
1681
|
+
overflow: hidden;
|
|
1682
|
+
clip-path: inset(50%);
|
|
1683
|
+
white-space: nowrap;
|
|
1684
|
+
border: 0;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/* Tabs inside a page header (a content item's tabs under its title) stick
|
|
1688
|
+
like any page tabs: the header's boxes step aside on phones so the tabs'
|
|
1689
|
+
sticky range is the page, not the header. */
|
|
1690
|
+
.smrt-admin-shell[data-phone-top]
|
|
1691
|
+
:global([data-page-header]:has([data-shell-tabs])),
|
|
1692
|
+
.smrt-admin-shell[data-phone-top]
|
|
1693
|
+
:global([data-page-header-extra]:has([data-shell-tabs])) {
|
|
1694
|
+
display: contents;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.smrt-admin-shell[data-phone-top] :global([data-shell-tabs]) {
|
|
1698
|
+
position: sticky;
|
|
1699
|
+
inset-block-start: var(--smrt-admin-shell-phone-top-size);
|
|
1700
|
+
z-index: 6;
|
|
1701
|
+
background: var(--smrt-color-surface);
|
|
1702
|
+
transition: transform var(--smrt-admin-shell-chrome-duration)
|
|
1703
|
+
var(--smrt-easing-standard, ease);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
.smrt-admin-shell[data-phone-top][data-chrome-hidden]
|
|
1707
|
+
:global([data-shell-tabs]) {
|
|
1708
|
+
transform: translateY(
|
|
1709
|
+
calc(-100% - var(--smrt-admin-shell-phone-top-size))
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
.smrt-admin-shell[data-viewport='phone'] :global([data-form-action-bar]) {
|
|
1714
|
+
position: fixed;
|
|
1715
|
+
inset: auto 0 0;
|
|
1716
|
+
z-index: 12;
|
|
1717
|
+
display: flex;
|
|
1718
|
+
flex-flow: row nowrap;
|
|
1719
|
+
align-items: center;
|
|
1720
|
+
gap: var(--smrt-spacing-2);
|
|
1721
|
+
margin: 0;
|
|
1722
|
+
padding: var(--smrt-spacing-2) var(--smrt-spacing-4)
|
|
1723
|
+
calc(var(--smrt-spacing-2) + env(safe-area-inset-bottom));
|
|
1724
|
+
border-block-start: 1px solid var(--smrt-color-outline-variant);
|
|
1725
|
+
background: var(--smrt-color-surface);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.smrt-admin-shell[data-viewport='phone'] :global([data-form-action-bar] > *) {
|
|
1729
|
+
flex: 1 1 0;
|
|
1730
|
+
min-block-size: 2.75rem;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
:global(:root[data-keyboard-open])
|
|
1734
|
+
.smrt-admin-shell[data-viewport='phone']
|
|
1735
|
+
:global([data-form-action-bar]) {
|
|
1736
|
+
display: none;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.smrt-admin-shell[data-bottom-bar='form'] .smrt-admin-shell__main {
|
|
1740
|
+
padding-block-end: calc(5rem + env(safe-area-inset-bottom));
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/* The phone bottom bar sits at the bottom of the dynamic viewport so the
|
|
1744
|
+
browser's collapsing toolbar never hides it. */
|
|
1745
|
+
.smrt-admin-shell[data-bottom-bar='nav'],
|
|
1746
|
+
.smrt-admin-shell[data-bottom-bar='form'] {
|
|
1747
|
+
min-block-size: 100dvh;
|
|
1748
|
+
block-size: 100dvh;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
851
1751
|
@media (max-width: 48rem) {
|
|
852
1752
|
.smrt-admin-shell {
|
|
853
1753
|
grid-template-columns: 0 minmax(0, 1fr) 0;
|
|
854
1754
|
grid-template-rows:
|
|
1755
|
+
var(--smrt-admin-shell-header-track)
|
|
855
1756
|
var(--smrt-admin-shell-top-track) minmax(0, 1fr)
|
|
856
|
-
var(--smrt-admin-shell-bottom-track)
|
|
1757
|
+
var(--smrt-admin-shell-bottom-track)
|
|
1758
|
+
var(--smrt-admin-shell-phone-bar-track);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
.smrt-admin-shell__resizer {
|
|
1762
|
+
display: none;
|
|
857
1763
|
}
|
|
858
1764
|
|
|
859
1765
|
.smrt-admin-shell__edge--left,
|
|
860
1766
|
.smrt-admin-shell__edge--right {
|
|
861
1767
|
position: absolute;
|
|
862
1768
|
grid-area: auto;
|
|
863
|
-
inset-block:
|
|
864
|
-
|
|
1769
|
+
inset-block: calc(
|
|
1770
|
+
var(--smrt-admin-shell-header-track) +
|
|
1771
|
+
var(--smrt-admin-shell-top-track)
|
|
1772
|
+
)
|
|
1773
|
+
calc(
|
|
1774
|
+
var(--smrt-admin-shell-bottom-track) +
|
|
1775
|
+
var(--smrt-admin-shell-phone-bar-track)
|
|
1776
|
+
);
|
|
865
1777
|
inline-size: min(22rem, 86vw);
|
|
866
1778
|
transform: translateX(-100%);
|
|
867
1779
|
transition: transform var(--smrt-duration-short2)
|
|
@@ -882,6 +1794,46 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
882
1794
|
transform: translateX(0);
|
|
883
1795
|
}
|
|
884
1796
|
|
|
1797
|
+
/* Right edge as a bottom sheet. */
|
|
1798
|
+
.smrt-admin-shell__edge--right[data-phone='sheet'] {
|
|
1799
|
+
display: grid;
|
|
1800
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1801
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
1802
|
+
inset-inline: 0;
|
|
1803
|
+
inset-block-start: calc(
|
|
1804
|
+
var(--smrt-admin-shell-header-track) +
|
|
1805
|
+
var(--smrt-admin-shell-top-track) + var(--smrt-spacing-8)
|
|
1806
|
+
);
|
|
1807
|
+
inline-size: auto;
|
|
1808
|
+
border-inline-start: 0;
|
|
1809
|
+
border-start-start-radius: var(--smrt-radius-lg);
|
|
1810
|
+
border-start-end-radius: var(--smrt-radius-lg);
|
|
1811
|
+
background: var(--smrt-color-surface);
|
|
1812
|
+
box-shadow: var(--smrt-elevation-3);
|
|
1813
|
+
transform: translateY(100%);
|
|
1814
|
+
visibility: hidden;
|
|
1815
|
+
transition:
|
|
1816
|
+
transform var(--smrt-duration-short2) var(--smrt-easing-standard),
|
|
1817
|
+
visibility 0s linear var(--smrt-duration-short2);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
.smrt-admin-shell__edge--right[data-phone='sheet'][data-state='expanded'] {
|
|
1821
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1822
|
+
transform: none;
|
|
1823
|
+
visibility: visible;
|
|
1824
|
+
transition: transform var(--smrt-duration-short2)
|
|
1825
|
+
var(--smrt-easing-standard);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
.smrt-admin-shell__edge--right[data-phone='sheet'] .smrt-admin-shell__rail {
|
|
1829
|
+
display: none;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
.smrt-admin-shell__edge--right[data-phone='sheet'] .smrt-admin-shell__panel--right {
|
|
1833
|
+
grid-column: 1;
|
|
1834
|
+
grid-row: 2;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
885
1837
|
.smrt-admin-shell__edge--top,
|
|
886
1838
|
.smrt-admin-shell__edge--bottom {
|
|
887
1839
|
grid-template-columns: minmax(0, 1fr);
|
|
@@ -917,8 +1869,14 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
917
1869
|
}
|
|
918
1870
|
|
|
919
1871
|
@media (prefers-reduced-motion: reduce) {
|
|
1872
|
+
.smrt-admin-shell {
|
|
1873
|
+
--smrt-admin-shell-chrome-duration: 0ms;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
920
1876
|
.smrt-admin-shell__edge--left,
|
|
921
|
-
.smrt-admin-shell__edge--right
|
|
1877
|
+
.smrt-admin-shell__edge--right,
|
|
1878
|
+
.smrt-admin-shell__edge--right[data-phone='sheet'],
|
|
1879
|
+
.smrt-admin-shell__edge--right[data-phone='sheet'][data-state='expanded'] {
|
|
922
1880
|
transition: none;
|
|
923
1881
|
}
|
|
924
1882
|
|