@happyvertical/smrt-svelte 0.55.5 → 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 (134) 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/audit/AuditList.svelte +51 -10
  47. package/dist/components/audit/AuditList.svelte.d.ts +13 -1
  48. package/dist/components/audit/AuditList.svelte.d.ts.map +1 -1
  49. package/dist/components/audit/RecordHistory.svelte +168 -10
  50. package/dist/components/audit/RecordHistory.svelte.d.ts +11 -1
  51. package/dist/components/audit/RecordHistory.svelte.d.ts.map +1 -1
  52. package/dist/components/audit/index.d.ts +1 -1
  53. package/dist/components/audit/index.d.ts.map +1 -1
  54. package/dist/components/audit/types.d.ts +18 -0
  55. package/dist/components/audit/types.d.ts.map +1 -1
  56. package/dist/components/forms/AddressInput.svelte +2 -15
  57. package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
  58. package/dist/components/forms/DateRangeInput.svelte +2 -15
  59. package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
  60. package/dist/components/forms/DateTimeInput.svelte +8 -15
  61. package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
  62. package/dist/components/forms/MeasurementInput.svelte +2 -15
  63. package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
  64. package/dist/components/forms/MoneyInput.svelte +8 -25
  65. package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
  66. package/dist/components/forms/NumberInput.svelte +7 -15
  67. package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
  68. package/dist/components/forms/PhoneInput.svelte +7 -15
  69. package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
  70. package/dist/components/forms/SelectInput.svelte +34 -40
  71. package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
  72. package/dist/components/forms/TextInput.svelte +20 -37
  73. package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
  74. package/dist/components/forms/TextareaInput.svelte +19 -35
  75. package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
  76. package/dist/components/sortable/Sortable.svelte +6 -33
  77. package/dist/components/sortable/Sortable.svelte.d.ts.map +1 -1
  78. package/dist/components/sortable/announce.d.ts +5 -0
  79. package/dist/components/sortable/announce.d.ts.map +1 -0
  80. package/dist/components/sortable/announce.js +33 -0
  81. package/dist/components/workspace/admin-shell/AdminShell.svelte +424 -58
  82. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +27 -2
  83. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
  84. package/dist/components/workspace/admin-shell/ShellBrand.svelte +86 -0
  85. package/dist/components/workspace/admin-shell/ShellBrand.svelte.d.ts +18 -0
  86. package/dist/components/workspace/admin-shell/ShellBrand.svelte.d.ts.map +1 -0
  87. package/dist/components/workspace/admin-shell/ShellIconButton.svelte +77 -0
  88. package/dist/components/workspace/admin-shell/ShellIconButton.svelte.d.ts +26 -0
  89. package/dist/components/workspace/admin-shell/ShellIconButton.svelte.d.ts.map +1 -0
  90. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte +88 -16
  91. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte.d.ts.map +1 -1
  92. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte +531 -0
  93. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte.d.ts +26 -0
  94. package/dist/components/workspace/admin-shell/ShellNavEditor.svelte.d.ts.map +1 -0
  95. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte +57 -0
  96. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte.d.ts +22 -0
  97. package/dist/components/workspace/admin-shell/ShellSectionIcon.svelte.d.ts.map +1 -0
  98. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte +290 -0
  99. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte.d.ts +32 -0
  100. package/dist/components/workspace/admin-shell/ShellSectionMenu.svelte.d.ts.map +1 -0
  101. package/dist/components/workspace/admin-shell/TenantNav.svelte +67 -2
  102. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +8 -0
  103. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
  104. package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts +67 -0
  105. package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts.map +1 -1
  106. package/dist/components/workspace/admin-shell/layout-controller.svelte.js +129 -1
  107. package/dist/components/workspace/admin-shell/layout.d.ts +122 -0
  108. package/dist/components/workspace/admin-shell/layout.d.ts.map +1 -1
  109. package/dist/components/workspace/admin-shell/layout.js +410 -10
  110. package/dist/components/workspace/admin-shell/settings.js +4 -4
  111. package/dist/components/workspace/admin-shell/shell-icons.d.ts +45 -0
  112. package/dist/components/workspace/admin-shell/shell-icons.d.ts.map +1 -0
  113. package/dist/components/workspace/admin-shell/shell-icons.js +66 -0
  114. package/dist/components/workspace/admin-shell/slot-labels.d.ts +33 -0
  115. package/dist/components/workspace/admin-shell/slot-labels.d.ts.map +1 -0
  116. package/dist/components/workspace/admin-shell/slot-labels.js +33 -0
  117. package/dist/components/workspace/admin-shell/slots.d.ts +50 -0
  118. package/dist/components/workspace/admin-shell/slots.d.ts.map +1 -0
  119. package/dist/components/workspace/admin-shell/slots.js +77 -0
  120. package/dist/components/workspace/admin-shell/state.svelte.d.ts +35 -0
  121. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
  122. package/dist/components/workspace/admin-shell/state.svelte.js +111 -0
  123. package/dist/components/workspace/admin-shell/types.d.ts +101 -1
  124. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
  125. package/dist/components/workspace/index.d.ts +9 -2
  126. package/dist/components/workspace/index.d.ts.map +1 -1
  127. package/dist/components/workspace/index.js +6 -0
  128. package/dist/i18n/strings.workspace.d.ts +49 -0
  129. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  130. package/dist/i18n/strings.workspace.js +49 -0
  131. package/package.json +30 -19
  132. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts +0 -51
  133. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts.map +0 -1
  134. package/dist/browser-ai/adapters/stt/whisper-wasm.js +0 -380
