sh3-core 0.1.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 (134) hide show
  1. package/dist/Shell.svelte +185 -0
  2. package/dist/Shell.svelte.d.ts +4 -0
  3. package/dist/api.d.ts +22 -0
  4. package/dist/api.js +45 -0
  5. package/dist/apps/lifecycle.d.ts +37 -0
  6. package/dist/apps/lifecycle.js +153 -0
  7. package/dist/apps/registry.svelte.d.ts +37 -0
  8. package/dist/apps/registry.svelte.js +60 -0
  9. package/dist/apps/types.d.ts +61 -0
  10. package/dist/apps/types.js +10 -0
  11. package/dist/assets/icons.svg +1119 -0
  12. package/dist/auth/auth.svelte.d.ts +44 -0
  13. package/dist/auth/auth.svelte.js +119 -0
  14. package/dist/auth/index.d.ts +1 -0
  15. package/dist/auth/index.js +1 -0
  16. package/dist/build.d.ts +29 -0
  17. package/dist/build.js +85 -0
  18. package/dist/contract.d.ts +20 -0
  19. package/dist/contract.js +28 -0
  20. package/dist/documents/backends.d.ts +17 -0
  21. package/dist/documents/backends.js +156 -0
  22. package/dist/documents/config.d.ts +7 -0
  23. package/dist/documents/config.js +27 -0
  24. package/dist/documents/handle.d.ts +6 -0
  25. package/dist/documents/handle.js +154 -0
  26. package/dist/documents/http-backend.d.ts +22 -0
  27. package/dist/documents/http-backend.js +78 -0
  28. package/dist/documents/index.d.ts +6 -0
  29. package/dist/documents/index.js +8 -0
  30. package/dist/documents/notifications.d.ts +9 -0
  31. package/dist/documents/notifications.js +39 -0
  32. package/dist/documents/types.d.ts +97 -0
  33. package/dist/documents/types.js +12 -0
  34. package/dist/host-entry.d.ts +9 -0
  35. package/dist/host-entry.js +15 -0
  36. package/dist/host.d.ts +13 -0
  37. package/dist/host.js +73 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +13 -0
  40. package/dist/layout/DragPreview.svelte +63 -0
  41. package/dist/layout/DragPreview.svelte.d.ts +3 -0
  42. package/dist/layout/LayoutRenderer.svelte +260 -0
  43. package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
  44. package/dist/layout/SlotContainer.svelte +140 -0
  45. package/dist/layout/SlotContainer.svelte.d.ts +8 -0
  46. package/dist/layout/SlotDropZone.svelte +122 -0
  47. package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
  48. package/dist/layout/drag.svelte.d.ts +45 -0
  49. package/dist/layout/drag.svelte.js +191 -0
  50. package/dist/layout/inspection.d.ts +52 -0
  51. package/dist/layout/inspection.js +157 -0
  52. package/dist/layout/ops.d.ts +78 -0
  53. package/dist/layout/ops.js +281 -0
  54. package/dist/layout/slotHostPool.svelte.d.ts +36 -0
  55. package/dist/layout/slotHostPool.svelte.js +229 -0
  56. package/dist/layout/store.svelte.d.ts +39 -0
  57. package/dist/layout/store.svelte.js +150 -0
  58. package/dist/layout/tree-walk.d.ts +15 -0
  59. package/dist/layout/tree-walk.js +33 -0
  60. package/dist/layout/types.d.ts +108 -0
  61. package/dist/layout/types.js +25 -0
  62. package/dist/overlays/ModalFrame.svelte +87 -0
  63. package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
  64. package/dist/overlays/PopupFrame.svelte +85 -0
  65. package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
  66. package/dist/overlays/ToastItem.svelte +77 -0
  67. package/dist/overlays/ToastItem.svelte.d.ts +9 -0
  68. package/dist/overlays/focusTrap.d.ts +1 -0
  69. package/dist/overlays/focusTrap.js +64 -0
  70. package/dist/overlays/modal.d.ts +9 -0
  71. package/dist/overlays/modal.js +141 -0
  72. package/dist/overlays/popup.d.ts +9 -0
  73. package/dist/overlays/popup.js +108 -0
  74. package/dist/overlays/roots.d.ts +4 -0
  75. package/dist/overlays/roots.js +31 -0
  76. package/dist/overlays/toast.d.ts +6 -0
  77. package/dist/overlays/toast.js +93 -0
  78. package/dist/overlays/types.d.ts +31 -0
  79. package/dist/overlays/types.js +15 -0
  80. package/dist/primitives/.gitkeep +0 -0
  81. package/dist/primitives/ResizableSplitter.svelte +333 -0
  82. package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
  83. package/dist/primitives/TabbedPanel.svelte +305 -0
  84. package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
  85. package/dist/registry/client.d.ts +74 -0
  86. package/dist/registry/client.js +118 -0
  87. package/dist/registry/index.d.ts +13 -0
  88. package/dist/registry/index.js +14 -0
  89. package/dist/registry/installer.d.ts +53 -0
  90. package/dist/registry/installer.js +170 -0
  91. package/dist/registry/integrity.d.ts +32 -0
  92. package/dist/registry/integrity.js +92 -0
  93. package/dist/registry/loader.d.ts +50 -0
  94. package/dist/registry/loader.js +145 -0
  95. package/dist/registry/schema.d.ts +47 -0
  96. package/dist/registry/schema.js +180 -0
  97. package/dist/registry/storage.d.ts +37 -0
  98. package/dist/registry/storage.js +101 -0
  99. package/dist/registry/types.d.ts +245 -0
  100. package/dist/registry/types.js +14 -0
  101. package/dist/registry-shard/RegistryView.svelte +561 -0
  102. package/dist/registry-shard/RegistryView.svelte.d.ts +3 -0
  103. package/dist/registry-shard/registryApp.d.ts +10 -0
  104. package/dist/registry-shard/registryApp.js +24 -0
  105. package/dist/registry-shard/registryShard.svelte.d.ts +45 -0
  106. package/dist/registry-shard/registryShard.svelte.js +125 -0
  107. package/dist/shards/activate.svelte.d.ts +45 -0
  108. package/dist/shards/activate.svelte.js +124 -0
  109. package/dist/shards/registry.d.ts +4 -0
  110. package/dist/shards/registry.js +28 -0
  111. package/dist/shards/types.d.ts +155 -0
  112. package/dist/shards/types.js +20 -0
  113. package/dist/shell-shard/ShellHome.svelte +285 -0
  114. package/dist/shell-shard/ShellHome.svelte.d.ts +3 -0
  115. package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
  116. package/dist/shell-shard/shellShard.svelte.js +47 -0
  117. package/dist/shellRuntime.svelte.d.ts +27 -0
  118. package/dist/shellRuntime.svelte.js +27 -0
  119. package/dist/state/backends.d.ts +26 -0
  120. package/dist/state/backends.js +99 -0
  121. package/dist/state/types.d.ts +38 -0
  122. package/dist/state/types.js +15 -0
  123. package/dist/state/zones.svelte.d.ts +52 -0
  124. package/dist/state/zones.svelte.js +141 -0
  125. package/dist/store/InstalledView.svelte +201 -0
  126. package/dist/store/InstalledView.svelte.d.ts +3 -0
  127. package/dist/store/StoreView.svelte +470 -0
  128. package/dist/store/StoreView.svelte.d.ts +3 -0
  129. package/dist/store/storeApp.d.ts +11 -0
  130. package/dist/store/storeApp.js +26 -0
  131. package/dist/store/storeShard.svelte.d.ts +29 -0
  132. package/dist/store/storeShard.svelte.js +99 -0
  133. package/dist/tokens.css +79 -0
  134. package/package.json +50 -0
