@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.
Files changed (124) hide show
  1. package/AGENTS.md +17 -2
  2. package/README.md +83 -4
  3. package/agents/workspace.md +202 -5
  4. package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -1
  5. package/dist/browser-ai/adapters/stt/dictation-source.js +8 -2
  6. package/dist/browser-ai/adapters/stt/factory.d.ts +6 -3
  7. package/dist/browser-ai/adapters/stt/factory.d.ts.map +1 -1
  8. package/dist/browser-ai/adapters/stt/factory.js +19 -9
  9. package/dist/browser-ai/adapters/stt/index.d.ts +3 -1
  10. package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
  11. package/dist/browser-ai/adapters/stt/index.js +3 -1
  12. package/dist/browser-ai/adapters/stt/local-speech-model.d.ts +111 -0
  13. package/dist/browser-ai/adapters/stt/local-speech-model.d.ts.map +1 -0
  14. package/dist/browser-ai/adapters/stt/local-speech-model.js +377 -0
  15. package/dist/browser-ai/adapters/stt/local-speech.d.ts +82 -0
  16. package/dist/browser-ai/adapters/stt/local-speech.d.ts.map +1 -0
  17. package/dist/browser-ai/adapters/stt/local-speech.js +317 -0
  18. package/dist/browser-ai/adapters/stt/local-speech.worker.d.ts +2 -0
  19. package/dist/browser-ai/adapters/stt/local-speech.worker.d.ts.map +1 -0
  20. package/dist/browser-ai/adapters/stt/local-speech.worker.js +32 -0
  21. package/dist/browser-ai/adapters/stt/transformers-env.d.ts +8 -0
  22. package/dist/browser-ai/adapters/stt/transformers-env.d.ts.map +1 -0
  23. package/dist/browser-ai/adapters/stt/transformers-env.js +17 -0
  24. package/dist/browser-ai/adapters/stt/types.d.ts +54 -9
  25. package/dist/browser-ai/adapters/stt/types.d.ts.map +1 -1
  26. package/dist/browser-ai/capabilities/speech-probe.d.ts +29 -0
  27. package/dist/browser-ai/capabilities/speech-probe.d.ts.map +1 -0
  28. package/dist/browser-ai/capabilities/speech-probe.js +19 -0
  29. package/dist/browser-ai/core/errors.d.ts +9 -0
  30. package/dist/browser-ai/core/errors.d.ts.map +1 -1
  31. package/dist/browser-ai/core/errors.js +12 -0
  32. package/dist/browser-ai/index.d.ts +1 -0
  33. package/dist/browser-ai/index.d.ts.map +1 -1
  34. package/dist/browser-ai/index.js +1 -0
  35. package/dist/components/app/AppShell.svelte +587 -33
  36. package/dist/components/app/AppShell.svelte.d.ts +75 -4
  37. package/dist/components/app/AppShell.svelte.d.ts.map +1 -1
  38. package/dist/components/app/DockToggles.svelte +0 -1
  39. package/dist/components/app/dock-toggle.d.ts +7 -1
  40. package/dist/components/app/dock-toggle.d.ts.map +1 -1
  41. package/dist/components/app/index.d.ts +2 -1
  42. package/dist/components/app/index.d.ts.map +1 -1
  43. package/dist/components/app/slot-item.d.ts +21 -0
  44. package/dist/components/app/slot-item.d.ts.map +1 -0
  45. package/dist/components/app/slot-item.js +1 -0
  46. package/dist/components/forms/AddressInput.svelte +2 -15
  47. package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
  48. package/dist/components/forms/DateRangeInput.svelte +2 -15
  49. package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
  50. package/dist/components/forms/DateTimeInput.svelte +8 -15
  51. package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
  52. package/dist/components/forms/MeasurementInput.svelte +2 -15
  53. package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
  54. package/dist/components/forms/MoneyInput.svelte +8 -25
  55. package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
  56. package/dist/components/forms/NumberInput.svelte +7 -15
  57. package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
  58. package/dist/components/forms/PhoneInput.svelte +7 -15
  59. package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
  60. package/dist/components/forms/SelectInput.svelte +34 -40
  61. package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
  62. package/dist/components/forms/TextInput.svelte +20 -37
  63. package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
  64. package/dist/components/forms/TextareaInput.svelte +19 -35
  65. package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
  66. package/dist/components/sortable/Sortable.svelte +6 -33
  67. package/dist/components/sortable/Sortable.svelte.d.ts.map +1 -1
  68. package/dist/components/sortable/announce.d.ts +5 -0
  69. package/dist/components/sortable/announce.d.ts.map +1 -0
  70. package/dist/components/sortable/announce.js +33 -0
  71. package/dist/components/workspace/admin-shell/AdminShell.svelte +424 -58
  72. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +27 -2
  73. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
  74. package/dist/components/workspace/admin-shell/ShellBrand.svelte +86 -0
  75. package/dist/components/workspace/admin-shell/ShellBrand.svelte.d.ts +18 -0
  76. package/dist/components/workspace/admin-shell/ShellBrand.svelte.d.ts.map +1 -0
  77. package/dist/components/workspace/admin-shell/ShellIconButton.svelte +77 -0
  78. package/dist/components/workspace/admin-shell/ShellIconButton.svelte.d.ts +26 -0
  79. package/dist/components/workspace/admin-shell/ShellIconButton.svelte.d.ts.map +1 -0
  80. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte +88 -16
  81. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte.d.ts.map +1 -1
  82. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte +531 -0
  83. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte.d.ts +26 -0
  84. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte.d.ts.map +1 -0
  85. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte +57 -0
  86. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte.d.ts +22 -0
  87. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte.d.ts.map +1 -0
  88. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte +290 -0
  89. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte.d.ts +32 -0
  90. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte.d.ts.map +1 -0
  91. package/dist/components/workspace/admin-shell/TenantNav.svelte +67 -2
  92. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +8 -0
  93. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
  94. package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts +67 -0
  95. package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts.map +1 -1
  96. package/dist/components/workspace/admin-shell/layout-controller.svelte.js +129 -1
  97. package/dist/components/workspace/admin-shell/layout.d.ts +122 -0
  98. package/dist/components/workspace/admin-shell/layout.d.ts.map +1 -1
  99. package/dist/components/workspace/admin-shell/layout.js +410 -10
  100. package/dist/components/workspace/admin-shell/settings.js +4 -4
  101. package/dist/components/workspace/admin-shell/shell-icons.d.ts +45 -0
  102. package/dist/components/workspace/admin-shell/shell-icons.d.ts.map +1 -0
  103. package/dist/components/workspace/admin-shell/shell-icons.js +66 -0
  104. package/dist/components/workspace/admin-shell/slot-labels.d.ts +33 -0
  105. package/dist/components/workspace/admin-shell/slot-labels.d.ts.map +1 -0
  106. package/dist/components/workspace/admin-shell/slot-labels.js +33 -0
  107. package/dist/components/workspace/admin-shell/slots.d.ts +50 -0
  108. package/dist/components/workspace/admin-shell/slots.d.ts.map +1 -0
  109. package/dist/components/workspace/admin-shell/slots.js +77 -0
  110. package/dist/components/workspace/admin-shell/state.svelte.d.ts +35 -0
  111. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
  112. package/dist/components/workspace/admin-shell/state.svelte.js +111 -0
  113. package/dist/components/workspace/admin-shell/types.d.ts +101 -1
  114. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
  115. package/dist/components/workspace/index.d.ts +9 -2
  116. package/dist/components/workspace/index.d.ts.map +1 -1
  117. package/dist/components/workspace/index.js +6 -0
  118. package/dist/i18n/strings.workspace.d.ts +49 -0
  119. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  120. package/dist/i18n/strings.workspace.js +49 -0
  121. package/package.json +30 -19
  122. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts +0 -51
  123. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts.map +0 -1
  124. package/dist/browser-ai/adapters/stt/whisper-wasm.js +0 -380
