sh3-core 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +9 -0
  2. package/dist/Shell.svelte +283 -0
  3. package/dist/Shell.svelte.d.ts +5 -0
  4. package/dist/api.d.ts +28 -0
  5. package/dist/api.js +50 -0
  6. package/dist/app/admin/ApiKeysView.svelte +169 -0
  7. package/dist/app/admin/ApiKeysView.svelte.d.ts +3 -0
  8. package/dist/app/admin/AuthSettingsView.svelte +105 -0
  9. package/dist/app/admin/AuthSettingsView.svelte.d.ts +3 -0
  10. package/dist/app/admin/SystemView.svelte +73 -0
  11. package/dist/app/admin/SystemView.svelte.d.ts +3 -0
  12. package/dist/app/admin/UsersView.svelte +188 -0
  13. package/dist/app/admin/UsersView.svelte.d.ts +3 -0
  14. package/dist/app/admin/adminApp.d.ts +7 -0
  15. package/dist/app/admin/adminApp.js +25 -0
  16. package/dist/app/admin/adminShard.svelte.d.ts +4 -0
  17. package/dist/app/admin/adminShard.svelte.js +62 -0
  18. package/dist/app/store/InstalledView.svelte +246 -0
  19. package/dist/app/store/InstalledView.svelte.d.ts +3 -0
  20. package/dist/app/store/StoreView.svelte +522 -0
  21. package/dist/app/store/StoreView.svelte.d.ts +3 -0
  22. package/dist/app/store/storeApp.d.ts +10 -0
  23. package/dist/app/store/storeApp.js +26 -0
  24. package/dist/app/store/storeShard.svelte.d.ts +38 -0
  25. package/dist/app/store/storeShard.svelte.js +218 -0
  26. package/dist/apps/lifecycle.d.ts +42 -0
  27. package/dist/apps/lifecycle.js +184 -0
  28. package/dist/apps/registry.svelte.d.ts +40 -0
  29. package/dist/apps/registry.svelte.js +59 -0
  30. package/dist/apps/terminal/manifest.d.ts +8 -0
  31. package/dist/apps/terminal/manifest.js +13 -0
  32. package/dist/apps/terminal/terminal-app.d.ts +7 -0
  33. package/dist/apps/terminal/terminal-app.js +14 -0
  34. package/dist/apps/types.d.ts +93 -0
  35. package/dist/apps/types.js +10 -0
  36. package/dist/artifact.d.ts +32 -0
  37. package/dist/artifact.js +1 -0
  38. package/dist/assets/SH3.png +0 -0
  39. package/dist/assets/icons.svg +1126 -0
  40. package/dist/assets.d.ts +13 -0
  41. package/dist/auth/GuestBanner.svelte +134 -0
  42. package/dist/auth/GuestBanner.svelte.d.ts +3 -0
  43. package/dist/auth/SignInWall.svelte +203 -0
  44. package/dist/auth/SignInWall.svelte.d.ts +7 -0
  45. package/dist/auth/auth.svelte.d.ts +69 -0
  46. package/dist/auth/auth.svelte.js +165 -0
  47. package/dist/auth/index.d.ts +2 -0
  48. package/dist/auth/index.js +1 -0
  49. package/dist/auth/types.d.ts +41 -0
  50. package/dist/auth/types.js +6 -0
  51. package/dist/build.d.ts +49 -0
  52. package/dist/build.js +236 -0
  53. package/dist/contract.d.ts +20 -0
  54. package/dist/contract.js +28 -0
  55. package/dist/createShell.d.ts +24 -0
  56. package/dist/createShell.js +131 -0
  57. package/dist/documents/backends.d.ts +17 -0
  58. package/dist/documents/backends.js +156 -0
  59. package/dist/documents/config.d.ts +7 -0
  60. package/dist/documents/config.js +27 -0
  61. package/dist/documents/handle.d.ts +6 -0
  62. package/dist/documents/handle.js +154 -0
  63. package/dist/documents/http-backend.d.ts +22 -0
  64. package/dist/documents/http-backend.js +78 -0
  65. package/dist/documents/index.d.ts +6 -0
  66. package/dist/documents/index.js +8 -0
  67. package/dist/documents/notifications.d.ts +9 -0
  68. package/dist/documents/notifications.js +39 -0
  69. package/dist/documents/types.d.ts +97 -0
  70. package/dist/documents/types.js +12 -0
  71. package/dist/env/client.d.ts +44 -0
  72. package/dist/env/client.js +106 -0
  73. package/dist/env/index.d.ts +2 -0
  74. package/dist/env/index.js +1 -0
  75. package/dist/env/types.d.ts +12 -0
  76. package/dist/env/types.js +8 -0
  77. package/dist/host-entry.d.ts +13 -0
  78. package/dist/host-entry.js +17 -0
  79. package/dist/host.d.ts +15 -0
  80. package/dist/host.js +86 -0
  81. package/dist/index.d.ts +4 -0
  82. package/dist/index.js +14 -0
  83. package/dist/layout/DragPreview.svelte +63 -0
  84. package/dist/layout/DragPreview.svelte.d.ts +3 -0
  85. package/dist/layout/LayoutRenderer.svelte +262 -0
  86. package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
  87. package/dist/layout/SlotContainer.svelte +140 -0
  88. package/dist/layout/SlotContainer.svelte.d.ts +8 -0
  89. package/dist/layout/SlotDropZone.svelte +122 -0
  90. package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
  91. package/dist/layout/drag.svelte.d.ts +45 -0
  92. package/dist/layout/drag.svelte.js +200 -0
  93. package/dist/layout/inspection.d.ts +72 -0
  94. package/dist/layout/inspection.js +209 -0
  95. package/dist/layout/ops.d.ts +100 -0
  96. package/dist/layout/ops.js +310 -0
  97. package/dist/layout/slotHostPool.svelte.d.ts +36 -0
  98. package/dist/layout/slotHostPool.svelte.js +229 -0
  99. package/dist/layout/store.svelte.d.ts +39 -0
  100. package/dist/layout/store.svelte.js +153 -0
  101. package/dist/layout/tree-walk.d.ts +15 -0
  102. package/dist/layout/tree-walk.js +33 -0
  103. package/dist/layout/types.d.ts +108 -0
  104. package/dist/layout/types.js +25 -0
  105. package/dist/migrations/shell-rename.d.ts +16 -0
  106. package/dist/migrations/shell-rename.js +48 -0
  107. package/dist/overlays/ModalFrame.svelte +87 -0
  108. package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
  109. package/dist/overlays/PopupFrame.svelte +85 -0
  110. package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
  111. package/dist/overlays/ToastItem.svelte +77 -0
  112. package/dist/overlays/ToastItem.svelte.d.ts +9 -0
  113. package/dist/overlays/focusTrap.d.ts +1 -0
  114. package/dist/overlays/focusTrap.js +64 -0
  115. package/dist/overlays/modal.d.ts +9 -0
  116. package/dist/overlays/modal.js +141 -0
  117. package/dist/overlays/popup.d.ts +9 -0
  118. package/dist/overlays/popup.js +108 -0
  119. package/dist/overlays/roots.d.ts +4 -0
  120. package/dist/overlays/roots.js +31 -0
  121. package/dist/overlays/toast.d.ts +6 -0
  122. package/dist/overlays/toast.js +93 -0
  123. package/dist/overlays/types.d.ts +31 -0
  124. package/dist/overlays/types.js +15 -0
  125. package/dist/platform/index.d.ts +10 -0
  126. package/dist/platform/index.js +33 -0
  127. package/dist/platform/tauri-backend.d.ts +15 -0
  128. package/dist/platform/tauri-backend.js +58 -0
  129. package/dist/primitives/.gitkeep +0 -0
  130. package/dist/primitives/ResizableSplitter.svelte +333 -0
  131. package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
  132. package/dist/primitives/TabbedPanel.svelte +305 -0
  133. package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
  134. package/dist/primitives/base.css +42 -0
  135. package/dist/registry/client.d.ts +74 -0
  136. package/dist/registry/client.js +117 -0
  137. package/dist/registry/index.d.ts +13 -0
  138. package/dist/registry/index.js +14 -0
  139. package/dist/registry/installer.d.ts +53 -0
  140. package/dist/registry/installer.js +168 -0
  141. package/dist/registry/integrity.d.ts +32 -0
  142. package/dist/registry/integrity.js +92 -0
  143. package/dist/registry/loader.d.ts +50 -0
  144. package/dist/registry/loader.js +145 -0
  145. package/dist/registry/schema.d.ts +47 -0
  146. package/dist/registry/schema.js +185 -0
  147. package/dist/registry/storage.d.ts +37 -0
  148. package/dist/registry/storage.js +101 -0
  149. package/dist/registry/types.d.ts +262 -0
  150. package/dist/registry/types.js +14 -0
  151. package/dist/server-shard/types.d.ts +67 -0
  152. package/dist/server-shard/types.js +13 -0
  153. package/dist/sh3core-shard/ShellHome.svelte +192 -0
  154. package/dist/sh3core-shard/ShellHome.svelte.d.ts +3 -0
  155. package/dist/sh3core-shard/ShellTitle.svelte +171 -0
  156. package/dist/sh3core-shard/ShellTitle.svelte.d.ts +3 -0
  157. package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +2 -0
  158. package/dist/sh3core-shard/sh3coreShard.svelte.js +53 -0
  159. package/dist/shards/activate.svelte.d.ts +52 -0
  160. package/dist/shards/activate.svelte.js +186 -0
  161. package/dist/shards/registry.d.ts +4 -0
  162. package/dist/shards/registry.js +28 -0
  163. package/dist/shards/types.d.ts +207 -0
  164. package/dist/shards/types.js +20 -0
  165. package/dist/shell-shard/InputLine.svelte +133 -0
  166. package/dist/shell-shard/InputLine.svelte.d.ts +11 -0
  167. package/dist/shell-shard/ScrollbackView.svelte +47 -0
  168. package/dist/shell-shard/ScrollbackView.svelte.d.ts +7 -0
  169. package/dist/shell-shard/Terminal.svelte +122 -0
  170. package/dist/shell-shard/Terminal.svelte.d.ts +8 -0
  171. package/dist/shell-shard/entries/PromptEntry.svelte +25 -0
  172. package/dist/shell-shard/entries/PromptEntry.svelte.d.ts +7 -0
  173. package/dist/shell-shard/entries/RichEntry.svelte +19 -0
  174. package/dist/shell-shard/entries/RichEntry.svelte.d.ts +8 -0
  175. package/dist/shell-shard/entries/StatusEntry.svelte +22 -0
  176. package/dist/shell-shard/entries/StatusEntry.svelte.d.ts +7 -0
  177. package/dist/shell-shard/entries/TextEntry.svelte +25 -0
  178. package/dist/shell-shard/entries/TextEntry.svelte.d.ts +7 -0
  179. package/dist/shell-shard/manifest.d.ts +2 -0
  180. package/dist/shell-shard/manifest.js +11 -0
  181. package/dist/shell-shard/protocol.d.ts +90 -0
  182. package/dist/shell-shard/protocol.js +11 -0
  183. package/dist/shell-shard/registry.d.ts +69 -0
  184. package/dist/shell-shard/registry.js +47 -0
  185. package/dist/shell-shard/rich/AppCard.svelte +25 -0
  186. package/dist/shell-shard/rich/AppCard.svelte.d.ts +10 -0
  187. package/dist/shell-shard/rich/AppsTable.svelte +29 -0
  188. package/dist/shell-shard/rich/AppsTable.svelte.d.ts +12 -0
  189. package/dist/shell-shard/rich/EnvTable.svelte +27 -0
  190. package/dist/shell-shard/rich/EnvTable.svelte.d.ts +8 -0
  191. package/dist/shell-shard/rich/HelpTable.svelte +29 -0
  192. package/dist/shell-shard/rich/HelpTable.svelte.d.ts +12 -0
  193. package/dist/shell-shard/rich/HistoryList.svelte +37 -0
  194. package/dist/shell-shard/rich/HistoryList.svelte.d.ts +9 -0
  195. package/dist/shell-shard/rich/ShardsTable.svelte +28 -0
  196. package/dist/shell-shard/rich/ShardsTable.svelte.d.ts +12 -0
  197. package/dist/shell-shard/rich/ViewsTable.svelte +31 -0
  198. package/dist/shell-shard/rich/ViewsTable.svelte.d.ts +13 -0
  199. package/dist/shell-shard/rich/ZoneTree.svelte +19 -0
  200. package/dist/shell-shard/rich/ZoneTree.svelte.d.ts +8 -0
  201. package/dist/shell-shard/rich/ZonesTable.svelte +27 -0
  202. package/dist/shell-shard/rich/ZonesTable.svelte.d.ts +11 -0
  203. package/dist/shell-shard/scrollback.svelte.d.ts +36 -0
  204. package/dist/shell-shard/scrollback.svelte.js +43 -0
  205. package/dist/shell-shard/session-client.svelte.d.ts +23 -0
  206. package/dist/shell-shard/session-client.svelte.js +120 -0
  207. package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
  208. package/dist/shell-shard/shellShard.svelte.js +139 -0
  209. package/dist/shell-shard/verbs/apps.d.ts +3 -0
  210. package/dist/shell-shard/verbs/apps.js +50 -0
  211. package/dist/shell-shard/verbs/clear.d.ts +2 -0
  212. package/dist/shell-shard/verbs/clear.js +7 -0
  213. package/dist/shell-shard/verbs/help.d.ts +2 -0
  214. package/dist/shell-shard/verbs/help.js +21 -0
  215. package/dist/shell-shard/verbs/history.d.ts +2 -0
  216. package/dist/shell-shard/verbs/history.js +20 -0
  217. package/dist/shell-shard/verbs/index.d.ts +2 -0
  218. package/dist/shell-shard/verbs/index.js +29 -0
  219. package/dist/shell-shard/verbs/session.d.ts +5 -0
  220. package/dist/shell-shard/verbs/session.js +65 -0
  221. package/dist/shell-shard/verbs/shards.d.ts +2 -0
  222. package/dist/shell-shard/verbs/shards.js +14 -0
  223. package/dist/shell-shard/verbs/views.d.ts +4 -0
  224. package/dist/shell-shard/verbs/views.js +90 -0
  225. package/dist/shell-shard/verbs/zones.d.ts +3 -0
  226. package/dist/shell-shard/verbs/zones.js +38 -0
  227. package/dist/shellRuntime.svelte.d.ts +27 -0
  228. package/dist/shellRuntime.svelte.js +27 -0
  229. package/dist/state/backends.d.ts +26 -0
  230. package/dist/state/backends.js +99 -0
  231. package/dist/state/manage.d.ts +14 -0
  232. package/dist/state/manage.js +40 -0
  233. package/dist/state/types.d.ts +55 -0
  234. package/dist/state/types.js +17 -0
  235. package/dist/state/zones.svelte.d.ts +53 -0
  236. package/dist/state/zones.svelte.js +141 -0
  237. package/dist/theme.d.ts +28 -0
  238. package/dist/theme.js +92 -0
  239. package/dist/tokens.css +102 -0
  240. package/dist/version.d.ts +2 -0
  241. package/dist/version.js +2 -0
  242. package/package.json +60 -0