@@ -0,0 +1,531 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The navigation as it looks on the site, made editable in place: every
4
+ * section heading and item has a grip (drag, or Space plus arrow keys), section
5
+ * headings carry overlay icon buttons (show, title, host actions) and open a
6
+ * floating toolbar (rename, title, hide, delete, host actions), and a "New
7
+ * section" button ends the list. Rendered by `AppShell` in place of
8
+ * `TenantNav` while the layout is being edited.
9
+ */
10
+ import { Input } from '@happyvertical/smrt-ui/forms';
11
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
12
+ import { Button } from '@happyvertical/smrt-ui/ui';
13
+ import { type Component, type Snippet, tick, untrack } from 'svelte';
14
+ import { M } from '../../../i18n/strings.workspace.js';
15
+ import Sortable from '../../sortable/Sortable.svelte';
16
+ import type {
17
+ SortableContainer,
18
+ SortableContainerMove,
19
+ SortableItem,
20
+ SortableItemMove,
21
+ } from '../../sortable/types.js';
22
+ import { useShellLayout } from './layout-context.js';
23
+ import type { ShellLayoutController } from './layout-controller.svelte.js';
24
+ import ShellIconButton from './ShellIconButton.svelte';
25
+ import ShellSectionIcon from './ShellSectionIcon.svelte';
26
+ import {
27
+ SHELL_DEFAULT_SECTION_ICON,
28
+ SHELL_SECTION_ICONS,
29
+ } from './shell-icons.js';
30
+ import type { ShellSectionActionsContext } from './types.js';
31
+
32
+ interface Props {
33
+ /** The layout API to edit; defaults to the nearest `AppShell`'s. */
34
+ controller?: ShellLayoutController;
35
+ /** Accessible name for the navigation landmark. */
36
+ 'aria-label'?: string;
37
+ /** Component used to render icon names, as for `TenantNav`. */
38
+ iconComponent?: Component<{ name: string; size?: number }>;
39
+ /** Host icon buttons for each section (overlay and floating toolbar). */
40
+ sectionActions?: Snippet<[ShellSectionActionsContext]>;
41
+ /**
42
+ * `'sections'` edits only the sections (grip, rename, show, delete, icon
43
+ * picker); their entries are edited on the section's own page with
44
+ * `ShellSectionMenu`. Default `'items'`.
45
+ */
46
+ navMode?: 'items' | 'sections';
47
+ }
48
+
49
+ let {
50
+ controller: provided,
51
+ 'aria-label': ariaLabel,
52
+ iconComponent: IconComponent,
53
+ sectionActions,
54
+ navMode = 'items',
55
+ }: Props = $props();
56
+
57
+ const { t } = useI18n();
58
+ const layout = untrack(() => provided ?? useShellLayout());
59
+
60
+ interface NavContainer extends SortableContainer {
61
+ hidden: boolean;
62
+ root: boolean;
63
+ custom: boolean;
64
+ titleVisible: boolean;
65
+ count: number;
66
+ icon: string;
67
+ }
68
+
69
+ const sections = $derived(layout.sections);
70
+ const containers = $derived<NavContainer[]>(
71
+ sections.map((section) => ({
72
+ id: section.id,
73
+ label: section.heading ?? t(M['ui.shell_layout_editor.top_level']),
74
+ fixed: section.group === null,
75
+ root: section.group === null,
76
+ hidden: section.hidden,
77
+ custom: section.custom,
78
+ titleVisible: section.titleVisible,
79
+ count: section.items.length,
80
+ icon: section.icon ?? SHELL_DEFAULT_SECTION_ICON,
81
+ })),
82
+ );
83
+ const items = $derived<SortableItem[]>(
84
+ (navMode === 'sections' ? [] : sections).flatMap((section) =>
85
+ section.items.map((entry) => ({
86
+ id: entry.id,
87
+ containerId: section.id,
88
+ label: entry.label,
89
+ })),
90
+ ),
91
+ );
92
+ const itemDefaults = $derived(
93
+ new Map(
94
+ sections.flatMap((section) =>
95
+ section.items.map((entry) => [entry.id, entry.defaultLabel] as const),
96
+ ),
97
+ ),
98
+ );
99
+ const itemIcons = $derived(
100
+ new Map(
101
+ sections.flatMap((section) =>
102
+ section.items.map((entry) => [entry.id, entry.item.icon] as const),
103
+ ),
104
+ ),
105
+ );
106
+
107
+ let rootEl: HTMLElement | undefined = $state();
108
+
109
+ // One floating toolbar at a time. `pinned` (opened by click/tap) stays until
110
+ // dismissed; an unpinned one (opened by hovering with a fine pointer) closes
111
+ // when the pointer leaves the section.
112
+ let open = $state<{ id: string; pinned: boolean } | null>(null);
113
+
114
+ function canHover(): boolean {
115
+ return (
116
+ typeof window !== 'undefined' &&
117
+ window.matchMedia?.('(hover: hover) and (pointer: fine)').matches === true
118
+ );
119
+ }
120
+
121
+ function sectionEl(id: string): HTMLElement | null {
122
+ return (
123
+ rootEl?.querySelector<HTMLElement>(
124
+ `[data-nav-section="${CSS.escape(id)}"]`,
125
+ ) ?? null
126
+ );
127
+ }
128
+
129
+ function onhover(id: string, event: PointerEvent): void {
130
+ if (event.pointerType === 'touch' || !canHover()) return;
131
+ if (open?.pinned) return;
132
+ open = { id, pinned: false };
133
+ }
134
+
135
+ async function onactivate(id: string): Promise<void> {
136
+ if (open?.id === id && open.pinned) {
137
+ open = null;
138
+ return;
139
+ }
140
+ open = { id, pinned: true };
141
+ await tick();
142
+ const input = sectionEl(id)?.querySelector<HTMLInputElement>(
143
+ 'input[data-section-name]',
144
+ );
145
+ input?.focus();
146
+ input?.select();
147
+ }
148
+
149
+ function onleave(id: string): void {
150
+ if (open?.id === id && !open.pinned) open = null;
151
+ }
152
+
153
+ function onkeydown(id: string, event: KeyboardEvent): void {
154
+ if (event.key !== 'Escape' || open?.id !== id) return;
155
+ event.preventDefault();
156
+ event.stopPropagation();
157
+ // An open icon picker closes first; the toolbar stays.
158
+ if (iconPicker === id) {
159
+ iconPicker = null;
160
+ pickerButton(id)?.focus();
161
+ return;
162
+ }
163
+ open = null;
164
+ sectionEl(id)
165
+ ?.querySelector<HTMLElement>('button[data-section-title]')
166
+ ?.focus();
167
+ }
168
+
169
+ function onfocusout(id: string, event: FocusEvent): void {
170
+ if (open?.id !== id || !open.pinned) return;
171
+ const next = event.relatedTarget;
172
+ if (next instanceof Node && !sectionEl(id)?.contains(next)) open = null;
173
+ }
174
+
175
+ $effect(() => {
176
+ const current = open;
177
+ if (!current?.pinned || typeof document === 'undefined') return;
178
+ const dismiss = (event: PointerEvent) => {
179
+ if (
180
+ event.target instanceof Node &&
181
+ !sectionEl(current.id)?.contains(event.target)
182
+ ) {
183
+ open = null;
184
+ }
185
+ };
186
+ document.addEventListener('pointerdown', dismiss, true);
187
+ return () => document.removeEventListener('pointerdown', dismiss, true);
188
+ });
189
+
190
+ // The section whose icon picker is open (inside its toolbar).
191
+ let iconPicker = $state<string | null>(null);
192
+
193
+ function pickerButton(id: string): HTMLElement | null {
194
+ return (
195
+ sectionEl(id)?.querySelector<HTMLElement>('button[data-section-icon]') ??
196
+ null
197
+ );
198
+ }
199
+
200
+ function chooseIcon(id: string, icon: string): void {
201
+ layout.setSectionIcon(id, icon);
202
+ iconPicker = null;
203
+ pickerButton(id)?.focus();
204
+ }
205
+
206
+ // The picker lives in the toolbar: it closes with it.
207
+ $effect(() => {
208
+ if (iconPicker !== null && open?.id !== iconPicker) iconPicker = null;
209
+ });
210
+
211
+ function humanizeIcon(name: string): string {
212
+ return name.replace(/([A-Z])/g, ' $1').toLowerCase();
213
+ }
214
+
215
+ // The item whose name is being edited in place (one at a time).
216
+ let renamingItem = $state<string | null>(null);
217
+
218
+ async function startRenameItem(id: string): Promise<void> {
219
+ renamingItem = id;
220
+ await tick();
221
+ const input = rootEl?.querySelector<HTMLInputElement>(
222
+ `input[data-item-name="${CSS.escape(id)}"]`,
223
+ );
224
+ input?.focus();
225
+ input?.select();
226
+ }
227
+
228
+ function commitRenameItem(id: string, value: string): void {
229
+ // Blur after Escape or a save lands here too; only the open editor commits.
230
+ if (renamingItem !== id) return;
231
+ renamingItem = null;
232
+ layout.renameItem(id, value);
233
+ }
234
+
235
+ function cancelRenameItem(id: string): void {
236
+ renamingItem = null;
237
+ rootEl
238
+ ?.querySelector<HTMLElement>(`button[data-item-rename="${CSS.escape(id)}"]`)
239
+ ?.focus();
240
+ }
241
+
242
+ function setShown(id: string, shown: boolean): void {
243
+ if (shown) layout.show(id);
244
+ else layout.hide(id);
245
+ }
246
+
247
+ function rename(id: string, input: HTMLInputElement): void {
248
+ layout.renameSection(id, input.value);
249
+ // Blank or rejected input snaps back to the section's real name.
250
+ input.value = containers.find((entry) => entry.id === id)?.label ?? '';
251
+ }
252
+
253
+ async function createSection(): Promise<void> {
254
+ const id = layout.createSection(
255
+ t(M['ui.shell_layout_editor.new_section_label']),
256
+ );
257
+ if (!id) return;
258
+ await tick();
259
+ await onactivate(id);
260
+ }
261
+
262
+ function deleteSection(id: string, label: string, count: number): void {
263
+ if (
264
+ count > 0 &&
265
+ !globalThis.confirm?.(
266
+ t(M['ui.shell_layout_editor.delete_confirm'], { label }),
267
+ )
268
+ ) {
269
+ return;
270
+ }
271
+ open = null;
272
+ layout.deleteSection(id);
273
+ }
274
+
275
+ function onmove(move: SortableItemMove<SortableItem, NavContainer>): void {
276
+ layout.moveItem(move.item.id, move.target.containerId, move.target.index);
277
+ }
278
+
279
+ function oncontainermove(move: SortableContainerMove<NavContainer>): void {
280
+ layout.moveSection(move.container.id, move.target);
281
+ }
282
+ </script>
283
+
284
+ {#snippet toggles(entry: NavContainer)}
285
+ <ShellIconButton
286
+ icon={entry.hidden ? 'eyeOff' : 'eye'}
287
+ pressed={!entry.hidden}
288
+ label={t(M['ui.shell_layout_editor.show_entry'], { label: entry.label })}
289
+ onclick={() => setShown(entry.id, entry.hidden)}
290
+ />
291
+ {#if navMode !== 'sections'}
292
+ <ShellIconButton
293
+ icon={entry.titleVisible ? 'heading' : 'headingOff'}
294
+ pressed={entry.titleVisible}
295
+ label={t(M['ui.shell_layout_editor.show_title_for'], { label: entry.label })}
296
+ onclick={() => layout.setSectionTitleVisible(entry.id, !entry.titleVisible)}
297
+ />
298
+ {/if}
299
+ {/snippet}
300
+
301
+ {#snippet actions(entry: NavContainer)}
302
+ {@render sectionActions?.({
303
+ sectionId: entry.id,
304
+ label: entry.label,
305
+ custom: entry.custom,
306
+ editing: true,
307
+ })}
308
+ {/snippet}
309
+
310
+ <nav
311
+ class="smrt-nav-editor"
312
+ data-layout-editing=""
313
+ data-nav-mode={navMode}
314
+ aria-label={ariaLabel || t(M['ui.shell_layout_editor.sortable_label'])}
315
+ bind:this={rootEl}
316
+ >
317
+ <Sortable
318
+ {containers}
319
+ {items}
320
+ label={t(M['ui.shell_layout_editor.sortable_label'])}
321
+ reorderContainers={true}
322
+ {onmove}
323
+ {oncontainermove}
324
+ >
325
+ {#snippet containerHeader({ container })}
326
+ {@const entry = containers.find((candidate) => candidate.id === container.id)}
327
+ {#if !entry}
328
+ <strong>{container.label}</strong>
329
+ {:else if entry.root}
330
+ <span class="smrt-nav-editor__top">{container.label}</span>
331
+ {:else}
332
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
333
+ <div
334
+ class="smrt-nav-editor__head"
335
+ data-nav-section={entry.id}
336
+ data-toolbar-open={open?.id === entry.id ? '' : undefined}
337
+ onpointerleave={() => onleave(entry.id)}
338
+ onkeydown={(event) => onkeydown(entry.id, event)}
339
+ onfocusout={(event) => onfocusout(entry.id, event)}
340
+ >
341
+ <!-- raw-primitive-allow: heading button that opens the section toolbar -->
342
+ <button
343
+ type="button"
344
+ class="smrt-nav-editor__title"
345
+ class:smrt-nav-editor__title--off={!entry.titleVisible}
346
+ class:smrt-nav-editor__title--hidden={entry.hidden}
347
+ data-section-title=""
348
+ aria-haspopup="true"
349
+ aria-expanded={open?.id === entry.id}
350
+ aria-label={t(M['ui.layout_edit.edit_section'], { label: entry.label })}
351
+ onpointerenter={(event) => onhover(entry.id, event)}
352
+ onclick={() => onactivate(entry.id)}
353
+ >{#if navMode === 'sections'}<span class="smrt-nav-editor__section-icon"><ShellSectionIcon name={entry.icon} iconComponent={IconComponent} /></span>{/if}{entry.label}</button>
354
+ <span class="smrt-nav-editor__overlay">
355
+ {@render toggles(entry)}
356
+ {@render actions(entry)}
357
+ </span>
358
+ {#if open?.id === entry.id}
359
+ <div
360
+ class="smrt-nav-editor__toolbar"
361
+ role="group"
362
+ aria-label={t(M['ui.layout_edit.section_options'], { label: entry.label })}
363
+ data-testid="section-toolbar"
364
+ >
365
+ <Input
366
+ class="smrt-nav-editor__name"
367
+ interaction={false}
368
+ data-section-name={entry.id}
369
+ aria-label={t(M['ui.shell_layout_editor.section_name'], { label: entry.label })}
370
+ value={entry.label}
371
+ onchange={(event) => rename(entry.id, event.currentTarget)}
372
+ onkeydown={(event) => {
373
+ if (event.key === 'Enter') {
374
+ event.preventDefault();
375
+ rename(entry.id, event.currentTarget);
376
+ }
377
+ }}
378
+ />
379
+ {@render toggles(entry)}
380
+ {#if navMode === 'sections'}
381
+ <ShellIconButton
382
+ data-section-icon={entry.id}
383
+ aria-haspopup="true"
384
+ aria-expanded={iconPicker === entry.id}
385
+ label={t(M['ui.shell_layout_editor.section_icon'], { label: entry.label })}
386
+ onclick={() => (iconPicker = iconPicker === entry.id ? null : entry.id)}
387
+ >
388
+ <ShellSectionIcon name={entry.icon} iconComponent={IconComponent} />
389
+ </ShellIconButton>
390
+ {/if}
391
+ {#if entry.custom}
392
+ <ShellIconButton
393
+ icon="trash"
394
+ label={t(M['ui.shell_layout_editor.delete_section'], { label: entry.label })}
395
+ onclick={() => deleteSection(entry.id, entry.label, entry.count)}
396
+ />
397
+ {/if}
398
+ {@render actions(entry)}
399
+ {#if navMode === 'sections' && iconPicker === entry.id}
400
+ <div
401
+ class="smrt-nav-editor__icons"
402
+ role="group"
403
+ aria-label={t(M['ui.shell_layout_editor.section_icon'], { label: entry.label })}
404
+ >
405
+ {#each SHELL_SECTION_ICONS as name (name)}
406
+ <ShellIconButton
407
+ data-icon-option={name}
408
+ pressed={entry.icon === name}
409
+ label={t(M['ui.shell_layout_editor.icon_option'], { icon: humanizeIcon(name) })}
410
+ onclick={() => chooseIcon(entry.id, name)}
411
+ >
412
+ <ShellSectionIcon {name} />
413
+ </ShellIconButton>
414
+ {/each}
415
+ </div>
416
+ {/if}
417
+ </div>
418
+ {/if}
419
+ </div>
420
+ {/if}
421
+ {/snippet}
422
+ {#snippet item({ item: entry, container })}
423
+ {@const hidden = layout.isHidden(entry.id)}
424
+ {@const icon = itemIcons.get(entry.id)}
425
+ {@const original = itemDefaults.get(entry.id)}
426
+ <span
427
+ class="smrt-nav-editor__row"
428
+ class:smrt-nav-editor__row--muted={hidden || layout.isHidden(container.id)}
429
+ >
430
+ {#if icon}
431
+ <span class="smrt-nav-editor__icon" aria-hidden="true">
432
+ {#if IconComponent}
433
+ <IconComponent name={icon} size={18} />
434
+ {:else}
435
+ {icon}
436
+ {/if}
437
+ </span>
438
+ {/if}
439
+ {#if renamingItem === entry.id}
440
+ <Input
441
+ class="smrt-nav-editor__item-name"
442
+ interaction={false}
443
+ data-item-name={entry.id}
444
+ aria-label={t(M['ui.shell_layout_editor.item_name'], { label: entry.label })}
445
+ value={entry.label}
446
+ onblur={(event) => commitRenameItem(entry.id, event.currentTarget.value)}
447
+ onkeydown={(event) => {
448
+ if (event.key === 'Enter') {
449
+ event.preventDefault();
450
+ event.stopPropagation();
451
+ const value = event.currentTarget.value;
452
+ commitRenameItem(entry.id, value);
453
+ cancelRenameItem(entry.id);
454
+ } else if (event.key === 'Escape') {
455
+ event.preventDefault();
456
+ event.stopPropagation();
457
+ cancelRenameItem(entry.id);
458
+ }
459
+ }}
460
+ />
461
+ {:else}
462
+ <strong title={original !== entry.label ? original : undefined}>{entry.label}</strong>
463
+ {/if}
464
+ </span>
465
+ <ShellIconButton
466
+ icon="edit"
467
+ data-item-rename={entry.id}
468
+ label={t(M['ui.shell_layout_editor.rename_item'], { label: entry.label })}
469
+ onclick={() => startRenameItem(entry.id)}
470
+ />
471
+ {#if original !== undefined && original !== entry.label}
472
+ <ShellIconButton
473
+ icon="undo"
474
+ label={t(M['ui.shell_layout_editor.reset_item'], { label: entry.label, original })}
475
+ onclick={() => layout.renameItem(entry.id, null)}
476
+ />
477
+ {/if}
478
+ <ShellIconButton
479
+ icon={hidden ? 'eyeOff' : 'eye'}
480
+ pressed={!hidden}
481
+ label={t(M['ui.shell_layout_editor.show_entry'], { label: entry.label })}
482
+ onclick={() => setShown(entry.id, hidden)}
483
+ />
484
+ {/snippet}
485
+ </Sortable>
486
+ <Button variant="secondary" size="sm" class="smrt-nav-editor__new" onclick={createSection}>
487
+ {t(M['ui.shell_layout_editor.new_section'])}
488
+ </Button>
489
+ </nav>
490
+
491
+ <style>
492
+ .smrt-nav-editor { display: grid; gap: var(--smrt-spacing-2); min-inline-size: 0; }
493
+ /* Make the generic Sortable read as the real navigation. */
494
+ .smrt-nav-editor :global(.smrt-sortable) { gap: var(--smrt-spacing-1); }
495
+ .smrt-nav-editor :global(.smrt-sortable__containers) { gap: var(--smrt-spacing-1); }
496
+ .smrt-nav-editor :global(.smrt-sortable__container) { padding: 0; border: 0; background: transparent; gap: var(--smrt-spacing-1); }
497
+ .smrt-nav-editor :global(.smrt-sortable__item) { padding: 0; border: 0; border-radius: var(--smrt-radius-medium); background: transparent; }
498
+ .smrt-nav-editor :global(.smrt-sortable__item:hover) { background: var(--smrt-color-surface-container-high); }
499
+ .smrt-nav-editor :global(.smrt-sortable__items) { padding-inline-start: var(--smrt-spacing-3); min-block-size: 0; }
500
+ .smrt-nav-editor :global(.smrt-sortable__header) { min-block-size: 0; }
501
+ .smrt-nav-editor :global(.smrt-sortable__empty) { padding-block: 0; }
502
+ .smrt-nav-editor :global(.smrt-sortable__handle) { inline-size: 1.5rem; block-size: 2rem; }
503
+
504
+ .smrt-nav-editor__top { color: var(--smrt-color-on-surface-variant); font: var(--smrt-typography-label-small-font); text-transform: uppercase; letter-spacing: 0.04em; }
505
+ .smrt-nav-editor__head { position: relative; flex: 1 1 auto; display: flex; align-items: center; gap: var(--smrt-spacing-1); min-inline-size: 0; }
506
+ .smrt-nav-editor__title { flex: 1 1 auto; min-inline-size: 0; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 0; border-radius: var(--smrt-radius-medium); background: transparent; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-large-size); font-weight: var(--smrt-typography-weight-semibold); text-align: start; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
507
+ .smrt-nav-editor__title:hover, .smrt-nav-editor__head[data-toolbar-open] .smrt-nav-editor__title { background: var(--smrt-color-surface-container-high); }
508
+ .smrt-nav-editor__title:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
509
+ .smrt-nav-editor__title--off { font-style: italic; font-weight: var(--smrt-typography-weight-normal, normal); }
510
+ .smrt-nav-editor__title--hidden { opacity: 0.55; }
511
+ .smrt-nav-editor__overlay { display: inline-flex; align-items: center; flex: 0 0 auto; }
512
+ .smrt-nav-editor__toolbar { position: absolute; inset-block-start: 100%; inset-inline: 0; z-index: 40; display: flex; flex-wrap: wrap; align-items: center; gap: var(--smrt-spacing-1); padding: var(--smrt-spacing-2); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-medium); background: var(--smrt-color-surface-container-high); box-shadow: var(--smrt-elevation-2, 0 4px 12px rgb(0 0 0 / 0.2)); }
513
+ :global(.smrt-nav-editor__name) { flex: 1 1 100%; min-inline-size: 0; }
514
+ .smrt-nav-editor__row { flex: 1 1 auto; min-inline-size: 0; display: flex; align-items: center; gap: var(--smrt-spacing-2); padding: var(--smrt-spacing-2) var(--smrt-spacing-1); color: var(--smrt-color-on-surface); }
515
+ .smrt-nav-editor__row--muted { opacity: 0.55; }
516
+ .smrt-nav-editor__row strong { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--smrt-typography-weight-medium, 500); }
517
+ /* Keep editor rows inside the sidebar: the implicit grid column would size to
518
+ the rows' min-content and push the trailing icons past the clipped rail. */
519
+ .smrt-nav-editor :global(.smrt-sortable) { grid-template-columns: minmax(0, 1fr); }
520
+ .smrt-nav-editor :global(.smrt-sortable__container),
521
+ .smrt-nav-editor :global(.smrt-sortable__items),
522
+ .smrt-nav-editor :global(.smrt-sortable__item) { min-inline-size: 0; max-inline-size: 100%; box-sizing: border-box; }
523
+ .smrt-nav-editor__icon { display: inline-grid; place-items: center; inline-size: 1.25rem; block-size: 1.25rem; min-inline-size: 1.25rem; }
524
+ :global(.smrt-nav-editor__item-name) { flex: 1 1 auto; min-inline-size: 0; }
525
+ :global(.smrt-nav-editor__new) { justify-self: start; }
526
+ /* Sections mode: the entries are edited on the section's page. */
527
+ .smrt-nav-editor[data-nav-mode='sections'] :global(.smrt-sortable__items) { display: none; }
528
+ .smrt-nav-editor__section-icon { display: inline-flex; margin-inline-end: var(--smrt-spacing-2); vertical-align: middle; }
529
+ .smrt-nav-editor__icons { flex: 1 1 100%; display: grid; grid-template-columns: repeat(auto-fill, minmax(2rem, 1fr)); gap: var(--smrt-spacing-1); padding-block-start: var(--smrt-spacing-1); }
530
+ .smrt-nav-editor__icons :global(.smrt-shell-icon-button[aria-pressed='true']) { background: var(--smrt-color-primary-container); color: var(--smrt-color-on-primary-container); }
531
+ </style>
@@ -0,0 +1,26 @@
1
+ import { type Component, type Snippet } from 'svelte';
2
+ import type { ShellLayoutController } from './layout-controller.svelte.js';
3
+ import type { ShellSectionActionsContext } from './types.js';
4
+ interface Props {
5
+ /** The layout API to edit; defaults to the nearest `AppShell`'s. */
6
+ controller?: ShellLayoutController;
7
+ /** Accessible name for the navigation landmark. */
8
+ 'aria-label'?: string;
9
+ /** Component used to render icon names, as for `TenantNav`. */
10
+ iconComponent?: Component<{
11
+ name: string;
12
+ size?: number;
13
+ }>;
14
+ /** Host icon buttons for each section (overlay and floating toolbar). */
15
+ sectionActions?: Snippet<[ShellSectionActionsContext]>;
16
+ /**
17
+ * `'sections'` edits only the sections (grip, rename, show, delete, icon
18
+ * picker); their entries are edited on the section's own page with
19
+ * `ShellSectionMenu`. Default `'items'`.
20
+ */
21
+ navMode?: 'items' | 'sections';
22
+ }
23
+ declare const ShellNavEditor: Component<Props, {}, "">;
24
+ type ShellNavEditor = ReturnType<typeof ShellNavEditor>;
25
+ export default ShellNavEditor;
26
+ //# sourceMappingURL=ShellNavEditor.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShellNavEditor.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/ShellNavEditor.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAiB,MAAM,QAAQ,CAAC;AAUrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAO3E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAG7D,UAAU,KAAK;IACb,oEAAoE;IACpE,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,SAAS,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC;IACvD;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;CAChC;AA+WD,QAAA,MAAM,cAAc,0BAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Draws a section or entry icon by name: a built-in shell icon
4
+ * (`SHELL_ICON_PATHS`) as an inline SVG, else the host's `iconComponent`, else
5
+ * the name itself as text. Decorative (`aria-hidden`); callers name the
6
+ * control.
7
+ */
8
+ import type { Component } from 'svelte';
9
+ import {
10
+ isShellIconName,
11
+ SHELL_DEFAULT_SECTION_ICON,
12
+ SHELL_ICON_PATHS,
13
+ } from './shell-icons.js';
14
+
15
+ const warned = new Set<string>();
16
+
17
+ interface Props {
18
+ /** Icon name: a built-in shell icon, else whatever `iconComponent` knows. */
19
+ name: string;
20
+ /** Edge length in pixels (default 18). */
21
+ size?: number;
22
+ /** Renders names that are not built-in shell icons. */
23
+ iconComponent?: Component<{ name: string; size?: number }>;
24
+ }
25
+
26
+ let { name, size = 18, iconComponent: Host }: Props = $props();
27
+
28
+ // An unknown name with no host renderer never prints as text: it falls back to
29
+ // the default glyph, with one dev warning per name.
30
+ const fallback = $derived(!isShellIconName(name) && !Host);
31
+ $effect(() => {
32
+ if (
33
+ fallback &&
34
+ (import.meta as { env?: { DEV?: boolean } }).env?.DEV &&
35
+ !warned.has(name)
36
+ ) {
37
+ warned.add(name);
38
+ console.warn(
39
+ `[smrt-svelte] Unknown shell icon "${name}"; using "${SHELL_DEFAULT_SECTION_ICON}".`,
40
+ );
41
+ }
42
+ });
43
+ </script>
44
+
45
+ <span class="smrt-shell-section-icon" aria-hidden="true">
46
+ {#if isShellIconName(name)}
47
+ <svg viewBox="0 0 24 24" width={size} height={size} fill="currentColor" focusable="false"><path d={SHELL_ICON_PATHS[name]} /></svg>
48
+ {:else if Host}
49
+ <Host {name} {size} />
50
+ {:else}
51
+ <svg viewBox="0 0 24 24" width={size} height={size} fill="currentColor" focusable="false"><path d={SHELL_ICON_PATHS[SHELL_DEFAULT_SECTION_ICON]} /></svg>
52
+ {/if}
53
+ </span>
54
+
55
+ <style>
56
+ .smrt-shell-section-icon { display: inline-grid; place-items: center; flex: 0 0 auto; }
57
+ </style>
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Draws a section or entry icon by name: a built-in shell icon
3
+ * (`SHELL_ICON_PATHS`) as an inline SVG, else the host's `iconComponent`, else
4
+ * the name itself as text. Decorative (`aria-hidden`); callers name the
5
+ * control.
6
+ */
7
+ import type { Component } from 'svelte';
8
+ interface Props {
9
+ /** Icon name: a built-in shell icon, else whatever `iconComponent` knows. */
10
+ name: string;
11
+ /** Edge length in pixels (default 18). */
12
+ size?: number;
13
+ /** Renders names that are not built-in shell icons. */
14
+ iconComponent?: Component<{
15
+ name: string;
16
+ size?: number;
17
+ }>;
18
+ }
19
+ declare const ShellSectionIcon: Component<Props, {}, "">;
20
+ type ShellSectionIcon = ReturnType<typeof ShellSectionIcon>;
21
+ export default ShellSectionIcon;
22
+ //# sourceMappingURL=ShellSectionIcon.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShellSectionIcon.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/ShellSectionIcon.svelte.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAQxC,UAAU,KAAK;IACb,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,aAAa,CAAC,EAAE,SAAS,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5D;AA0CD,QAAA,MAAM,gBAAgB,0BAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}