@@ -11,16 +11,19 @@ function expandedSize(shell: ModuleShellState, edge: ModulePanelEdge): string {
11
11
 
12
12
  function trackFor(shell: ModuleShellState, edge: ModulePanelEdge): string {
13
13
  const state = shell.panels[edge];
14
- const config = shell.config.panels[edge];
15
14
  if (state === 'hidden') return '0rem';
16
- if (state === 'collapsed') return config.collapsedSize;
17
- if (shell.presentationFor(edge) === 'overlay') return config.collapsedSize;
15
+ if (state === 'collapsed') return collapsedSize(shell, edge);
16
+ if (shell.presentationFor(edge) === 'overlay')
17
+ return collapsedSize(shell, edge);
18
18
  return expandedSize(shell, edge);
19
19
  }
20
20
 
21
21
  function collapsedSize(shell: ModuleShellState, edge: ModulePanelEdge): string {
22
22
  if (shell.panels[edge] === 'hidden') return '0rem';
23
- return shell.config.panels[edge].collapsedSize;
23
+ const config = shell.config.panels[edge];
24
+ // A railless edge has nothing to show while closed.
25
+ if (config.rail === false) return '0rem';
26
+ return config.collapsedSize;
24
27
  }
25
28
 
26
29
  function buildLayoutStyle(shell: ModuleShellState): string {
@@ -47,6 +50,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
47
50
  import type { Snippet } from 'svelte';
48
51
  import { M } from '../../../i18n/strings.workspace.js';
49
52
  import ActivityBadge from './ActivityBadge.svelte';
53
+ import ShellBrand from './ShellBrand.svelte';
50
54
  import { setAdminShell } from './context.js';
51
55
  import {
52
56
  formatHotkeyBinding,
@@ -62,10 +66,17 @@ function buildLayoutStyle(shell: ModuleShellState): string {
62
66
  } from './mobile-shell-dom.js';
63
67
  import { clampPanelSize, resolveHotkey } from './settings.js';
64
68
  import { createShellState, type ShellState } from './state.svelte.js';
69
+ import {
70
+ resolveSlot,
71
+ type ShellRegion,
72
+ type ShellSlot,
73
+ slotRegion,
74
+ } from './slots.js';
65
75
  import type {
66
76
  AdminShellPhoneOptions,
67
77
  AdminShellProps,
68
78
  PanelEdge,
79
+ ShellLayoutEditSurface,
69
80
  ShellFocusTool,
70
81
  ShellViewport,
71
82
  } from './types.js';
@@ -85,6 +96,26 @@ function buildLayoutStyle(shell: ModuleShellState): string {
85
96
  tenantRailFooter?: Snippet;
86
97
  /** Content for the left tenant panel. */
87
98
  tenantPanel?: Snippet;
99
+ /**
100
+ * Host content for the region slots (`header.start|center|end`,
101
+ * `footer.start|center|end`, `leftSidebar.header|footer`,
102
+ * `rightSidebar.header|footer`). A slot whose region is not visible moves
103
+ * along its documented fallback chain (see `slotFallbackChain`).
104
+ */
105
+ slots?: Partial<Record<ShellSlot, Snippet>>;
106
+ /**
107
+ * In-place layout editing (`AppShell` supplies it): while `active`,
108
+ * slots render as drop zones and hidden regions as strips.
109
+ */
110
+ layoutEdit?: ShellLayoutEditSurface;
111
+ /** The shell's root element (bindable), e.g. to confine drag hit-testing. */
112
+ rootElement?: HTMLElement;
113
+ /**
114
+ * Room kept clear for a floating control (AppShell's floating layout
115
+ * toggle): CSS lengths for the header's end, the right sidebar's top and
116
+ * main's top. Part of the root's style so re-renders never drop it.
117
+ */
118
+ floatingReserve?: { inline?: string; block?: string; main?: string };
88
119
  /** Content for the tenant panel footer. */
89
120
  tenantFooter?: Snippet;
90
121
  /** Content for the right focus rail. */
@@ -152,7 +183,10 @@ function buildLayoutStyle(shell: ModuleShellState): string {
152
183
  let {
153
184
  title = 'SMRT',
154
185
  homeHref,
155
- showTenantToggle = true,
186
+ showTenantToggle,
187
+ edgeToggles = false,
188
+ hotkeys,
189
+ brandInSlot = false,
156
190
  logoSrc,
157
191
  logoAlt = '',
158
192
  brand,
@@ -169,6 +203,10 @@ function buildLayoutStyle(shell: ModuleShellState): string {
169
203
  tenantRailFooter,
170
204
  tenantPanel,
171
205
  tenantFooter,
206
+ slots,
207
+ layoutEdit,
208
+ rootElement = $bindable(),
209
+ floatingReserve,
172
210
  focusRail,
173
211
  focusPanel,
174
212
  systemBar,
@@ -266,6 +304,13 @@ function buildLayoutStyle(shell: ModuleShellState): string {
266
304
  (edge) => panelState(edge) === 'expanded' && shell.isEdgeShown(edge),
267
305
  ),
268
306
  );
307
+ const reserveStyle = $derived(
308
+ [
309
+ `--smrt-shell-floating-reserve-inline: ${floatingReserve?.inline ?? '0px'}`,
310
+ `--smrt-shell-floating-reserve-block: ${floatingReserve?.block ?? '0px'}`,
311
+ `--smrt-shell-floating-reserve-main: ${floatingReserve?.main ?? '0px'}`,
312
+ ].join('; '),
313
+ );
269
314
  const chromeStyle = $derived(
270
315
  [
271
316
  `--smrt-admin-shell-header-track: ${showHeader ? 'var(--smrt-admin-shell-header-size)' : '0rem'}`,
@@ -451,6 +496,9 @@ function buildLayoutStyle(shell: ModuleShellState): string {
451
496
  event.preventDefault();
452
497
  return;
453
498
  }
499
+ // While editing the layout, Escape belongs to the editor (cancel a
500
+ // move, close a toolbar, then leave edit mode), not to the panels.
501
+ if (layoutEdit?.active) return;
454
502
  if (shell.closeTopmostExpanded()) event.preventDefault();
455
503
  return;
456
504
  }
@@ -461,10 +509,13 @@ function buildLayoutStyle(shell: ModuleShellState): string {
461
509
  shell.settings,
462
510
  );
463
511
  if (!action) return;
464
- event.preventDefault();
465
512
  if (action.type === 'show-shortcuts') {
513
+ if (!shortcutEdges.some(hotkeyActive)) return;
514
+ event.preventDefault();
466
515
  shortcutsOpen = true;
467
516
  } else {
517
+ if (!hotkeyActive(action.edge)) return;
518
+ event.preventDefault();
468
519
  shell.togglePanel(action.edge);
469
520
  }
470
521
  }
@@ -619,7 +670,53 @@ function buildLayoutStyle(shell: ModuleShellState): string {
619
670
  (!edgeExpanded('left') && !tenantRailFooter)),
620
671
  );
621
672
 
622
- const layoutStyle = $derived(buildLayoutStyle(shell));
673
+ function regionVisible(region: ShellRegion): boolean {
674
+ return shell.isRegionVisible(region);
675
+ }
676
+
677
+ /**
678
+ * Whether a region shows its hide control in edit mode: a sidebar has one as
679
+ * soon as its rail is rendered, even while collapsed (its slots then fall
680
+ * back, but the region itself is still there to hide).
681
+ */
682
+ function regionControllable(region: ShellRegion): boolean {
683
+ if (region === 'leftSidebar') return shell.isEdgeShown('left');
684
+ if (region === 'rightSidebar') return shell.isEdgeShown('right');
685
+ return regionVisible(region);
686
+ }
687
+
688
+ /** Slots whose content lands at `target` (own content plus fallbacks). */
689
+ const slotContent = $derived.by(() => {
690
+ const placed: Partial<Record<ShellSlot, Snippet[]>> = {};
691
+ for (const [name, snippet] of Object.entries(slots ?? {}) as [
692
+ ShellSlot,
693
+ Snippet | undefined,
694
+ ][]) {
695
+ if (!snippet) continue;
696
+ const target = resolveSlot(name, regionVisible);
697
+ if (target) (placed[target] ??= []).push(snippet);
698
+ }
699
+ return placed;
700
+ });
701
+
702
+ const STRIP_TRACK = '1.75rem';
703
+ const REGION_TRACK: Record<ShellRegion, string> = {
704
+ header: 'top',
705
+ footer: 'bottom',
706
+ leftSidebar: 'left',
707
+ rightSidebar: 'right',
708
+ };
709
+ const stripRegions = $derived(
710
+ layoutEdit?.active && layoutEdit.strip ? (layoutEdit.hiddenRegions ?? []) : [],
711
+ );
712
+ const layoutStyle = $derived(
713
+ [
714
+ buildLayoutStyle(shell),
715
+ ...stripRegions.map(
716
+ (region) => `--smrt-admin-shell-${REGION_TRACK[region]}-track: ${STRIP_TRACK}`,
717
+ ),
718
+ ].join('; '),
719
+ );
623
720
 
624
721
  // Top/bottom edges reserve grid tracks for corner snippets. When a corner
625
722
  // isn't mounted its track must collapse, otherwise the band is squeezed
@@ -647,10 +744,42 @@ function buildLayoutStyle(shell: ModuleShellState): string {
647
744
  function keepsContent(edge: PanelEdge): boolean {
648
745
  return (
649
746
  panelState(edge) === 'collapsed' &&
650
- shell.config.panels[edge].keepMounted === true
747
+ shell.config.panels[edge].keepMounted === true &&
748
+ !((edge === 'top' || edge === 'bottom') && shell.isInline(edge))
651
749
  );
652
750
  }
653
751
 
752
+ // While editing, a closed railless right edge that has focus tools gets a
753
+ // slim indicator tab so its content stays reachable; the host's header
754
+ // toggle covers normal use.
755
+ const editIndicator = $derived(
756
+ !!layoutEdit?.active &&
757
+ !isPhone &&
758
+ shell.config.panels.right.rail === false &&
759
+ shell.isEdgeShown('right') &&
760
+ panelState('right') === 'collapsed' &&
761
+ shell.focusTools.length > 0
762
+ ? shell.focusTools
763
+ : null,
764
+ );
765
+ let openedByIndicator = false;
766
+ function openFromIndicator(): void {
767
+ const tools = shell.focusTools;
768
+ if (tools.length === 0) return;
769
+ const tool = tools.find((candidate) => candidate.id === shell.activeFocusToolId) ?? tools[0];
770
+ shell.openFocusTool(tool.id);
771
+ openedByIndicator = true;
772
+ }
773
+ $effect(() => {
774
+ if (layoutEdit?.active) return;
775
+ untrack(() => {
776
+ if (openedByIndicator && shell.panels.right === 'expanded') {
777
+ shell.collapsePanel('right');
778
+ }
779
+ openedByIndicator = false;
780
+ });
781
+ });
782
+
654
783
  function panelMounted(edge: PanelEdge): boolean {
655
784
  return shownOpen(edge) || keepsContent(edge);
656
785
  }
@@ -665,8 +794,55 @@ function buildLayoutStyle(shell: ModuleShellState): string {
665
794
  );
666
795
  }
667
796
 
797
+ function toggleOn(edge: PanelEdge): boolean {
798
+ return typeof edgeToggles === 'boolean'
799
+ ? edgeToggles
800
+ : edgeToggles[edge] === true;
801
+ }
802
+
803
+ /** The edge draws its toggle button (phones always get one in the left drawer). */
804
+ function showToggle(edge: PanelEdge): boolean {
805
+ let on = toggleOn(edge);
806
+ if (edge === 'left') {
807
+ if (showTenantToggle === false) on = false;
808
+ else if (showTenantToggle === true) on = true;
809
+ }
810
+ return on;
811
+ }
812
+
813
+ const dropdownEdges = $derived({
814
+ top: showToggle('top') || toggleOn('top') || hotkeys === true,
815
+ left: showToggle('left') || toggleOn('left') || hotkeys === true,
816
+ right: showToggle('right') || toggleOn('right') || hotkeys === true,
817
+ bottom: showToggle('bottom') || toggleOn('bottom') || hotkeys === true,
818
+ });
819
+ // Edges without a drop-down are laid out inline (see `ShellState.isInline`).
820
+ shell.setInlineEdges(
821
+ untrack(() => ({
822
+ top: !dropdownEdges.top,
823
+ left: !dropdownEdges.left,
824
+ right: !dropdownEdges.right,
825
+ bottom: !dropdownEdges.bottom,
826
+ })),
827
+ );
828
+ $effect(() => {
829
+ const next = dropdownEdges;
830
+ shell.setInlineEdges({
831
+ top: !next.top,
832
+ left: !next.left,
833
+ right: !next.right,
834
+ bottom: !next.bottom,
835
+ });
836
+ });
837
+
838
+ function hotkeyActive(edge: PanelEdge): boolean {
839
+ if (hotkeys !== undefined) return hotkeys;
840
+ return toggleOn(edge) || showToggle(edge);
841
+ }
842
+
668
843
  function showsHotkeyFor(edge: PanelEdge): boolean {
669
844
  return (
845
+ hotkeyActive(edge) &&
670
846
  shell.settings.hotkeysEnabled !== false &&
671
847
  resolveHotkey(edge, shell.config.panels[edge], shell.settings) !== null
672
848
  );
@@ -774,30 +950,53 @@ function buildLayoutStyle(shell: ModuleShellState): string {
774
950
  {/if}
775
951
  {/snippet}
776
952
 
777
- {#snippet brandContent(compact: boolean)}
778
- {#if brand}
779
- {@render brand({ compact })}
780
- {:else}
781
- {#if logoSrc}<img class="smrt-admin-shell__logo" src={logoSrc} alt={logoAlt} />{/if}
782
- {#if compact}
783
- {#if !logoSrc}<span aria-hidden="true">{title.charAt(0)}</span>{/if}
784
- {:else}
785
- <div class="smrt-admin-shell__brand-text"><strong>{title}</strong>{#if subtitle}<span>{subtitle}</span>{/if}</div>
953
+ {#snippet shellBrand(compact = false)}
954
+ <ShellBrand {title} {subtitle} {homeHref} {logoSrc} {logoAlt} {brand} {compact} />
955
+ {/snippet}
956
+
957
+ {#snippet slotGroup(name: ShellSlot)}
958
+ {#if layoutEdit?.active}
959
+ {#if regionVisible(slotRegion(name))}
960
+ <div
961
+ class="smrt-admin-shell__slot smrt-admin-shell__slot--{name.replace('.', '-')} smrt-admin-shell__slot--editing"
962
+ data-slot={name}
963
+ data-smrt-edit-zone={name}
964
+ data-drop-target={layoutEdit.highlight === name ? '' : undefined}
965
+ >
966
+ {@render layoutEdit.zone(name)}
967
+ </div>
786
968
  {/if}
969
+ {:else if slotContent[name]?.length}
970
+ <div
971
+ class="smrt-admin-shell__slot smrt-admin-shell__slot--{name.replace('.', '-')}"
972
+ data-slot={name}
973
+ >
974
+ {#each slotContent[name] ?? [] as content}{@render content()}{/each}
975
+ </div>
787
976
  {/if}
788
977
  {/snippet}
789
978
 
790
- {#snippet shellBrand(compact = false)}
791
- {#if homeHref}
792
- <a class="smrt-admin-shell__brand smrt-admin-shell__brand-link" class:compact href={homeHref} aria-label={title || t(M['ui.admin_shell.home'])}>
793
- {@render brandContent(compact)}
794
- </a>
795
- {:else}
796
- <div class="smrt-admin-shell__brand" class:compact>{@render brandContent(compact)}</div>
979
+ {#snippet regionEdit(region: ShellRegion)}
980
+ {#if layoutEdit?.active && layoutEdit.regionControl && regionControllable(region)}
981
+ <div class="smrt-admin-shell__region-edit" data-region-edit={region}>
982
+ {@render layoutEdit.regionControl(region)}
983
+ </div>
984
+ {/if}
985
+ {/snippet}
986
+
987
+ {#snippet regionStrip(region: ShellRegion)}
988
+ {#if stripRegions.includes(region)}
989
+ <div
990
+ class="smrt-admin-shell__strip smrt-admin-shell__strip--{region}"
991
+ data-region-strip={region}
992
+ >
993
+ {@render layoutEdit?.strip?.(region)}
994
+ </div>
797
995
  {/if}
798
996
  {/snippet}
799
997
 
800
998
  <div
999
+ bind:this={rootElement}
801
1000
  class="smrt-admin-shell"
802
1001
  data-top-state={panelState('top')}
803
1002
  data-left-state={panelState('left')}
@@ -808,7 +1007,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
808
1007
  data-phone-top={showPhoneTop ? '' : undefined}
809
1008
  data-chrome-hidden={chromeHidden ? '' : undefined}
810
1009
  data-resizing={resizing ?? undefined}
811
- style={`${layoutStyle}; ${chromeStyle}`}
1010
+ style={`${layoutStyle}; ${chromeStyle}; ${reserveStyle}`}
812
1011
  >
813
1012
  {#if showHeader}
814
1013
  <div
@@ -820,6 +1019,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
820
1019
  </div>
821
1020
  {/if}
822
1021
 
1022
+ {@render regionStrip('header')}
823
1023
  {#if panelState('top') !== 'hidden'}
824
1024
  <header
825
1025
  id="smrt-admin-shell-top-panel"
@@ -840,19 +1040,23 @@ function buildLayoutStyle(shell: ModuleShellState): string {
840
1040
  {#if appBar}
841
1041
  {@render appBar()}
842
1042
  {:else}
843
- {@render shellBrand()}
844
- {@render edgeToggle('top')}
1043
+ {#if !brandInSlot}{@render shellBrand()}{/if}
1044
+ {#if showToggle('top')}{@render edgeToggle('top')}{/if}
845
1045
  {/if}
846
- {#if shell.config.panels.left.initial !== 'hidden' && shell.layoutPanels.left?.visible !== false && (!isPhone || shell.phonePresentation('left') !== 'hidden')}
1046
+ {#if !(layoutEdit?.active && !isPhone) && shell.config.panels.left.initial !== 'hidden' && shell.layoutPanels.left?.visible !== false && (!isPhone || shell.phonePresentation('left') !== 'hidden')}
847
1047
  <div class="smrt-admin-shell__tenant-opener" class:restore-hidden={panelState('left') === 'hidden'}>
848
1048
  <Button variant="ghost" size="sm" aria-label={t(M['ui.admin_shell.menu'])} aria-expanded={edgeExpanded('left')} aria-controls="smrt-admin-shell-left-panel" onclick={() => shell.setPanelState('left', edgeExpanded('left') ? 'collapsed' : 'expanded')}>{t(M['ui.admin_shell.menu'])}</Button>
849
1049
  </div>
850
1050
  {/if}
1051
+ {@render slotGroup('header.start')}
1052
+ {@render slotGroup('header.center')}
851
1053
  {#if account || footerInHeader}
852
1054
  <div class="smrt-admin-shell__account">
853
1055
  {#if account}{@render account()}{:else if tenantFooter}{@render tenantFooter()}{/if}
854
1056
  </div>
855
1057
  {/if}
1058
+ {@render slotGroup('header.end')}
1059
+ {@render regionEdit('header')}
856
1060
  </div>
857
1061
  {#if topRightCorner}
858
1062
  <div class="smrt-admin-shell__corner smrt-admin-shell__corner--top-right">
@@ -877,6 +1081,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
877
1081
  </header>
878
1082
  {/if}
879
1083
 
1084
+ {@render regionStrip('leftSidebar')}
880
1085
  {#if shell.isEdgeShown('left')}
881
1086
  <!-- tabindex -1 only while overlaid, so the panel can take focus on open -->
882
1087
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
@@ -886,7 +1091,6 @@ function buildLayoutStyle(shell: ModuleShellState): string {
886
1091
  data-state={closingEdge === 'left' ? 'expanded' : panelState('left')}
887
1092
  data-closing={closingEdge === 'left' ? '' : undefined}
888
1093
  data-presentation={shell.presentationFor('left')}
889
- role="navigation"
890
1094
  aria-label={labelFor('left')}
891
1095
  tabindex={overlayEdge === 'left' ? -1 : undefined}
892
1096
  inert={(isPhone && !edgeExpanded('left')) ||
@@ -902,15 +1106,15 @@ function buildLayoutStyle(shell: ModuleShellState): string {
902
1106
  >
903
1107
  {@render resizer('left')}
904
1108
  <div class="smrt-admin-shell__rail">
905
- {#if (panelState('top') === 'hidden' || !edgeExpanded('left')) && (homeHref || logoSrc || brand)}
906
- {@render shellBrand(!edgeExpanded('left'))}
907
- {/if}
908
- {#if showTenantToggle}{@render edgeToggle('left')}{/if}
1109
+ {@render regionEdit('leftSidebar')}
1110
+ {#if (panelState('top') === 'hidden' || !edgeExpanded('left')) && (homeHref || logoSrc || brand)}{@render shellBrand(!edgeExpanded('left'))}{/if}
1111
+ {#if showToggle('left') || isPhone}{@render edgeToggle('left')}{/if}
909
1112
  {#if shownOpen('left') || (tenantPanel && keepsContent('left'))}
910
1113
  <div
911
1114
  class="smrt-admin-shell__tenant-stack"
912
1115
  hidden={!shownOpen('left')}
913
1116
  >
1117
+ {@render slotGroup('leftSidebar.header')}
914
1118
  <div class="smrt-admin-shell__tenant-content">
915
1119
  {#if tenantPanel}
916
1120
  {@render tenantPanel()}
@@ -923,6 +1127,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
923
1127
  {@render tenantFooter()}
924
1128
  </div>
925
1129
  {/if}
1130
+ {@render slotGroup('leftSidebar.footer')}
926
1131
  </div>
927
1132
  {/if}
928
1133
  {#if !shownOpen('left') && (tenantRail || tenantRailFooter)}
@@ -1005,6 +1210,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1005
1210
  ></button>
1006
1211
  {/if}
1007
1212
 
1213
+ {@render regionStrip('rightSidebar')}
1008
1214
  {#if shell.isEdgeShown('right')}
1009
1215
  {@const rightPhone = isPhone ? shell.phonePresentation('right') : undefined}
1010
1216
  <!-- tabindex -1 only while overlaid, so the panel can take focus on open -->
@@ -1047,7 +1253,10 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1047
1253
  </div>
1048
1254
  {/if}
1049
1255
  <div class="smrt-admin-shell__rail">
1050
- {#if focusRail}
1256
+ {@render regionEdit('rightSidebar')}
1257
+ {#if shell.config.panels.right.rail === false}
1258
+ <!-- Railless: no tool buttons; the host's toggle opens and closes it. -->
1259
+ {:else if focusRail}
1051
1260
  {@render focusRail()}
1052
1261
  {:else if shell.focusTools.length > 0}
1053
1262
  <nav class="smrt-admin-shell__focus-rail" aria-label={t(M['ui.admin_shell.focus_tools'])}>
@@ -1077,7 +1286,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1077
1286
  </button>
1078
1287
  {/each}
1079
1288
  </nav>
1080
- {:else}
1289
+ {:else if showToggle('right')}
1081
1290
  {@render edgeToggle('right')}
1082
1291
  {/if}
1083
1292
  </div>
@@ -1087,14 +1296,40 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1087
1296
  tabindex="-1"
1088
1297
  hidden={!shownOpen('right')}
1089
1298
  >
1299
+ {@render slotGroup('rightSidebar.header')}
1090
1300
  {#key shell.activeFocusToolId}
1091
1301
  {@render focusContent(resolveActiveFocusTool())}
1092
1302
  {/key}
1303
+ {@render slotGroup('rightSidebar.footer')}
1093
1304
  </div>
1094
1305
  {/if}
1095
1306
  </aside>
1096
1307
  {/if}
1097
1308
 
1309
+ {#if editIndicator}
1310
+ <!-- raw-primitive-allow: shell chrome toggle, not a content button -->
1311
+ <button
1312
+ type="button"
1313
+ class="smrt-admin-shell__edit-indicator"
1314
+ data-edit-indicator="right"
1315
+ aria-expanded="false"
1316
+ aria-controls="smrt-admin-shell-right-panel"
1317
+ aria-label={t(M['ui.admin_shell.open_panel'], {
1318
+ label: editIndicator.map((tool) => tool.label).join(', '),
1319
+ })}
1320
+ onclick={openFromIndicator}
1321
+ >
1322
+ {#if editIndicator[0].icon}
1323
+ {@const Icon = editIndicator[0].icon}
1324
+ <Icon />
1325
+ {/if}
1326
+ <span class="smrt-admin-shell__edit-indicator-label">
1327
+ {editIndicator.map((tool) => tool.label).join(', ')}
1328
+ </span>
1329
+ </button>
1330
+ {/if}
1331
+
1332
+ {@render regionStrip('footer')}
1098
1333
  {#if panelState('bottom') !== 'hidden'}
1099
1334
  <footer
1100
1335
  id="smrt-admin-shell-bottom-panel"
@@ -1112,8 +1347,12 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1112
1347
  class="smrt-admin-shell__band smrt-admin-shell__band--bottom"
1113
1348
  style:--band-column={2}
1114
1349
  >
1350
+ {@render slotGroup('footer.start')}
1351
+ {@render slotGroup('footer.center')}
1115
1352
  {#if systemBar}{@render systemBar()}{/if}
1116
- {#if systemPanel || !systemBar}{@render edgeToggle('bottom')}{/if}
1353
+ {@render slotGroup('footer.end')}
1354
+ {@render regionEdit('footer')}
1355
+ {#if showToggle('bottom') && (systemPanel || !systemBar)}{@render edgeToggle('bottom')}{/if}
1117
1356
  </div>
1118
1357
  {#if bottomRightCorner}
1119
1358
  <div class="smrt-admin-shell__corner smrt-admin-shell__corner--bottom-right">
@@ -1194,6 +1433,9 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1194
1433
 
1195
1434
  <style>
1196
1435
  .smrt-admin-shell {
1436
+ --smrt-shell-floating-reserve-inline: 0px;
1437
+ --smrt-shell-floating-reserve-block: 0px;
1438
+ --smrt-shell-floating-reserve-main: 0px;
1197
1439
  --smrt-admin-shell-left-track: 0rem;
1198
1440
  --smrt-admin-shell-right-track: 0rem;
1199
1441
  --smrt-admin-shell-top-track: 0rem;
@@ -1226,7 +1468,12 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1226
1468
  ancestor (a tinted page behind cards, for instance). */
1227
1469
  background: var(--smrt-admin-shell-background, var(--smrt-color-surface));
1228
1470
  color: var(--smrt-color-on-surface);
1471
+ /* `clip`, not `hidden`: a hidden box is still programmatically scrollable
1472
+ (focus / scrollIntoView on a grid-track child shifts the whole shell
1473
+ and its sidebars); a clipped box can never scroll. Only main and the
1474
+ sidebars scroll. */
1229
1475
  overflow: hidden;
1476
+ overflow: clip;
1230
1477
  /* Themed, thin scrollbars for every scroller inside the shell. Override
1231
1478
  --smrt-admin-shell-scrollbar-track / -thumb / -thumb-hover to retint. */
1232
1479
  scrollbar-color: var(--smrt-admin-shell-scrollbar-thumb, color-mix(in srgb, var(--smrt-color-on-surface) 26%, transparent))
@@ -1278,7 +1525,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1278
1525
  box-sizing: border-box;
1279
1526
  min-inline-size: 0;
1280
1527
  min-block-size: 0;
1281
- padding: 0 var(--smrt-spacing-3) 0 var(--smrt-spacing-2);
1528
+ padding: 0 calc(var(--smrt-spacing-3) + var(--smrt-shell-floating-reserve-inline)) 0 var(--smrt-spacing-2);
1282
1529
  /* Clip long titles sideways only: menus and popovers opened from the
1283
1530
  header's actions (a notifications bell, an account menu) must be able
1284
1531
  to drop below it. `clip` (unlike `hidden`) allows a visible block axis. */
@@ -1345,6 +1592,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1345
1592
  min-width: 0;
1346
1593
  min-height: 0;
1347
1594
  overflow: auto;
1595
+ padding-block-start: var(--smrt-shell-floating-reserve-main);
1348
1596
  background: var(--smrt-color-surface);
1349
1597
  }
1350
1598
 
@@ -1363,27 +1611,12 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1363
1611
  grid-column: var(--band-column, 2);
1364
1612
  }
1365
1613
 
1366
- .smrt-admin-shell__brand {
1367
- display: flex;
1368
- align-items: center;
1369
- gap: var(--smrt-spacing-2);
1370
- min-width: 0;
1371
- color: inherit;
1372
- text-decoration: none;
1373
- }
1374
- .smrt-admin-shell__brand-text { display: grid; min-width: 0; }
1375
- .smrt-admin-shell__brand.compact { justify-content: center; margin-block-end: var(--smrt-spacing-2); }
1376
- .smrt-admin-shell__brand-link:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
1377
- .smrt-admin-shell__logo { inline-size: 2rem; block-size: 2rem; object-fit: contain; flex-shrink: 0; }
1378
-
1379
- .smrt-admin-shell__brand strong,
1380
- .smrt-admin-shell__brand span {
1381
- overflow: hidden;
1382
- text-overflow: ellipsis;
1383
- white-space: nowrap;
1614
+ /* The header's slots can render in the top band too: keep the floating
1615
+ layout toggle's corner clear there as well. */
1616
+ .smrt-admin-shell__band--top {
1617
+ padding-inline-end: calc(var(--smrt-spacing-4) + var(--smrt-shell-floating-reserve-inline));
1384
1618
  }
1385
1619
 
1386
- .smrt-admin-shell__brand span,
1387
1620
  .smrt-admin-shell__empty {
1388
1621
  color: var(--smrt-color-on-surface-variant);
1389
1622
  }
@@ -1436,6 +1669,39 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1436
1669
  background: var(--smrt-color-surface-container-high);
1437
1670
  }
1438
1671
 
1672
+ /* Edit-mode tab for a closed railless right edge: pinned to the shell's
1673
+ right edge, vertically centred, over the page without taking space. */
1674
+ .smrt-admin-shell__edit-indicator {
1675
+ grid-column: 1 / -1;
1676
+ grid-row: 3;
1677
+ justify-self: end;
1678
+ align-self: center;
1679
+ z-index: 21;
1680
+ display: inline-flex;
1681
+ align-items: center;
1682
+ gap: var(--smrt-spacing-1, 0.25rem);
1683
+ padding: var(--smrt-spacing-2, 0.5rem) var(--smrt-spacing-2, 0.5rem);
1684
+ border: 1px solid var(--smrt-color-outline-variant);
1685
+ border-inline-end: 0;
1686
+ border-start-start-radius: var(--smrt-radius-md, 8px);
1687
+ border-end-start-radius: var(--smrt-radius-md, 8px);
1688
+ background: var(--smrt-color-surface-container-high);
1689
+ color: var(--smrt-color-on-surface-variant);
1690
+ font: inherit;
1691
+ font-size: var(--smrt-typography-label-small-size, 0.75rem);
1692
+ cursor: pointer;
1693
+ }
1694
+
1695
+ .smrt-admin-shell__edit-indicator:hover {
1696
+ color: var(--smrt-color-primary);
1697
+ border-color: var(--smrt-color-primary);
1698
+ }
1699
+
1700
+ .smrt-admin-shell__edit-indicator :global(svg) {
1701
+ inline-size: 1rem;
1702
+ block-size: 1rem;
1703
+ }
1704
+
1439
1705
  .smrt-admin-shell__focus-tool-badge {
1440
1706
  position: absolute;
1441
1707
  inset-block-start: -0.25rem;
@@ -1487,19 +1753,118 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1487
1753
 
1488
1754
  .smrt-admin-shell__tenant-stack {
1489
1755
  flex: 1;
1490
- display: grid;
1491
- grid-template-rows: minmax(0, 1fr) auto;
1756
+ display: flex;
1757
+ flex-direction: column;
1492
1758
  gap: var(--smrt-spacing-3);
1493
1759
  min-width: 0;
1494
1760
  min-height: 0;
1495
1761
  }
1496
1762
 
1763
+ /* Only the navigation/content area grows; the sidebar header and footer
1764
+ slots (and the legacy tenant footer) keep their natural height. */
1497
1765
  .smrt-admin-shell__tenant-content {
1766
+ flex: 1 1 auto;
1498
1767
  min-width: 0;
1499
1768
  min-height: 0;
1500
1769
  overflow: auto;
1501
1770
  }
1502
1771
 
1772
+ .smrt-admin-shell__region-edit {
1773
+ display: flex;
1774
+ flex: 0 0 auto;
1775
+ align-items: center;
1776
+ justify-content: flex-end;
1777
+ }
1778
+
1779
+ .smrt-admin-shell__edge--top .smrt-admin-shell__region-edit,
1780
+ .smrt-admin-shell__edge--bottom .smrt-admin-shell__region-edit {
1781
+ margin-inline-start: auto;
1782
+ }
1783
+
1784
+ .smrt-admin-shell__slot {
1785
+ display: flex;
1786
+ align-items: center;
1787
+ gap: var(--smrt-spacing-1);
1788
+ min-width: 0;
1789
+ }
1790
+
1791
+ .smrt-admin-shell__slot--header-center,
1792
+ .smrt-admin-shell__slot--footer-center {
1793
+ flex: 1 1 auto;
1794
+ justify-content: center;
1795
+ }
1796
+
1797
+ .smrt-admin-shell__slot--header-end,
1798
+ .smrt-admin-shell__slot--footer-end {
1799
+ margin-inline-start: auto;
1800
+ }
1801
+
1802
+ .smrt-admin-shell__slot--leftSidebar-header,
1803
+ .smrt-admin-shell__slot--rightSidebar-header {
1804
+ padding-block-end: var(--smrt-spacing-3);
1805
+ border-block-end: 1px solid var(--smrt-color-outline-variant);
1806
+ }
1807
+
1808
+ .smrt-admin-shell__slot--leftSidebar-footer,
1809
+ .smrt-admin-shell__slot--rightSidebar-footer {
1810
+ padding-block-start: var(--smrt-spacing-3);
1811
+ border-block-start: 1px solid var(--smrt-color-outline-variant);
1812
+ }
1813
+
1814
+ .smrt-admin-shell__slot--rightSidebar-header {
1815
+ position: sticky;
1816
+ inset-block-start: 0;
1817
+ background: var(--smrt-color-surface);
1818
+ }
1819
+
1820
+ .smrt-admin-shell__slot--rightSidebar-footer {
1821
+ position: sticky;
1822
+ inset-block-end: 0;
1823
+ background: var(--smrt-color-surface);
1824
+ }
1825
+
1826
+ /* Layout editing: slots as dashed drop zones, hidden regions as strips. */
1827
+ .smrt-admin-shell__slot--editing {
1828
+ flex-wrap: wrap;
1829
+ box-sizing: border-box;
1830
+ min-inline-size: 4rem;
1831
+ min-block-size: 2.25rem;
1832
+ padding: var(--smrt-spacing-1);
1833
+ border: 1px dashed var(--smrt-color-outline);
1834
+ border-radius: var(--smrt-radius-medium, 0.5rem);
1835
+ background: color-mix(in srgb, var(--smrt-color-primary) 4%, transparent);
1836
+ }
1837
+ .smrt-admin-shell__slot--editing[data-drop-target] {
1838
+ border-style: solid;
1839
+ border-color: var(--smrt-color-primary);
1840
+ background: color-mix(in srgb, var(--smrt-color-primary) 14%, transparent);
1841
+ }
1842
+ .smrt-admin-shell__slot--editing.smrt-admin-shell__slot--header-center,
1843
+ .smrt-admin-shell__slot--editing.smrt-admin-shell__slot--footer-center {
1844
+ justify-content: flex-start;
1845
+ }
1846
+ .smrt-admin-shell__strip {
1847
+ display: flex;
1848
+ align-items: center;
1849
+ justify-content: center;
1850
+ gap: var(--smrt-spacing-2);
1851
+ box-sizing: border-box;
1852
+ min-inline-size: 0;
1853
+ min-block-size: 0;
1854
+ overflow: hidden;
1855
+ border: 1px dashed var(--smrt-color-outline);
1856
+ background: var(--smrt-color-surface-container-low);
1857
+ color: var(--smrt-color-on-surface-variant);
1858
+ font: var(--smrt-typography-label-small-font);
1859
+ z-index: 25;
1860
+ }
1861
+ .smrt-admin-shell__strip--header { grid-column: 1 / -1; grid-row: 2; }
1862
+ .smrt-admin-shell__strip--footer { grid-column: 1 / -1; grid-row: 4; }
1863
+ .smrt-admin-shell__strip--leftSidebar { grid-column: 1; grid-row: 3; writing-mode: vertical-rl; flex-direction: row; }
1864
+ .smrt-admin-shell__strip--rightSidebar { grid-column: 3; grid-row: 3; writing-mode: vertical-rl; flex-direction: row; }
1865
+ .smrt-admin-shell[data-viewport='phone'] .smrt-admin-shell__strip--leftSidebar,
1866
+ .smrt-admin-shell[data-viewport='phone'] .smrt-admin-shell__strip--rightSidebar { display: none; }
1867
+
1503
1868
  .smrt-admin-shell__tenant-footer {
1504
1869
  min-width: 0;
1505
1870
  padding-block-start: var(--smrt-spacing-3);
@@ -1548,6 +1913,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1548
1913
  .smrt-admin-shell__panel--right {
1549
1914
  grid-column: 1;
1550
1915
  grid-row: 1;
1916
+ padding-block-start: var(--smrt-shell-floating-reserve-block);
1551
1917
  }
1552
1918
 
1553
1919
  .smrt-admin-shell__edge-toggle-kbd {