@happyvertical/smrt-svelte 0.37.8 → 0.37.10

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 (90) hide show
  1. package/AGENTS.md +22 -86
  2. package/README.md +14 -10
  3. package/dist/components/workspace/MIGRATION.md +55 -0
  4. package/dist/components/workspace/README.md +22 -25
  5. package/dist/components/workspace/__tests__/AdminShell.test.js +55 -0
  6. package/dist/components/workspace/__tests__/admin-shell-hotkeys.test.js +34 -0
  7. package/dist/components/workspace/__tests__/admin-shell-settings.test.js +25 -0
  8. package/dist/components/workspace/__tests__/admin-shell-state.test.js +110 -0
  9. package/dist/components/workspace/__tests__/index.test.js +15 -24
  10. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts +2 -1
  11. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts.map +1 -1
  12. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.js +1 -1
  13. package/dist/components/workspace/admin-shell/ActivityBadge.svelte +25 -0
  14. package/dist/components/workspace/admin-shell/ActivityBadge.svelte.d.ts +9 -0
  15. package/dist/components/workspace/admin-shell/ActivityBadge.svelte.d.ts.map +1 -0
  16. package/dist/components/workspace/admin-shell/ActivityItem.svelte +85 -0
  17. package/dist/components/workspace/admin-shell/ActivityItem.svelte.d.ts +8 -0
  18. package/dist/components/workspace/admin-shell/ActivityItem.svelte.d.ts.map +1 -0
  19. package/dist/components/workspace/admin-shell/ActivityList.svelte +41 -0
  20. package/dist/components/workspace/admin-shell/ActivityList.svelte.d.ts +10 -0
  21. package/dist/components/workspace/admin-shell/ActivityList.svelte.d.ts.map +1 -0
  22. package/dist/components/workspace/admin-shell/ActivityToasts.svelte +140 -0
  23. package/dist/components/workspace/admin-shell/ActivityToasts.svelte.d.ts +16 -0
  24. package/dist/components/workspace/admin-shell/ActivityToasts.svelte.d.ts.map +1 -0
  25. package/dist/components/workspace/admin-shell/AdminShell.svelte +691 -0
  26. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +24 -0
  27. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -0
  28. package/dist/components/workspace/admin-shell/AppScopePanel.svelte +94 -0
  29. package/dist/components/workspace/admin-shell/AppScopePanel.svelte.d.ts +14 -0
  30. package/dist/components/workspace/admin-shell/AppScopePanel.svelte.d.ts.map +1 -0
  31. package/dist/components/workspace/admin-shell/HotkeyInput.svelte +74 -0
  32. package/dist/components/workspace/admin-shell/HotkeyInput.svelte.d.ts +11 -0
  33. package/dist/components/workspace/admin-shell/HotkeyInput.svelte.d.ts.map +1 -0
  34. package/dist/components/workspace/admin-shell/ShellCorner.svelte +25 -0
  35. package/dist/components/workspace/admin-shell/ShellCorner.svelte.d.ts +9 -0
  36. package/dist/components/workspace/admin-shell/ShellCorner.svelte.d.ts.map +1 -0
  37. package/dist/components/workspace/admin-shell/ShellDockTool.svelte +19 -0
  38. package/dist/components/workspace/admin-shell/ShellDockTool.svelte.d.ts +7 -0
  39. package/dist/components/workspace/admin-shell/ShellDockTool.svelte.d.ts.map +1 -0
  40. package/dist/components/workspace/admin-shell/ShellSettingsPanel.svelte +146 -0
  41. package/dist/components/workspace/admin-shell/ShellSettingsPanel.svelte.d.ts +4 -0
  42. package/dist/components/workspace/admin-shell/ShellSettingsPanel.svelte.d.ts.map +1 -0
  43. package/dist/components/workspace/admin-shell/ShortcutsOverlay.svelte +81 -0
  44. package/dist/components/workspace/admin-shell/ShortcutsOverlay.svelte.d.ts +7 -0
  45. package/dist/components/workspace/admin-shell/ShortcutsOverlay.svelte.d.ts.map +1 -0
  46. package/dist/components/workspace/admin-shell/SystemScopePanel.svelte +118 -0
  47. package/dist/components/workspace/admin-shell/SystemScopePanel.svelte.d.ts +9 -0
  48. package/dist/components/workspace/admin-shell/SystemScopePanel.svelte.d.ts.map +1 -0
  49. package/dist/components/workspace/admin-shell/SystemStatusChips.svelte +71 -0
  50. package/dist/components/workspace/admin-shell/SystemStatusChips.svelte.d.ts +8 -0
  51. package/dist/components/workspace/admin-shell/SystemStatusChips.svelte.d.ts.map +1 -0
  52. package/dist/components/workspace/admin-shell/TenantNav.svelte +101 -0
  53. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +10 -0
  54. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -0
  55. package/dist/components/workspace/admin-shell/context.d.ts +6 -0
  56. package/dist/components/workspace/admin-shell/context.d.ts.map +1 -0
  57. package/dist/components/workspace/admin-shell/context.js +16 -0
  58. package/dist/components/workspace/admin-shell/hotkeys.d.ts +14 -0
  59. package/dist/components/workspace/admin-shell/hotkeys.d.ts.map +1 -0
  60. package/dist/components/workspace/admin-shell/hotkeys.js +62 -0
  61. package/dist/components/workspace/admin-shell/settings.d.ts +14 -0
  62. package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -0
  63. package/dist/components/workspace/admin-shell/settings.js +139 -0
  64. package/dist/components/workspace/admin-shell/state.svelte.d.ts +47 -0
  65. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -0
  66. package/dist/components/workspace/admin-shell/state.svelte.js +252 -0
  67. package/dist/components/workspace/admin-shell/types.d.ts +137 -0
  68. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -0
  69. package/dist/components/workspace/admin-shell/types.js +13 -0
  70. package/dist/components/workspace/index.d.ts +25 -11
  71. package/dist/components/workspace/index.d.ts.map +1 -1
  72. package/dist/components/workspace/index.js +24 -10
  73. package/dist/i18n/strings.workspace.d.ts +30 -0
  74. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  75. package/dist/i18n/strings.workspace.js +31 -0
  76. package/dist/internal/logger.d.ts +13 -6
  77. package/dist/internal/logger.d.ts.map +1 -1
  78. package/dist/internal/logger.js +33 -8
  79. package/dist/internal/logger.test.js +54 -0
  80. package/dist/web/__tests__/harness.svelte +25 -0
  81. package/dist/web/__tests__/harness.svelte.d.ts +15 -0
  82. package/dist/web/__tests__/harness.svelte.d.ts.map +1 -0
  83. package/dist/web/__tests__/live-collection.svelte.test.js +184 -0
  84. package/dist/web/index.d.ts +13 -0
  85. package/dist/web/index.d.ts.map +1 -0
  86. package/dist/web/index.js +12 -0
  87. package/dist/web/live-collection.svelte.d.ts +132 -0
  88. package/dist/web/live-collection.svelte.d.ts.map +1 -0
  89. package/dist/web/live-collection.svelte.js +173 -0
  90. package/package.json +13 -4
