sh3-core 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/Shell.svelte +283 -0
- package/dist/Shell.svelte.d.ts +5 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +50 -0
- package/dist/app/admin/ApiKeysView.svelte +169 -0
- package/dist/app/admin/ApiKeysView.svelte.d.ts +3 -0
- package/dist/app/admin/AuthSettingsView.svelte +105 -0
- package/dist/app/admin/AuthSettingsView.svelte.d.ts +3 -0
- package/dist/app/admin/SystemView.svelte +73 -0
- package/dist/app/admin/SystemView.svelte.d.ts +3 -0
- package/dist/app/admin/UsersView.svelte +188 -0
- package/dist/app/admin/UsersView.svelte.d.ts +3 -0
- package/dist/app/admin/adminApp.d.ts +7 -0
- package/dist/app/admin/adminApp.js +25 -0
- package/dist/app/admin/adminShard.svelte.d.ts +4 -0
- package/dist/app/admin/adminShard.svelte.js +62 -0
- package/dist/app/store/InstalledView.svelte +246 -0
- package/dist/app/store/InstalledView.svelte.d.ts +3 -0
- package/dist/app/store/StoreView.svelte +522 -0
- package/dist/app/store/StoreView.svelte.d.ts +3 -0
- package/dist/app/store/storeApp.d.ts +10 -0
- package/dist/app/store/storeApp.js +26 -0
- package/dist/app/store/storeShard.svelte.d.ts +38 -0
- package/dist/app/store/storeShard.svelte.js +218 -0
- package/dist/apps/lifecycle.d.ts +42 -0
- package/dist/apps/lifecycle.js +184 -0
- package/dist/apps/registry.svelte.d.ts +40 -0
- package/dist/apps/registry.svelte.js +59 -0
- package/dist/apps/terminal/manifest.d.ts +8 -0
- package/dist/apps/terminal/manifest.js +13 -0
- package/dist/apps/terminal/terminal-app.d.ts +7 -0
- package/dist/apps/terminal/terminal-app.js +14 -0
- package/dist/apps/types.d.ts +93 -0
- package/dist/apps/types.js +10 -0
- package/dist/artifact.d.ts +32 -0
- package/dist/artifact.js +1 -0
- package/dist/assets/SH3.png +0 -0
- package/dist/assets/icons.svg +1126 -0
- package/dist/assets.d.ts +13 -0
- package/dist/auth/GuestBanner.svelte +134 -0
- package/dist/auth/GuestBanner.svelte.d.ts +3 -0
- package/dist/auth/SignInWall.svelte +203 -0
- package/dist/auth/SignInWall.svelte.d.ts +7 -0
- package/dist/auth/auth.svelte.d.ts +69 -0
- package/dist/auth/auth.svelte.js +165 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/types.d.ts +41 -0
- package/dist/auth/types.js +6 -0
- package/dist/build.d.ts +49 -0
- package/dist/build.js +236 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +28 -0
- package/dist/createShell.d.ts +24 -0
- package/dist/createShell.js +131 -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/env/client.d.ts +44 -0
- package/dist/env/client.js +106 -0
- package/dist/env/index.d.ts +2 -0
- package/dist/env/index.js +1 -0
- package/dist/env/types.d.ts +12 -0
- package/dist/env/types.js +8 -0
- package/dist/host-entry.d.ts +13 -0
- package/dist/host-entry.js +17 -0
- package/dist/host.d.ts +15 -0
- package/dist/host.js +86 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -0
- package/dist/layout/DragPreview.svelte +63 -0
- package/dist/layout/DragPreview.svelte.d.ts +3 -0
- package/dist/layout/LayoutRenderer.svelte +262 -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 +200 -0
- package/dist/layout/inspection.d.ts +72 -0
- package/dist/layout/inspection.js +209 -0
- package/dist/layout/ops.d.ts +100 -0
- package/dist/layout/ops.js +310 -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 +153 -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/migrations/shell-rename.d.ts +16 -0
- package/dist/migrations/shell-rename.js +48 -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/platform/index.d.ts +10 -0
- package/dist/platform/index.js +33 -0
- package/dist/platform/tauri-backend.d.ts +15 -0
- package/dist/platform/tauri-backend.js +58 -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/primitives/base.css +42 -0
- package/dist/registry/client.d.ts +74 -0
- package/dist/registry/client.js +117 -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 +168 -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 +185 -0
- package/dist/registry/storage.d.ts +37 -0
- package/dist/registry/storage.js +101 -0
- package/dist/registry/types.d.ts +262 -0
- package/dist/registry/types.js +14 -0
- package/dist/server-shard/types.d.ts +67 -0
- package/dist/server-shard/types.js +13 -0
- package/dist/sh3core-shard/ShellHome.svelte +192 -0
- package/dist/sh3core-shard/ShellHome.svelte.d.ts +3 -0
- package/dist/sh3core-shard/ShellTitle.svelte +171 -0
- package/dist/sh3core-shard/ShellTitle.svelte.d.ts +3 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +2 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +53 -0
- package/dist/shards/activate.svelte.d.ts +52 -0
- package/dist/shards/activate.svelte.js +186 -0
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +28 -0
- package/dist/shards/types.d.ts +207 -0
- package/dist/shards/types.js +20 -0
- package/dist/shell-shard/InputLine.svelte +133 -0
- package/dist/shell-shard/InputLine.svelte.d.ts +11 -0
- package/dist/shell-shard/ScrollbackView.svelte +47 -0
- package/dist/shell-shard/ScrollbackView.svelte.d.ts +7 -0
- package/dist/shell-shard/Terminal.svelte +122 -0
- package/dist/shell-shard/Terminal.svelte.d.ts +8 -0
- package/dist/shell-shard/entries/PromptEntry.svelte +25 -0
- package/dist/shell-shard/entries/PromptEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/entries/RichEntry.svelte +19 -0
- package/dist/shell-shard/entries/RichEntry.svelte.d.ts +8 -0
- package/dist/shell-shard/entries/StatusEntry.svelte +22 -0
- package/dist/shell-shard/entries/StatusEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/entries/TextEntry.svelte +25 -0
- package/dist/shell-shard/entries/TextEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/manifest.d.ts +2 -0
- package/dist/shell-shard/manifest.js +11 -0
- package/dist/shell-shard/protocol.d.ts +90 -0
- package/dist/shell-shard/protocol.js +11 -0
- package/dist/shell-shard/registry.d.ts +69 -0
- package/dist/shell-shard/registry.js +47 -0
- package/dist/shell-shard/rich/AppCard.svelte +25 -0
- package/dist/shell-shard/rich/AppCard.svelte.d.ts +10 -0
- package/dist/shell-shard/rich/AppsTable.svelte +29 -0
- package/dist/shell-shard/rich/AppsTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/EnvTable.svelte +27 -0
- package/dist/shell-shard/rich/EnvTable.svelte.d.ts +8 -0
- package/dist/shell-shard/rich/HelpTable.svelte +29 -0
- package/dist/shell-shard/rich/HelpTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/HistoryList.svelte +37 -0
- package/dist/shell-shard/rich/HistoryList.svelte.d.ts +9 -0
- package/dist/shell-shard/rich/ShardsTable.svelte +28 -0
- package/dist/shell-shard/rich/ShardsTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/ViewsTable.svelte +31 -0
- package/dist/shell-shard/rich/ViewsTable.svelte.d.ts +13 -0
- package/dist/shell-shard/rich/ZoneTree.svelte +19 -0
- package/dist/shell-shard/rich/ZoneTree.svelte.d.ts +8 -0
- package/dist/shell-shard/rich/ZonesTable.svelte +27 -0
- package/dist/shell-shard/rich/ZonesTable.svelte.d.ts +11 -0
- package/dist/shell-shard/scrollback.svelte.d.ts +36 -0
- package/dist/shell-shard/scrollback.svelte.js +43 -0
- package/dist/shell-shard/session-client.svelte.d.ts +23 -0
- package/dist/shell-shard/session-client.svelte.js +120 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
- package/dist/shell-shard/shellShard.svelte.js +139 -0
- package/dist/shell-shard/verbs/apps.d.ts +3 -0
- package/dist/shell-shard/verbs/apps.js +50 -0
- package/dist/shell-shard/verbs/clear.d.ts +2 -0
- package/dist/shell-shard/verbs/clear.js +7 -0
- package/dist/shell-shard/verbs/help.d.ts +2 -0
- package/dist/shell-shard/verbs/help.js +21 -0
- package/dist/shell-shard/verbs/history.d.ts +2 -0
- package/dist/shell-shard/verbs/history.js +20 -0
- package/dist/shell-shard/verbs/index.d.ts +2 -0
- package/dist/shell-shard/verbs/index.js +29 -0
- package/dist/shell-shard/verbs/session.d.ts +5 -0
- package/dist/shell-shard/verbs/session.js +65 -0
- package/dist/shell-shard/verbs/shards.d.ts +2 -0
- package/dist/shell-shard/verbs/shards.js +14 -0
- package/dist/shell-shard/verbs/views.d.ts +4 -0
- package/dist/shell-shard/verbs/views.js +90 -0
- package/dist/shell-shard/verbs/zones.d.ts +3 -0
- package/dist/shell-shard/verbs/zones.js +38 -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/manage.d.ts +14 -0
- package/dist/state/manage.js +40 -0
- package/dist/state/types.d.ts +55 -0
- package/dist/state/types.js +17 -0
- package/dist/state/zones.svelte.d.ts +53 -0
- package/dist/state/zones.svelte.js +141 -0
- package/dist/theme.d.ts +28 -0
- package/dist/theme.js +92 -0
- package/dist/tokens.css +102 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +60 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { LayoutNode, SplitNode, TabsNode, SlotNode, TabEntry } from './types';
|
|
2
|
+
/** A path down the tree: the list of child indices walked from the root. */
|
|
3
|
+
export type LayoutPath = number[];
|
|
4
|
+
/** A located tab: where it lives and which entry index inside its group. */
|
|
5
|
+
export interface LocatedTab {
|
|
6
|
+
tabsNode: TabsNode;
|
|
7
|
+
tabsPath: LayoutPath;
|
|
8
|
+
tabIndex: number;
|
|
9
|
+
entry: TabEntry;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Find a tab by its slotId anywhere in the tree. Returns null if no
|
|
13
|
+
* tab carries that slotId. Used by the drag engine to look up the
|
|
14
|
+
* source tab before any mutation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findTabBySlotId(root: LayoutNode, slotId: string): LocatedTab | null;
|
|
17
|
+
/** Find a standalone slot leaf by its slotId (only leaves, not tab entries). */
|
|
18
|
+
export declare function findSlotBySlotId(root: LayoutNode, slotId: string): {
|
|
19
|
+
node: SlotNode;
|
|
20
|
+
parent: SplitNode | null;
|
|
21
|
+
index: number;
|
|
22
|
+
} | null;
|
|
23
|
+
/**
|
|
24
|
+
* Remove a tab from its current location, returning the removed entry
|
|
25
|
+
* (or null if not found). The tabs group it was in may become empty
|
|
26
|
+
* and is cleaned up by `cleanupTree` after the caller has finished its
|
|
27
|
+
* reinsertion. The two-phase shape (mutate → cleanup) means the caller
|
|
28
|
+
* can remove a tab and re-insert it into the SAME tabs group without
|
|
29
|
+
* the group being collapsed mid-move.
|
|
30
|
+
*/
|
|
31
|
+
export declare function removeTabBySlotId(root: LayoutNode, slotId: string): TabEntry | null;
|
|
32
|
+
/**
|
|
33
|
+
* Insert a tab entry into an existing tabs group at a specific index.
|
|
34
|
+
* Clamps the index into range. The inserted tab becomes active — the
|
|
35
|
+
* user's drag landed there, so they want to see it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function insertTabIntoTabs(tabsNode: TabsNode, entry: TabEntry, index: number): void;
|
|
38
|
+
/**
|
|
39
|
+
* Atomic same-group tab move. Used by the drag engine when the drop
|
|
40
|
+
* target is the tab strip of the same group the source tab lives in.
|
|
41
|
+
*
|
|
42
|
+
* Why atomic: the naive two-step flow (`removeTabBySlotId` then
|
|
43
|
+
* `insertTabIntoTabs`) calls `Array.prototype.splice` twice against the
|
|
44
|
+
* same reactive `$state` array. Splice's internal `[[Delete]]`
|
|
45
|
+
* operations trip Svelte's proxy `deleteProperty` trap during the
|
|
46
|
+
* remove step, which can flush reactive consumers while `tabs.length`
|
|
47
|
+
* is transiently `N - 1`. A LayoutRenderer `$derived` evaluated in that
|
|
48
|
+
* window sees the shrunk array and the index-`N - 1` snippet body
|
|
49
|
+
* dereferences a now-`undefined` entry — crashing at `.slotId`. Going
|
|
50
|
+
* through a fresh non-reactive array and then reassigning `tabs` in
|
|
51
|
+
* one shot means the proxy only ever sees a single atomic transition
|
|
52
|
+
* from the old array to the new one.
|
|
53
|
+
*
|
|
54
|
+
* `postRemovalToIndex` is in post-removal coordinates, which matches
|
|
55
|
+
* the coordinate space drop targets use (see LayoutRenderer's
|
|
56
|
+
* `onStripHover`, which normalizes same-strip indices before setting
|
|
57
|
+
* the drop target).
|
|
58
|
+
*/
|
|
59
|
+
export declare function moveTabWithinTabs(tabsNode: TabsNode, fromIndex: number, postRemovalToIndex: number): void;
|
|
60
|
+
/** Which side of a split the dropped tab should land on. */
|
|
61
|
+
export type SplitSide = 'left' | 'right' | 'top' | 'bottom';
|
|
62
|
+
/**
|
|
63
|
+
* Split a slot-leaf (or tabs-leaf) into a new SplitNode, placing a
|
|
64
|
+
* single-tab group containing `entry` on the requested side and the
|
|
65
|
+
* existing node on the other side. The existing node is preserved
|
|
66
|
+
* as-is (it keeps its slotId, viewId, etc. — critical for re-parenting
|
|
67
|
+
* of the untouched side).
|
|
68
|
+
*
|
|
69
|
+
* Sides map to split direction:
|
|
70
|
+
* left/right → horizontal split, new tab goes left or right
|
|
71
|
+
* top/bottom → vertical split, new tab goes top or bottom
|
|
72
|
+
*
|
|
73
|
+
* The target is identified by a LayoutPath from the root, because
|
|
74
|
+
* splitting requires rewriting the parent's child array and we need
|
|
75
|
+
* the parent reference. If the target IS the root, the root itself is
|
|
76
|
+
* replaced — callers that pass the root by reference need the wrapper
|
|
77
|
+
* form, so this function returns the new subtree and the caller
|
|
78
|
+
* reassigns parent.children[i] = returned value (or reassigns root).
|
|
79
|
+
*/
|
|
80
|
+
export declare function makeSplitWithNewTab(existing: LayoutNode, entry: TabEntry, side: SplitSide): SplitNode;
|
|
81
|
+
/**
|
|
82
|
+
* Apply a slot-split as a tree mutation: find the target node at the
|
|
83
|
+
* given path and replace it with a new split. Handles the root case
|
|
84
|
+
* (path = []) by mutating the root's fields in place. The root object
|
|
85
|
+
* identity is preserved so Svelte's $state proxy keeps its reactivity.
|
|
86
|
+
*/
|
|
87
|
+
export declare function splitNodeAtPath(root: LayoutNode, path: LayoutPath, entry: TabEntry, side: SplitSide): void;
|
|
88
|
+
/** Walk a LayoutPath and return the node at its tip, or null. */
|
|
89
|
+
export declare function nodeAtPath(root: LayoutNode, path: LayoutPath): LayoutNode | null;
|
|
90
|
+
/**
|
|
91
|
+
* Post-mutation cleanup pass. Removes empty tabs groups from their
|
|
92
|
+
* parents and collapses single-child splits. Runs until the tree
|
|
93
|
+
* stabilizes (a collapse can reveal another single-child split one
|
|
94
|
+
* level up).
|
|
95
|
+
*
|
|
96
|
+
* Called once at the end of every drag commit. Called separately from
|
|
97
|
+
* the mutation primitives so callers can do "remove then insert into
|
|
98
|
+
* the same group" without the group being collapsed between calls.
|
|
99
|
+
*/
|
|
100
|
+
export declare function cleanupTree(root: LayoutNode): void;
|
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
* Atomic same-group tab move. Used by the drag engine when the drop
|
|
120
|
+
* target is the tab strip of the same group the source tab lives in.
|
|
121
|
+
*
|
|
122
|
+
* Why atomic: the naive two-step flow (`removeTabBySlotId` then
|
|
123
|
+
* `insertTabIntoTabs`) calls `Array.prototype.splice` twice against the
|
|
124
|
+
* same reactive `$state` array. Splice's internal `[[Delete]]`
|
|
125
|
+
* operations trip Svelte's proxy `deleteProperty` trap during the
|
|
126
|
+
* remove step, which can flush reactive consumers while `tabs.length`
|
|
127
|
+
* is transiently `N - 1`. A LayoutRenderer `$derived` evaluated in that
|
|
128
|
+
* window sees the shrunk array and the index-`N - 1` snippet body
|
|
129
|
+
* dereferences a now-`undefined` entry — crashing at `.slotId`. Going
|
|
130
|
+
* through a fresh non-reactive array and then reassigning `tabs` in
|
|
131
|
+
* one shot means the proxy only ever sees a single atomic transition
|
|
132
|
+
* from the old array to the new one.
|
|
133
|
+
*
|
|
134
|
+
* `postRemovalToIndex` is in post-removal coordinates, which matches
|
|
135
|
+
* the coordinate space drop targets use (see LayoutRenderer's
|
|
136
|
+
* `onStripHover`, which normalizes same-strip indices before setting
|
|
137
|
+
* the drop target).
|
|
138
|
+
*/
|
|
139
|
+
export function moveTabWithinTabs(tabsNode, fromIndex, postRemovalToIndex) {
|
|
140
|
+
const next = tabsNode.tabs.slice();
|
|
141
|
+
const [entry] = next.splice(fromIndex, 1);
|
|
142
|
+
const clamped = Math.max(0, Math.min(postRemovalToIndex, next.length));
|
|
143
|
+
next.splice(clamped, 0, entry);
|
|
144
|
+
tabsNode.tabs = next;
|
|
145
|
+
tabsNode.activeTab = clamped;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Split a slot-leaf (or tabs-leaf) into a new SplitNode, placing a
|
|
149
|
+
* single-tab group containing `entry` on the requested side and the
|
|
150
|
+
* existing node on the other side. The existing node is preserved
|
|
151
|
+
* as-is (it keeps its slotId, viewId, etc. — critical for re-parenting
|
|
152
|
+
* of the untouched side).
|
|
153
|
+
*
|
|
154
|
+
* Sides map to split direction:
|
|
155
|
+
* left/right → horizontal split, new tab goes left or right
|
|
156
|
+
* top/bottom → vertical split, new tab goes top or bottom
|
|
157
|
+
*
|
|
158
|
+
* The target is identified by a LayoutPath from the root, because
|
|
159
|
+
* splitting requires rewriting the parent's child array and we need
|
|
160
|
+
* the parent reference. If the target IS the root, the root itself is
|
|
161
|
+
* replaced — callers that pass the root by reference need the wrapper
|
|
162
|
+
* form, so this function returns the new subtree and the caller
|
|
163
|
+
* reassigns parent.children[i] = returned value (or reassigns root).
|
|
164
|
+
*/
|
|
165
|
+
export function makeSplitWithNewTab(existing, entry, side) {
|
|
166
|
+
const direction = side === 'left' || side === 'right' ? 'horizontal' : 'vertical';
|
|
167
|
+
const newGroup = {
|
|
168
|
+
type: 'tabs',
|
|
169
|
+
activeTab: 0,
|
|
170
|
+
tabs: [entry],
|
|
171
|
+
};
|
|
172
|
+
const newFirst = side === 'left' || side === 'top';
|
|
173
|
+
const children = newFirst ? [newGroup, existing] : [existing, newGroup];
|
|
174
|
+
return {
|
|
175
|
+
type: 'split',
|
|
176
|
+
direction,
|
|
177
|
+
sizes: [1, 1], // 50/50 by default; splitter drag adjusts from there
|
|
178
|
+
children,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Apply a slot-split as a tree mutation: find the target node at the
|
|
183
|
+
* given path and replace it with a new split. Handles the root case
|
|
184
|
+
* (path = []) by mutating the root's fields in place. The root object
|
|
185
|
+
* identity is preserved so Svelte's $state proxy keeps its reactivity.
|
|
186
|
+
*/
|
|
187
|
+
export function splitNodeAtPath(root, path, entry, side) {
|
|
188
|
+
const target = nodeAtPath(root, path);
|
|
189
|
+
if (!target)
|
|
190
|
+
return;
|
|
191
|
+
const replacement = makeSplitWithNewTab(target, entry, side);
|
|
192
|
+
if (path.length === 0) {
|
|
193
|
+
// Replace root contents in place. The root is a discriminated
|
|
194
|
+
// union; to rewrite it we cast once through `unknown` and then to
|
|
195
|
+
// a loose record shape, overwrite fields, and clear stale keys
|
|
196
|
+
// from the previous node kind so the union stays well-formed.
|
|
197
|
+
const rootAsRecord = root;
|
|
198
|
+
// Clear stale keys first so Object.assign doesn't leave a hybrid.
|
|
199
|
+
delete rootAsRecord.tabs;
|
|
200
|
+
delete rootAsRecord.activeTab;
|
|
201
|
+
delete rootAsRecord.slotId;
|
|
202
|
+
delete rootAsRecord.viewId;
|
|
203
|
+
delete rootAsRecord.direction;
|
|
204
|
+
delete rootAsRecord.sizes;
|
|
205
|
+
delete rootAsRecord.pinned;
|
|
206
|
+
delete rootAsRecord.children;
|
|
207
|
+
Object.assign(rootAsRecord, replacement);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const parentPath = path.slice(0, -1);
|
|
211
|
+
const indexInParent = path[path.length - 1];
|
|
212
|
+
const parent = nodeAtPath(root, parentPath);
|
|
213
|
+
if (!parent || parent.type !== 'split')
|
|
214
|
+
return;
|
|
215
|
+
parent.children[indexInParent] = replacement;
|
|
216
|
+
}
|
|
217
|
+
/** Walk a LayoutPath and return the node at its tip, or null. */
|
|
218
|
+
export function nodeAtPath(root, path) {
|
|
219
|
+
let cur = root;
|
|
220
|
+
for (const idx of path) {
|
|
221
|
+
if (cur.type !== 'split')
|
|
222
|
+
return null;
|
|
223
|
+
if (idx < 0 || idx >= cur.children.length)
|
|
224
|
+
return null;
|
|
225
|
+
cur = cur.children[idx];
|
|
226
|
+
}
|
|
227
|
+
return cur;
|
|
228
|
+
}
|
|
229
|
+
// ---------- Cleanup --------------------------------------------------------
|
|
230
|
+
/**
|
|
231
|
+
* Post-mutation cleanup pass. Removes empty tabs groups from their
|
|
232
|
+
* parents and collapses single-child splits. Runs until the tree
|
|
233
|
+
* stabilizes (a collapse can reveal another single-child split one
|
|
234
|
+
* level up).
|
|
235
|
+
*
|
|
236
|
+
* Called once at the end of every drag commit. Called separately from
|
|
237
|
+
* the mutation primitives so callers can do "remove then insert into
|
|
238
|
+
* the same group" without the group being collapsed between calls.
|
|
239
|
+
*/
|
|
240
|
+
export function cleanupTree(root) {
|
|
241
|
+
let changed = true;
|
|
242
|
+
while (changed) {
|
|
243
|
+
changed = cleanupPass(root, null, 0);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function cleanupPass(node, parent, indexInParent) {
|
|
247
|
+
if (node.type === 'split') {
|
|
248
|
+
// Recurse first so we collapse bottom-up.
|
|
249
|
+
let recursed = false;
|
|
250
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
251
|
+
if (cleanupPass(node.children[i], node, i))
|
|
252
|
+
recursed = true;
|
|
253
|
+
}
|
|
254
|
+
// Drop empty tabs children — unless the tabs node is persistent.
|
|
255
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
256
|
+
const child = node.children[i];
|
|
257
|
+
if (child.type === 'tabs' && child.tabs.length === 0 && !child.persistent) {
|
|
258
|
+
node.children.splice(i, 1);
|
|
259
|
+
node.sizes.splice(i, 1);
|
|
260
|
+
if (node.pinned)
|
|
261
|
+
node.pinned.splice(i, 1);
|
|
262
|
+
if (node.collapsed)
|
|
263
|
+
node.collapsed.splice(i, 1);
|
|
264
|
+
recursed = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Collapse a split that has one (or zero) children left. With zero
|
|
268
|
+
// children the split is meaningless; with one child, the split is
|
|
269
|
+
// redundant and the surviving child should take its place.
|
|
270
|
+
if (node.children.length <= 1 && parent) {
|
|
271
|
+
if (node.children.length === 1) {
|
|
272
|
+
parent.children[indexInParent] = node.children[0];
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// Zero children — shouldn't normally happen, but remove to keep
|
|
276
|
+
// the tree well-formed.
|
|
277
|
+
parent.children.splice(indexInParent, 1);
|
|
278
|
+
parent.sizes.splice(indexInParent, 1);
|
|
279
|
+
if (parent.pinned)
|
|
280
|
+
parent.pinned.splice(indexInParent, 1);
|
|
281
|
+
if (parent.collapsed)
|
|
282
|
+
parent.collapsed.splice(indexInParent, 1);
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
// Root-level split with one child: promote the surviving child to
|
|
287
|
+
// root by overwriting the root's fields in place (same technique
|
|
288
|
+
// as splitNodeAtPath's root case — preserves $state proxy identity).
|
|
289
|
+
if (node.children.length === 1 && !parent) {
|
|
290
|
+
const survivor = node.children[0];
|
|
291
|
+
const rootAsRecord = node;
|
|
292
|
+
// Clear all split-specific keys.
|
|
293
|
+
delete rootAsRecord.direction;
|
|
294
|
+
delete rootAsRecord.sizes;
|
|
295
|
+
delete rootAsRecord.pinned;
|
|
296
|
+
delete rootAsRecord.collapsed;
|
|
297
|
+
delete rootAsRecord.children;
|
|
298
|
+
Object.assign(rootAsRecord, survivor);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
return recursed;
|
|
302
|
+
}
|
|
303
|
+
if (node.type === 'tabs') {
|
|
304
|
+
// Empty tabs at the root level has no parent to drop it from; the
|
|
305
|
+
// caller is expected to handle the "layout is empty" case
|
|
306
|
+
// elsewhere (phase 7). We do nothing here.
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
@@ -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
|
+
}
|