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,281 @@
1
+ /*
2
+ * Layout ops — pure(-ish) mutations on a LayoutNode tree.
3
+ *
4
+ * All drag-reorganize gestures commit through this module. The drag
5
+ * engine figures out *what* the user meant (which tab, which target,
6
+ * which drop zone); the ops figure out *how* to edit the tree so the
7
+ * result is well-formed.
8
+ *
9
+ * Mutation style:
10
+ * Trees are mutated in place. The root is a Svelte `$state` object
11
+ * owned by the module-level `layoutStore` (see layout/store.svelte.ts),
12
+ * so in-place edits are observed by the layout renderer automatically.
13
+ * Since phase 7, that store is the workspace state-zone proxy, so each
14
+ * mutation also triggers a debounced flush to localStorage — no action
15
+ * required from op callers. Returning a new
16
+ * tree each time would also work but would force callers to reassign
17
+ * the root — messier and no upside here.
18
+ *
19
+ * Invariants the ops preserve:
20
+ * 1. No empty TabsNode. A tab group that loses its last tab is
21
+ * removed from its parent.
22
+ * 2. No single-child split. A split whose children collapse to one
23
+ * is replaced by that child (direction and proportion of the
24
+ * parent reassert themselves naturally).
25
+ * 3. Sizes array length matches children length. removeAt / insertAt
26
+ * splice the size array alongside the children array.
27
+ * 4. activeTab stays in range after tab removal/insertion. When the
28
+ * active tab itself moves, activeTab follows it.
29
+ *
30
+ * Not enforced (intentionally):
31
+ * - Uniqueness of slotIds across the tree. The slot host pool keys
32
+ * by slotId; duplicates would collide. The ops assume upstream
33
+ * doesn't produce duplicates; the drag engine only moves existing
34
+ * tabs, so no new slotIds are introduced.
35
+ */
36
+ /**
37
+ * Find a tab by its slotId anywhere in the tree. Returns null if no
38
+ * tab carries that slotId. Used by the drag engine to look up the
39
+ * source tab before any mutation.
40
+ */
41
+ export function findTabBySlotId(root, slotId) {
42
+ const walk = (node, path) => {
43
+ if (node.type === 'tabs') {
44
+ const idx = node.tabs.findIndex((t) => t.slotId === slotId);
45
+ if (idx >= 0) {
46
+ return { tabsNode: node, tabsPath: path, tabIndex: idx, entry: node.tabs[idx] };
47
+ }
48
+ }
49
+ else if (node.type === 'split') {
50
+ for (let i = 0; i < node.children.length; i++) {
51
+ const hit = walk(node.children[i], [...path, i]);
52
+ if (hit)
53
+ return hit;
54
+ }
55
+ }
56
+ return null;
57
+ };
58
+ return walk(root, []);
59
+ }
60
+ /** Find a standalone slot leaf by its slotId (only leaves, not tab entries). */
61
+ export function findSlotBySlotId(root, slotId) {
62
+ const walk = (node, parent, index) => {
63
+ if (node.type === 'slot') {
64
+ if (node.slotId === slotId)
65
+ return { node, parent, index };
66
+ return null;
67
+ }
68
+ if (node.type === 'split') {
69
+ for (let i = 0; i < node.children.length; i++) {
70
+ const hit = walk(node.children[i], node, i);
71
+ if (hit)
72
+ return hit;
73
+ }
74
+ }
75
+ return null;
76
+ };
77
+ return walk(root, null, 0);
78
+ }
79
+ // ---------- Tab removal ----------------------------------------------------
80
+ /**
81
+ * Remove a tab from its current location, returning the removed entry
82
+ * (or null if not found). The tabs group it was in may become empty
83
+ * and is cleaned up by `cleanupTree` after the caller has finished its
84
+ * reinsertion. The two-phase shape (mutate → cleanup) means the caller
85
+ * can remove a tab and re-insert it into the SAME tabs group without
86
+ * the group being collapsed mid-move.
87
+ */
88
+ export function removeTabBySlotId(root, slotId) {
89
+ const located = findTabBySlotId(root, slotId);
90
+ if (!located)
91
+ return null;
92
+ const { tabsNode, tabIndex, entry } = located;
93
+ tabsNode.tabs.splice(tabIndex, 1);
94
+ // Keep activeTab sensible: clamp, and shift down if we removed an
95
+ // earlier tab than the active one.
96
+ if (tabsNode.tabs.length === 0) {
97
+ tabsNode.activeTab = 0;
98
+ }
99
+ else if (tabIndex < tabsNode.activeTab) {
100
+ tabsNode.activeTab -= 1;
101
+ }
102
+ else if (tabsNode.activeTab >= tabsNode.tabs.length) {
103
+ tabsNode.activeTab = tabsNode.tabs.length - 1;
104
+ }
105
+ return entry;
106
+ }
107
+ // ---------- Tab insertion --------------------------------------------------
108
+ /**
109
+ * Insert a tab entry into an existing tabs group at a specific index.
110
+ * Clamps the index into range. The inserted tab becomes active — the
111
+ * user's drag landed there, so they want to see it.
112
+ */
113
+ export function insertTabIntoTabs(tabsNode, entry, index) {
114
+ const clamped = Math.max(0, Math.min(index, tabsNode.tabs.length));
115
+ tabsNode.tabs.splice(clamped, 0, entry);
116
+ tabsNode.activeTab = clamped;
117
+ }
118
+ /**
119
+ * Split a slot-leaf (or tabs-leaf) into a new SplitNode, placing a
120
+ * single-tab group containing `entry` on the requested side and the
121
+ * existing node on the other side. The existing node is preserved
122
+ * as-is (it keeps its slotId, viewId, etc. — critical for re-parenting
123
+ * of the untouched side).
124
+ *
125
+ * Sides map to split direction:
126
+ * left/right → horizontal split, new tab goes left or right
127
+ * top/bottom → vertical split, new tab goes top or bottom
128
+ *
129
+ * The target is identified by a LayoutPath from the root, because
130
+ * splitting requires rewriting the parent's child array and we need
131
+ * the parent reference. If the target IS the root, the root itself is
132
+ * replaced — callers that pass the root by reference need the wrapper
133
+ * form, so this function returns the new subtree and the caller
134
+ * reassigns parent.children[i] = returned value (or reassigns root).
135
+ */
136
+ export function makeSplitWithNewTab(existing, entry, side) {
137
+ const direction = side === 'left' || side === 'right' ? 'horizontal' : 'vertical';
138
+ const newGroup = {
139
+ type: 'tabs',
140
+ activeTab: 0,
141
+ tabs: [entry],
142
+ };
143
+ const newFirst = side === 'left' || side === 'top';
144
+ const children = newFirst ? [newGroup, existing] : [existing, newGroup];
145
+ return {
146
+ type: 'split',
147
+ direction,
148
+ sizes: [1, 1], // 50/50 by default; splitter drag adjusts from there
149
+ children,
150
+ };
151
+ }
152
+ /**
153
+ * Apply a slot-split as a tree mutation: find the target node at the
154
+ * given path and replace it with a new split. Handles the root case
155
+ * (path = []) by mutating the root's fields in place. The root object
156
+ * identity is preserved so Svelte's $state proxy keeps its reactivity.
157
+ */
158
+ export function splitNodeAtPath(root, path, entry, side) {
159
+ const target = nodeAtPath(root, path);
160
+ if (!target)
161
+ return;
162
+ const replacement = makeSplitWithNewTab(target, entry, side);
163
+ if (path.length === 0) {
164
+ // Replace root contents in place. The root is a discriminated
165
+ // union; to rewrite it we cast once through `unknown` and then to
166
+ // a loose record shape, overwrite fields, and clear stale keys
167
+ // from the previous node kind so the union stays well-formed.
168
+ const rootAsRecord = root;
169
+ // Clear stale keys first so Object.assign doesn't leave a hybrid.
170
+ delete rootAsRecord.tabs;
171
+ delete rootAsRecord.activeTab;
172
+ delete rootAsRecord.slotId;
173
+ delete rootAsRecord.viewId;
174
+ delete rootAsRecord.direction;
175
+ delete rootAsRecord.sizes;
176
+ delete rootAsRecord.pinned;
177
+ delete rootAsRecord.children;
178
+ Object.assign(rootAsRecord, replacement);
179
+ return;
180
+ }
181
+ const parentPath = path.slice(0, -1);
182
+ const indexInParent = path[path.length - 1];
183
+ const parent = nodeAtPath(root, parentPath);
184
+ if (!parent || parent.type !== 'split')
185
+ return;
186
+ parent.children[indexInParent] = replacement;
187
+ }
188
+ /** Walk a LayoutPath and return the node at its tip, or null. */
189
+ export function nodeAtPath(root, path) {
190
+ let cur = root;
191
+ for (const idx of path) {
192
+ if (cur.type !== 'split')
193
+ return null;
194
+ if (idx < 0 || idx >= cur.children.length)
195
+ return null;
196
+ cur = cur.children[idx];
197
+ }
198
+ return cur;
199
+ }
200
+ // ---------- Cleanup --------------------------------------------------------
201
+ /**
202
+ * Post-mutation cleanup pass. Removes empty tabs groups from their
203
+ * parents and collapses single-child splits. Runs until the tree
204
+ * stabilizes (a collapse can reveal another single-child split one
205
+ * level up).
206
+ *
207
+ * Called once at the end of every drag commit. Called separately from
208
+ * the mutation primitives so callers can do "remove then insert into
209
+ * the same group" without the group being collapsed between calls.
210
+ */
211
+ export function cleanupTree(root) {
212
+ let changed = true;
213
+ while (changed) {
214
+ changed = cleanupPass(root, null, 0);
215
+ }
216
+ }
217
+ function cleanupPass(node, parent, indexInParent) {
218
+ if (node.type === 'split') {
219
+ // Recurse first so we collapse bottom-up.
220
+ let recursed = false;
221
+ for (let i = 0; i < node.children.length; i++) {
222
+ if (cleanupPass(node.children[i], node, i))
223
+ recursed = true;
224
+ }
225
+ // Drop empty tabs children — unless the tabs node is persistent.
226
+ for (let i = node.children.length - 1; i >= 0; i--) {
227
+ const child = node.children[i];
228
+ if (child.type === 'tabs' && child.tabs.length === 0 && !child.persistent) {
229
+ node.children.splice(i, 1);
230
+ node.sizes.splice(i, 1);
231
+ if (node.pinned)
232
+ node.pinned.splice(i, 1);
233
+ if (node.collapsed)
234
+ node.collapsed.splice(i, 1);
235
+ recursed = true;
236
+ }
237
+ }
238
+ // Collapse a split that has one (or zero) children left. With zero
239
+ // children the split is meaningless; with one child, the split is
240
+ // redundant and the surviving child should take its place.
241
+ if (node.children.length <= 1 && parent) {
242
+ if (node.children.length === 1) {
243
+ parent.children[indexInParent] = node.children[0];
244
+ }
245
+ else {
246
+ // Zero children — shouldn't normally happen, but remove to keep
247
+ // the tree well-formed.
248
+ parent.children.splice(indexInParent, 1);
249
+ parent.sizes.splice(indexInParent, 1);
250
+ if (parent.pinned)
251
+ parent.pinned.splice(indexInParent, 1);
252
+ if (parent.collapsed)
253
+ parent.collapsed.splice(indexInParent, 1);
254
+ }
255
+ return true;
256
+ }
257
+ // Root-level split with one child: promote the surviving child to
258
+ // root by overwriting the root's fields in place (same technique
259
+ // as splitNodeAtPath's root case — preserves $state proxy identity).
260
+ if (node.children.length === 1 && !parent) {
261
+ const survivor = node.children[0];
262
+ const rootAsRecord = node;
263
+ // Clear all split-specific keys.
264
+ delete rootAsRecord.direction;
265
+ delete rootAsRecord.sizes;
266
+ delete rootAsRecord.pinned;
267
+ delete rootAsRecord.collapsed;
268
+ delete rootAsRecord.children;
269
+ Object.assign(rootAsRecord, survivor);
270
+ return true;
271
+ }
272
+ return recursed;
273
+ }
274
+ if (node.type === 'tabs') {
275
+ // Empty tabs at the root level has no parent to drop it from; the
276
+ // caller is expected to handle the "layout is empty" case
277
+ // elsewhere (phase 7). We do nothing here.
278
+ return false;
279
+ }
280
+ return false;
281
+ }
@@ -0,0 +1,36 @@
1
+ import type { ViewHandle } from '../shards/types';
2
+ /**
3
+ * Acquire (or create) the pooled host for a slot. The caller is
4
+ * expected to `appendChild` the returned host into its own wrapper —
5
+ * the pool does not know which wrapper owns the host at any given time,
6
+ * and that is intentional. The same host may be passed around.
7
+ */
8
+ export declare function acquireSlotHost(slotId: string, viewId: string | null, label: string): HTMLDivElement;
9
+ /**
10
+ * Release the pooled host. If this was the last reference, a
11
+ * destruction is queued to run in a microtask; a later acquire before
12
+ * that microtask cancels the destroy (the re-parent case).
13
+ */
14
+ export declare function releaseSlotHost(slotId: string): void;
15
+ /**
16
+ * Test / teardown helper — destroys every pooled host immediately. Used
17
+ * by HMR boundaries and tests; not part of normal runtime flow.
18
+ */
19
+ export declare function resetSlotHostPool(): void;
20
+ /**
21
+ * Read the current ViewHandle for a slot. Returns undefined if the slot
22
+ * is not in the pool or hasn't finished mounting yet. Used by the close
23
+ * protocol to check closable and call canClose().
24
+ */
25
+ export declare function getSlotHandle(slotId: string): ViewHandle | undefined;
26
+ /**
27
+ * Read the dirty state for a slot. Returns false if the slot is not in
28
+ * the pool. Used by the tab strip to render the dirty indicator.
29
+ */
30
+ export declare function isSlotDirty(slotId: string): boolean;
31
+ /**
32
+ * Read the closable state for a slot. Returns false if the slot is not
33
+ * in the pool or hasn't finished mounting yet. Reactive — Svelte will
34
+ * re-render when the deferred mount sets the flag.
35
+ */
36
+ export declare function isSlotClosable(slotId: string): boolean;
@@ -0,0 +1,229 @@
1
+ /*
2
+ * Slot host pool — the re-parenting contract, made operational.
3
+ *
4
+ * docs/design/layout.md:70 — "the layout engine moves the existing
5
+ * container element rather than destroying and recreating it". This
6
+ * module is where that happens. Views are mounted into detached
7
+ * `<div.slot-host>` elements owned by the pool, keyed by slotId, and
8
+ * survive across arbitrary Svelte remounts of their containing
9
+ * SlotContainer.
10
+ *
11
+ * Why a pool, not per-component state:
12
+ * When a user drags a tab from one TabbedPanel to another, the slot's
13
+ * SlotContainer leaves one Svelte subtree and reappears in another.
14
+ * Svelte will tear down the old component instance and mount a new one
15
+ * — even though the slot logically hasn't changed. If the view lives
16
+ * inside that component's own DOM, it dies with it. Pooling the host
17
+ * outside the Svelte tree and letting each SlotContainer instance
18
+ * merely *attach* the pooled host to its wrapper decouples the view's
19
+ * lifetime from the component's lifetime.
20
+ *
21
+ * Refcount + deferred destroy:
22
+ * A tab-move produces a teardown/mount pair in the same microtask.
23
+ * Naive lifecycle (destroy on refcount 0) would destroy the view
24
+ * between those two events. Instead, a 0-refcount schedules a
25
+ * destroy in a microtask; if someone re-acquires before the microtask
26
+ * runs, the destroy is cancelled. Result: in-tick moves survive,
27
+ * genuine removals (tab closed, layout discards the slotId) still
28
+ * tear the view down promptly.
29
+ *
30
+ * Opt-out:
31
+ * ViewHandle.remountOnMove (unused in phase 6) is the GL/Safari
32
+ * edge-case escape hatch reserved by the design. Not wired yet —
33
+ * phase 6 has no view that needs it.
34
+ */
35
+ import { getView } from '../shards/registry';
36
+ const pool = new Map();
37
+ const pendingDestroy = new Set();
38
+ /**
39
+ * Reactive dirty-state map. Keyed by slotId, values are $state so
40
+ * Svelte tracks reads in `isSlotDirty()` and re-renders the tab strip
41
+ * when a shard calls `setDirty()`. Separate from PooledHost because
42
+ * the pool's plain Map is not reactive.
43
+ */
44
+ const dirtyState = $state({});
45
+ /**
46
+ * Reactive closable-state map. Same pattern as dirtyState — the pool's
47
+ * plain Map is not reactive, so closable flags derived from ViewHandle
48
+ * would never trigger a re-render. This record is $state so Svelte
49
+ * tracks reads in `isSlotClosable()` and re-renders the tab strip once
50
+ * the deferred mount completes and sets the flag.
51
+ */
52
+ const closableState = $state({});
53
+ /*
54
+ * Detaching the view mount from the caller's effect scope.
55
+ *
56
+ * `acquireSlotHost` is called from inside SlotContainer's `$effect`.
57
+ * Svelte 5 attributes any reactive subscriptions created during that
58
+ * call to the currently-active effect — and `$effect.root` is NOT
59
+ * sufficient to escape this, because `active_effect` is still set
60
+ * when the root scope runs synchronously inline. When the caller's
61
+ * effect is later torn down (because SlotContainer remounts under a
62
+ * new branch of the layout tree), those subscriptions are severed.
63
+ * The pool keeps the view's DOM alive across the remount, but its
64
+ * `$derived`s and `$effect`s stop firing — the exact symptom we hit.
65
+ *
66
+ * Fix: return the host element synchronously, but defer the
67
+ * `factory.mount(host)` call to a `queueMicrotask`. Microtasks run
68
+ * with a clean execution stack, so `active_effect` is null when mount
69
+ * runs — the view's reactive graph is a true top-level root with no
70
+ * ancestor effect to be destroyed by. The one-microtask delay before
71
+ * the view appears is invisible to the user: microtasks run before
72
+ * the browser paints, so the initial frame still shows the view.
73
+ *
74
+ * A `cancelled` flag guards against the edge case where the entry
75
+ * is destroyed before its deferred mount ever runs (e.g. rapid
76
+ * add-then-remove of a slot during a drag).
77
+ */
78
+ function createHost(slotId, viewId, label) {
79
+ const host = document.createElement('div');
80
+ host.className = 'slot-host';
81
+ host.dataset.slotId = slotId;
82
+ // Position:absolute inset:0 so the host fills whichever wrapper it is
83
+ // attached to. The wrapper is what the layout engine sizes; the host
84
+ // just tracks it. Styles are set inline (not in a class) so consumers
85
+ // don't need to import a stylesheet to get correct layout.
86
+ host.style.position = 'absolute';
87
+ host.style.inset = '0';
88
+ host.style.minWidth = '0';
89
+ host.style.minHeight = '0';
90
+ let cancelled = false;
91
+ const entry = {
92
+ host,
93
+ handle: undefined,
94
+ viewId,
95
+ label,
96
+ refcount: 0,
97
+ resizeObserver: undefined,
98
+ cancelPendingMount: () => {
99
+ cancelled = true;
100
+ },
101
+ };
102
+ queueMicrotask(() => {
103
+ var _a, _b;
104
+ if (cancelled)
105
+ return;
106
+ const factory = viewId ? getView(viewId) : undefined;
107
+ const ctx = {
108
+ slotId,
109
+ viewId: viewId !== null && viewId !== void 0 ? viewId : '',
110
+ label,
111
+ setDirty(dirty) {
112
+ dirtyState[slotId] = dirty;
113
+ },
114
+ };
115
+ entry.handle = factory === null || factory === void 0 ? void 0 : factory.mount(host, ctx);
116
+ if ((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) {
117
+ closableState[slotId] = true;
118
+ }
119
+ // The pool owns the ResizeObserver so its lifetime matches the
120
+ // view handle's lifetime, not the containing SlotContainer's.
121
+ // Moving the host between wrappers (drag-reorganize) keeps the
122
+ // same observer, and the view keeps receiving size updates
123
+ // through the move.
124
+ if ((_b = entry.handle) === null || _b === void 0 ? void 0 : _b.onResize) {
125
+ const onResize = entry.handle.onResize.bind(entry.handle);
126
+ entry.resizeObserver = new ResizeObserver((entries) => {
127
+ for (const e of entries) {
128
+ const box = e.contentRect;
129
+ onResize(Math.round(box.width), Math.round(box.height));
130
+ }
131
+ });
132
+ entry.resizeObserver.observe(host);
133
+ }
134
+ });
135
+ return entry;
136
+ }
137
+ /**
138
+ * Acquire (or create) the pooled host for a slot. The caller is
139
+ * expected to `appendChild` the returned host into its own wrapper —
140
+ * the pool does not know which wrapper owns the host at any given time,
141
+ * and that is intentional. The same host may be passed around.
142
+ */
143
+ export function acquireSlotHost(slotId, viewId, label) {
144
+ // If the slot was about to be destroyed, cancel — this acquire is the
145
+ // "other half" of a re-parent (teardown was the previous container).
146
+ pendingDestroy.delete(slotId);
147
+ let entry = pool.get(slotId);
148
+ if (!entry) {
149
+ entry = createHost(slotId, viewId, label);
150
+ pool.set(slotId, entry);
151
+ }
152
+ entry.refcount++;
153
+ return entry.host;
154
+ }
155
+ /**
156
+ * Release the pooled host. If this was the last reference, a
157
+ * destruction is queued to run in a microtask; a later acquire before
158
+ * that microtask cancels the destroy (the re-parent case).
159
+ */
160
+ export function releaseSlotHost(slotId) {
161
+ const entry = pool.get(slotId);
162
+ if (!entry)
163
+ return;
164
+ entry.refcount--;
165
+ if (entry.refcount > 0)
166
+ return;
167
+ pendingDestroy.add(slotId);
168
+ queueMicrotask(() => {
169
+ var _a, _b;
170
+ if (!pendingDestroy.has(slotId))
171
+ return;
172
+ pendingDestroy.delete(slotId);
173
+ const current = pool.get(slotId);
174
+ if (!current || current.refcount > 0)
175
+ return; // re-acquired, keep
176
+ (_a = current.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
177
+ (_b = current.handle) === null || _b === void 0 ? void 0 : _b.unmount();
178
+ current.cancelPendingMount();
179
+ current.host.remove();
180
+ pool.delete(slotId);
181
+ delete dirtyState[slotId];
182
+ delete closableState[slotId];
183
+ });
184
+ }
185
+ /**
186
+ * Test / teardown helper — destroys every pooled host immediately. Used
187
+ * by HMR boundaries and tests; not part of normal runtime flow.
188
+ */
189
+ export function resetSlotHostPool() {
190
+ var _a, _b;
191
+ pendingDestroy.clear();
192
+ for (const entry of pool.values()) {
193
+ (_a = entry.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
194
+ (_b = entry.handle) === null || _b === void 0 ? void 0 : _b.unmount();
195
+ entry.cancelPendingMount();
196
+ entry.host.remove();
197
+ }
198
+ pool.clear();
199
+ for (const key of Object.keys(dirtyState))
200
+ delete dirtyState[key];
201
+ for (const key of Object.keys(closableState))
202
+ delete closableState[key];
203
+ }
204
+ /**
205
+ * Read the current ViewHandle for a slot. Returns undefined if the slot
206
+ * is not in the pool or hasn't finished mounting yet. Used by the close
207
+ * protocol to check closable and call canClose().
208
+ */
209
+ export function getSlotHandle(slotId) {
210
+ var _a;
211
+ return (_a = pool.get(slotId)) === null || _a === void 0 ? void 0 : _a.handle;
212
+ }
213
+ /**
214
+ * Read the dirty state for a slot. Returns false if the slot is not in
215
+ * the pool. Used by the tab strip to render the dirty indicator.
216
+ */
217
+ export function isSlotDirty(slotId) {
218
+ var _a;
219
+ return (_a = dirtyState[slotId]) !== null && _a !== void 0 ? _a : false;
220
+ }
221
+ /**
222
+ * Read the closable state for a slot. Returns false if the slot is not
223
+ * in the pool or hasn't finished mounting yet. Reactive — Svelte will
224
+ * re-render when the deferred mount sets the flag.
225
+ */
226
+ export function isSlotClosable(slotId) {
227
+ var _a;
228
+ return (_a = closableState[slotId]) !== null && _a !== void 0 ? _a : false;
229
+ }
@@ -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
+ };