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.
- package/dist/Shell.svelte +185 -0
- package/dist/Shell.svelte.d.ts +4 -0
- package/dist/api.d.ts +22 -0
- package/dist/api.js +45 -0
- package/dist/apps/lifecycle.d.ts +37 -0
- package/dist/apps/lifecycle.js +153 -0
- package/dist/apps/registry.svelte.d.ts +37 -0
- package/dist/apps/registry.svelte.js +60 -0
- package/dist/apps/types.d.ts +61 -0
- package/dist/apps/types.js +10 -0
- package/dist/assets/icons.svg +1119 -0
- package/dist/auth/auth.svelte.d.ts +44 -0
- package/dist/auth/auth.svelte.js +119 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/build.d.ts +29 -0
- package/dist/build.js +85 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +28 -0
- package/dist/documents/backends.d.ts +17 -0
- package/dist/documents/backends.js +156 -0
- package/dist/documents/config.d.ts +7 -0
- package/dist/documents/config.js +27 -0
- package/dist/documents/handle.d.ts +6 -0
- package/dist/documents/handle.js +154 -0
- package/dist/documents/http-backend.d.ts +22 -0
- package/dist/documents/http-backend.js +78 -0
- package/dist/documents/index.d.ts +6 -0
- package/dist/documents/index.js +8 -0
- package/dist/documents/notifications.d.ts +9 -0
- package/dist/documents/notifications.js +39 -0
- package/dist/documents/types.d.ts +97 -0
- package/dist/documents/types.js +12 -0
- package/dist/host-entry.d.ts +9 -0
- package/dist/host-entry.js +15 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/layout/DragPreview.svelte +63 -0
- package/dist/layout/DragPreview.svelte.d.ts +3 -0
- package/dist/layout/LayoutRenderer.svelte +260 -0
- package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
- package/dist/layout/SlotContainer.svelte +140 -0
- package/dist/layout/SlotContainer.svelte.d.ts +8 -0
- package/dist/layout/SlotDropZone.svelte +122 -0
- package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
- package/dist/layout/drag.svelte.d.ts +45 -0
- package/dist/layout/drag.svelte.js +191 -0
- package/dist/layout/inspection.d.ts +52 -0
- package/dist/layout/inspection.js +157 -0
- package/dist/layout/ops.d.ts +78 -0
- package/dist/layout/ops.js +281 -0
- package/dist/layout/slotHostPool.svelte.d.ts +36 -0
- package/dist/layout/slotHostPool.svelte.js +229 -0
- package/dist/layout/store.svelte.d.ts +39 -0
- package/dist/layout/store.svelte.js +150 -0
- package/dist/layout/tree-walk.d.ts +15 -0
- package/dist/layout/tree-walk.js +33 -0
- package/dist/layout/types.d.ts +108 -0
- package/dist/layout/types.js +25 -0
- package/dist/overlays/ModalFrame.svelte +87 -0
- package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
- package/dist/overlays/PopupFrame.svelte +85 -0
- package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
- package/dist/overlays/ToastItem.svelte +77 -0
- package/dist/overlays/ToastItem.svelte.d.ts +9 -0
- package/dist/overlays/focusTrap.d.ts +1 -0
- package/dist/overlays/focusTrap.js +64 -0
- package/dist/overlays/modal.d.ts +9 -0
- package/dist/overlays/modal.js +141 -0
- package/dist/overlays/popup.d.ts +9 -0
- package/dist/overlays/popup.js +108 -0
- package/dist/overlays/roots.d.ts +4 -0
- package/dist/overlays/roots.js +31 -0
- package/dist/overlays/toast.d.ts +6 -0
- package/dist/overlays/toast.js +93 -0
- package/dist/overlays/types.d.ts +31 -0
- package/dist/overlays/types.js +15 -0
- package/dist/primitives/.gitkeep +0 -0
- package/dist/primitives/ResizableSplitter.svelte +333 -0
- package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
- package/dist/primitives/TabbedPanel.svelte +305 -0
- package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
- package/dist/registry/client.d.ts +74 -0
- package/dist/registry/client.js +118 -0
- package/dist/registry/index.d.ts +13 -0
- package/dist/registry/index.js +14 -0
- package/dist/registry/installer.d.ts +53 -0
- package/dist/registry/installer.js +170 -0
- package/dist/registry/integrity.d.ts +32 -0
- package/dist/registry/integrity.js +92 -0
- package/dist/registry/loader.d.ts +50 -0
- package/dist/registry/loader.js +145 -0
- package/dist/registry/schema.d.ts +47 -0
- package/dist/registry/schema.js +180 -0
- package/dist/registry/storage.d.ts +37 -0
- package/dist/registry/storage.js +101 -0
- package/dist/registry/types.d.ts +245 -0
- package/dist/registry/types.js +14 -0
- package/dist/registry-shard/RegistryView.svelte +561 -0
- package/dist/registry-shard/RegistryView.svelte.d.ts +3 -0
- package/dist/registry-shard/registryApp.d.ts +10 -0
- package/dist/registry-shard/registryApp.js +24 -0
- package/dist/registry-shard/registryShard.svelte.d.ts +45 -0
- package/dist/registry-shard/registryShard.svelte.js +125 -0
- package/dist/shards/activate.svelte.d.ts +45 -0
- package/dist/shards/activate.svelte.js +124 -0
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +28 -0
- package/dist/shards/types.d.ts +155 -0
- package/dist/shards/types.js +20 -0
- package/dist/shell-shard/ShellHome.svelte +285 -0
- package/dist/shell-shard/ShellHome.svelte.d.ts +3 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
- package/dist/shell-shard/shellShard.svelte.js +47 -0
- package/dist/shellRuntime.svelte.d.ts +27 -0
- package/dist/shellRuntime.svelte.js +27 -0
- package/dist/state/backends.d.ts +26 -0
- package/dist/state/backends.js +99 -0
- package/dist/state/types.d.ts +38 -0
- package/dist/state/types.js +15 -0
- package/dist/state/zones.svelte.d.ts +52 -0
- package/dist/state/zones.svelte.js +141 -0
- package/dist/store/InstalledView.svelte +201 -0
- package/dist/store/InstalledView.svelte.d.ts +3 -0
- package/dist/store/StoreView.svelte +470 -0
- package/dist/store/StoreView.svelte.d.ts +3 -0
- package/dist/store/storeApp.d.ts +11 -0
- package/dist/store/storeApp.js +26 -0
- package/dist/store/storeShard.svelte.d.ts +29 -0
- package/dist/store/storeShard.svelte.js +99 -0
- package/dist/tokens.css +79 -0
- 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
|
+
};
|