@happyvertical/smrt-svelte 0.55.6 → 0.55.7
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 +17 -2
- package/README.md +83 -4
- package/agents/workspace.md +202 -5
- package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/dictation-source.js +8 -2
- package/dist/browser-ai/adapters/stt/factory.d.ts +6 -3
- package/dist/browser-ai/adapters/stt/factory.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/factory.js +19 -9
- package/dist/browser-ai/adapters/stt/index.d.ts +3 -1
- package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/index.js +3 -1
- package/dist/browser-ai/adapters/stt/local-speech-model.d.ts +111 -0
- package/dist/browser-ai/adapters/stt/local-speech-model.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/local-speech-model.js +377 -0
- package/dist/browser-ai/adapters/stt/local-speech.d.ts +82 -0
- package/dist/browser-ai/adapters/stt/local-speech.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/local-speech.js +317 -0
- package/dist/browser-ai/adapters/stt/local-speech.worker.d.ts +2 -0
- package/dist/browser-ai/adapters/stt/local-speech.worker.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/local-speech.worker.js +32 -0
- package/dist/browser-ai/adapters/stt/transformers-env.d.ts +8 -0
- package/dist/browser-ai/adapters/stt/transformers-env.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/transformers-env.js +17 -0
- package/dist/browser-ai/adapters/stt/types.d.ts +54 -9
- package/dist/browser-ai/adapters/stt/types.d.ts.map +1 -1
- package/dist/browser-ai/capabilities/speech-probe.d.ts +29 -0
- package/dist/browser-ai/capabilities/speech-probe.d.ts.map +1 -0
- package/dist/browser-ai/capabilities/speech-probe.js +19 -0
- package/dist/browser-ai/core/errors.d.ts +9 -0
- package/dist/browser-ai/core/errors.d.ts.map +1 -1
- package/dist/browser-ai/core/errors.js +12 -0
- package/dist/browser-ai/index.d.ts +1 -0
- package/dist/browser-ai/index.d.ts.map +1 -1
- package/dist/browser-ai/index.js +1 -0
- package/dist/components/app/AppShell.svelte +587 -33
- package/dist/components/app/AppShell.svelte.d.ts +75 -4
- package/dist/components/app/AppShell.svelte.d.ts.map +1 -1
- package/dist/components/app/DockToggles.svelte +0 -1
- package/dist/components/app/dock-toggle.d.ts +7 -1
- package/dist/components/app/dock-toggle.d.ts.map +1 -1
- package/dist/components/app/index.d.ts +2 -1
- package/dist/components/app/index.d.ts.map +1 -1
- package/dist/components/app/slot-item.d.ts +21 -0
- package/dist/components/app/slot-item.d.ts.map +1 -0
- package/dist/components/app/slot-item.js +1 -0
- package/dist/components/forms/AddressInput.svelte +2 -15
- package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateRangeInput.svelte +2 -15
- package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateTimeInput.svelte +8 -15
- package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/MeasurementInput.svelte +2 -15
- package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/MoneyInput.svelte +8 -25
- package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/NumberInput.svelte +7 -15
- package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/PhoneInput.svelte +7 -15
- package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/SelectInput.svelte +34 -40
- package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextInput.svelte +20 -37
- package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextareaInput.svelte +19 -35
- package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
- package/dist/components/sortable/Sortable.svelte +6 -33
- package/dist/components/sortable/Sortable.svelte.d.ts.map +1 -1
- package/dist/components/sortable/announce.d.ts +5 -0
- package/dist/components/sortable/announce.d.ts.map +1 -0
- package/dist/components/sortable/announce.js +33 -0
- package/dist/components/workspace/admin-shell/AdminShell.svelte +424 -58
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +27 -2
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/ShellBrand.svelte +86 -0
- package/dist/components/workspace/admin-shell/ShellBrand.svelte.d.ts +18 -0
- package/dist/components/workspace/admin-shell/ShellBrand.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellIconButton.svelte +77 -0
- package/dist/components/workspace/admin-shell/ShellIconButton.svelte.d.ts +26 -0
- package/dist/components/workspace/admin-shell/ShellIconButton.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte +88 -16
- package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/ShellNavEditor.svelte +531 -0
- package/dist/components/workspace/admin-shell/ShellNavEditor.svelte.d.ts +26 -0
- package/dist/components/workspace/admin-shell/ShellNavEditor.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte +57 -0
- package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte.d.ts +22 -0
- package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte +290 -0
- package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte.d.ts +32 -0
- package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/TenantNav.svelte +67 -2
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +8 -0
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts +67 -0
- package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/layout-controller.svelte.js +129 -1
- package/dist/components/workspace/admin-shell/layout.d.ts +122 -0
- package/dist/components/workspace/admin-shell/layout.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/layout.js +410 -10
- package/dist/components/workspace/admin-shell/settings.js +4 -4
- package/dist/components/workspace/admin-shell/shell-icons.d.ts +45 -0
- package/dist/components/workspace/admin-shell/shell-icons.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/shell-icons.js +66 -0
- package/dist/components/workspace/admin-shell/slot-labels.d.ts +33 -0
- package/dist/components/workspace/admin-shell/slot-labels.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/slot-labels.js +33 -0
- package/dist/components/workspace/admin-shell/slots.d.ts +50 -0
- package/dist/components/workspace/admin-shell/slots.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/slots.js +77 -0
- package/dist/components/workspace/admin-shell/state.svelte.d.ts +35 -0
- package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/state.svelte.js +111 -0
- package/dist/components/workspace/admin-shell/types.d.ts +101 -1
- package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
- package/dist/components/workspace/index.d.ts +9 -2
- package/dist/components/workspace/index.d.ts.map +1 -1
- package/dist/components/workspace/index.js +6 -0
- package/dist/i18n/strings.workspace.d.ts +49 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -1
- package/dist/i18n/strings.workspace.js +49 -0
- package/package.json +30 -19
- package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts +0 -51
- package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts.map +0 -1
- package/dist/browser-ai/adapters/stt/whisper-wasm.js +0 -380
|
@@ -9,40 +9,71 @@ import '@happyvertical/smrt-ui/themes/styles/base.css';
|
|
|
9
9
|
import '@happyvertical/smrt-ui/themes/styles/fonts.css';
|
|
10
10
|
import type { DataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
|
|
11
11
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
12
|
-
import { type Snippet, untrack } from 'svelte';
|
|
12
|
+
import { type Component, type Snippet, tick, untrack } from 'svelte';
|
|
13
|
+
import { M as SORTABLE_STRINGS } from '../../i18n/strings.sortable.js';
|
|
13
14
|
import { M } from '../../i18n/strings.workspace.js';
|
|
14
15
|
import Provider from '../../Provider.svelte';
|
|
15
16
|
import type { User } from '../../state/app-state.js';
|
|
16
17
|
import type { WebMcpProviderConfig } from '../../web/webmcp-provider.js';
|
|
18
|
+
import { formatSortableAnnouncement } from '../sortable/announce.js';
|
|
19
|
+
import { createSortable } from '../sortable/controller.svelte.js';
|
|
17
20
|
import AdminShell from '../workspace/admin-shell/AdminShell.svelte';
|
|
18
21
|
import AppScopePanel from '../workspace/admin-shell/AppScopePanel.svelte';
|
|
19
22
|
import {
|
|
20
23
|
applyShellLayout,
|
|
21
24
|
normalizeShellLayout,
|
|
25
|
+
resolveShellPlacements,
|
|
22
26
|
type ShellLayout,
|
|
23
27
|
} from '../workspace/admin-shell/layout.js';
|
|
24
28
|
import { setShellLayout } from '../workspace/admin-shell/layout-context.js';
|
|
25
29
|
import { ShellLayoutController } from '../workspace/admin-shell/layout-controller.svelte.js';
|
|
30
|
+
import ShellBrand from '../workspace/admin-shell/ShellBrand.svelte';
|
|
31
|
+
import ShellIconButton from '../workspace/admin-shell/ShellIconButton.svelte';
|
|
32
|
+
import ShellNavEditor from '../workspace/admin-shell/ShellNavEditor.svelte';
|
|
26
33
|
import { resolveShellConfig } from '../workspace/admin-shell/settings.js';
|
|
34
|
+
import {
|
|
35
|
+
SHELL_REGION_MESSAGES,
|
|
36
|
+
SHELL_SLOT_MESSAGES,
|
|
37
|
+
SHELL_SLOT_SHORT_MESSAGES,
|
|
38
|
+
} from '../workspace/admin-shell/slot-labels.js';
|
|
39
|
+
import {
|
|
40
|
+
resolveSlot,
|
|
41
|
+
SHELL_SLOTS,
|
|
42
|
+
type ShellPlacementItem,
|
|
43
|
+
type ShellRegion,
|
|
44
|
+
type ShellSlot,
|
|
45
|
+
shellDockItemId,
|
|
46
|
+
shellHostSlotItemId,
|
|
47
|
+
slotRegion,
|
|
48
|
+
} from '../workspace/admin-shell/slots.js';
|
|
27
49
|
import { createShellState } from '../workspace/admin-shell/state.svelte.js';
|
|
28
50
|
import TenantNav from '../workspace/admin-shell/TenantNav.svelte';
|
|
29
51
|
import {
|
|
52
|
+
type AdminShellProps,
|
|
30
53
|
PANEL_EDGES,
|
|
31
54
|
type PanelEdge,
|
|
32
55
|
type ShellNavGroup,
|
|
33
56
|
type ShellNavItem,
|
|
34
57
|
type ShellPanelDefaults,
|
|
58
|
+
type ShellSectionActionsContext,
|
|
35
59
|
} from '../workspace/admin-shell/types.js';
|
|
36
60
|
import DockSlot from './DockSlot.svelte';
|
|
37
61
|
import DockToggles from './DockToggles.svelte';
|
|
38
62
|
import type { DockToggle } from './dock-toggle.js';
|
|
39
63
|
import RuntimeDiagnosticsWebMcp from './RuntimeDiagnosticsWebMcp.svelte';
|
|
64
|
+
import type { ShellSlotItem } from './slot-item.js';
|
|
40
65
|
|
|
41
66
|
interface Props {
|
|
42
67
|
/** Application name shown in the shell brand and app panel. */
|
|
43
68
|
title?: string;
|
|
44
69
|
/** Secondary line under the application name in the shell brand. */
|
|
45
70
|
subtitle?: string;
|
|
71
|
+
/** Logo shown before the title in the shell brand (and as the compact rail mark). */
|
|
72
|
+
logoSrc?: string;
|
|
73
|
+
/** Alternative text for the logo (decorative by default: the title names the app). */
|
|
74
|
+
logoAlt?: string;
|
|
75
|
+
/** Where the brand links to (e.g. the app's home); unlinked when omitted. */
|
|
76
|
+
homeHref?: string;
|
|
46
77
|
/** localStorage key for the user's shell layout preferences. */
|
|
47
78
|
storageKey?: string;
|
|
48
79
|
/** Navigation rendered in the tenant (left) panel. */
|
|
@@ -94,7 +125,7 @@ interface Props {
|
|
|
94
125
|
*/
|
|
95
126
|
dock?: Snippet<[DataSurfaceRegistry]>;
|
|
96
127
|
/**
|
|
97
|
-
* Icon buttons
|
|
128
|
+
* Icon buttons, each placed in a shell `slot` (default `header.end`), each toggling
|
|
98
129
|
* the `ShellDockTool` with the same id open and closed. `aria-pressed` and
|
|
99
130
|
* `aria-expanded` follow the dock, focus moves into the dock when it
|
|
100
131
|
* opens and back to the button when it closes (Escape included). The
|
|
@@ -102,12 +133,72 @@ interface Props {
|
|
|
102
133
|
* drive the same dock from code with `useShellDock()`.
|
|
103
134
|
*/
|
|
104
135
|
dockToggles?: DockToggle[];
|
|
136
|
+
/**
|
|
137
|
+
* Host content for any shell slot (`header.start|center|end`,
|
|
138
|
+
* `footer.start|center|end`, `leftSidebar.header|footer`,
|
|
139
|
+
* `rightSidebar.header|footer`). Rendered before dock toggles placed in the
|
|
140
|
+
* same slot. Each snippet is one movable item with id `slot:<slot>`; use
|
|
141
|
+
* `slotItems` for individually movable content.
|
|
142
|
+
*/
|
|
143
|
+
slots?: Partial<Record<ShellSlot, Snippet>>;
|
|
144
|
+
/**
|
|
145
|
+
* Host items with stable ids, each in a default slot. Users can move them
|
|
146
|
+
* between slots in the layout editor (`ShellLayout.placements`). Rendered
|
|
147
|
+
* after `slots` content and before dock toggles in the same slot.
|
|
148
|
+
*/
|
|
149
|
+
slotItems?: ShellSlotItem[];
|
|
150
|
+
/**
|
|
151
|
+
* Opt in to editing the layout in place. Adds the built-in shell item
|
|
152
|
+
* `item:layout-edit` (a pencil toggle, default slot `header.end`, or
|
|
153
|
+
* `{ slot }`); while on, slots render as labelled drop zones, items and
|
|
154
|
+
* navigation get grips, section headings get overlay icons and a floating
|
|
155
|
+
* toolbar, and hidden regions render as strips with a show control. Hosts
|
|
156
|
+
* and assistants drive it with `useShellLayout()` (`editing`,
|
|
157
|
+
* `setEditing`). Off (the default) leaves existing apps unchanged.
|
|
158
|
+
*
|
|
159
|
+
* `{ floating: true }` instead renders the toggle as a fixed round button
|
|
160
|
+
* in the top-right corner of the shell: not a placeable item (no slot, no
|
|
161
|
+
* grip), so hidden regions never displace it. The header and right sidebar
|
|
162
|
+
* reserve room for it so it covers none of their controls.
|
|
163
|
+
*/
|
|
164
|
+
layoutEditing?: boolean | { slot?: ShellSlot; floating?: boolean };
|
|
165
|
+
/**
|
|
166
|
+
* Host icon buttons for each navigation section while the layout is edited
|
|
167
|
+
* (e.g. an Options gear or Help), rendered in the section's overlay icons
|
|
168
|
+
* and its floating toolbar. Receives the section id, label, whether it is
|
|
169
|
+
* user-created, and `editing`.
|
|
170
|
+
*/
|
|
171
|
+
sectionActions?: Snippet<[ShellSectionActionsContext]>;
|
|
172
|
+
/**
|
|
173
|
+
* `'items'` (default) lists every section's entries in the left sidebar.
|
|
174
|
+
* `'sections'` lists only the sections, one icon link each, to the
|
|
175
|
+
* section's own page (`ShellNavGroup.href`, else `sectionHref`, else its
|
|
176
|
+
* first entry); render the entries there with `ShellSectionMenu`. In edit
|
|
177
|
+
* mode the sidebar edits sections (grip, rename, hide, icon); entries are
|
|
178
|
+
* edited on the section page.
|
|
179
|
+
*/
|
|
180
|
+
navMode?: 'items' | 'sections';
|
|
181
|
+
/** Section page for groups without an `href` (e.g. user-created ones). */
|
|
182
|
+
sectionHref?: (sectionId: string) => string | undefined;
|
|
183
|
+
/** Renders host icon names for sections and entries. */
|
|
184
|
+
iconComponent?: Component<{ name: string; size?: number }>;
|
|
185
|
+
/**
|
|
186
|
+
* Edge toggle buttons and WASD drop-down panels (see `AdminShell`).
|
|
187
|
+
* Default `false`: regions are laid out inline and no toggles or hotkeys
|
|
188
|
+
* exist. Pass `true` (or a per-edge map) for the previous behaviour.
|
|
189
|
+
*/
|
|
190
|
+
edgeToggles?: AdminShellProps['edgeToggles'];
|
|
191
|
+
/** Keyboard shortcuts; default follows `edgeToggles` (see `AdminShell`). */
|
|
192
|
+
hotkeys?: boolean;
|
|
105
193
|
children: Snippet;
|
|
106
194
|
}
|
|
107
195
|
|
|
108
196
|
let {
|
|
109
197
|
title = 'SMRT',
|
|
110
198
|
subtitle,
|
|
199
|
+
logoSrc,
|
|
200
|
+
logoAlt,
|
|
201
|
+
homeHref,
|
|
111
202
|
storageKey = 'smrt-app-shell',
|
|
112
203
|
nav = [],
|
|
113
204
|
navGroups = [],
|
|
@@ -127,10 +218,84 @@ let {
|
|
|
127
218
|
appPanelDocs,
|
|
128
219
|
dock,
|
|
129
220
|
dockToggles = [],
|
|
221
|
+
slots: hostSlots,
|
|
222
|
+
slotItems = [],
|
|
223
|
+
layoutEditing = false,
|
|
224
|
+
sectionActions,
|
|
225
|
+
navMode = 'items',
|
|
226
|
+
sectionHref,
|
|
227
|
+
iconComponent,
|
|
228
|
+
edgeToggles = false,
|
|
229
|
+
hotkeys,
|
|
130
230
|
children,
|
|
131
231
|
}: Props = $props();
|
|
232
|
+
/** Stable id of the built-in brand item (title, subtitle). */
|
|
233
|
+
const SHELL_BRAND_ITEM_ID = 'item:brand';
|
|
234
|
+
/** Stable id of the built-in edit-layout toggle item. */
|
|
235
|
+
const layoutFloating = $derived(
|
|
236
|
+
typeof layoutEditing === 'object' && layoutEditing.floating === true,
|
|
237
|
+
);
|
|
238
|
+
const SHELL_LAYOUT_EDIT_ITEM_ID = 'item:layout-edit';
|
|
132
239
|
const { t } = useI18n();
|
|
133
|
-
|
|
240
|
+
// Every movable item, in default order: legacy `slots` snippets, `slotItems`,
|
|
241
|
+
// then dock toggles. Ids are stable (`slot:<slot>`, the host's id,
|
|
242
|
+
// `dock:<tool>`); the first of a duplicated id wins.
|
|
243
|
+
interface PlacedEntry extends ShellPlacementItem {
|
|
244
|
+
render?: Snippet;
|
|
245
|
+
toggle?: DockToggle;
|
|
246
|
+
/** Stays interactive and is not draggable while the layout is edited. */
|
|
247
|
+
fixed?: boolean;
|
|
248
|
+
}
|
|
249
|
+
const entries = $derived.by(() => {
|
|
250
|
+
const out: PlacedEntry[] = [];
|
|
251
|
+
const seen = new Set<string>();
|
|
252
|
+
const add = (entry: PlacedEntry) => {
|
|
253
|
+
if (seen.has(entry.id)) return;
|
|
254
|
+
seen.add(entry.id);
|
|
255
|
+
out.push(entry);
|
|
256
|
+
};
|
|
257
|
+
add({
|
|
258
|
+
id: SHELL_BRAND_ITEM_ID,
|
|
259
|
+
label: t(M['ui.app_shell.brand_item']),
|
|
260
|
+
slot: 'header.start',
|
|
261
|
+
render: brandItem,
|
|
262
|
+
});
|
|
263
|
+
for (const slot of SHELL_SLOTS) {
|
|
264
|
+
const render = hostSlots?.[slot];
|
|
265
|
+
if (render) {
|
|
266
|
+
add({
|
|
267
|
+
id: shellHostSlotItemId(slot),
|
|
268
|
+
label: t(M['ui.app_shell.slot_content'], {
|
|
269
|
+
slot: t(SHELL_SLOT_MESSAGES[slot]),
|
|
270
|
+
}),
|
|
271
|
+
slot,
|
|
272
|
+
render,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const item of slotItems) add({ ...item });
|
|
277
|
+
for (const toggle of dockToggles) {
|
|
278
|
+
add({
|
|
279
|
+
id: shellDockItemId(toggle.tool),
|
|
280
|
+
label: toggle.label,
|
|
281
|
+
slot: toggle.slot ?? 'header.end',
|
|
282
|
+
toggle,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (layoutEditing && !layoutFloating) {
|
|
286
|
+
add({
|
|
287
|
+
id: SHELL_LAYOUT_EDIT_ITEM_ID,
|
|
288
|
+
label: t(M['ui.layout_edit.toggle']),
|
|
289
|
+
slot:
|
|
290
|
+
typeof layoutEditing === 'object'
|
|
291
|
+
? (layoutEditing.slot ?? 'header.end')
|
|
292
|
+
: 'header.end',
|
|
293
|
+
render: layoutToggle,
|
|
294
|
+
fixed: true,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
});
|
|
134
299
|
// The shell state lives here (not inside AdminShell) so the layout can reach
|
|
135
300
|
// it: panel overrides apply to it, and the user's layout is stored in it when
|
|
136
301
|
// the host does not own persistence.
|
|
@@ -155,6 +320,31 @@ const applied = $derived(
|
|
|
155
320
|
);
|
|
156
321
|
const hasNav = $derived(applied.nav.length > 0 || applied.groups.length > 0);
|
|
157
322
|
|
|
323
|
+
const slotIds = $derived(resolveShellPlacements(entries, effectiveLayout));
|
|
324
|
+
const entriesFor = (name: ShellSlot): PlacedEntry[] => {
|
|
325
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
326
|
+
return slotIds[name].flatMap((id) => byId.get(id) ?? []);
|
|
327
|
+
};
|
|
328
|
+
const slotSnippets: Record<ShellSlot, Snippet> = {
|
|
329
|
+
'header.start': slot_0,
|
|
330
|
+
'header.center': slot_1,
|
|
331
|
+
'header.end': slot_2,
|
|
332
|
+
'footer.start': slot_3,
|
|
333
|
+
'footer.center': slot_4,
|
|
334
|
+
'footer.end': slot_5,
|
|
335
|
+
'leftSidebar.header': slot_6,
|
|
336
|
+
'leftSidebar.footer': slot_7,
|
|
337
|
+
'rightSidebar.header': slot_8,
|
|
338
|
+
'rightSidebar.footer': slot_9,
|
|
339
|
+
};
|
|
340
|
+
const shellSlots = $derived.by(() => {
|
|
341
|
+
const out: Partial<Record<ShellSlot, Snippet>> = {};
|
|
342
|
+
for (const name of SHELL_SLOTS) {
|
|
343
|
+
if (slotIds[name].length > 0) out[name] = slotSnippets[name];
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
});
|
|
347
|
+
|
|
158
348
|
// Starting-state edits made through the layout API, applied once the layout
|
|
159
349
|
// carrying them is in force (or dropped if the host answered differently).
|
|
160
350
|
const pendingStart = new Map<PanelEdge, 'collapsed' | 'expanded'>();
|
|
@@ -169,38 +359,348 @@ $effect(() => {
|
|
|
169
359
|
pendingStart.clear();
|
|
170
360
|
});
|
|
171
361
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
if (!controlled) {
|
|
193
|
-
if (onlayoutchange) localLayout = next;
|
|
194
|
-
else shell.setLayout(next);
|
|
362
|
+
const layoutApi = new ShellLayoutController({
|
|
363
|
+
editable: () => !!layoutEditing,
|
|
364
|
+
nav: () => nav,
|
|
365
|
+
groups: () => navGroups,
|
|
366
|
+
panels: () => config,
|
|
367
|
+
layout: () => effectiveLayout,
|
|
368
|
+
items: () => entries.map(({ id, label, slot }) => ({ id, label, slot })),
|
|
369
|
+
commit(next) {
|
|
370
|
+
// A changed starting state is an explicit edit: show it now. It waits
|
|
371
|
+
// for the layout to actually change (the host may reject the edit) and
|
|
372
|
+
// is never done for a loaded layout (hydration, a late `layout` prop),
|
|
373
|
+
// so loading cannot erase the user's own open/closed toggle.
|
|
374
|
+
for (const edge of PANEL_EDGES) {
|
|
375
|
+
const before = effectiveLayout.panels?.[edge]?.initial;
|
|
376
|
+
const after = next.panels?.[edge]?.initial;
|
|
377
|
+
if (before === after) continue;
|
|
378
|
+
const state = after ?? resolveShellConfig(config).panels[edge].initial;
|
|
379
|
+
if (state === 'collapsed' || state === 'expanded') {
|
|
380
|
+
pendingStart.set(edge, state);
|
|
195
381
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
382
|
+
}
|
|
383
|
+
if (!controlled) {
|
|
384
|
+
if (onlayoutchange) localLayout = next;
|
|
385
|
+
else shell.setLayout(next);
|
|
386
|
+
}
|
|
387
|
+
onlayoutchange?.(next);
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
setShellLayout(layoutApi);
|
|
391
|
+
|
|
392
|
+
// ---- In-place layout editing -------------------------------------------
|
|
393
|
+
const editing = $derived(layoutApi.editing);
|
|
394
|
+
let shellRoot = $state<HTMLElement | undefined>();
|
|
395
|
+
// The floating toggle sits over the header's end (header visible), the top of
|
|
396
|
+
// the right sidebar (header hidden) or the top of main (neither); reserve that room so it covers no
|
|
397
|
+
// control. AdminShell reads these custom properties.
|
|
398
|
+
const floatingReserve = $derived.by(() => {
|
|
399
|
+
if (!layoutFloating) return undefined;
|
|
400
|
+
const size = 'calc(2.75rem + var(--smrt-spacing-3))';
|
|
401
|
+
const headerShown = shell.isRegionVisible('header');
|
|
402
|
+
// Header hidden: the button sits over whichever region is topmost at the
|
|
403
|
+
// right edge: the right sidebar when shown, otherwise the main content.
|
|
404
|
+
const sidebarShown = shell.isEdgeShown('right');
|
|
405
|
+
return {
|
|
406
|
+
inline: headerShown ? size : '0px',
|
|
407
|
+
block: !headerShown && sidebarShown ? size : '0px',
|
|
408
|
+
main: !headerShown && !sidebarShown ? size : '0px',
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
let modeMessage = $state('');
|
|
412
|
+
let wasEditing = false;
|
|
413
|
+
$effect(() => {
|
|
414
|
+
const now = editing;
|
|
415
|
+
if (now === wasEditing) return;
|
|
416
|
+
wasEditing = now;
|
|
417
|
+
modeMessage = t(
|
|
418
|
+
now ? M['ui.layout_edit.announce_on'] : M['ui.layout_edit.announce_off'],
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
/** Visual (row-major) order of the drop zones, which is also the keyboard order. */
|
|
423
|
+
const ZONE_ORDER: readonly ShellSlot[] = [
|
|
424
|
+
'header.start',
|
|
425
|
+
'header.center',
|
|
426
|
+
'header.end',
|
|
427
|
+
'leftSidebar.header',
|
|
428
|
+
'rightSidebar.header',
|
|
429
|
+
'leftSidebar.footer',
|
|
430
|
+
'rightSidebar.footer',
|
|
431
|
+
'footer.start',
|
|
432
|
+
'footer.center',
|
|
433
|
+
'footer.end',
|
|
434
|
+
];
|
|
435
|
+
const regionVisible = (region: ShellRegion): boolean =>
|
|
436
|
+
shell.isRegionVisible(region);
|
|
437
|
+
const zones = $derived(
|
|
438
|
+
ZONE_ORDER.filter((slot) => regionVisible(slotRegion(slot))),
|
|
439
|
+
);
|
|
440
|
+
function zoneLabel(slot: ShellSlot): string {
|
|
441
|
+
return t(M['ui.layout_edit.zone'], {
|
|
442
|
+
region: t(SHELL_REGION_MESSAGES[slotRegion(slot)]),
|
|
443
|
+
slot: t(SHELL_SLOT_SHORT_MESSAGES[slot]),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// Items shown in a zone: those placed there, then those that fall back to it
|
|
447
|
+
// because their own region is hidden or collapsed.
|
|
448
|
+
function zoneEntries(zone: ShellSlot): PlacedEntry[] {
|
|
449
|
+
const own = entriesFor(zone);
|
|
450
|
+
const fallen = SHELL_SLOTS.filter(
|
|
451
|
+
(name) => name !== zone && resolveSlot(name, regionVisible) === zone,
|
|
452
|
+
).flatMap((name) => entriesFor(name));
|
|
453
|
+
return [...own, ...fallen];
|
|
454
|
+
}
|
|
455
|
+
const movableIn = (zone: string): PlacedEntry[] =>
|
|
456
|
+
zoneEntries(zone as ShellSlot).filter((entry) => !entry.fixed);
|
|
457
|
+
|
|
458
|
+
const slotSortable = createSortable({
|
|
459
|
+
root: () => shellRoot,
|
|
460
|
+
selectors: {
|
|
461
|
+
container: '[data-smrt-edit-zone]',
|
|
462
|
+
containerKey: 'smrtEditZone',
|
|
463
|
+
item: '[data-smrt-edit-item]',
|
|
464
|
+
itemKey: 'smrtEditItem',
|
|
465
|
+
},
|
|
466
|
+
containers: () => zones.map((slot) => ({ id: slot, label: zoneLabel(slot) })),
|
|
467
|
+
itemIds: (zone) => movableIn(zone).map((entry) => entry.id),
|
|
468
|
+
itemLabel: (id) =>
|
|
469
|
+
entries.find((entry) => entry.id === id && !entry.fixed)?.label,
|
|
470
|
+
allowSameContainerReorder: () => false,
|
|
471
|
+
enabled: () => editing,
|
|
472
|
+
orientation: () => 'vertical',
|
|
473
|
+
announce: (announcement) => formatSortableAnnouncement(t, announcement),
|
|
474
|
+
focusTarget: (element) =>
|
|
475
|
+
element.querySelector<HTMLElement>('[data-smrt-edit-handle]'),
|
|
476
|
+
commit(move) {
|
|
477
|
+
layoutApi.placeItem(move.itemId, move.target.containerId as ShellSlot);
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Native HTML drag has no per-zone listeners (the zones belong to AdminShell):
|
|
482
|
+
// delegate from the shell root.
|
|
483
|
+
$effect(() => {
|
|
484
|
+
const root = shellRoot;
|
|
485
|
+
if (!root || !editing) return;
|
|
486
|
+
const over = (event: DragEvent) => {
|
|
487
|
+
if (slotSortable.drag) event.preventDefault();
|
|
488
|
+
};
|
|
489
|
+
const drop = (event: DragEvent) => {
|
|
490
|
+
if (!slotSortable.drag || !(event.target instanceof Element)) return;
|
|
491
|
+
const zone = event.target.closest<HTMLElement>('[data-smrt-edit-zone]');
|
|
492
|
+
if (zone?.dataset.smrtEditZone) {
|
|
493
|
+
slotSortable.dropOnContainer(event, zone.dataset.smrtEditZone);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
root.addEventListener('dragover', over);
|
|
497
|
+
root.addEventListener('drop', drop);
|
|
498
|
+
return () => {
|
|
499
|
+
root.removeEventListener('dragover', over);
|
|
500
|
+
root.removeEventListener('drop', drop);
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const REGION_OF_EDGE: Record<PanelEdge, ShellRegion> = {
|
|
505
|
+
top: 'header',
|
|
506
|
+
bottom: 'footer',
|
|
507
|
+
left: 'leftSidebar',
|
|
508
|
+
right: 'rightSidebar',
|
|
509
|
+
};
|
|
510
|
+
const hiddenRegions = $derived(
|
|
511
|
+
layoutApi.panels
|
|
512
|
+
.filter((panel) => panel.available && !panel.visible)
|
|
513
|
+
.map((panel) => REGION_OF_EDGE[panel.edge]),
|
|
199
514
|
);
|
|
515
|
+
const editSurface = $derived({
|
|
516
|
+
active: editing,
|
|
517
|
+
highlight:
|
|
518
|
+
(slotSortable.drag?.target.containerId as ShellSlot | undefined) ?? null,
|
|
519
|
+
zone: editZone,
|
|
520
|
+
hiddenRegions,
|
|
521
|
+
strip: editStrip,
|
|
522
|
+
regionControl: editRegionControl,
|
|
523
|
+
});
|
|
524
|
+
const visibleRegionCount = $derived(
|
|
525
|
+
layoutApi.panels.filter((panel) => panel.available && panel.visible).length,
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// Editing reveals every shown sidebar so its header/footer drop zones exist
|
|
529
|
+
// (a collapsed one only has a rail); leaving restores what was collapsed.
|
|
530
|
+
// A railless edge (`rail: false`) is left as the user has it: closed stays
|
|
531
|
+
// closed (AdminShell offers an indicator tab to open it while editing).
|
|
532
|
+
let revealedByEdit: ('left' | 'right')[] = [];
|
|
533
|
+
$effect(() => {
|
|
534
|
+
if (editing) {
|
|
535
|
+
untrack(() => {
|
|
536
|
+
if (shell.viewport === 'phone') return;
|
|
537
|
+
for (const edge of ['left', 'right'] as const) {
|
|
538
|
+
if (shell.config.panels[edge].rail === false) continue;
|
|
539
|
+
// Opened for now only: nothing is saved, so the user's own panel
|
|
540
|
+
// state survives edit mode, a reload and a page unload.
|
|
541
|
+
if (shell.revealPanelTemporarily(edge)) revealedByEdit.push(edge);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
untrack(() => {
|
|
547
|
+
for (const edge of revealedByEdit) shell.endTemporaryReveal(edge);
|
|
548
|
+
revealedByEdit = [];
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Escape leaves edit mode, unless it is cancelling something first: a section
|
|
553
|
+
// toolbar (closed by ShellNavEditor, which marks the event handled) or a
|
|
554
|
+
// keyboard move in progress.
|
|
555
|
+
$effect(() => {
|
|
556
|
+
if (!editing) return;
|
|
557
|
+
const onKey = (event: KeyboardEvent) => {
|
|
558
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
559
|
+
if (slotSortable.drag) return;
|
|
560
|
+
if (shellRoot?.querySelector('[data-toolbar-open]')) return;
|
|
561
|
+
layoutApi.setEditing(false);
|
|
562
|
+
};
|
|
563
|
+
window.addEventListener('keydown', onKey);
|
|
564
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
async function toggleEditing(): Promise<void> {
|
|
568
|
+
layoutApi.setEditing(!layoutApi.editing);
|
|
569
|
+
await tick();
|
|
570
|
+
}
|
|
571
|
+
const EDGE_OF_REGION: Record<ShellRegion, PanelEdge> = {
|
|
572
|
+
header: 'top',
|
|
573
|
+
footer: 'bottom',
|
|
574
|
+
leftSidebar: 'left',
|
|
575
|
+
rightSidebar: 'right',
|
|
576
|
+
};
|
|
200
577
|
</script>
|
|
201
578
|
|
|
202
|
-
{#snippet
|
|
203
|
-
|
|
579
|
+
{#snippet slotBody(name: ShellSlot)}
|
|
580
|
+
{#each entriesFor(name) as entry (entry.id)}
|
|
581
|
+
{#if entry.toggle}
|
|
582
|
+
<DockToggles toggles={[entry.toggle]} />
|
|
583
|
+
{:else if entry.render}
|
|
584
|
+
{@render entry.render()}
|
|
585
|
+
{/if}
|
|
586
|
+
{/each}
|
|
587
|
+
{/snippet}
|
|
588
|
+
{#snippet slot_0()}
|
|
589
|
+
{@render slotBody('header.start')}
|
|
590
|
+
{/snippet}
|
|
591
|
+
{#snippet slot_1()}
|
|
592
|
+
{@render slotBody('header.center')}
|
|
593
|
+
{/snippet}
|
|
594
|
+
{#snippet slot_2()}
|
|
595
|
+
{@render slotBody('header.end')}
|
|
596
|
+
{/snippet}
|
|
597
|
+
{#snippet slot_3()}
|
|
598
|
+
{@render slotBody('footer.start')}
|
|
599
|
+
{/snippet}
|
|
600
|
+
{#snippet slot_4()}
|
|
601
|
+
{@render slotBody('footer.center')}
|
|
602
|
+
{/snippet}
|
|
603
|
+
{#snippet slot_5()}
|
|
604
|
+
{@render slotBody('footer.end')}
|
|
605
|
+
{/snippet}
|
|
606
|
+
{#snippet slot_6()}
|
|
607
|
+
{@render slotBody('leftSidebar.header')}
|
|
608
|
+
{/snippet}
|
|
609
|
+
{#snippet slot_7()}
|
|
610
|
+
{@render slotBody('leftSidebar.footer')}
|
|
611
|
+
{/snippet}
|
|
612
|
+
{#snippet slot_8()}
|
|
613
|
+
{@render slotBody('rightSidebar.header')}
|
|
614
|
+
{/snippet}
|
|
615
|
+
{#snippet slot_9()}
|
|
616
|
+
{@render slotBody('rightSidebar.footer')}
|
|
617
|
+
{/snippet}
|
|
618
|
+
|
|
619
|
+
{#snippet layoutToggle()}
|
|
620
|
+
<span class="smrt-layout-toggle" class:smrt-layout-toggle--floating={layoutFloating} data-testid="layout-edit-toggle">
|
|
621
|
+
<ShellIconButton
|
|
622
|
+
icon="edit"
|
|
623
|
+
pressed={editing}
|
|
624
|
+
label={t(M['ui.layout_edit.toggle'])}
|
|
625
|
+
tooltip={t(M['ui.layout_edit.toggle'])}
|
|
626
|
+
onclick={toggleEditing}
|
|
627
|
+
/>
|
|
628
|
+
</span>
|
|
629
|
+
{/snippet}
|
|
630
|
+
|
|
631
|
+
{#snippet entryBody(entry: PlacedEntry)}
|
|
632
|
+
{#if entry.toggle}
|
|
633
|
+
<DockToggles toggles={[entry.toggle]} />
|
|
634
|
+
{:else if entry.render}
|
|
635
|
+
{@render entry.render()}
|
|
636
|
+
{/if}
|
|
637
|
+
{/snippet}
|
|
638
|
+
|
|
639
|
+
{#snippet editZone(slot: ShellSlot)}
|
|
640
|
+
<span class="smrt-edit-zone__label">{zoneLabel(slot)}</span>
|
|
641
|
+
{#each zoneEntries(slot) as entry (entry.id)}
|
|
642
|
+
{#if entry.fixed}
|
|
643
|
+
{@render entryBody(entry)}
|
|
644
|
+
{:else}
|
|
645
|
+
{@const dragging = slotSortable.drag?.itemId === entry.id}
|
|
646
|
+
<span
|
|
647
|
+
class="smrt-edit-item"
|
|
648
|
+
class:smrt-edit-item--dragging={dragging}
|
|
649
|
+
data-smrt-edit-item={entry.id}
|
|
650
|
+
>
|
|
651
|
+
<!-- raw-primitive-allow: native button owns keyboard pickup and HTML drag/drop -->
|
|
652
|
+
<button
|
|
653
|
+
type="button"
|
|
654
|
+
class="smrt-edit-item__grip"
|
|
655
|
+
data-smrt-edit-handle=""
|
|
656
|
+
draggable={slotSortable.nativeDraggable}
|
|
657
|
+
aria-label={t(SORTABLE_STRINGS['ui.sortable.move'], { item: entry.label })}
|
|
658
|
+
title={t(SORTABLE_STRINGS['ui.sortable.move'], { item: entry.label })}
|
|
659
|
+
aria-pressed={dragging}
|
|
660
|
+
onclick={() => slotSortable.consumeClick(entry.id)}
|
|
661
|
+
onkeydown={(event) => slotSortable.keydown(event, entry.id)}
|
|
662
|
+
onblur={(event) => slotSortable.blur(event, entry.id)}
|
|
663
|
+
onpointerdown={(event) => slotSortable.pointerDown(event, entry.id)}
|
|
664
|
+
onpointermove={(event) => slotSortable.pointerMove(event)}
|
|
665
|
+
onpointerup={(event) => slotSortable.pointerUp(event)}
|
|
666
|
+
onpointercancel={(event) => slotSortable.pointerCancel(event)}
|
|
667
|
+
ondragstart={(event) => slotSortable.dragStart(event, entry.id)}
|
|
668
|
+
ondragend={() => slotSortable.dragEnd()}
|
|
669
|
+
><span aria-hidden="true">⠿</span></button>
|
|
670
|
+
<span class="smrt-edit-item__body" inert>{@render entryBody(entry)}</span>
|
|
671
|
+
</span>
|
|
672
|
+
{/if}
|
|
673
|
+
{/each}
|
|
674
|
+
{/snippet}
|
|
675
|
+
|
|
676
|
+
{#snippet brandItem()}
|
|
677
|
+
<ShellBrand {title} {subtitle} {logoSrc} {logoAlt} {homeHref} />
|
|
678
|
+
{/snippet}
|
|
679
|
+
|
|
680
|
+
{#snippet editRegionControl(region: ShellRegion)}
|
|
681
|
+
{@const label = t(SHELL_REGION_MESSAGES[region])}
|
|
682
|
+
{@const last = visibleRegionCount <= 1}
|
|
683
|
+
<ShellIconButton
|
|
684
|
+
icon="eyeOff"
|
|
685
|
+
size={16}
|
|
686
|
+
disabled={last}
|
|
687
|
+
label={t(M['ui.layout_edit.hide_region'], { region: label })}
|
|
688
|
+
tooltip={last
|
|
689
|
+
? t(M['ui.layout_edit.hide_last_region'], { region: label })
|
|
690
|
+
: t(M['ui.layout_edit.hide_region'], { region: label })}
|
|
691
|
+
onclick={() => layoutApi.setPanel(EDGE_OF_REGION[region], { visible: false })}
|
|
692
|
+
/>
|
|
693
|
+
{/snippet}
|
|
694
|
+
|
|
695
|
+
{#snippet editStrip(region: ShellRegion)}
|
|
696
|
+
{@const label = t(SHELL_REGION_MESSAGES[region])}
|
|
697
|
+
<span>{t(M['ui.layout_edit.region_hidden'], { region: label })}</span>
|
|
698
|
+
<ShellIconButton
|
|
699
|
+
icon="eye"
|
|
700
|
+
size={16}
|
|
701
|
+
label={t(M['ui.layout_edit.show_region'], { region: label })}
|
|
702
|
+
onclick={() => layoutApi.setPanel(EDGE_OF_REGION[region], { visible: true })}
|
|
703
|
+
/>
|
|
204
704
|
{/snippet}
|
|
205
705
|
|
|
206
706
|
<Provider {webmcp} {user} {permissions}>
|
|
@@ -209,9 +709,18 @@ setShellLayout(
|
|
|
209
709
|
<AdminShell
|
|
210
710
|
{title}
|
|
211
711
|
{subtitle}
|
|
712
|
+
{logoSrc}
|
|
713
|
+
{logoAlt}
|
|
714
|
+
{homeHref}
|
|
212
715
|
state={shell}
|
|
213
716
|
path={currentHref}
|
|
214
|
-
|
|
717
|
+
slots={shellSlots}
|
|
718
|
+
layoutEdit={editSurface}
|
|
719
|
+
{edgeToggles}
|
|
720
|
+
{hotkeys}
|
|
721
|
+
brandInSlot
|
|
722
|
+
bind:rootElement={shellRoot}
|
|
723
|
+
{floatingReserve}
|
|
215
724
|
>
|
|
216
725
|
{#snippet appPanel()}
|
|
217
726
|
<AppScopePanel
|
|
@@ -229,17 +738,49 @@ setShellLayout(
|
|
|
229
738
|
</AppScopePanel>
|
|
230
739
|
{/snippet}
|
|
231
740
|
|
|
741
|
+
{#snippet tenantRail()}
|
|
742
|
+
{#if navMode === 'sections' && !editing && hasNav}
|
|
743
|
+
<TenantNav
|
|
744
|
+
collapsed
|
|
745
|
+
items={applied.nav}
|
|
746
|
+
groups={applied.groups}
|
|
747
|
+
{currentHref}
|
|
748
|
+
{navMode}
|
|
749
|
+
{sectionHref}
|
|
750
|
+
{iconComponent}
|
|
751
|
+
aria-label={t(M['ui.app_shell.navigation'])}
|
|
752
|
+
/>
|
|
753
|
+
{/if}
|
|
754
|
+
{/snippet}
|
|
755
|
+
|
|
232
756
|
{#snippet tenantPanel()}
|
|
233
|
-
{#if
|
|
757
|
+
{#if editing}
|
|
758
|
+
<ShellNavEditor
|
|
759
|
+
aria-label={t(M['ui.app_shell.navigation'])}
|
|
760
|
+
{sectionActions}
|
|
761
|
+
{navMode}
|
|
762
|
+
{iconComponent}
|
|
763
|
+
/>
|
|
764
|
+
{:else if hasNav}
|
|
234
765
|
<TenantNav
|
|
235
766
|
items={applied.nav}
|
|
236
767
|
groups={applied.groups}
|
|
237
768
|
{currentHref}
|
|
769
|
+
{navMode}
|
|
770
|
+
{sectionHref}
|
|
771
|
+
{iconComponent}
|
|
238
772
|
aria-label={t(M['ui.app_shell.navigation'])}
|
|
239
773
|
/>
|
|
240
774
|
{/if}
|
|
241
775
|
{/snippet}
|
|
242
776
|
|
|
777
|
+
<div class="smrt-layout-live" role="status" aria-live="polite">{modeMessage}</div>
|
|
778
|
+
{#if editing}
|
|
779
|
+
<div class="smrt-layout-live" aria-live="assertive" aria-atomic="true">{slotSortable.announcement}</div>
|
|
780
|
+
{/if}
|
|
781
|
+
{#if layoutFloating}
|
|
782
|
+
{@render layoutToggle()}
|
|
783
|
+
{/if}
|
|
243
784
|
{#if dock}
|
|
244
785
|
<DockSlot {dock} />
|
|
245
786
|
{/if}
|
|
@@ -252,4 +793,17 @@ setShellLayout(
|
|
|
252
793
|
a {
|
|
253
794
|
color: var(--smrt-color-primary);
|
|
254
795
|
}
|
|
796
|
+
.smrt-layout-toggle { display: inline-flex; align-items: center; gap: var(--smrt-spacing-1); }
|
|
797
|
+
.smrt-layout-toggle--floating { position: fixed; z-index: 45; inset-block-start: max(var(--smrt-spacing-3), env(safe-area-inset-top)); inset-inline-end: max(var(--smrt-spacing-3), env(safe-area-inset-right)); border-radius: var(--smrt-radius-full); background: var(--smrt-color-surface-container-high); box-shadow: var(--smrt-elevation-2); }
|
|
798
|
+
.smrt-layout-toggle--floating :global(.smrt-shell-icon-button) { border-radius: var(--smrt-radius-full); }
|
|
799
|
+
.smrt-layout-toggle :global(.smrt-shell-icon-button[aria-pressed='true']) { background: var(--smrt-color-primary-container); color: var(--smrt-color-on-primary-container); }
|
|
800
|
+
.smrt-layout-live { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
|
801
|
+
.smrt-edit-zone__label { flex: 0 0 auto; color: var(--smrt-color-on-surface-variant); font: var(--smrt-typography-label-small-font); white-space: nowrap; }
|
|
802
|
+
.smrt-edit-item { display: inline-flex; align-items: center; min-inline-size: 0; border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-medium); background: var(--smrt-color-surface); }
|
|
803
|
+
.smrt-edit-item--dragging { opacity: 0.55; }
|
|
804
|
+
.smrt-edit-item__body { display: inline-flex; align-items: center; min-inline-size: 0; pointer-events: none; }
|
|
805
|
+
.smrt-edit-item__grip { display: inline-grid; place-items: center; flex: 0 0 auto; inline-size: 1.5rem; block-size: 2rem; padding: 0; border: 0; border-radius: var(--smrt-radius-sm); background: transparent; color: var(--smrt-color-on-surface-variant); cursor: grab; touch-action: none; }
|
|
806
|
+
.smrt-edit-item__grip:hover { background: var(--smrt-color-surface-container-high); }
|
|
807
|
+
.smrt-edit-item__grip:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
|
|
808
|
+
.smrt-edit-item__grip[aria-pressed='true'] { background: var(--smrt-color-primary-container); color: var(--smrt-color-on-primary-container); }
|
|
255
809
|
</style>
|