@@ -0,0 +1,39 @@
1
+ import type { LayoutNode } from './types';
2
+ import type { App } from '../apps/types';
3
+ /**
4
+ * Attach an app: create or hydrate its workspace-zone layout proxy,
5
+ * enforce the blueprint version gate, and take a refcount hold on all
6
+ * of the app's slot ids so root swaps don't destroy its pooled hosts.
7
+ * Does NOT switch the active root. Call switchToApp() separately.
8
+ */
9
+ export declare function attachApp(app: App): void;
10
+ /**
11
+ * Detach the currently-attached app. Releases its refcount holds; the
12
+ * pool's microtask cleanup drops the pooled hosts if they also have no
13
+ * active renderer refs. Must be called before attaching a different app.
14
+ */
15
+ export declare function detachApp(): void;
16
+ export declare function switchToHome(): void;
17
+ export declare function switchToApp(): void;
18
+ /**
19
+ * The currently-rendered root. LayoutRenderer reads this through the
20
+ * `layoutStore` export below. Home uses the framework constant;
21
+ * app uses the workspace-zone proxy's `root` (which is reactive, so
22
+ * mutations from splitter/drag/ops reach the renderer unchanged).
23
+ */
24
+ export declare function activeLayout(): LayoutNode;
25
+ export declare function getActiveRoot(): 'home' | 'app';
26
+ export declare function getAttachedAppId(): string | null;
27
+ /**
28
+ * Preserved for callers that still read `layoutStore.root`. The getter
29
+ * delegates to `activeLayout()` so every read walks through the
30
+ * manager. Writes to `layoutStore.root` are disallowed (mutation is
31
+ * expected to happen on the returned tree's nodes in place, as in
32
+ * phase 7 — splitter drags mutate `sizes[i]`, tab clicks mutate
33
+ * `activeTab`, drag-commit calls `ops.ts` functions that mutate
34
+ * children arrays). Nothing in the codebase currently reassigns
35
+ * `layoutStore.root`, so this getter-only shape is sufficient.
36
+ */
37
+ export declare const layoutStore: {
38
+ readonly root: LayoutNode;
39
+ };
@@ -0,0 +1,153 @@
1
+ /*
2
+ * Layout manager — owns both the framework-constant shell home layout
3
+ * and the currently-active app's persisted layout, and swaps between
4
+ * them without tearing down the held tree.
5
+ *
6
+ * The manager is the sole owner of "which layout root is being rendered
7
+ * right now". LayoutRenderer reads `layoutStore.root` (a getter on the
8
+ * active tree); drag.svelte.ts and any other mutation site do the same.
9
+ * Neither needs to know whether the active tree is home or an app.
10
+ *
11
+ * Refcount-hold discipline:
12
+ * The slot host pool is refcount-based with a microtask-deferred
13
+ * destroy. A root swap (app → home) causes the app's SlotContainers
14
+ * to unmount and release their pool entries; home's slots have
15
+ * different ids, so nothing re-acquires the app's pool entries before
16
+ * the microtask fires, and the app's views would be destroyed. To
17
+ * prevent this, attaching an app calls `acquireSlotHost` for every
18
+ * slot id in the app's current layout tree once (in addition to what
19
+ * the renderer does when the tree is active). That hold keeps
20
+ * refcount ≥ 1 across swaps. Detaching an app releases the holds.
21
+ *
22
+ * Home does not need a hold — it is either rendered or unmounted
23
+ * entirely (there is no "held home while rendering app" state).
24
+ *
25
+ * Orphan cleanup:
26
+ * The pre-phase-8 shell wrote to `sh3:workspace:__shell__`. Phase 8
27
+ * switches to per-app keys; the old entry would otherwise sit as dead
28
+ * data. On first load after upgrade, the manager clears that orphan
29
+ * unconditionally (clearing a non-existent entry is a no-op).
30
+ */
31
+ import { createStateZones, peekZone, clearZone } from '../state/zones.svelte';
32
+ import { acquireSlotHost, releaseSlotHost } from './slotHostPool.svelte';
33
+ import { collectSlotRefs } from './tree-walk';
34
+ // ---------- orphan cleanup of pre-phase-8 shell layout key ----------------
35
+ // Legacy pre-phase-8 orphan cleanup. The literal '__shell__' here is
36
+ // intentional — it clears data written under the old reserved id before
37
+ // this shard was restructured. Do not replace with '__sh3core__'.
38
+ clearZone('workspace', '__shell__');
39
+ // ---------- home layout (framework constant, in-memory only) --------------
40
+ /**
41
+ * The home layout is a single slot wrapping the sh3core:home view. The
42
+ * slot id is reserved (`sh3core.home`) and stable so the pool entry for
43
+ * home survives across boot/launch cycles.
44
+ */
45
+ const HOME_LAYOUT = {
46
+ type: 'slot',
47
+ slotId: 'sh3core.home',
48
+ viewId: 'sh3core:home',
49
+ };
50
+ let appEntry = $state(null);
51
+ let activeRoot = $state('home');
52
+ // ---------- public (within-framework) API ---------------------------------
53
+ /**
54
+ * Attach an app: create or hydrate its workspace-zone layout proxy,
55
+ * enforce the blueprint version gate, and take a refcount hold on all
56
+ * of the app's slot ids so root swaps don't destroy its pooled hosts.
57
+ * Does NOT switch the active root. Call switchToApp() separately.
58
+ */
59
+ export function attachApp(app) {
60
+ if (appEntry) {
61
+ throw new Error(`Layout manager cannot attach app "${app.manifest.id}": app "${appEntry.appId}" is still attached`);
62
+ }
63
+ const shardId = `__sh3core__:app:${app.manifest.id}`;
64
+ // Version gate: if a stored blob's layoutVersion doesn't match the
65
+ // app's current declaration, discard it so createStateZones falls
66
+ // back to the defaults (the app's initialLayout).
67
+ const stored = peekZone('workspace', shardId);
68
+ if (stored != null) {
69
+ const asBlob = stored;
70
+ if (asBlob.layoutVersion !== app.manifest.layoutVersion) {
71
+ clearZone('workspace', shardId);
72
+ }
73
+ }
74
+ const state = createStateZones(shardId, {
75
+ workspace: {
76
+ layoutVersion: app.manifest.layoutVersion,
77
+ root: app.initialLayout,
78
+ },
79
+ });
80
+ const proxy = state.workspace;
81
+ // Take a refcount hold on every slot in the app's tree. These holds
82
+ // keep the pooled hosts alive across home⇄app swaps. They are
83
+ // acquired without attaching the returned host anywhere — the
84
+ // LayoutRenderer still acquires its own refs when it mounts the
85
+ // tree, so the active rendering doesn't double-hold harmfully (the
86
+ // pool's destroy logic just sees refcount 2, then 1 on release).
87
+ const refs = collectSlotRefs(proxy.root);
88
+ const heldSlotIds = [];
89
+ for (const { slotId, viewId, label } of refs) {
90
+ acquireSlotHost(slotId, viewId, label);
91
+ heldSlotIds.push(slotId);
92
+ }
93
+ appEntry = { appId: app.manifest.id, proxy, heldSlotIds };
94
+ }
95
+ /**
96
+ * Detach the currently-attached app. Releases its refcount holds; the
97
+ * pool's microtask cleanup drops the pooled hosts if they also have no
98
+ * active renderer refs. Must be called before attaching a different app.
99
+ */
100
+ export function detachApp() {
101
+ if (!appEntry)
102
+ return;
103
+ for (const slotId of appEntry.heldSlotIds) {
104
+ releaseSlotHost(slotId);
105
+ }
106
+ appEntry = null;
107
+ // If we detach while the active root is 'app', the renderer now has
108
+ // nothing to show; callers must switchToHome() before or after
109
+ // detachApp. We don't auto-switch here so the ordering is explicit.
110
+ }
111
+ export function switchToHome() {
112
+ activeRoot = 'home';
113
+ }
114
+ export function switchToApp() {
115
+ if (!appEntry) {
116
+ throw new Error('Cannot switchToApp: no app is attached');
117
+ }
118
+ activeRoot = 'app';
119
+ }
120
+ /**
121
+ * The currently-rendered root. LayoutRenderer reads this through the
122
+ * `layoutStore` export below. Home uses the framework constant;
123
+ * app uses the workspace-zone proxy's `root` (which is reactive, so
124
+ * mutations from splitter/drag/ops reach the renderer unchanged).
125
+ */
126
+ export function activeLayout() {
127
+ if (activeRoot === 'app' && appEntry)
128
+ return appEntry.proxy.root;
129
+ return HOME_LAYOUT;
130
+ }
131
+ export function getActiveRoot() {
132
+ return activeRoot;
133
+ }
134
+ export function getAttachedAppId() {
135
+ var _a;
136
+ return (_a = appEntry === null || appEntry === void 0 ? void 0 : appEntry.appId) !== null && _a !== void 0 ? _a : null;
137
+ }
138
+ // ---------- `layoutStore` back-compat shim -------------------------------
139
+ /**
140
+ * Preserved for callers that still read `layoutStore.root`. The getter
141
+ * delegates to `activeLayout()` so every read walks through the
142
+ * manager. Writes to `layoutStore.root` are disallowed (mutation is
143
+ * expected to happen on the returned tree's nodes in place, as in
144
+ * phase 7 — splitter drags mutate `sizes[i]`, tab clicks mutate
145
+ * `activeTab`, drag-commit calls `ops.ts` functions that mutate
146
+ * children arrays). Nothing in the codebase currently reassigns
147
+ * `layoutStore.root`, so this getter-only shape is sufficient.
148
+ */
149
+ export const layoutStore = {
150
+ get root() {
151
+ return activeLayout();
152
+ },
153
+ };
@@ -0,0 +1,15 @@
1
+ import type { LayoutNode } from './types';
2
+ /**
3
+ * Collect the slot id / view id pairs of every slot leaf (including the
4
+ * slots embedded inside tabs entries) in a layout tree. Used by the
5
+ * layout manager to hold pool refs for a non-rendered but still-resident
6
+ * app tree. Order is a pre-order walk; the returned list may contain
7
+ * the same slot id twice if the tree is malformed, but ops.ts maintains
8
+ * the invariant that slot ids are unique so that is not a concern in
9
+ * practice.
10
+ */
11
+ export declare function collectSlotRefs(tree: LayoutNode): {
12
+ slotId: string;
13
+ viewId: string | null;
14
+ label: string;
15
+ }[];
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Layout tree traversal utilities shared between the layout manager
3
+ * and other consumers (refcount-hold, diagnostic inspection).
4
+ */
5
+ /**
6
+ * Collect the slot id / view id pairs of every slot leaf (including the
7
+ * slots embedded inside tabs entries) in a layout tree. Used by the
8
+ * layout manager to hold pool refs for a non-rendered but still-resident
9
+ * app tree. Order is a pre-order walk; the returned list may contain
10
+ * the same slot id twice if the tree is malformed, but ops.ts maintains
11
+ * the invariant that slot ids are unique so that is not a concern in
12
+ * practice.
13
+ */
14
+ export function collectSlotRefs(tree) {
15
+ const out = [];
16
+ const walk = (node) => {
17
+ if (node.type === 'slot') {
18
+ out.push({ slotId: node.slotId, viewId: node.viewId, label: node.viewId || node.slotId });
19
+ return;
20
+ }
21
+ if (node.type === 'tabs') {
22
+ for (const t of node.tabs) {
23
+ out.push({ slotId: t.slotId, viewId: t.viewId, label: t.label });
24
+ }
25
+ return;
26
+ }
27
+ // split
28
+ for (const c of node.children)
29
+ walk(c);
30
+ };
31
+ walk(tree);
32
+ return out;
33
+ }
@@ -0,0 +1,108 @@
1
+ /** Axis along which a split node divides its children. */
2
+ export type SplitDirection = 'horizontal' | 'vertical';
3
+ /** How a child of a split node is sized. */
4
+ export type SizeMode = 'fr' | 'px';
5
+ /**
6
+ * A layout node that divides its area into two or more children along an axis.
7
+ * Children are sized proportionally (`fr`) or pixel-pinned (`px`). Supports
8
+ * per-child collapsed state so panels can be hidden without removing them.
9
+ */
10
+ export interface SplitNode {
11
+ type: 'split';
12
+ /** Axis along which children are arranged. */
13
+ direction: SplitDirection;
14
+ /**
15
+ * Per-child size. Interpretation depends on the parallel `pinned` entry:
16
+ * - 'fr' (default): proportional weight; reflows on resize.
17
+ * - 'px': pixel-pinned; held fixed while 'fr' siblings absorb deltas.
18
+ */
19
+ sizes: number[];
20
+ /** Per-child sizing mode. Omitted entries default to 'fr'. */
21
+ pinned?: SizeMode[];
22
+ /** Per-child collapsed state. Omitted means all expanded. */
23
+ collapsed?: boolean[];
24
+ /** Ordered child nodes. Length must equal `sizes` length. */
25
+ children: LayoutNode[];
26
+ }
27
+ /**
28
+ * A single tab descriptor inside a `TabsNode`. Each entry corresponds to one
29
+ * slot: `slotId` is the stable identifier the framework uses to key persistent
30
+ * state, `viewId` names the shard view to mount (null means the slot is empty).
31
+ */
32
+ export interface TabEntry {
33
+ /** Stable identifier for the slot. Persisted with the layout. */
34
+ slotId: string;
35
+ /** View id to mount into this slot, or null for an empty slot. */
36
+ viewId: string | null;
37
+ /** Human-readable label shown in the tab strip. */
38
+ label: string;
39
+ /** Optional icon hint (not yet rendered in phase 8). */
40
+ icon?: string;
41
+ }
42
+ /**
43
+ * A layout node that groups one or more slots as tabs, showing one at a time.
44
+ * The active tab index drives which slot is visible; the others stay mounted
45
+ * in the background so they survive tab switches without re-initialization.
46
+ */
47
+ export interface TabsNode {
48
+ type: 'tabs';
49
+ tabs: TabEntry[];
50
+ activeTab: number;
51
+ /**
52
+ * If true, the node is not pruned by cleanupTree when its last tab is
53
+ * closed. The layout renderer shows an empty-state placeholder instead.
54
+ * Typically set by app layout blueprints on structural tab groups.
55
+ */
56
+ persistent?: boolean;
57
+ /**
58
+ * Custom renderer for the empty state when all tabs are closed (only
59
+ * meaningful when `persistent` is true). Called with the container
60
+ * element; the app has full control over what's rendered.
61
+ * Runtime-only — not serialized with the layout tree.
62
+ */
63
+ emptyRenderer?: (container: HTMLElement) => void;
64
+ }
65
+ /**
66
+ * A leaf layout node that holds a single mounted view. `slotId` is the stable
67
+ * identifier used to key view state; `viewId` is the shard-registered view to
68
+ * mount. A null `viewId` means the slot is present in the tree but empty.
69
+ */
70
+ export interface SlotNode {
71
+ type: 'slot';
72
+ /** Stable identifier for this slot. Persisted with the layout. */
73
+ slotId: string;
74
+ /** View id to mount into this slot, or null for an empty slot. */
75
+ viewId: string | null;
76
+ }
77
+ /**
78
+ * Union of all layout node kinds. The recursive tree is composed entirely of
79
+ * these three types: `split` → children, `tabs` → slots, `slot` → leaf.
80
+ */
81
+ export type LayoutNode = SplitNode | TabsNode | SlotNode;
82
+ /**
83
+ * Schema version for persisted layouts. Bump this when the shape of
84
+ * `LayoutNode` (or anything reachable from it) changes incompatibly.
85
+ * A stored entry whose version does not match is discarded on boot and
86
+ * the default tree takes over — phase 7 deliberately does not ship a
87
+ * migration framework, only the hook for one.
88
+ */
89
+ export declare const LAYOUT_SCHEMA_VERSION = 3;
90
+ /**
91
+ * The wire shape of a persisted layout in the workspace state zone.
92
+ * One blob per shell (or per program, once per-program layouts exist);
93
+ * the version field gates compatibility.
94
+ */
95
+ export interface PersistedLayout {
96
+ version: typeof LAYOUT_SCHEMA_VERSION;
97
+ root: LayoutNode;
98
+ }
99
+ /**
100
+ * Per-app layout blob written to the workspace state zone under
101
+ * `__sh3core__:app:<appId>`. The `layoutVersion` is the app's own
102
+ * `AppManifest.layoutVersion`; on launch a mismatch discards the blob
103
+ * and the app's `initialLayout` is used.
104
+ */
105
+ export interface AppLayoutBlob {
106
+ layoutVersion: number;
107
+ root: LayoutNode;
108
+ }
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Layout tree types — the single source of truth for docked layout topology.
3
+ *
4
+ * See docs/design/layout.md for rationale. The tree is recursive with three
5
+ * node kinds:
6
+ *
7
+ * - split: horizontal or vertical, with proportional sizes and optional
8
+ * per-child pixel pinning.
9
+ * - tabs: a tab group with one active tab at a time. Each tab owns a slot.
10
+ * - slot: leaf. Carries a stable slotId so state can be keyed to it, and
11
+ * a viewId that names the view a shard should mount into it.
12
+ * viewId is null for empty slots.
13
+ *
14
+ * Views are mounted into slots by the shard registry (phase 4). Trees are
15
+ * serialized to the workspace state zone for persistence (phase 7) — see
16
+ * `PersistedLayout` at the bottom of this file.
17
+ */
18
+ /**
19
+ * Schema version for persisted layouts. Bump this when the shape of
20
+ * `LayoutNode` (or anything reachable from it) changes incompatibly.
21
+ * A stored entry whose version does not match is discarded on boot and
22
+ * the default tree takes over — phase 7 deliberately does not ship a
23
+ * migration framework, only the hook for one.
24
+ */
25
+ export const LAYOUT_SCHEMA_VERSION = 3;
@@ -0,0 +1,16 @@
1
+ export interface WorkspaceZoneStore {
2
+ keys(): string[];
3
+ read(key: string): unknown;
4
+ write(key: string, value: unknown): void;
5
+ delete(key: string): void;
6
+ }
7
+ /**
8
+ * Run the `__shell__` → `__sh3core__` rename migration. Call once at boot,
9
+ * before any shard activates. If the flag is already set, returns immediately.
10
+ *
11
+ * @param zone An adapter around the workspace state-zone backend used for
12
+ * iterating and rewriting persisted shard-prefixed keys.
13
+ * @param storage The localStorage-like object (pass `globalThis.localStorage`
14
+ * in the browser; pass a mock in tests).
15
+ */
16
+ export declare function runShellRenameMigration(zone: WorkspaceZoneStore, storage: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>): void;
@@ -0,0 +1,48 @@
1
+ /*
2
+ * One-time migration of persisted keys that were written under the former
3
+ * `__shell__` pseudo-shard id. Runs once per browser, gated by a
4
+ * localStorage flag. Idempotent — safe to call on every boot.
5
+ *
6
+ * Scope:
7
+ * 1. localStorage: sh3:user:__shell__:theme → sh3:user:__sh3core__:theme
8
+ * 2. workspace state zone: every key beginning with `__shell__:`
9
+ * (includes `__shell__:last-app` and `__shell__:app:<appId>` entries)
10
+ * is copied to `__sh3core__:<rest>` and the old key is deleted.
11
+ *
12
+ * See docs/superpowers/specs/2026-04-10-shell-shard-design.md § Step 0.
13
+ */
14
+ const FLAG_KEY = 'sh3:migrations:shell-rename:done';
15
+ const OLD_PREFIX = '__shell__:';
16
+ const NEW_PREFIX = '__sh3core__:';
17
+ const OLD_THEME_KEY = 'sh3:user:__shell__:theme';
18
+ const NEW_THEME_KEY = 'sh3:user:__sh3core__:theme';
19
+ /**
20
+ * Run the `__shell__` → `__sh3core__` rename migration. Call once at boot,
21
+ * before any shard activates. If the flag is already set, returns immediately.
22
+ *
23
+ * @param zone An adapter around the workspace state-zone backend used for
24
+ * iterating and rewriting persisted shard-prefixed keys.
25
+ * @param storage The localStorage-like object (pass `globalThis.localStorage`
26
+ * in the browser; pass a mock in tests).
27
+ */
28
+ export function runShellRenameMigration(zone, storage) {
29
+ if (storage.getItem(FLAG_KEY)) {
30
+ return;
31
+ }
32
+ // 1. Migrate localStorage theme key
33
+ const theme = storage.getItem(OLD_THEME_KEY);
34
+ if (theme !== null && storage.getItem(NEW_THEME_KEY) === null) {
35
+ storage.setItem(NEW_THEME_KEY, theme);
36
+ storage.removeItem(OLD_THEME_KEY);
37
+ }
38
+ // 2. Migrate workspace state zone keys with the old prefix
39
+ for (const key of zone.keys()) {
40
+ if (!key.startsWith(OLD_PREFIX))
41
+ continue;
42
+ const newKey = NEW_PREFIX + key.slice(OLD_PREFIX.length);
43
+ const value = zone.read(key);
44
+ zone.write(newKey, value);
45
+ zone.delete(key);
46
+ }
47
+ storage.setItem(FLAG_KEY, '1');
48
+ }
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ /*
3
+ * ModalFrame — the internal wrapper that the modal manager mounts into
4
+ * a per-modal host div inside the layer-4 root.
5
+ *
6
+ * Responsibilities:
7
+ * - Span the full layer as a transparent centering container so every
8
+ * modal box renders in the middle of the shell.
9
+ * - Catch pointer events on the area around the box so clicks outside
10
+ * the top modal do not fall through to modals beneath or to the
11
+ * underlying layout.
12
+ * - Dynamically render the caller's content component with its props.
13
+ * - Install a focus trap on the dialog box for as long as the frame
14
+ * lives, restoring previous focus on teardown.
15
+ *
16
+ * The frame does NOT render a backdrop. The modal manager owns a single
17
+ * shared backdrop element under the layer-4 root whose opacity scales
18
+ * (capped) with stack depth — otherwise per-frame backdrops compound
19
+ * into full black once a few modals are stacked, and every layer also
20
+ * pays its own composite cost.
21
+ *
22
+ * Escape-to-close and backdrop-click policy are decided by the manager,
23
+ * not the frame — the manager holds the stack and knows which modal is
24
+ * on top. The frame just receives a `close` callback and invokes it if
25
+ * the caller wants a built-in close button (there is none by default —
26
+ * content owns its chrome).
27
+ */
28
+
29
+ import type { Component } from 'svelte';
30
+ import { createFocusTrap } from './focusTrap';
31
+
32
+ let {
33
+ Content,
34
+ contentProps,
35
+ close,
36
+ boxStyle,
37
+ }: {
38
+ Content: Component<Record<string, unknown>>;
39
+ contentProps: Record<string, unknown>;
40
+ close: () => void;
41
+ boxStyle?: string;
42
+ } = $props();
43
+
44
+ let box: HTMLDivElement;
45
+
46
+ $effect(() => {
47
+ if (!box) return;
48
+ return createFocusTrap(box);
49
+ });
50
+ </script>
51
+
52
+ <div class="modal-frame" role="presentation">
53
+ <div
54
+ class="modal-box"
55
+ role="dialog"
56
+ aria-modal="true"
57
+ tabindex="-1"
58
+ bind:this={box}
59
+ style={boxStyle}
60
+ >
61
+ <Content {...contentProps} {close} />
62
+ </div>
63
+ </div>
64
+
65
+ <style>
66
+ .modal-frame {
67
+ position: absolute;
68
+ inset: 0;
69
+ display: grid;
70
+ place-items: center;
71
+ /* Transparent but pointer-capturing so clicks around the box are
72
+ swallowed rather than falling through to modals beneath. */
73
+ pointer-events: auto;
74
+ }
75
+ .modal-box {
76
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
77
+ color: var(--shell-fg);
78
+ border: 1px solid var(--shell-border-strong);
79
+ border-radius: var(--shell-radius);
80
+ min-width: 320px;
81
+ max-width: min(640px, 90vw);
82
+ max-height: 90vh;
83
+ overflow: auto;
84
+ box-shadow: 0 20px 48px rgba(0, 0, 0, 0.5);
85
+ outline: none;
86
+ }
87
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Component } from 'svelte';
2
+ type $$ComponentProps = {
3
+ Content: Component<Record<string, unknown>>;
4
+ contentProps: Record<string, unknown>;
5
+ close: () => void;
6
+ boxStyle?: string;
7
+ };
8
+ declare const ModalFrame: Component<$$ComponentProps, {}, "">;
9
+ type ModalFrame = ReturnType<typeof ModalFrame>;
10
+ export default ModalFrame;
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ /*
3
+ * PopupFrame — positioned wrapper for a popup's content component.
4
+ *
5
+ * Takes an anchor rect in viewport coordinates and places itself at
6
+ * bottom-start with a viewport overflow clamp. The measurement happens
7
+ * after mount via an $effect so we can read the real frame size; an
8
+ * initial render off-screen hides the flicker while we measure.
9
+ *
10
+ * Positioning policy for phase 5:
11
+ * - Preferred: bottom-start (top = anchor.bottom + 4, left = anchor.left)
12
+ * - If the frame's right edge would exit the viewport, shift left so it
13
+ * sits flush with the right edge minus a small margin.
14
+ * - If the frame's bottom edge would exit the viewport, flip to above
15
+ * the anchor (top = anchor.top - frameHeight - 4).
16
+ * - If it still doesn't fit, clamp to the viewport.
17
+ *
18
+ * Additional placements (right-start, left-start, etc.) arrive when a
19
+ * real shard needs them; phase 5 doesn't.
20
+ */
21
+
22
+ import type { Component } from 'svelte';
23
+
24
+ let {
25
+ Content,
26
+ contentProps,
27
+ anchorRect,
28
+ close,
29
+ }: {
30
+ Content: Component<Record<string, unknown>>;
31
+ contentProps: Record<string, unknown>;
32
+ anchorRect: DOMRect;
33
+ close: () => void;
34
+ } = $props();
35
+
36
+ let frame: HTMLDivElement;
37
+ let top = $state(-9999);
38
+ let left = $state(-9999);
39
+
40
+ $effect(() => {
41
+ if (!frame) return;
42
+ const rect = frame.getBoundingClientRect();
43
+ const margin = 4;
44
+ const vw = window.innerWidth;
45
+ const vh = window.innerHeight;
46
+
47
+ let t = anchorRect.bottom + margin;
48
+ let l = anchorRect.left;
49
+
50
+ if (l + rect.width > vw - margin) {
51
+ l = Math.max(margin, vw - rect.width - margin);
52
+ }
53
+ if (t + rect.height > vh - margin) {
54
+ const flipped = anchorRect.top - rect.height - margin;
55
+ t = flipped >= margin ? flipped : Math.max(margin, vh - rect.height - margin);
56
+ }
57
+
58
+ top = t;
59
+ left = l;
60
+ });
61
+ </script>
62
+
63
+ <div
64
+ class="popup-frame"
65
+ role="menu"
66
+ tabindex="-1"
67
+ style="top: {top}px; left: {left}px;"
68
+ bind:this={frame}
69
+ >
70
+ <Content {...contentProps} {close} />
71
+ </div>
72
+
73
+ <style>
74
+ .popup-frame {
75
+ position: absolute;
76
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
77
+ color: var(--shell-fg);
78
+ border: 1px solid var(--shell-border-strong);
79
+ border-radius: var(--shell-radius-sm);
80
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
81
+ min-width: 120px;
82
+ outline: none;
83
+ pointer-events: auto;
84
+ }
85
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Component } from 'svelte';
2
+ type $$ComponentProps = {
3
+ Content: Component<Record<string, unknown>>;
4
+ contentProps: Record<string, unknown>;
5
+ anchorRect: DOMRect;
6
+ close: () => void;
7
+ };
8
+ declare const PopupFrame: Component<$$ComponentProps, {}, "">;
9
+ type PopupFrame = ReturnType<typeof PopupFrame>;
10
+ export default PopupFrame;