@happyvertical/smrt-svelte 0.51.39 → 0.53.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 +214 -1
- package/agents/components.md +8 -1
- package/agents/workspace.md +24 -0
- package/dist/Provider.svelte +19 -15
- package/dist/Provider.svelte.d.ts +2 -2
- package/dist/Provider.svelte.d.ts.map +1 -1
- package/dist/__tests__/provider-mode-harness.svelte +14 -0
- package/dist/__tests__/provider-mode-harness.svelte.d.ts +11 -0
- package/dist/__tests__/provider-mode-harness.svelte.d.ts.map +1 -0
- package/dist/__tests__/provider-mode-probe.svelte +13 -0
- package/dist/__tests__/provider-mode-probe.svelte.d.ts +8 -0
- package/dist/__tests__/provider-mode-probe.svelte.d.ts.map +1 -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/FileUpload.svelte +120 -16
- package/dist/components/forms/FileUpload.svelte.d.ts +7 -1
- package/dist/components/forms/FileUpload.svelte.d.ts.map +1 -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__/FileUpload.native-form.test.js +249 -0
- package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte +24 -0
- package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/FileUploadReset.fixture.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 +597 -0
- package/dist/components/workspace/__tests__/AdminShell.test.js +257 -1
- package/dist/components/workspace/__tests__/TenantNav.test.js +115 -0
- package/dist/components/workspace/__tests__/WorkspaceAccountMenu.test.js +6 -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-state.test.js +25 -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 +1099 -59
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +44 -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 +116 -13
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +8 -2
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +16 -2
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts +2 -0
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.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 +57 -1
- package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/state.svelte.js +156 -6
- package/dist/components/workspace/admin-shell/types.d.ts +115 -1
- 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.forms.d.ts +3 -0
- package/dist/i18n/strings.forms.d.ts.map +1 -1
- package/dist/i18n/strings.forms.js +3 -0
- package/dist/i18n/strings.workspace.d.ts +14 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -1
- package/dist/i18n/strings.workspace.js +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/provider-mode.test.js +86 -0
- package/dist/state/app-state.svelte.d.ts.map +1 -1
- package/dist/state/app-state.svelte.js +8 -8
- 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 +17 -17
|
@@ -31,6 +31,17 @@ restoration at mobile and desktop widths.
|
|
|
31
31
|
- `ActivityBadge`, `ActivityList`, `ActivityItem`, `ActivityToasts`
|
|
32
32
|
- `AppScopePanel`, `SystemStatusChips`, `SystemScopePanel`
|
|
33
33
|
- `tenantNavFromManifest`
|
|
34
|
+
- Responsive chrome: `ShellNavToggle`, `ShellTitle`, `PhoneTopBar`,
|
|
35
|
+
`PhoneBottomBar`, the pure `mobile-shell` helpers (`ScrollChrome`,
|
|
36
|
+
`shellPageTrailFor`, `phoneTopBarFor`, `bottomBarMode`, `keyboardLikelyOpen`, …) and their DOM
|
|
37
|
+
installers (`installShellViewport`, `installScrollChrome`,
|
|
38
|
+
`installKeyboardWatcher`, `watchFormActionBar`)
|
|
39
|
+
|
|
40
|
+
`AdminShell` takes `header`, `phoneTopBar`, `phoneBottomBar` and `overlays`
|
|
41
|
+
snippets plus `phone`, `pinChrome` and `path` props; edges take
|
|
42
|
+
`viewportDefaults`, `phone`, `resizable`, `persist` and `keepMounted`; nav
|
|
43
|
+
items take `attention`. See the package README
|
|
44
|
+
("Responsive chrome and resizable edges") for the contract.
|
|
34
45
|
|
|
35
46
|
Use `AdminShell`'s `tenantFooter` snippet with `WorkspaceAccountMenu` to keep a
|
|
36
47
|
single-line tenant/account control pinned below scrolling tenant navigation.
|
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdminShell's responsive chrome: header row, viewport defaults, phone top
|
|
3
|
+
* and bottom bars, form action bars, scrim, navigation, phone presentations
|
|
4
|
+
* of the right edge, and resizable side edges.
|
|
5
|
+
*/
|
|
6
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
|
7
|
+
import { createRawSnippet, flushSync, tick } from 'svelte';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import AdminShell from '../admin-shell/AdminShell.svelte';
|
|
10
|
+
import { createShellState } from '../admin-shell/state.svelte.js';
|
|
11
|
+
import { ADMIN_SHELL_REGION_IDS } from '../admin-shell/types.js';
|
|
12
|
+
function html(markup) {
|
|
13
|
+
return createRawSnippet(() => ({ render: () => markup }));
|
|
14
|
+
}
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
let width = 1280;
|
|
17
|
+
const originalMatchMedia = window.matchMedia;
|
|
18
|
+
function setWidth(next) {
|
|
19
|
+
width = next;
|
|
20
|
+
for (const listener of listeners)
|
|
21
|
+
listener();
|
|
22
|
+
}
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
width = 1280;
|
|
25
|
+
window.matchMedia = ((query) => {
|
|
26
|
+
const max = /max-width:\s*([\d.]+)rem/.exec(query);
|
|
27
|
+
const min = /min-width:\s*([\d.]+)rem/.exec(query);
|
|
28
|
+
return {
|
|
29
|
+
get matches() {
|
|
30
|
+
if (query.includes('prefers-reduced-motion'))
|
|
31
|
+
return false;
|
|
32
|
+
return ((!max || width <= Number(max[1]) * 16) &&
|
|
33
|
+
(!min || width >= Number(min[1]) * 16));
|
|
34
|
+
},
|
|
35
|
+
media: query,
|
|
36
|
+
addEventListener: (_, listener) => listeners.add(listener),
|
|
37
|
+
removeEventListener: (_, listener) => listeners.delete(listener),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
listeners.clear();
|
|
43
|
+
window.matchMedia = originalMatchMedia;
|
|
44
|
+
});
|
|
45
|
+
const config = {
|
|
46
|
+
top: false,
|
|
47
|
+
left: {
|
|
48
|
+
label: 'Navigation',
|
|
49
|
+
initial: 'expanded',
|
|
50
|
+
hotkey: null,
|
|
51
|
+
viewportDefaults: {
|
|
52
|
+
phone: 'collapsed',
|
|
53
|
+
tablet: 'collapsed',
|
|
54
|
+
desktop: 'expanded',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
right: { label: 'Assistant', initial: 'collapsed', hotkey: null },
|
|
58
|
+
bottom: false,
|
|
59
|
+
};
|
|
60
|
+
async function settle() {
|
|
61
|
+
flushSync();
|
|
62
|
+
await tick();
|
|
63
|
+
flushSync();
|
|
64
|
+
}
|
|
65
|
+
function shellRoot() {
|
|
66
|
+
return document.querySelector('.smrt-admin-shell');
|
|
67
|
+
}
|
|
68
|
+
describe('AdminShell header and viewport', () => {
|
|
69
|
+
it('renders a header row above the edges on desktop, with public region ids', async () => {
|
|
70
|
+
const shell = createShellState({ config });
|
|
71
|
+
render(AdminShell, {
|
|
72
|
+
props: {
|
|
73
|
+
state: shell,
|
|
74
|
+
header: createRawSnippet((ctx) => ({
|
|
75
|
+
render: () => `<span>Header ${ctx().viewport}</span>`,
|
|
76
|
+
})),
|
|
77
|
+
phoneTopBar: html('<span>phone top</span>'),
|
|
78
|
+
phoneBottomBar: html('<nav>phone bar</nav>'),
|
|
79
|
+
children: html('<p>page</p>'),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
await settle();
|
|
83
|
+
const header = document.getElementById(ADMIN_SHELL_REGION_IDS.header);
|
|
84
|
+
expect(header?.textContent).toContain('Header desktop');
|
|
85
|
+
expect(document.getElementById(ADMIN_SHELL_REGION_IDS.main)?.tagName).toBe('MAIN');
|
|
86
|
+
expect(document.getElementById(ADMIN_SHELL_REGION_IDS.left)).not.toBeNull();
|
|
87
|
+
expect(shellRoot().dataset.viewport).toBe('desktop');
|
|
88
|
+
expect(shellRoot().getAttribute('style')).toContain('--smrt-admin-shell-header-track: var(--smrt-admin-shell-header-size)');
|
|
89
|
+
expect(screen.queryByTestId('admin-shell-phone-top')).toBeNull();
|
|
90
|
+
expect(screen.queryByTestId('admin-shell-phone-bar')).toBeNull();
|
|
91
|
+
expect(shell.panels.left).toBe('expanded');
|
|
92
|
+
});
|
|
93
|
+
it('follows viewport classes: tablet collapses the nav, desktop reopens it', async () => {
|
|
94
|
+
const shell = createShellState({ config });
|
|
95
|
+
render(AdminShell, {
|
|
96
|
+
props: { state: shell, children: html('<p>page</p>') },
|
|
97
|
+
});
|
|
98
|
+
await settle();
|
|
99
|
+
setWidth(900);
|
|
100
|
+
flushSync();
|
|
101
|
+
expect(shell.viewport).toBe('tablet');
|
|
102
|
+
expect(shell.panels.left).toBe('collapsed');
|
|
103
|
+
setWidth(1400);
|
|
104
|
+
flushSync();
|
|
105
|
+
expect(shell.panels.left).toBe('expanded');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('AdminShell on a phone', () => {
|
|
109
|
+
beforeEach(() => setWidth(390));
|
|
110
|
+
function renderPhone(extra = {}, page = '<p>page</p>') {
|
|
111
|
+
const shell = createShellState({ config });
|
|
112
|
+
const result = render(AdminShell, {
|
|
113
|
+
props: {
|
|
114
|
+
state: shell,
|
|
115
|
+
header: html('<span>desktop header</span>'),
|
|
116
|
+
phoneTopBar: createRawSnippet((ctx) => ({
|
|
117
|
+
render: () => `<span>top ${ctx().hidden ? 'hidden' : 'shown'}</span>`,
|
|
118
|
+
})),
|
|
119
|
+
phoneBottomBar: html('<nav aria-label="Main">bar</nav>'),
|
|
120
|
+
phone: { scrim: true, swipeToClose: true },
|
|
121
|
+
path: '/a',
|
|
122
|
+
children: html(page),
|
|
123
|
+
...extra,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return { shell, ...result };
|
|
127
|
+
}
|
|
128
|
+
it('swaps the header for the phone top bar and shows the bottom bar', async () => {
|
|
129
|
+
const { shell } = renderPhone();
|
|
130
|
+
await settle();
|
|
131
|
+
expect(shell.viewport).toBe('phone');
|
|
132
|
+
expect(shell.panels.left).toBe('collapsed');
|
|
133
|
+
expect(screen.queryByTestId('admin-shell-header')).toBeNull();
|
|
134
|
+
expect(screen.getByTestId('admin-shell-phone-top').textContent).toContain('top shown');
|
|
135
|
+
expect(screen.getByTestId('admin-shell-phone-bar')).toBeInTheDocument();
|
|
136
|
+
expect(shellRoot().dataset.bottomBar).toBe('nav');
|
|
137
|
+
expect(shellRoot().hasAttribute('data-phone-top')).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
it('lets a form action bar replace the bottom bar', async () => {
|
|
140
|
+
renderPhone({}, '<form><div data-form-action-bar><button>Save</button></div></form>');
|
|
141
|
+
await settle();
|
|
142
|
+
expect(screen.queryByTestId('admin-shell-phone-bar')).toBeNull();
|
|
143
|
+
expect(shellRoot().dataset.bottomBar).toBe('form');
|
|
144
|
+
});
|
|
145
|
+
it('dims the page behind an open drawer; the scrim closes it', async () => {
|
|
146
|
+
const { shell } = renderPhone();
|
|
147
|
+
await settle();
|
|
148
|
+
expect(screen.queryByTestId('admin-shell-scrim')).toBeNull();
|
|
149
|
+
shell.expandPanel('left');
|
|
150
|
+
flushSync();
|
|
151
|
+
const scrim = screen.getByTestId('admin-shell-scrim');
|
|
152
|
+
expect(scrim).toHaveAttribute('aria-label', 'Close Navigation');
|
|
153
|
+
await fireEvent.click(scrim);
|
|
154
|
+
expect(shell.panels.left).toBe('collapsed');
|
|
155
|
+
});
|
|
156
|
+
it('keeps a closed phone drawer out of the tab order and away from screen readers', async () => {
|
|
157
|
+
const { shell } = renderPhone();
|
|
158
|
+
await settle();
|
|
159
|
+
const left = document.getElementById('smrt-admin-shell-left-panel');
|
|
160
|
+
expect(left.inert).toBe(true);
|
|
161
|
+
shell.expandPanel('left');
|
|
162
|
+
flushSync();
|
|
163
|
+
expect(left.inert).toBe(false);
|
|
164
|
+
setWidth(1280);
|
|
165
|
+
await settle();
|
|
166
|
+
expect(left.inert).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('moves focus into the phone drawer without animation enumeration', async () => {
|
|
169
|
+
const { shell } = renderPhone({
|
|
170
|
+
tenantPanel: html('<a href="/phone-target">Phone navigation target</a>'),
|
|
171
|
+
});
|
|
172
|
+
await settle();
|
|
173
|
+
const left = document.getElementById('smrt-admin-shell-left-panel');
|
|
174
|
+
Object.defineProperty(left, 'getAnimations', { value: undefined });
|
|
175
|
+
shell.expandPanel('left');
|
|
176
|
+
await settle();
|
|
177
|
+
await waitFor(() => expect(left.contains(document.activeElement)).toBe(true));
|
|
178
|
+
});
|
|
179
|
+
it('closes an open drawer when the path changes', async () => {
|
|
180
|
+
const { shell, rerender } = renderPhone();
|
|
181
|
+
await settle();
|
|
182
|
+
shell.expandPanel('left');
|
|
183
|
+
flushSync();
|
|
184
|
+
await rerender({ path: '/b' });
|
|
185
|
+
await settle();
|
|
186
|
+
expect(shell.panels.left).toBe('collapsed');
|
|
187
|
+
});
|
|
188
|
+
it('does not render a phone-hidden right edge and renders a sheet with a grabber', async () => {
|
|
189
|
+
const hiddenShell = createShellState({
|
|
190
|
+
config: {
|
|
191
|
+
...config,
|
|
192
|
+
right: { label: 'Assistant', phone: 'hidden', initial: 'expanded' },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
const first = render(AdminShell, {
|
|
196
|
+
props: { state: hiddenShell, children: html('<p>page</p>') },
|
|
197
|
+
});
|
|
198
|
+
await settle();
|
|
199
|
+
expect(document.getElementById(ADMIN_SHELL_REGION_IDS.right)).toBeNull();
|
|
200
|
+
first.unmount();
|
|
201
|
+
const sheetShell = createShellState({
|
|
202
|
+
config: {
|
|
203
|
+
...config,
|
|
204
|
+
right: { label: 'Assistant', phone: 'sheet', initial: 'expanded' },
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
render(AdminShell, {
|
|
208
|
+
props: {
|
|
209
|
+
state: sheetShell,
|
|
210
|
+
focusPanel: html('<p>chat</p>'),
|
|
211
|
+
phone: { scrim: true },
|
|
212
|
+
children: html('<p>page</p>'),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
await settle();
|
|
216
|
+
const aside = document.getElementById(ADMIN_SHELL_REGION_IDS.right);
|
|
217
|
+
expect(aside.dataset.phone).toBe('sheet');
|
|
218
|
+
expect(aside.querySelector('.smrt-admin-shell__sheet-grabber')).not.toBeNull();
|
|
219
|
+
expect(screen.getByTestId('admin-shell-scrim')).toHaveAttribute('aria-label', 'Close Assistant');
|
|
220
|
+
});
|
|
221
|
+
it('renders host overlays with the viewport and bottom bar mode', async () => {
|
|
222
|
+
renderPhone({
|
|
223
|
+
overlays: createRawSnippet((ctx) => ({
|
|
224
|
+
render: () => `<div data-testid="overlay">${ctx().viewport}/${ctx().bottomBar}</div>`,
|
|
225
|
+
})),
|
|
226
|
+
});
|
|
227
|
+
await settle();
|
|
228
|
+
expect(screen.getByTestId('overlay').textContent).toBe('phone/nav');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe('AdminShell resizable edges', () => {
|
|
232
|
+
function storeAdapter() {
|
|
233
|
+
const writes = [];
|
|
234
|
+
return {
|
|
235
|
+
writes,
|
|
236
|
+
read: () => null,
|
|
237
|
+
write: (delta) => {
|
|
238
|
+
writes.push(JSON.parse(JSON.stringify(delta)));
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function renderResizable(overrides = {}) {
|
|
243
|
+
const adapter = storeAdapter();
|
|
244
|
+
const shell = createShellState({
|
|
245
|
+
config: {
|
|
246
|
+
...config,
|
|
247
|
+
right: {
|
|
248
|
+
label: 'Assistant',
|
|
249
|
+
initial: 'expanded',
|
|
250
|
+
expandedSize: '28rem',
|
|
251
|
+
resizable: { min: 320, max: 640, step: 10 },
|
|
252
|
+
hotkey: null,
|
|
253
|
+
},
|
|
254
|
+
...overrides,
|
|
255
|
+
},
|
|
256
|
+
settingsAdapter: adapter,
|
|
257
|
+
});
|
|
258
|
+
const view = render(AdminShell, {
|
|
259
|
+
props: {
|
|
260
|
+
state: shell,
|
|
261
|
+
focusPanel: html('<p>chat</p>'),
|
|
262
|
+
children: html('<p>page</p>'),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
flushSync();
|
|
266
|
+
return { shell, adapter, view };
|
|
267
|
+
}
|
|
268
|
+
it('does not render a separator unless the edge is resizable (backward compatible)', () => {
|
|
269
|
+
const shell = createShellState({
|
|
270
|
+
config: { right: { initial: 'expanded' } },
|
|
271
|
+
});
|
|
272
|
+
render(AdminShell, {
|
|
273
|
+
props: { state: shell, children: html('<p>page</p>') },
|
|
274
|
+
});
|
|
275
|
+
expect(document.querySelector('[role="separator"]')).toBeNull();
|
|
276
|
+
});
|
|
277
|
+
it('exposes an accessible, keyboard-resizable separator', async () => {
|
|
278
|
+
const { shell, adapter } = renderResizable();
|
|
279
|
+
const separator = screen.getByRole('separator', {
|
|
280
|
+
name: 'Resize Assistant',
|
|
281
|
+
});
|
|
282
|
+
expect(separator).toHaveAttribute('aria-orientation', 'vertical');
|
|
283
|
+
expect(separator).toHaveAttribute('aria-controls', ADMIN_SHELL_REGION_IDS.right);
|
|
284
|
+
expect(separator).toHaveAttribute('aria-valuemin', '320');
|
|
285
|
+
expect(separator).toHaveAttribute('aria-valuemax', '640');
|
|
286
|
+
expect(separator).toHaveAttribute('aria-valuenow', '448');
|
|
287
|
+
expect(separator).toHaveAttribute('tabindex', '0');
|
|
288
|
+
await fireEvent.keyDown(separator, { key: 'ArrowLeft' });
|
|
289
|
+
expect(shell.panelSize('right')).toBe(458);
|
|
290
|
+
expect(separator).toHaveAttribute('aria-valuenow', '458');
|
|
291
|
+
await fireEvent.keyDown(separator, { key: 'ArrowRight', shiftKey: true });
|
|
292
|
+
expect(shell.panelSize('right')).toBe(418);
|
|
293
|
+
await fireEvent.keyDown(separator, { key: 'End' });
|
|
294
|
+
expect(shell.panelSize('right')).toBe(640);
|
|
295
|
+
await fireEvent.keyDown(separator, { key: 'Home' });
|
|
296
|
+
expect(shell.panelSize('right')).toBe(320);
|
|
297
|
+
expect(adapter.writes.at(-1)?.sizes).toEqual({ right: 320 });
|
|
298
|
+
expect(shellRoot().getAttribute('style')).toContain('--smrt-admin-shell-right-track: 320px');
|
|
299
|
+
await fireEvent.keyDown(separator, { key: 'Enter' });
|
|
300
|
+
expect(shell.panelSize('right')).toBeNull();
|
|
301
|
+
expect(separator).toHaveAttribute('aria-valuenow', '448');
|
|
302
|
+
});
|
|
303
|
+
it('resizes by dragging, persisting once at the end, and resets on double-click', async () => {
|
|
304
|
+
const { shell, adapter } = renderResizable();
|
|
305
|
+
const separator = screen.getByTestId('admin-shell-resizer-right');
|
|
306
|
+
const before = adapter.writes.length;
|
|
307
|
+
await fireEvent.pointerDown(separator, { button: 0, clientX: 800 });
|
|
308
|
+
expect(shellRoot().dataset.resizing).toBe('right');
|
|
309
|
+
await fireEvent.pointerMove(separator, { clientX: 760 });
|
|
310
|
+
expect(shell.panelSize('right')).toBe(488);
|
|
311
|
+
await fireEvent.pointerMove(separator, { clientX: 700 });
|
|
312
|
+
expect(shell.panelSize('right')).toBe(548);
|
|
313
|
+
expect(adapter.writes.length).toBe(before);
|
|
314
|
+
await fireEvent.pointerUp(separator, { clientX: 700 });
|
|
315
|
+
expect(shellRoot().dataset.resizing).toBeUndefined();
|
|
316
|
+
expect(adapter.writes.length).toBe(before + 1);
|
|
317
|
+
expect(adapter.writes.at(-1)?.sizes).toEqual({ right: 548 });
|
|
318
|
+
await fireEvent.dblClick(separator);
|
|
319
|
+
expect(shell.panelSize('right')).toBeNull();
|
|
320
|
+
});
|
|
321
|
+
it('ends the drag when pointer capture is lost, persisting once', async () => {
|
|
322
|
+
const { shell, adapter } = renderResizable();
|
|
323
|
+
const separator = screen.getByTestId('admin-shell-resizer-right');
|
|
324
|
+
const before = adapter.writes.length;
|
|
325
|
+
await fireEvent.pointerDown(separator, { button: 0, clientX: 800 });
|
|
326
|
+
await fireEvent.pointerMove(separator, { clientX: 760 });
|
|
327
|
+
expect(shellRoot().dataset.resizing).toBe('right');
|
|
328
|
+
// The browser took the capture away (a system gesture, a window switch)
|
|
329
|
+
// without a pointerup.
|
|
330
|
+
await fireEvent(separator, new Event('lostpointercapture'));
|
|
331
|
+
expect(shellRoot().dataset.resizing).toBeUndefined();
|
|
332
|
+
expect(adapter.writes.length).toBe(before + 1);
|
|
333
|
+
await fireEvent.pointerMove(separator, { clientX: 600 });
|
|
334
|
+
expect(shell.panelSize('right')).toBe(488);
|
|
335
|
+
await fireEvent.pointerUp(separator, { clientX: 600 });
|
|
336
|
+
expect(adapter.writes.length).toBe(before + 1);
|
|
337
|
+
});
|
|
338
|
+
it('drops an in-flight drag when the shell unmounts', async () => {
|
|
339
|
+
const { shell, view } = renderResizable();
|
|
340
|
+
const separator = screen.getByTestId('admin-shell-resizer-right');
|
|
341
|
+
await fireEvent.pointerDown(separator, { button: 0, clientX: 800 });
|
|
342
|
+
await fireEvent.pointerMove(separator, { clientX: 760 });
|
|
343
|
+
expect(shell.panelSize('right')).toBe(488);
|
|
344
|
+
view.unmount();
|
|
345
|
+
// The detached handle no longer drives the (still live) shell state.
|
|
346
|
+
await fireEvent.pointerMove(separator, { clientX: 600 });
|
|
347
|
+
expect(shell.panelSize('right')).toBe(488);
|
|
348
|
+
});
|
|
349
|
+
it('grows a left edge to the right', async () => {
|
|
350
|
+
const { shell } = renderResizable({
|
|
351
|
+
left: {
|
|
352
|
+
label: 'Navigation',
|
|
353
|
+
initial: 'expanded',
|
|
354
|
+
expandedSize: '16rem',
|
|
355
|
+
resizable: true,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
const separator = screen.getByRole('separator', {
|
|
359
|
+
name: 'Resize Navigation',
|
|
360
|
+
});
|
|
361
|
+
await fireEvent.keyDown(separator, { key: 'ArrowRight' });
|
|
362
|
+
expect(shell.panelSize('left')).toBe(272);
|
|
363
|
+
});
|
|
364
|
+
it('hides the separator while collapsed and on phones', async () => {
|
|
365
|
+
const { shell } = renderResizable();
|
|
366
|
+
shell.collapsePanel('right');
|
|
367
|
+
flushSync();
|
|
368
|
+
expect(screen.queryByRole('separator')).toBeNull();
|
|
369
|
+
shell.expandPanel('right');
|
|
370
|
+
flushSync();
|
|
371
|
+
expect(screen.getByRole('separator')).toBeInTheDocument();
|
|
372
|
+
setWidth(390);
|
|
373
|
+
flushSync();
|
|
374
|
+
expect(screen.queryByRole('separator')).toBeNull();
|
|
375
|
+
});
|
|
376
|
+
it('keeps the size out of storage when the edge opts out', async () => {
|
|
377
|
+
const { shell, adapter } = renderResizable({
|
|
378
|
+
right: {
|
|
379
|
+
label: 'Assistant',
|
|
380
|
+
initial: 'expanded',
|
|
381
|
+
resizable: true,
|
|
382
|
+
persist: { size: false },
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
await fireEvent.keyDown(screen.getByRole('separator'), { key: 'End' });
|
|
386
|
+
expect(shell.panelSize('right')).toBe(720);
|
|
387
|
+
expect(adapter.writes.at(-1)?.sizes).toBeUndefined();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
describe('AdminShell overlay edges (overlayMedia)', () => {
|
|
391
|
+
function overlayShell() {
|
|
392
|
+
return createShellState({
|
|
393
|
+
config: {
|
|
394
|
+
...config,
|
|
395
|
+
right: {
|
|
396
|
+
label: 'Assistant',
|
|
397
|
+
initial: 'collapsed',
|
|
398
|
+
hotkey: null,
|
|
399
|
+
collapsedSize: '0rem',
|
|
400
|
+
expandedSize: '28rem',
|
|
401
|
+
resizable: { min: 320, max: 640 },
|
|
402
|
+
overlayMedia: '(max-width: 99.9375rem)',
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function rightEdge() {
|
|
408
|
+
return document.getElementById(ADMIN_SHELL_REGION_IDS.right);
|
|
409
|
+
}
|
|
410
|
+
it('slides over the page below the query and docks above it', async () => {
|
|
411
|
+
setWidth(1280);
|
|
412
|
+
const shell = overlayShell();
|
|
413
|
+
render(AdminShell, {
|
|
414
|
+
props: { state: shell, children: html('<button>page action</button>') },
|
|
415
|
+
});
|
|
416
|
+
await settle();
|
|
417
|
+
shell.expandPanel('right');
|
|
418
|
+
await settle();
|
|
419
|
+
expect(shell.presentationFor('right')).toBe('overlay');
|
|
420
|
+
expect(rightEdge().dataset.presentation).toBe('overlay');
|
|
421
|
+
// The page keeps its width: the right track stays collapsed.
|
|
422
|
+
expect(shellRoot().getAttribute('style')).toContain('--smrt-admin-shell-right-track: 0rem');
|
|
423
|
+
expect(screen.getByTestId('admin-shell-overlay-scrim')).toBeTruthy();
|
|
424
|
+
expect(document.getElementById('smrt-admin-shell-main')?.inert).toBe(true);
|
|
425
|
+
expect(screen.queryByTestId('admin-shell-resizer-right')).toBeNull();
|
|
426
|
+
setWidth(1600);
|
|
427
|
+
await settle();
|
|
428
|
+
expect(shell.presentationFor('right')).toBe('push');
|
|
429
|
+
expect(rightEdge().dataset.presentation).toBe('push');
|
|
430
|
+
expect(shellRoot().getAttribute('style')).toContain('--smrt-admin-shell-right-track: 28rem');
|
|
431
|
+
expect(screen.queryByTestId('admin-shell-overlay-scrim')).toBeNull();
|
|
432
|
+
expect(document.getElementById('smrt-admin-shell-main')?.inert).toBe(false);
|
|
433
|
+
expect(screen.getByTestId('admin-shell-resizer-right')).toBeTruthy();
|
|
434
|
+
});
|
|
435
|
+
it('closes on a scrim click or Escape, moving focus in and back', async () => {
|
|
436
|
+
setWidth(1100);
|
|
437
|
+
const shell = overlayShell();
|
|
438
|
+
render(AdminShell, {
|
|
439
|
+
props: { state: shell, children: html('<p>page</p>') },
|
|
440
|
+
});
|
|
441
|
+
await settle();
|
|
442
|
+
const opener = document.createElement('button');
|
|
443
|
+
document.body.append(opener);
|
|
444
|
+
opener.focus();
|
|
445
|
+
shell.expandPanel('right');
|
|
446
|
+
await settle();
|
|
447
|
+
expect(document.activeElement).toBe(rightEdge());
|
|
448
|
+
await fireEvent.keyDown(window, { key: 'Escape' });
|
|
449
|
+
await settle();
|
|
450
|
+
expect(shell.panels.right).toBe('collapsed');
|
|
451
|
+
expect(document.activeElement).toBe(opener);
|
|
452
|
+
opener.remove();
|
|
453
|
+
shell.expandPanel('right');
|
|
454
|
+
await settle();
|
|
455
|
+
await fireEvent.click(screen.getByTestId('admin-shell-overlay-scrim'));
|
|
456
|
+
await settle();
|
|
457
|
+
expect(shell.panels.right).toBe('collapsed');
|
|
458
|
+
shell.expandPanel('right');
|
|
459
|
+
await settle();
|
|
460
|
+
await fireEvent.keyDown(window, { key: 'Escape' });
|
|
461
|
+
await settle();
|
|
462
|
+
expect(shell.panels.right).toBe('collapsed');
|
|
463
|
+
});
|
|
464
|
+
it('does not open an overlay on load, but still restores a docked edge', async () => {
|
|
465
|
+
const stored = { panels: { right: 'expanded' } };
|
|
466
|
+
const adapter = { read: () => stored, write: vi.fn() };
|
|
467
|
+
const make = () => createShellState({
|
|
468
|
+
config: {
|
|
469
|
+
...config,
|
|
470
|
+
right: {
|
|
471
|
+
label: 'Assistant',
|
|
472
|
+
initial: 'collapsed',
|
|
473
|
+
hotkey: null,
|
|
474
|
+
overlayMedia: '(max-width: 99.9375rem)',
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
settingsAdapter: adapter,
|
|
478
|
+
});
|
|
479
|
+
setWidth(1280);
|
|
480
|
+
const narrow = make();
|
|
481
|
+
const first = render(AdminShell, {
|
|
482
|
+
props: { state: narrow, children: html('<p>page</p>') },
|
|
483
|
+
});
|
|
484
|
+
await settle();
|
|
485
|
+
await settle();
|
|
486
|
+
expect(narrow.panels.right).toBe('collapsed');
|
|
487
|
+
expect(adapter.write).not.toHaveBeenCalled();
|
|
488
|
+
first.unmount();
|
|
489
|
+
setWidth(1700);
|
|
490
|
+
const wide = make();
|
|
491
|
+
render(AdminShell, {
|
|
492
|
+
props: { state: wide, children: html('<p>page</p>') },
|
|
493
|
+
});
|
|
494
|
+
await settle();
|
|
495
|
+
await settle();
|
|
496
|
+
expect(wide.panels.right).toBe('expanded');
|
|
497
|
+
});
|
|
498
|
+
it('slides a closed overlay back out before hiding it', async () => {
|
|
499
|
+
vi.useFakeTimers();
|
|
500
|
+
try {
|
|
501
|
+
setWidth(1100);
|
|
502
|
+
const shell = overlayShell();
|
|
503
|
+
render(AdminShell, {
|
|
504
|
+
props: { state: shell, children: html('<p>page</p>') },
|
|
505
|
+
});
|
|
506
|
+
await settle();
|
|
507
|
+
shell.expandPanel('right');
|
|
508
|
+
await settle();
|
|
509
|
+
shell.collapsePanel('right');
|
|
510
|
+
await settle();
|
|
511
|
+
// Closed at once for the page (not inert), still shown while it slides.
|
|
512
|
+
expect(document.getElementById('smrt-admin-shell-main')?.inert).toBe(false);
|
|
513
|
+
expect(rightEdge().dataset.state).toBe('expanded');
|
|
514
|
+
expect(rightEdge().hasAttribute('data-closing')).toBe(true);
|
|
515
|
+
expect(rightEdge().inert).toBe(true);
|
|
516
|
+
const scrim = screen.getByTestId('admin-shell-overlay-scrim');
|
|
517
|
+
expect(scrim.hasAttribute('data-closing')).toBe(true);
|
|
518
|
+
await fireEvent.click(scrim);
|
|
519
|
+
expect(shell.panels.right).toBe('collapsed');
|
|
520
|
+
vi.advanceTimersByTime(700);
|
|
521
|
+
await settle();
|
|
522
|
+
expect(rightEdge().dataset.state).toBe('collapsed');
|
|
523
|
+
expect(rightEdge().hasAttribute('data-closing')).toBe(false);
|
|
524
|
+
expect(screen.queryByTestId('admin-shell-overlay-scrim')).toBeNull();
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
vi.useRealTimers();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
it('skips the slide-out when reduced motion is preferred', async () => {
|
|
531
|
+
setWidth(1100);
|
|
532
|
+
const base = window.matchMedia;
|
|
533
|
+
window.matchMedia = ((query) => query.includes('prefers-reduced-motion')
|
|
534
|
+
? {
|
|
535
|
+
matches: true,
|
|
536
|
+
media: query,
|
|
537
|
+
addEventListener: () => { },
|
|
538
|
+
removeEventListener: () => { },
|
|
539
|
+
}
|
|
540
|
+
: base(query));
|
|
541
|
+
const shell = overlayShell();
|
|
542
|
+
render(AdminShell, {
|
|
543
|
+
props: { state: shell, children: html('<p>page</p>') },
|
|
544
|
+
});
|
|
545
|
+
await settle();
|
|
546
|
+
shell.expandPanel('right');
|
|
547
|
+
await settle();
|
|
548
|
+
shell.collapsePanel('right');
|
|
549
|
+
await settle();
|
|
550
|
+
expect(rightEdge().dataset.state).toBe('collapsed');
|
|
551
|
+
expect(screen.queryByTestId('admin-shell-overlay-scrim')).toBeNull();
|
|
552
|
+
});
|
|
553
|
+
it('keeps the phone presentation on phones', async () => {
|
|
554
|
+
setWidth(390);
|
|
555
|
+
const shell = overlayShell();
|
|
556
|
+
render(AdminShell, {
|
|
557
|
+
props: { state: shell, children: html('<p>page</p>') },
|
|
558
|
+
});
|
|
559
|
+
await settle();
|
|
560
|
+
shell.expandPanel('right');
|
|
561
|
+
await settle();
|
|
562
|
+
expect(shell.presentationFor('right')).toBe('push');
|
|
563
|
+
expect(screen.queryByTestId('admin-shell-overlay-scrim')).toBeNull();
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
describe('AdminShell backward compatibility', () => {
|
|
567
|
+
it('adds no header, phone chrome, overlays, or scrim without the new props', async () => {
|
|
568
|
+
setWidth(390);
|
|
569
|
+
const shell = createShellState({
|
|
570
|
+
config: { left: { initial: 'expanded' } },
|
|
571
|
+
});
|
|
572
|
+
render(AdminShell, {
|
|
573
|
+
props: { state: shell, children: html('<p>page</p>') },
|
|
574
|
+
});
|
|
575
|
+
await settle();
|
|
576
|
+
expect(screen.queryByTestId('admin-shell-header')).toBeNull();
|
|
577
|
+
expect(screen.queryByTestId('admin-shell-phone-top')).toBeNull();
|
|
578
|
+
expect(screen.queryByTestId('admin-shell-phone-bar')).toBeNull();
|
|
579
|
+
expect(screen.queryByTestId('admin-shell-scrim')).toBeNull();
|
|
580
|
+
expect(document.querySelector('.smrt-admin-shell__overlays')).toBeNull();
|
|
581
|
+
// Without viewport defaults the left edge keeps its configured state.
|
|
582
|
+
expect(shell.panels.left).toBe('expanded');
|
|
583
|
+
expect(shellRoot().getAttribute('style')).toContain('--smrt-admin-shell-header-track: 0rem');
|
|
584
|
+
});
|
|
585
|
+
it('still renders a system bar in the bottom edge', () => {
|
|
586
|
+
const shell = createShellState();
|
|
587
|
+
render(AdminShell, {
|
|
588
|
+
props: {
|
|
589
|
+
state: shell,
|
|
590
|
+
systemBar: html('<span>system chips</span>'),
|
|
591
|
+
children: html('<p>page</p>'),
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
expect(document.getElementById(ADMIN_SHELL_REGION_IDS.bottom)?.textContent).toContain('system chips');
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
vi.setConfig({ testTimeout: 30000 });
|