package/AGENTS.md CHANGED
@@ -233,99 +233,35 @@ await expectNoA11yViolations(container); // axe; color-contrast off (jsdom has n
233
233
  - `@happyvertical/smrt-types` (shared types) — includes the identity data contracts (`User`, `Role`, `Membership`, `Tenant`) the role/membership components type against, so no dependency on `smrt-users` / `smrt-profiles` is needed
234
234
  - `@happyvertical/smrt-ui` (UI runtime: primitives, theme system, i18n client, module registry) is a hard `dependency`. The agent-admin shells that used to type against `@happyvertical/smrt-agents/ui` moved to `@happyvertical/smrt-agents/svelte` (#1589), so `smrt-agents` is no longer a dependency here — this drops smrt-svelte below smrt-agents in the package DAG.
235
235
  - `@happyvertical/smrt-languages` is a hard `dependency` (not an optional peer): the Node-only `/i18n/server` subpath imports its resolver. The browser bundle still excludes it — the client `/i18n` layer never imports the languages root, so it tree-shakes out.
236
- - `@happyvertical/logger` (SDK) is a `dependency` — the browser-safe console logger used for voice/AI error reporting in the form components.
236
+ - `@happyvertical/logger` (SDK) is a `dependency` — the console logger used for voice/AI error reporting in the form components. Consume it **only** through `src/internal/logger.ts`, never `createLogger()` at module scope: `createLogger()` reads `HAVE_LOGGER_LEVEL` from `process.env`, so a top-level call throws `ReferenceError: process is not defined` in the browser and kills client-side hydration under `vite dev` (prod builds tree-shake/define it away, so this only bites in dev). The `internal/logger` wrapper constructs the logger lazily and falls back to a bare `ConsoleLogger` when `process.env` is absent, keeping this browser-reachable module (imported by `Provider` + the form primitives) safe.
237
237
  - Peer (all optional): `svelte` >=5.18.2, plus the browser-AI engines (`@huggingface/transformers`, `@mlc-ai/web-llm`, `@remotion/whisper-web`, `@xenova/transformers`) and `chrono-node`.
238
238
 
239
- ## Workspace shell primitives
239
+ ## AdminShell workspace surface
240
240
 
241
- The `./workspace` subpath (`src/components/workspace/`) holds admin-shell primitives:
242
- `WorkspaceShell`, `NavTree`, `Breadcrumbs`, and `ToolsDock` (plus `defineToolsDock` /
243
- `useToolsDock`). Shared types live in `workspace/types.ts` and are re-exported via the
244
- subpath barrel.
241
+ The `./workspace` subpath (`src/components/workspace/`) is the canonical
242
+ AdminShell family for SMRT admin web apps. It exports the four-edge shell
243
+ contract (`AdminShell`, `ShellState`, settings, hotkeys, focus tools,
244
+ activities, tenant nav, app/system panels) from `workspace/admin-shell/`.
245
245
 
246
- **Layering**: primitives first (this folder), opinionated wrapper second (`AdminShell` — deferred),
247
- domain-specific tools live outside the framework in consumer packages.
246
+ The first-generation workspace family (`WorkspaceShell`, `RoleShell`,
247
+ `NavTree`, `Breadcrumbs`, `ToolsDock`) remains in source as migration reference
248
+ only. Do not re-export it from the public `./workspace` barrel just to preserve
249
+ compatibility.
248
250
 
249
251
  **Principles**:
250
- - SvelteKit-agnostic — no `$app/state` or `$app/navigation` imports
251
- - SSR-safe guard all `window` / `localStorage` access
252
+ - SvelteKit-agnostic core — no `$app/state` or `$app/navigation` imports
253
+ - SSR-safe public shell import/render path; browser listeners and localStorage
254
+ activate after mount
252
255
  - No token bridges — consume `var(--smrt-color-*)` directly
253
- - Tool IDs are arbitrary strings (extensible, not an enum)
254
-
255
- **State-mirroring recipes** (issue #1235):
256
- - Dock events. `'dock:state-changed'` fires on `open()`/`close()`/`toggle()` and
257
- on availability-driven `activeTool` clears (payload: `{ isOpen, activeTool }`).
258
- `'dock:context-changed'` fires on `setContext()` with a different reference
259
- (payload: `{ context }`). Legacy `'dock:change'` (payload: `{ isOpen, activeTool, context }`)
260
- still fires on every observable transition (incl. badge-only availability
261
- refresh) for back-compat with consumers mirroring `availableTools` — it's
262
- `@deprecated`; prefer the granular pair. The `'dock:*'` prefix is reserved
263
- for built-ins; consumer events should pick a different namespace.
264
- - `WorkspaceShell` exposes `bind:mobileNavOpen` so consumers can lift the drawer
265
- state. Pair it with `<NavTree onNavigate={() => mobileNavOpen = false} />` to
266
- close the drawer on navigation without any DOM querying.
267
- - `ToolDef.iconComponent?: Component` renders a custom icon (lucide-svelte etc.)
268
- inside the rail glyph (and as a leading glyph in topbar layout). Takes
269
- precedence over `icon: string`, then `label.charAt(0)` as last resort.
270
- - `dock.refreshAvailability()` forces a re-run of `fetchAvailability` with the
271
- current context. `setContext()` short-circuits on strict-equal references —
272
- use refresh when a side-channel event (websocket, button) signals availability
273
- or badges changed without a context change.
274
- - Typed `defineToolsDock<TData, TActions>`. The factory's two generics flow
275
- into `fetchAvailability`'s `ctx` param and through `ToolsDockContext<TData, TActions>`
276
- for tool components. Inside a tool, type `context` locally:
277
- `let { context }: { context: ToolsDockContext<MyData, MyActions> | null } = $props();`.
278
- `context?.actions?.foo()` is then fully typed — no per-consumer redeclaration.
279
- `ToolDef` itself is no longer generic (stored as a homogeneous `ToolDef[]`);
280
- the consumer-side cast at registration is gone, the typed surface lives on
281
- the component's `context` prop instead.
282
- - Layout positioning. `<ToolsDock layout='topbar'>` renders its own
283
- `position: fixed` panel — **do not also use `<WorkspaceShell>`'s `inspector`
284
- snippet** in that mode (the two panels overlap with no z-index coordination).
285
- `'rail'` layout is safe to compose alongside `inspector` — its panel sits
286
- inside the dock's own aside.
287
-
288
- ### RoleShell
289
-
290
- Opinionated thin wrapper for multi-role admin shells. Pass a `RoleConfig[]` list
291
- and the current role id; renders `<WorkspaceShell>` + `<NavTree>` + `<Breadcrumbs>`
292
- wired together. Role colors flow through as `--smrt-role-color` CSS custom property.
293
-
294
- ```svelte
295
- <script lang="ts">
296
- import { page } from '$app/state';
297
- import { RoleShell } from '@happyvertical/smrt-svelte/workspace';
298
- import AccountMenu from '$lib/AccountMenu.svelte';
299
- import { ROLE_CONFIGS } from '$lib/roles';
300
-
301
- let { data, children } = $props();
302
- let mobileNavOpen = $state(false);
303
- </script>
304
-
305
- <RoleShell
306
- roles={ROLE_CONFIGS}
307
- currentRole={data.currentRole}
308
- currentPath={page.url.pathname}
309
- bind:mobileNavOpen
310
- >
311
- {#snippet sidebarFooter()}
312
- <AccountMenu user={data.user} />
313
- {/snippet}
314
- {@render children?.()}
315
- </RoleShell>
316
- ```
317
-
318
- The `{@render children?.()}` call is the Svelte 5 idiom for rendering a
319
- layout's child route content — replace with the equivalent slot/render call
320
- for your framework if you're not using SvelteKit's `+layout.svelte` flow.
321
-
322
- The shell intentionally doesn't know about specific role IDs — consumers pick
323
- whatever set their app needs. Use this for role-based admin dashboards; use
324
- `<WorkspaceShell>` directly for non-role apps.
325
-
326
- See epic [happyvertical/smrt#1226](https://github.com/happyvertical/smrt/issues/1226) for context;
327
- implementations land via #1227 (`WorkspaceShell`), #1228 (`NavTree`/`Breadcrumbs`), and #1229
328
- (`ToolsDock` + registry).
256
+ - App-owned configuration for hidden edges, push/overlay presentation, and
257
+ exclusivity groups
258
+ - User-owned preferences persist as sparse `ShellSettingsDelta` values
259
+ - Focus tools may register imperatively through `ShellState` or declaratively
260
+ through Svelte helpers
261
+ - Shell activities are client-side records; server jobs, polling, WebSockets,
262
+ and `smrt-web` SSE can feed them through app adapters
263
+
264
+ See `src/components/workspace/MIGRATION.md` for the old-to-new concept map.
329
265
 
330
266
  ### Dock availability gates (server-side)
331
267
 
package/README.md CHANGED
@@ -80,40 +80,44 @@ pnpm add @happyvertical/smrt-svelte
80
80
  </ThemeProvider>
81
81
  ```
82
82
 
83
- ### Workspace Navigation
83
+ ### Admin Workspace
84
84
 
85
85
  ```svelte
86
86
  <script lang="ts">
87
87
  import { manifest } from '$lib/smrt-manifest';
88
88
  import {
89
- RoleShell,
90
- navTreeFromManifest,
89
+ AdminShell,
90
+ TenantNav,
91
+ tenantNavFromManifest,
91
92
  } from '@happyvertical/smrt-svelte/workspace';
92
93
 
93
94
  let { children } = $props();
94
95
 
95
- const sections = navTreeFromManifest(manifest, {
96
+ const sections = tenantNavFromManifest(manifest, {
96
97
  sectionHints: {
97
98
  '@happyvertical/smrt-content': 'Content',
98
99
  '@happyvertical/smrt-profiles': 'Profiles',
99
100
  },
100
101
  });
101
102
 
102
- const roles = [{ id: 'admin', label: 'Admin', sections }];
103
103
  </script>
104
104
 
105
- <RoleShell {roles} currentRole="admin" currentPath="/admin/articles">
105
+ <AdminShell title="Admin">
106
+ {#snippet tenantPanel()}
107
+ <TenantNav items={sections} currentHref="/admin/articles" />
108
+ {/snippet}
109
+
106
110
  {@render children?.()}
107
- </RoleShell>
111
+ </AdminShell>
108
112
  ```
109
113
 
110
114
  Filter the same manifest by role permissions when only a subset of resources
111
115
  should be visible:
112
116
 
113
117
  ```ts
114
- import { navTreeFromManifest } from '@happyvertical/smrt-svelte/workspace';
118
+ import { tenantNavFromManifest } from '@happyvertical/smrt-svelte/workspace';
115
119
 
116
- const editorSections = navTreeFromManifest(manifest, {
120
+ const editorSections = tenantNavFromManifest(manifest, {
117
121
  permittedResources: [
118
122
  '@happyvertical/smrt-content:Article',
119
123
  '@happyvertical/smrt-content:Document',
@@ -137,7 +141,7 @@ const editorSections = navTreeFromManifest(manifest, {
137
141
  | `@happyvertical/smrt-svelte/ui` | UI primitives (Button, Card, Badge, Pagination) |
138
142
  | `@happyvertical/smrt-svelte/themes` | ThemeProvider, presets (material/glass/studio), CSS generation |
139
143
  | `@happyvertical/smrt-svelte/registry` | ModuleUIRegistry for agent admin panels |
140
- | `@happyvertical/smrt-svelte/workspace` | WorkspaceShell, RoleShell, NavTree, Breadcrumbs, ToolsDock, and manifest nav helpers |
144
+ | `@happyvertical/smrt-svelte/workspace` | AdminShell, ShellState, tenant nav, focus tools, settings, activities, and system/app panels |
141
145
  | `@happyvertical/smrt-svelte/browser-ai` | Browser AI client (STT/TTS/LLM adapters, capability detection) |
142
146
  | `@happyvertical/smrt-svelte/browser-ai/svelte` | Svelte AI components (VoiceInput, CapabilityGate, etc.) |
143
147
  | `@happyvertical/smrt-svelte/styles/tokens.css` | Design tokens CSS |
@@ -0,0 +1,55 @@
1
+ # AdminShell Migration
2
+
3
+ The first-generation workspace family (`WorkspaceShell`, `RoleShell`,
4
+ `NavTree`, `Breadcrumbs`, `ToolsDock`) is no longer the public
5
+ `@happyvertical/smrt-svelte/workspace` surface. Treat those source files as a
6
+ migration reference while moving apps to `AdminShell`.
7
+
8
+ ## Concept Map
9
+
10
+ | First-generation concept | AdminShell concept |
11
+ | --- | --- |
12
+ | `WorkspaceShell` outer layout | `AdminShell` four-edge layout |
13
+ | `RoleShell` wrapper | App-owned config + `TenantNav` |
14
+ | `NavTree` | `TenantNav` fed by app data or `tenantNavFromManifest()` |
15
+ | `ToolsDock` / `defineToolsDock` | Focus edge tools through `ShellState.registerFocusTool()` or `ShellDockTool` |
16
+ | Dock localStorage | Sparse `ShellSettingsDelta` through `ShellSettingsAdapter` |
17
+ | App-specific job panels | `SystemScopePanel` fed by app stores/endpoints |
18
+
19
+ ## Minimal Shape
20
+
21
+ ```svelte
22
+ <script lang="ts">
23
+ import {
24
+ AdminShell,
25
+ TenantNav,
26
+ SystemStatusChips,
27
+ } from '@happyvertical/smrt-svelte/workspace';
28
+ </script>
29
+
30
+ <AdminShell title="My App">
31
+ {#snippet tenantPanel()}
32
+ <TenantNav items={navItems} currentHref={currentPath} />
33
+ {/snippet}
34
+
35
+ {#snippet systemBar()}
36
+ <SystemStatusChips chips={statusChips} />
37
+ {/snippet}
38
+
39
+ {@render children?.()}
40
+ </AdminShell>
41
+ ```
42
+
43
+ ## Notes
44
+
45
+ - `hidden` means the app omitted an edge; users normally persist only
46
+ collapsed/expanded panel deltas.
47
+ - Push versus overlay is an app decision per edge.
48
+ - Expanded panels are independent by default. Use matching `exclusiveGroup`
49
+ values on panel config to make panels mutually exclusive.
50
+ - Hotkeys use `KeyboardEvent.code` so the WASD cluster remains physical across
51
+ keyboard layouts.
52
+ - URL synchronization is intentionally adapter-owned. The shell exposes
53
+ serializable state and commands; apps decide their route/query shape.
54
+ - Shell activities are client-side records. Server jobs, polling, WebSockets,
55
+ and `smrt-web` SSE live updates can all feed them through adapters.
@@ -1,34 +1,31 @@
1
1
  # workspace/
2
2
 
3
- Upstream admin shell primitives for SMRT consumer apps. These pieces are
4
- SvelteKit-agnostic, SSR-safe, and carry no domain coupling — they slot into
5
- any Svelte 5 app that wants a sidebar + topbar + tools-dock layout.
3
+ Canonical AdminShell family for SMRT admin web apps.
6
4
 
7
- ## Tracking
5
+ `@happyvertical/smrt-svelte/workspace` now exports the four-edge AdminShell
6
+ contract:
8
7
 
9
- - Epic: [happyvertical/smrt#1226](https://github.com/happyvertical/smrt/issues/1226)
10
- - Implementers:
11
- - `WorkspaceShell` [#1227](https://github.com/happyvertical/smrt/issues/1227)
12
- - `NavTree`, `Breadcrumbs` [#1228](https://github.com/happyvertical/smrt/issues/1228)
13
- - `ToolsDock`, `defineToolsDock`, `useToolsDock` — [#1229](https://github.com/happyvertical/smrt/issues/1229)
8
+ - App scope on the top edge
9
+ - Tenant scope on the left edge
10
+ - Focus scope on the right edge
11
+ - System scope on the bottom edge
14
12
 
15
- ## Shared types (this PR)
13
+ The shell owns geometry, keyboard behavior, settings deltas, responsive
14
+ presentation, focus tool registration, activity registry/watch components, and
15
+ optional SMRT-fed content seams. It does not depend on jobs, users, tenancy, or
16
+ SvelteKit route APIs; apps pass data, endpoints, and permission-filtered nav in.
16
17
 
17
- Exported from `./types.js` via the barrel:
18
+ ## Main Exports
18
19
 
19
- - `NavItem`, `BreadcrumbItem`
20
- - `ToolDef`, `AvailableTool`, `ToolsDockContext`, `ToolsDockApi`
20
+ - `AdminShell`
21
+ - `ShellState` / `createShellState`
22
+ - `ShellSettingsPanel`, `HotkeyInput`, `ShortcutsOverlay`
23
+ - `ShellDockTool`, `TenantNav`
24
+ - `ActivityBadge`, `ActivityList`, `ActivityItem`, `ActivityToasts`
25
+ - `AppScopePanel`, `SystemStatusChips`, `SystemScopePanel`
26
+ - `tenantNavFromManifest`
21
27
 
22
- ## Planned component exports
28
+ ## Migration
23
29
 
24
- - `WorkspaceShell` (#1227) outer shell layout primitive
25
- - `NavTree`, `Breadcrumbs` (#1228) — navigation primitives consuming `NavItem` / `BreadcrumbItem`
26
- - `ToolsDock` + `defineToolsDock` + `useToolsDock` (#1229) — right-rail tools dock + registry
27
-
28
- ## Principles
29
-
30
- - **SvelteKit-agnostic** — no `$app/state` or `$app/navigation` imports
31
- - **SSR-safe** — guard all `window` / `localStorage` access
32
- - **No token bridges** — consume `var(--smrt-color-*)` tokens directly
33
- - **No domain coupling** — domain-specific tools live in consumer packages
34
- - **Extensible tool IDs** — tool IDs are arbitrary strings, not an enum
30
+ See [MIGRATION.md](./MIGRATION.md) for the first-generation workspace migration
31
+ map.
@@ -0,0 +1,55 @@
1
+ import { createRawSnippet, flushSync, mount, tick, unmount } from 'svelte';
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
+ import AdminShell from '../admin-shell/AdminShell.svelte';
4
+ function textSnippet(text) {
5
+ return createRawSnippet(() => ({
6
+ render: () => `<span>${text}</span>`,
7
+ }));
8
+ }
9
+ let container;
10
+ beforeEach(() => {
11
+ container = document.createElement('div');
12
+ document.body.appendChild(container);
13
+ });
14
+ afterEach(() => {
15
+ container.remove();
16
+ });
17
+ describe('AdminShell', () => {
18
+ it('renders the four edge shell and body content', () => {
19
+ const component = mount(AdminShell, {
20
+ target: container,
21
+ props: {
22
+ title: 'Ops',
23
+ children: textSnippet('main work'),
24
+ },
25
+ });
26
+ try {
27
+ expect(container.querySelector('.smrt-admin-shell')).not.toBeNull();
28
+ expect(container.querySelector('header')?.textContent).toContain('Ops');
29
+ expect(container.querySelector('main')?.textContent).toContain('main work');
30
+ expect(container.querySelector('[role="navigation"]')).not.toBeNull();
31
+ }
32
+ finally {
33
+ unmount(component);
34
+ }
35
+ });
36
+ it('toggles a physical-code panel hotkey', async () => {
37
+ const component = mount(AdminShell, {
38
+ target: container,
39
+ props: {
40
+ children: textSnippet('main work'),
41
+ },
42
+ });
43
+ try {
44
+ const shell = container.querySelector('.smrt-admin-shell');
45
+ expect(shell?.getAttribute('data-top-state')).toBe('collapsed');
46
+ await tick();
47
+ window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyW' }));
48
+ flushSync();
49
+ expect(shell?.getAttribute('data-top-state')).toBe('expanded');
50
+ }
51
+ finally {
52
+ unmount(component);
53
+ }
54
+ });
55
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { hotkeyMatchesEvent, shellActionFromKeyboardEvent, } from '../admin-shell/hotkeys.js';
3
+ import { resolveShellConfig } from '../admin-shell/settings.js';
4
+ function keyEvent(init) {
5
+ return new KeyboardEvent('keydown', init);
6
+ }
7
+ describe('AdminShell hotkeys', () => {
8
+ it('matches physical KeyboardEvent.code bindings', () => {
9
+ expect(hotkeyMatchesEvent({ code: 'KeyW' }, keyEvent({ code: 'KeyW' }))).toBe(true);
10
+ expect(hotkeyMatchesEvent({ code: 'KeyW' }, keyEvent({ code: 'KeyZ' }))).toBe(false);
11
+ });
12
+ it('maps WASD codes to shell edges', () => {
13
+ const { panels } = resolveShellConfig();
14
+ expect(shellActionFromKeyboardEvent(keyEvent({ code: 'KeyW' }), panels)).toEqual({ type: 'toggle-panel', edge: 'top' });
15
+ expect(shellActionFromKeyboardEvent(keyEvent({ code: 'KeyA' }), panels)).toEqual({ type: 'toggle-panel', edge: 'left' });
16
+ expect(shellActionFromKeyboardEvent(keyEvent({ code: 'KeyS' }), panels)).toEqual({ type: 'toggle-panel', edge: 'bottom' });
17
+ expect(shellActionFromKeyboardEvent(keyEvent({ code: 'KeyD' }), panels)).toEqual({ type: 'toggle-panel', edge: 'right' });
18
+ });
19
+ it('suppresses panel hotkeys in editable controls', () => {
20
+ const input = document.createElement('input');
21
+ const event = keyEvent({ code: 'KeyW' });
22
+ Object.defineProperty(event, 'target', { value: input });
23
+ expect(shellActionFromKeyboardEvent(event, resolveShellConfig().panels)).toBe(null);
24
+ });
25
+ it('keeps the shortcuts overlay available on question mark', () => {
26
+ expect(shellActionFromKeyboardEvent(keyEvent({ key: '?', code: 'Slash', shiftKey: true }), resolveShellConfig().panels)).toEqual({ type: 'show-shortcuts' });
27
+ });
28
+ it('does not open shortcuts when typing ? in an editable control', () => {
29
+ const input = document.createElement('input');
30
+ const event = keyEvent({ key: '?', code: 'Slash', shiftKey: true });
31
+ Object.defineProperty(event, 'target', { value: input });
32
+ expect(shellActionFromKeyboardEvent(event, resolveShellConfig().panels)).toBe(null);
33
+ });
34
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mergeShellSettingsDelta, resolveHotkey, resolveInitialPanelState, resolveShellConfig, } from '../admin-shell/settings.js';
3
+ describe('AdminShell settings', () => {
4
+ it('treats false app config as a hidden edge', () => {
5
+ const config = resolveShellConfig({ right: false });
6
+ expect(config.panels.right.initial).toBe('hidden');
7
+ expect(resolveInitialPanelState('right', config.panels.right)).toBe('hidden');
8
+ expect(resolveHotkey('right', config.panels.right)).toBeNull();
9
+ });
10
+ it('resolves user panel deltas over app defaults', () => {
11
+ const config = resolveShellConfig({
12
+ left: { initial: 'collapsed' },
13
+ });
14
+ expect(resolveInitialPanelState('left', config.panels.left, {
15
+ panels: { left: 'expanded' },
16
+ })).toBe('expanded');
17
+ });
18
+ it('merges sparse deltas without replacing unrelated keys', () => {
19
+ expect(mergeShellSettingsDelta({ hotkeysEnabled: true, panels: { left: 'expanded' } }, { keymap: { top: { code: 'KeyQ' } } })).toEqual({
20
+ hotkeysEnabled: true,
21
+ panels: { left: 'expanded' },
22
+ keymap: { top: { code: 'KeyQ' } },
23
+ });
24
+ });
25
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createShellState } from '../admin-shell/state.svelte.js';
3
+ describe('ShellState', () => {
4
+ it('allows panels to expand independently by default', () => {
5
+ const shell = createShellState();
6
+ shell.expandPanel('top');
7
+ shell.expandPanel('bottom');
8
+ expect(shell.panels.top).toBe('expanded');
9
+ expect(shell.panels.bottom).toBe('expanded');
10
+ });
11
+ it('closes peers in the same exclusivity group', () => {
12
+ const shell = createShellState({
13
+ config: {
14
+ top: { exclusiveGroup: 'vertical' },
15
+ bottom: { exclusiveGroup: 'vertical' },
16
+ },
17
+ });
18
+ shell.expandPanel('top');
19
+ shell.expandPanel('bottom');
20
+ expect(shell.panels.top).toBe('collapsed');
21
+ expect(shell.panels.bottom).toBe('expanded');
22
+ });
23
+ it('registers and unregisters focus tools', () => {
24
+ const shell = createShellState();
25
+ const unregister = shell.registerFocusTool({
26
+ id: 'activity',
27
+ label: 'Activity',
28
+ });
29
+ expect(shell.focusTools.map((tool) => tool.id)).toEqual(['activity']);
30
+ expect(shell.activeFocusToolId).toBe('activity');
31
+ unregister();
32
+ expect(shell.focusTools).toEqual([]);
33
+ expect(shell.activeFocusToolId).toBeNull();
34
+ });
35
+ it('re-homes activities to system when their edge is hidden', () => {
36
+ const shell = createShellState({ config: { right: false } });
37
+ shell.upsertActivity({
38
+ id: 'encode-1',
39
+ label: 'Encode',
40
+ kind: 'video-encode',
41
+ scope: 'focus',
42
+ status: 'running',
43
+ });
44
+ expect(shell.listActivities()[0]?.edge).toBe('bottom');
45
+ expect(shell.activityBadge('bottom').count).toBe(1);
46
+ });
47
+ it('emits lifecycle events for activity updates', () => {
48
+ const shell = createShellState();
49
+ const listener = vi.fn();
50
+ shell.watchActivities(listener);
51
+ shell.upsertActivity({
52
+ id: 'import-1',
53
+ label: 'Import',
54
+ kind: 'import',
55
+ scope: 'system',
56
+ status: 'running',
57
+ });
58
+ shell.updateActivity('import-1', { status: 'completed' });
59
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: 'transition' }));
60
+ });
61
+ it('keeps an exclusivity-closed peer collapsed after settings re-resolve', () => {
62
+ const shell = createShellState({
63
+ config: {
64
+ top: { exclusiveGroup: 'vertical' },
65
+ bottom: { exclusiveGroup: 'vertical' },
66
+ },
67
+ });
68
+ shell.expandPanel('top');
69
+ shell.expandPanel('bottom');
70
+ // Re-applying settings (as hydrate / app-default changes do) must not
71
+ // resurrect the peer the exclusivity rule collapsed.
72
+ shell.applySettings({});
73
+ expect(shell.panels.top).toBe('collapsed');
74
+ expect(shell.panels.bottom).toBe('expanded');
75
+ });
76
+ it('re-homes to a visible edge when both the scope edge and system are hidden', () => {
77
+ const shell = createShellState({ config: { right: false, bottom: false } });
78
+ shell.upsertActivity({
79
+ id: 'encode-2',
80
+ label: 'Encode',
81
+ kind: 'video-encode',
82
+ scope: 'focus',
83
+ status: 'running',
84
+ });
85
+ const edge = shell.listActivities()[0]?.edge;
86
+ expect(edge).toBeDefined();
87
+ expect(shell.panels[edge]).not.toBe('hidden');
88
+ });
89
+ it('flags progress-only updates as upsert with a previous snapshot', () => {
90
+ const shell = createShellState();
91
+ const events = [];
92
+ shell.watchActivities((event) => {
93
+ if (event.type !== 'remove') {
94
+ events.push(`${event.type}:${event.previous ? 'has-prev' : 'new'}`);
95
+ }
96
+ });
97
+ shell.upsertActivity({
98
+ id: 'encode-3',
99
+ label: 'Encode',
100
+ kind: 'video-encode',
101
+ scope: 'system',
102
+ status: 'running',
103
+ progress: 10,
104
+ });
105
+ shell.updateActivity('encode-3', { progress: 40 });
106
+ // A new activity is a notify-worthy "started"; a progress-only tick is an
107
+ // upsert carrying `previous`, which the toaster filters out.
108
+ expect(events).toEqual(['upsert:new', 'upsert:has-prev']);
109
+ });
110
+ });
@@ -1,37 +1,28 @@
1
- /**
2
- * Smoke test: the workspace subpath barrel must export the new
3
- * NavTree and Breadcrumbs components (plus existing types).
4
- */
5
1
  import { describe, expect, it } from 'vitest';
6
2
  import * as workspace from '../index.js';
7
3
  describe('workspace barrel', () => {
8
- it('exports NavTree', () => {
9
- expect(workspace.NavTree).toBeDefined();
4
+ it('exports AdminShell', () => {
5
+ expect(workspace.AdminShell).toBeDefined();
10
6
  });
11
- it('exports Breadcrumbs', () => {
12
- expect(workspace.Breadcrumbs).toBeDefined();
7
+ it('exports shell state helpers', () => {
8
+ expect(workspace.createShellState).toBeDefined();
9
+ expect(workspace.resolveShellConfig).toBeDefined();
13
10
  });
14
- it('exports RoleShell', () => {
15
- expect(workspace.RoleShell).toBeDefined();
11
+ it('exports settings and activity components', () => {
12
+ expect(workspace.ShellSettingsPanel).toBeDefined();
13
+ expect(workspace.ActivityList).toBeDefined();
16
14
  });
17
- it('exports navTreeFromManifest helper', () => {
18
- expect(workspace.navTreeFromManifest).toBeDefined();
19
- expect(typeof workspace.navTreeFromManifest).toBe('function');
15
+ it('exports tenant nav helper from the manifest implementation', () => {
16
+ expect(workspace.tenantNavFromManifest).toBeDefined();
17
+ expect(typeof workspace.tenantNavFromManifest).toBe('function');
20
18
  });
21
19
  it('exports pluralizeClassName helper', () => {
22
20
  expect(workspace.pluralizeClassName).toBeDefined();
23
21
  expect(typeof workspace.pluralizeClassName).toBe('function');
24
22
  });
25
- it('re-exports ToolsDockEvents for typed dock:* event payloads', () => {
26
- // Compile-time assertion: the type must be importable from the barrel
27
- // so consumers can reference the built-in `'dock:*'` event payloads in
28
- // their own typed wrappers / stores.
29
- // (No runtime export; the `import type` above is the real check.)
30
- const _typeCheck = {
31
- isOpen: false,
32
- activeTool: null,
33
- context: null,
34
- };
35
- expect(_typeCheck.isOpen).toBe(false);
23
+ it('does not export first-generation shell components as first-class API', () => {
24
+ expect('WorkspaceShell' in workspace).toBe(false);
25
+ expect('RoleShell' in workspace).toBe(false);
26
+ expect('ToolsDock' in workspace).toBe(false);
36
27
  });
37
28
  });
@@ -23,7 +23,8 @@
23
23
  * If TypeScript ever rejects this file under strict checks, the regression
24
24
  * from review thread #1239/PRRT_kwDOQDruXs6CbBcK has returned.
25
25
  */
26
- import { type DefineToolsDockOptions, type ToolDef, type ToolsDockApi, type ToolsDockContext, type ToolsDockInstance } from '../../index.js';
26
+ import { type DefineToolsDockOptions, type ToolsDockInstance } from '../../tools-dock/define-tools-dock.svelte.js';
27
+ import type { ToolDef, ToolsDockApi, ToolsDockContext } from '../../types.js';
27
28
  import type { MyActions, MyData } from './typed-tool-types.js';
28
29
  /** Type-only export: a tool registry that uses the typed component. */
29
30
  export declare const typedTool: ToolDef;
@@ -1 +1 @@
1
- {"version":3,"file":"register-typed-tool.d.ts","sourceRoot":"","sources":["../../../../../src/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE/D,uEAAuE;AACvE,eAAO,MAAM,SAAS,EAAE,OAQvB,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,YAAY,EAAE,sBAAsB,CAAC,MAAM,EAAE,SAAS,CAElE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,gBAAgB,CAAC,MAAM,EAAE,SAAS,CAO5D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,gBAK5B,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,SAAS,CAAC,SAAS,MAAM,CAAC,GACrE,CAAC,GACD,KAAK,CAAC;AAEV;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,IAAI,iBAAiB,CAAC,MAAM,EAAE,SAAS,CAAC,CAmC3E"}
1
+ {"version":3,"file":"register-typed-tool.d.ts","sourceRoot":"","sources":["../../../../../src/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,iBAAiB,EACvB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE/D,uEAAuE;AACvE,eAAO,MAAM,SAAS,EAAE,OAQvB,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,YAAY,EAAE,sBAAsB,CAAC,MAAM,EAAE,SAAS,CAElE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,gBAAgB,CAAC,MAAM,EAAE,SAAS,CAO5D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,gBAK5B,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,SAAS,CAAC,SAAS,MAAM,CAAC,GACrE,CAAC,GACD,KAAK,CAAC;AAEV;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,IAAI,iBAAiB,CAAC,MAAM,EAAE,SAAS,CAAC,CAmC3E"}
@@ -23,7 +23,7 @@
23
23
  * If TypeScript ever rejects this file under strict checks, the regression
24
24
  * from review thread #1239/PRRT_kwDOQDruXs6CbBcK has returned.
25
25
  */
26
- import { defineToolsDock, } from '../../index.js';
26
+ import { defineToolsDock, } from '../../tools-dock/define-tools-dock.svelte.js';
27
27
  import TypedTool from './TypedTool.svelte';
28
28
  /** Type-only export: a tool registry that uses the typed component. */
29
29
  export const typedTool = {
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { Badge } from '@happyvertical/smrt-ui/ui';
3
+ import { useAdminShell } from './context.js';
4
+ import type { PanelEdge } from './types.js';
5
+
6
+ interface Props {
7
+ edge: PanelEdge;
8
+ label?: string;
9
+ }
10
+
11
+ let { edge, label = 'Running activities' }: Props = $props();
12
+ const shell = useAdminShell();
13
+ const badge = $derived(shell.activityBadge(edge));
14
+ </script>
15
+
16
+ {#if badge.count > 0 || badge.hasFailed}
17
+ <Badge
18
+ variant={badge.hasFailed ? 'error' : 'primary'}
19
+ size="sm"
20
+ title={label}
21
+ aria-label={`${label}: ${badge.count}`}
22
+ >
23
+ {badge.progress !== null ? `${Math.round(badge.progress)}%` : badge.count}
24
+ </Badge>
25
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { PanelEdge } from './types.js';
2
+ interface Props {
3
+ edge: PanelEdge;
4
+ label?: string;
5
+ }
6
+ declare const ActivityBadge: import("svelte").Component<Props, {}, "">;
7
+ type ActivityBadge = ReturnType<typeof ActivityBadge>;
8
+ export default ActivityBadge;
9
+ //# sourceMappingURL=ActivityBadge.svelte.d.ts.map