@@ -0,0 +1,185 @@
1
+ <script lang="ts">
2
+ /*
3
+ * <Shell> — top-level chrome component.
4
+ *
5
+ * Owns the tab bar, status bar, docked content area (layer 0), and the
6
+ * six overlay roots (layers 1-6). This is a stub implementation for
7
+ * phase 1: the content area is empty and the overlay roots are present
8
+ * but unmanaged. Layout tree rendering arrives in phase 2; overlay layer
9
+ * managers arrive in phase 5.
10
+ *
11
+ * The six overlay roots are mounted here (not lazily) so that layer
12
+ * managers in later phases have stable DOM targets to portal into.
13
+ */
14
+
15
+ import './tokens.css';
16
+ import LayoutRenderer from './layout/LayoutRenderer.svelte';
17
+ import DragPreview from './layout/DragPreview.svelte';
18
+ import type { OverlayLayer } from './overlays/types';
19
+ import { registerLayerRoot, unregisterLayerRoot } from './overlays/roots';
20
+ import { returnToHome, getActiveApp } from './api';
21
+ import iconsUrl from './assets/icons.svg';
22
+
23
+ // Layer metadata — order matches the stack in docs/design/layout.md.
24
+ // Index 0 here is layer 1 (floating panels); layer 0 is the content area.
25
+ const overlayLayers: { layer: number; name: OverlayLayer }[] = [
26
+ { layer: 1, name: 'floating' },
27
+ { layer: 2, name: 'drag-preview' },
28
+ { layer: 3, name: 'popup' },
29
+ { layer: 4, name: 'modal' },
30
+ { layer: 5, name: 'toast' },
31
+ { layer: 6, name: 'command' },
32
+ ];
33
+
34
+ // Populated by bind:this during render; registered with the overlay
35
+ // module via $effect after mount so layer managers (shell.modal,
36
+ // shell.popup, shell.toast) can find their target DOM roots.
37
+ const overlayRoots: Partial<Record<OverlayLayer, HTMLDivElement>> = $state({});
38
+
39
+ $effect(() => {
40
+ for (const { name } of overlayLayers) {
41
+ const el = overlayRoots[name];
42
+ if (el) registerLayerRoot(name, el);
43
+ }
44
+ return () => {
45
+ for (const { name } of overlayLayers) unregisterLayerRoot(name);
46
+ };
47
+ });
48
+ </script>
49
+
50
+ <div class="shell">
51
+ <header class="shell-tabbar" data-shell-region="tabbar">
52
+ <span class="shell-tabbar-brand">SH3</span>
53
+ {#if getActiveApp()}
54
+ <button
55
+ type="button"
56
+ class="shell-tabbar-home-button"
57
+ onclick={() => returnToHome()}
58
+ title="Home"
59
+ >
60
+ <svg class="shell-tabbar-home-icon" aria-hidden="true">
61
+ <use href="{iconsUrl}#house" />
62
+ </svg>
63
+ </button>
64
+ {/if}
65
+ </header>
66
+
67
+ <main class="shell-content" data-shell-region="content" data-shell-layer="0">
68
+ <LayoutRenderer />
69
+ </main>
70
+
71
+ <footer class="shell-statusbar" data-shell-region="statusbar">
72
+ <span class="shell-statusbar-alpha">alpha</span>
73
+ </footer>
74
+
75
+ <!--
76
+ Overlay roots. Each is absolutely positioned over the entire shell with
77
+ pointer-events: none by default; layer managers enable pointer events on
78
+ the specific surfaces they portal in.
79
+ -->
80
+ <div class="shell-overlays" aria-hidden="true">
81
+ {#each overlayLayers as { layer, name } (layer)}
82
+ <div
83
+ class="shell-overlay-root"
84
+ data-shell-overlay={name}
85
+ data-shell-layer={layer}
86
+ style="z-index: var(--shell-z-layer-{layer});"
87
+ bind:this={overlayRoots[name]}
88
+ >
89
+ {#if name === 'drag-preview'}
90
+ <DragPreview />
91
+ {/if}
92
+ </div>
93
+ {/each}
94
+ </div>
95
+ </div>
96
+
97
+ <style>
98
+ .shell {
99
+ display: grid;
100
+ grid-template-rows: var(--shell-tabbar-height) 1fr var(--shell-statusbar-height);
101
+ height: 100%;
102
+ width: 100%;
103
+ position: relative;
104
+ background: var(--shell-bg);
105
+ color: var(--shell-fg);
106
+ }
107
+
108
+ .shell-tabbar {
109
+ display: flex;
110
+ align-items: center;
111
+ gap: var(--shell-pad-md);
112
+ padding: 0 var(--shell-pad-md);
113
+ background: var(--shell-bg-elevated);
114
+ border-bottom: 1px solid var(--shell-border);
115
+ user-select: none;
116
+ }
117
+ .shell-tabbar-brand {
118
+ font-weight: 600;
119
+ color: var(--shell-accent);
120
+ letter-spacing: 0.5px;
121
+ }
122
+
123
+ .shell-content {
124
+ position: relative;
125
+ overflow: hidden;
126
+ background: var(--shell-bg);
127
+ min-width: 0;
128
+ min-height: 0;
129
+ }
130
+ .shell-statusbar {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: space-between;
134
+ padding: 0 var(--shell-pad-md);
135
+ background: var(--shell-bg-sunken);
136
+ border-top: 1px solid var(--shell-border);
137
+ color: var(--shell-fg-muted);
138
+ font-size: 11px;
139
+ user-select: none;
140
+ }
141
+
142
+ .shell-overlays {
143
+ position: absolute;
144
+ inset: 0;
145
+ pointer-events: none;
146
+ }
147
+ .shell-overlay-root {
148
+ position: absolute;
149
+ inset: 0;
150
+ pointer-events: none;
151
+ }
152
+
153
+ .shell-tabbar-home-button {
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ width: 24px;
158
+ height: 24px;
159
+ padding: 0;
160
+ background: transparent;
161
+ color: var(--shell-fg-muted);
162
+ border: 1px solid var(--shell-border);
163
+ border-radius: 4px;
164
+ cursor: pointer;
165
+ }
166
+ .shell-tabbar-home-button:hover {
167
+ color: var(--shell-fg);
168
+ border-color: var(--shell-fg-muted);
169
+ }
170
+ .shell-tabbar-home-icon {
171
+ width: 14px;
172
+ height: 14px;
173
+ }
174
+
175
+ .shell-statusbar-alpha {
176
+ font-size: 9px;
177
+ font-weight: 700;
178
+ text-transform: uppercase;
179
+ letter-spacing: 0.08em;
180
+ color: #fff;
181
+ background: var(--shell-accent);
182
+ padding: 1px 6px;
183
+ border-radius: 8px;
184
+ }
185
+ </style>
@@ -0,0 +1,4 @@
1
+ import './tokens.css';
2
+ declare const Shell: import("svelte").Component<Record<string, never>, {}, "">;
3
+ type Shell = ReturnType<typeof Shell>;
4
+ export default Shell;
package/dist/api.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export { shell } from './shellRuntime.svelte';
2
+ export type { Shell } from './shellRuntime.svelte';
3
+ export type { Shard, ShardManifest, ShardContext, ViewDeclaration, ViewFactory, ViewHandle, MountContext, } from './shards/types';
4
+ export type { LayoutNode, SplitNode, TabsNode, SlotNode, TabEntry, SplitDirection, SizeMode, } from './layout/types';
5
+ export type { ZoneSchema, ZoneName } from './state/types';
6
+ export type { StateZones } from './state/zones.svelte';
7
+ export type { App, AppManifest, AppContext } from './apps/types';
8
+ export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
9
+ export { launchApp, returnToHome } from './apps/lifecycle';
10
+ export { inspectActiveLayout, spliceIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, } from './layout/inspection';
11
+ export type { DocumentHandle, DocumentHandleOptions, DocumentFormat, DocumentMeta, DocumentChange, AutosaveController, } from './documents/types';
12
+ export { registeredShards, activeShards } from './shards/activate.svelte';
13
+ export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, } from './registry/types';
14
+ export type { ResolvedPackage } from './registry/client';
15
+ export { fetchRegistries, fetchBundle, buildPackageMeta } from './registry/client';
16
+ export { validateRegistryIndex } from './registry/schema';
17
+ export { isAdmin, getAuthHeader } from './auth/index';
18
+ /** Runtime feature flags for target-dependent behavior. */
19
+ export declare const capabilities: {
20
+ /** Whether this target supports hot-installing packages via dynamic import from blob URL. */
21
+ readonly hotInstall: boolean;
22
+ };
package/dist/api.js ADDED
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Public framework API — the single import path shards and apps are
3
+ * allowed to touch. The phase 9 package boundary turns this file (and
4
+ * only this file) into the published entry point.
5
+ *
6
+ * What belongs here:
7
+ * - Types that shards and apps consume (Shard, App, ShardContext, etc.)
8
+ * - The `shell` runtime singleton (modal/popup/toast)
9
+ * - Layout inspection/mutation helpers for advanced shards (added in
10
+ * Task 12)
11
+ * - Host actions callable from inside views (launchApp, returnToHome,
12
+ * listRegisteredApps — added in Task 7 and Task 10)
13
+ *
14
+ * What does NOT belong here:
15
+ * - `registerShard` / `registerApp` — those are host-only and live in
16
+ * `host.ts`. Shards and apps must not register each other.
17
+ * - Framework internals (state zone plumbing, layout manager internals,
18
+ * shard activation machinery).
19
+ *
20
+ * Anything re-exported from this file is part of the public contract.
21
+ * Phase 10 adds a validator that enforces the import-hygiene rule; for
22
+ * now the convention is documented and honored by hand.
23
+ */
24
+ // Runtime singleton.
25
+ export { shell } from './shellRuntime.svelte';
26
+ // Host actions callable from inside views (shell home, status bar, etc.).
27
+ export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
28
+ export { launchApp, returnToHome } from './apps/lifecycle';
29
+ // Layout inspection / mutation for advanced shards (diagnostic, etc.).
30
+ export { inspectActiveLayout, spliceIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, } from './layout/inspection';
31
+ // Shard introspection — read-only reactive maps exposing which shards are
32
+ // known to the host and which are currently active. Intended for diagnostic
33
+ // and tooling shards that need to visualize framework state. Phase 9
34
+ // addition: diagnostic used to reach `activate.svelte` directly via $lib;
35
+ // the package boundary requires routing through the public surface.
36
+ export { registeredShards, activeShards } from './shards/activate.svelte';
37
+ export { fetchRegistries, fetchBundle, buildPackageMeta } from './registry/client';
38
+ export { validateRegistryIndex } from './registry/schema';
39
+ // Admin mode (framework-internal components read admin status).
40
+ export { isAdmin, getAuthHeader } from './auth/index';
41
+ /** Runtime feature flags for target-dependent behavior. */
42
+ export const capabilities = {
43
+ /** Whether this target supports hot-installing packages via dynamic import from blob URL. */
44
+ hotInstall: typeof Blob !== 'undefined' && typeof URL.createObjectURL === 'function',
45
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Read the id of the last-launched app from the user state zone. Used at
3
+ * boot to decide whether to auto-launch. Returns null if the user last
4
+ * returned to home or no app has ever been launched.
5
+ */
6
+ export declare function readLastApp(): string | null;
7
+ /**
8
+ * Launch an app by id. Activates all required shards (idempotent for
9
+ * already-active shards), attaches the app's layout, calls `App.activate`,
10
+ * and switches the rendered root to the app tree.
11
+ *
12
+ * If a different app is already active, it is unloaded first. Launching the
13
+ * same app that is already active is a no-op on shards — it only switches
14
+ * back from the home view if needed.
15
+ *
16
+ * @param id - The `AppManifest.id` of the app to launch. Must be registered.
17
+ * @throws If the app is not registered or a required shard is not registered.
18
+ */
19
+ export declare function launchApp(id: string): Promise<void>;
20
+ /**
21
+ * Unload an active app. Calls `App.deactivate`, detaches the layout, and
22
+ * deactivates the app's non-self-starting required shards. Switches the
23
+ * rendered root to home. No-op if the app is not currently active.
24
+ *
25
+ * @param id - The `AppManifest.id` of the app to unload.
26
+ */
27
+ export declare function unloadApp(id: string): void;
28
+ /**
29
+ * Return to the shell home view without unloading the active app. The
30
+ * app's shards stay running, its layout proxy stays attached with its
31
+ * refcount hold intact, and its view containers stay alive in the pool.
32
+ * Launching the same app again is a root swap only.
33
+ *
34
+ * Writes `null` to `__shell__:last-app` so reloading the page while on
35
+ * home lands on home, not on the formerly-active app.
36
+ */
37
+ export declare function returnToHome(): void;
@@ -0,0 +1,153 @@
1
+ /*
2
+ * App lifecycle — launch, unload, return-to-home.
3
+ *
4
+ * All three functions operate on the shared apps registry and the layout
5
+ * manager. `launchApp` is public (shell home calls it); `unloadApp` is
6
+ * internal (called from launchApp when replacing a different app);
7
+ * `returnToHome` is public (shell UI calls it).
8
+ *
9
+ * "Last active app" is persisted in the user state zone under the
10
+ * reserved shardId `__shell__:last-app`. The value shape is simply
11
+ * `{ id: string | null }`. Writing happens on launch (id) and on
12
+ * return-to-home (null). Boot reads it to decide whether to auto-launch.
13
+ */
14
+ import { createStateZones } from '../state/zones.svelte';
15
+ import { activateShard, deactivateShard, registeredShards, } from '../shards/activate.svelte';
16
+ import { attachApp, detachApp, switchToApp, switchToHome, } from '../layout/store.svelte';
17
+ import { activeApp, getRegisteredApp } from './registry.svelte';
18
+ // ---------- last-active-app user zone ------------------------------------
19
+ /**
20
+ * Framework-reserved user-zone slot storing which app to boot into on
21
+ * the next session. Keyed under `__shell__:last-app` to avoid collision
22
+ * with any real shard. Reading/writing uses the same zones machinery as
23
+ * any shard — nothing special.
24
+ */
25
+ const lastAppState = createStateZones('__shell__:last-app', {
26
+ user: { id: null },
27
+ });
28
+ /**
29
+ * Read the id of the last-launched app from the user state zone. Used at
30
+ * boot to decide whether to auto-launch. Returns null if the user last
31
+ * returned to home or no app has ever been launched.
32
+ */
33
+ export function readLastApp() {
34
+ return lastAppState.user.id;
35
+ }
36
+ function writeLastApp(id) {
37
+ lastAppState.user.id = id;
38
+ }
39
+ // ---------- app-context state factories ----------------------------------
40
+ const appContexts = new Map();
41
+ function getOrCreateAppContext(appId) {
42
+ let ctx = appContexts.get(appId);
43
+ if (!ctx) {
44
+ ctx = {
45
+ state: (schema) => createStateZones(`__app__:${appId}`, schema),
46
+ };
47
+ appContexts.set(appId, ctx);
48
+ }
49
+ return ctx;
50
+ }
51
+ // ---------- launch --------------------------------------------------------
52
+ /**
53
+ * Launch an app by id. Activates all required shards (idempotent for
54
+ * already-active shards), attaches the app's layout, calls `App.activate`,
55
+ * and switches the rendered root to the app tree.
56
+ *
57
+ * If a different app is already active, it is unloaded first. Launching the
58
+ * same app that is already active is a no-op on shards — it only switches
59
+ * back from the home view if needed.
60
+ *
61
+ * @param id - The `AppManifest.id` of the app to launch. Must be registered.
62
+ * @throws If the app is not registered or a required shard is not registered.
63
+ */
64
+ export async function launchApp(id) {
65
+ var _a;
66
+ const app = getRegisteredApp(id);
67
+ if (!app) {
68
+ throw new Error(`Cannot launch app "${id}": not registered`);
69
+ }
70
+ // If a different app is already active, unload it first. Relaunching
71
+ // the same app (while it's active) is a no-op on shards and layout;
72
+ // we only swap the rendered root back to 'app' in case the user was
73
+ // on home.
74
+ if (activeApp.id && activeApp.id !== id) {
75
+ unloadApp(activeApp.id);
76
+ }
77
+ else if (activeApp.id === id) {
78
+ switchToApp();
79
+ writeLastApp(id);
80
+ return;
81
+ }
82
+ // Activate every required shard. `activateShard` is idempotent for
83
+ // already-active shards (self-starting shards, or shards shared with
84
+ // a previous app that stayed resident) so this is safe to call
85
+ // unconditionally.
86
+ for (const shardId of app.manifest.requiredShards) {
87
+ if (!registeredShards.has(shardId)) {
88
+ throw new Error(`App "${id}" requires shard "${shardId}" which is not registered`);
89
+ }
90
+ await activateShard(shardId);
91
+ }
92
+ // Attach the layout (creates the workspace-zone proxy with version
93
+ // gate) and run the app's optional activate hook.
94
+ attachApp(app);
95
+ void ((_a = app.activate) === null || _a === void 0 ? void 0 : _a.call(app, getOrCreateAppContext(id)));
96
+ activeApp.id = id;
97
+ switchToApp();
98
+ writeLastApp(id);
99
+ }
100
+ // ---------- unload --------------------------------------------------------
101
+ /**
102
+ * Unload an active app. Calls `App.deactivate`, detaches the layout, and
103
+ * deactivates the app's non-self-starting required shards. Switches the
104
+ * rendered root to home. No-op if the app is not currently active.
105
+ *
106
+ * @param id - The `AppManifest.id` of the app to unload.
107
+ */
108
+ export function unloadApp(id) {
109
+ var _a;
110
+ if (activeApp.id !== id)
111
+ return;
112
+ const app = getRegisteredApp(id);
113
+ if (!app)
114
+ return;
115
+ void ((_a = app.deactivate) === null || _a === void 0 ? void 0 : _a.call(app));
116
+ // Detach layout (releases the refcount holds; pool cleanup runs on
117
+ // the next microtask for any slots that no longer have a renderer).
118
+ // Switch to home first so LayoutRenderer stops reading the app's
119
+ // tree before detachApp drops its references.
120
+ switchToHome();
121
+ detachApp();
122
+ // Deactivate this app's required shards IF no other consumer needs
123
+ // them. Phase 8 has at most one app active at a time, so "no other
124
+ // consumer" reduces to "not self-starting AND not required by any
125
+ // other registered app that happens to already be active" — but we
126
+ // don't run multiple apps, so the only survivors are self-starters.
127
+ // The simple rule: deactivate a required shard unless it was
128
+ // self-starting (has an `autostart` field defined).
129
+ for (const shardId of app.manifest.requiredShards) {
130
+ const shard = registeredShards.get(shardId);
131
+ if (!shard)
132
+ continue;
133
+ if (shard.autostart)
134
+ continue; // self-starter stays running
135
+ deactivateShard(shardId);
136
+ }
137
+ activeApp.id = null;
138
+ appContexts.delete(id);
139
+ }
140
+ // ---------- return to home -----------------------------------------------
141
+ /**
142
+ * Return to the shell home view without unloading the active app. The
143
+ * app's shards stay running, its layout proxy stays attached with its
144
+ * refcount hold intact, and its view containers stay alive in the pool.
145
+ * Launching the same app again is a root swap only.
146
+ *
147
+ * Writes `null` to `__shell__:last-app` so reloading the page while on
148
+ * home lands on home, not on the formerly-active app.
149
+ */
150
+ export function returnToHome() {
151
+ switchToHome();
152
+ writeLastApp(null);
153
+ }
@@ -0,0 +1,37 @@
1
+ import type { App, AppManifest } from './types';
2
+ /**
3
+ * Reactive map of all registered apps keyed by `AppManifest.id`. Populated
4
+ * once at boot by the host's glob-discovery loop via `registerApp`. A future
5
+ * runtime loader may append entries at runtime.
6
+ */
7
+ export declare const registeredApps: Map<string, App>;
8
+ /**
9
+ * Reactive slot tracking the currently-active app id. Null when the shell is
10
+ * showing the home screen or no app has been launched yet. Phase 8 allows at
11
+ * most one active app at a time.
12
+ */
13
+ export declare const activeApp: {
14
+ id: string | null;
15
+ };
16
+ /**
17
+ * Register an app with the framework. Must be called before `launchApp`.
18
+ * Throws if an app with the same id is already registered.
19
+ *
20
+ * @param app - The app module to register.
21
+ */
22
+ export declare function registerApp(app: App): void;
23
+ /**
24
+ * Reactive snapshot of all registered app manifests. Shell home iterates
25
+ * this to populate its launcher list. Returns only manifests so callers
26
+ * don't reach into app bodies (initialLayout, activate hook) — those are
27
+ * launch-time concerns.
28
+ */
29
+ export declare function listRegisteredApps(): AppManifest[];
30
+ /** Reactive current-app manifest (or null when none is active). */
31
+ export declare function getActiveApp(): AppManifest | null;
32
+ /**
33
+ * Lookup for framework-internal use — returns the full App (not just the
34
+ * manifest) so lifecycle code can reach into `initialLayout` and the
35
+ * activate hook. Not re-exported through `api.ts`.
36
+ */
37
+ export declare function getRegisteredApp(id: string): App | undefined;
@@ -0,0 +1,60 @@
1
+ /*
2
+ * App registry — reactive record of every registered app, plus the
3
+ * single slot for the currently-active app.
4
+ *
5
+ * Registration is called from the host (main.ts glob loop or a future
6
+ * runtime loader). The shell home view reads `listRegisteredApps()`
7
+ * reactively so newly-registered apps appear in the list without
8
+ * reboot. `activeApp` tracks which app (if any) is currently launched;
9
+ * phase 8 allows at most one.
10
+ */
11
+ /**
12
+ * Reactive map of all registered apps keyed by `AppManifest.id`. Populated
13
+ * once at boot by the host's glob-discovery loop via `registerApp`. A future
14
+ * runtime loader may append entries at runtime.
15
+ */
16
+ export const registeredApps = $state(new Map());
17
+ /**
18
+ * Reactive slot tracking the currently-active app id. Null when the shell is
19
+ * showing the home screen or no app has been launched yet. Phase 8 allows at
20
+ * most one active app at a time.
21
+ */
22
+ export const activeApp = $state({ id: null });
23
+ /**
24
+ * Register an app with the framework. Must be called before `launchApp`.
25
+ * Throws if an app with the same id is already registered.
26
+ *
27
+ * @param app - The app module to register.
28
+ */
29
+ export function registerApp(app) {
30
+ const id = app.manifest.id;
31
+ if (registeredApps.has(id)) {
32
+ throw new Error(`App "${id}" is already registered`);
33
+ }
34
+ registeredApps.set(id, app);
35
+ }
36
+ /**
37
+ * Reactive snapshot of all registered app manifests. Shell home iterates
38
+ * this to populate its launcher list. Returns only manifests so callers
39
+ * don't reach into app bodies (initialLayout, activate hook) — those are
40
+ * launch-time concerns.
41
+ */
42
+ export function listRegisteredApps() {
43
+ return Array.from(registeredApps.values()).map((a) => a.manifest);
44
+ }
45
+ /** Reactive current-app manifest (or null when none is active). */
46
+ export function getActiveApp() {
47
+ var _a, _b;
48
+ const id = activeApp.id;
49
+ if (!id)
50
+ return null;
51
+ return (_b = (_a = registeredApps.get(id)) === null || _a === void 0 ? void 0 : _a.manifest) !== null && _b !== void 0 ? _b : null;
52
+ }
53
+ /**
54
+ * Lookup for framework-internal use — returns the full App (not just the
55
+ * manifest) so lifecycle code can reach into `initialLayout` and the
56
+ * activate hook. Not re-exported through `api.ts`.
57
+ */
58
+ export function getRegisteredApp(id) {
59
+ return registeredApps.get(id);
60
+ }
@@ -0,0 +1,61 @@
1
+ import type { LayoutNode } from '../layout/types';
2
+ import type { ZoneSchema } from '../state/types';
3
+ import type { StateZones } from '../state/zones.svelte';
4
+ /**
5
+ * Static description of an app. Declared by every app module and read by
6
+ * the shell launcher to populate the home screen list without running any
7
+ * activation code.
8
+ */
9
+ export interface AppManifest {
10
+ /** Unique app identifier. Must match the app's registration key. */
11
+ id: string;
12
+ /** Human-readable display name shown in the launcher. */
13
+ label: string;
14
+ /** Semver string for the app package. */
15
+ version: string;
16
+ /**
17
+ * Shard ids this app requires. All required shards are activated at
18
+ * launch time (skipping any already-active from a previous launch or
19
+ * from self-starting). Deactivation at unload deactivates only the
20
+ * shards this app is the last consumer of; shared shards and self-
21
+ * starting shards (diagnostic, __shell__) stay running.
22
+ */
23
+ requiredShards: string[];
24
+ /**
25
+ * Bump to invalidate persisted layouts. On launch, a persisted blob
26
+ * whose version doesn't match this is discarded and `initialLayout`
27
+ * is used instead. Phase 8 does not migrate — version bumps reset
28
+ * user customization.
29
+ */
30
+ layoutVersion: number;
31
+ }
32
+ /**
33
+ * Context object passed to `App.activate`. Provides app-scoped state zones
34
+ * that are namespaced to the app id, preventing collision with any shard
35
+ * that happens to share the same name.
36
+ */
37
+ export interface AppContext {
38
+ /**
39
+ * App-scoped state zones. The shardId underneath is the app id, so
40
+ * `state({ workspace: { x: 0 } }).workspace.x = 1` persists to
41
+ * `sh3:workspace:<appId>` with no collision risk against any shard
42
+ * of the same name.
43
+ */
44
+ state<T extends ZoneSchema>(schema: T): StateZones<T>;
45
+ }
46
+ /**
47
+ * An app module. An app is a composition document — it declares required
48
+ * shards and an initial layout blueprint, but does not register views or
49
+ * commands itself. The framework activates the required shards on launch,
50
+ * restores or applies the initial layout, and calls the optional hooks.
51
+ */
52
+ export interface App {
53
+ /** Static manifest describing the app and its requirements. */
54
+ manifest: AppManifest;
55
+ /** Starting layout tree. Used on first launch or when the persisted layout version mismatches. */
56
+ initialLayout: LayoutNode;
57
+ /** Optional hook called after all required shards are active and the layout is attached. */
58
+ activate?(ctx: AppContext): void | Promise<void>;
59
+ /** Optional hook called before the app's shards are deactivated and the layout is detached. */
60
+ deactivate?(): void | Promise<void>;
61
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * App contract — phase 8.
3
+ *
4
+ * An app is a composition document. It declares what shards it needs
5
+ * and what layout to start with, plus an optional activate hook for
6
+ * app-internal startup work. Apps are NOT shards — they do not register
7
+ * contributions, do not have their own views, and do not contribute to
8
+ * other apps' layouts. They compose existing shard views.
9
+ */
10
+ export {};