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,191 @@
1
+ /*
2
+ * Tab drag engine — state machine + global listeners + commit.
3
+ *
4
+ * Lifecycle:
5
+ * 1. TabbedPanel fires `beginTabDrag(slotId, entry, pointerEvent)`
6
+ * from pointerdown on a tab. The engine records the starting
7
+ * pointer position and the source slotId but does NOT enter the
8
+ * "dragging" phase yet.
9
+ * 2. A global pointermove listener watches for movement exceeding
10
+ * DRAG_THRESHOLD_PX. Clicks that never cross the threshold are
11
+ * not drags — they fall through to the tab's own click handler.
12
+ * 3. On threshold cross, the engine transitions to `dragging` and
13
+ * starts tracking the pointer; the status (visible ghost, drop
14
+ * indicators) is driven by the reactive state.
15
+ * 4. pointerup commits a drop target (if one is currently hovered)
16
+ * and tears down. No commit if the user released over nothing.
17
+ *
18
+ * State shape:
19
+ * The engine exposes a single `dragState` $state object. Components
20
+ * subscribe by reading its fields in a $derived or $effect. The
21
+ * fields:
22
+ * - phase: 'idle' | 'pending' | 'dragging'
23
+ * - source: { slotId, entry } | null
24
+ * - pointer: { x, y } current viewport coords while dragging
25
+ * - target: DropTarget | null — where the commit would land now
26
+ *
27
+ * Drop targets:
28
+ * Two kinds:
29
+ * { kind: 'strip', tabsNode, insertIndex } — between tabs in a strip
30
+ * { kind: 'split', path, side } — quadrant split of a node at path
31
+ * The owning components compute these from their bounding rects and
32
+ * call `setDropTarget` / `clearDropTarget` when the pointer enters /
33
+ * leaves. Multiple overlapping targets: the one most recently set
34
+ * wins (innermost component takes priority because it fires last).
35
+ *
36
+ * Commit:
37
+ * `commit()` is called on pointerup. It looks at dragState.target
38
+ * and runs the appropriate ops mutation against the shared root.
39
+ * The root is handed to the engine on init (via `initDragEngine`)
40
+ * because the engine is a module-level singleton and the root comes
41
+ * from the composition layer.
42
+ */
43
+ import { cleanupTree, insertTabIntoTabs, removeTabBySlotId, splitNodeAtPath, } from './ops';
44
+ import { layoutStore } from './store.svelte';
45
+ export const dragState = $state({
46
+ phase: 'idle',
47
+ source: null,
48
+ pointerX: 0,
49
+ pointerY: 0,
50
+ target: null,
51
+ });
52
+ /**
53
+ * True for exactly one macrotask after a drag ends, so the synthetic
54
+ * `click` the browser fires on the original tab (right after
55
+ * pointerup) can be ignored. Consumers check `suppressNextClick()` at
56
+ * the top of their click handler. Without this, the stale click would
57
+ * call the tab's `select(i)` on an index that no longer maps to the
58
+ * same tab after the layout mutation.
59
+ */
60
+ let clickSuppressedUntil = 0;
61
+ export function suppressNextClick() {
62
+ return performance.now() < clickSuppressedUntil;
63
+ }
64
+ const DRAG_THRESHOLD_PX = 4;
65
+ let pendingStartX = 0;
66
+ let pendingStartY = 0;
67
+ /**
68
+ * Begin a potential tab drag. Call from pointerdown on a tab element.
69
+ * This does not yet enter the dragging phase — movement past the
70
+ * threshold is required.
71
+ */
72
+ export function beginTabDrag(slotId, entry, event, tabElement) {
73
+ if (dragState.phase !== 'idle')
74
+ return;
75
+ const rect = tabElement.getBoundingClientRect();
76
+ dragState.phase = 'pending';
77
+ dragState.source = {
78
+ slotId,
79
+ entry,
80
+ startRect: rect,
81
+ offsetX: event.clientX - rect.left,
82
+ offsetY: event.clientY - rect.top,
83
+ };
84
+ dragState.pointerX = event.clientX;
85
+ dragState.pointerY = event.clientY;
86
+ dragState.target = null;
87
+ pendingStartX = event.clientX;
88
+ pendingStartY = event.clientY;
89
+ installGlobalListeners();
90
+ }
91
+ function installGlobalListeners() {
92
+ window.addEventListener('pointermove', onPointerMove, true);
93
+ window.addEventListener('pointerup', onPointerUp, true);
94
+ window.addEventListener('pointercancel', onPointerCancel, true);
95
+ // Lock selection / text caret during drag. Restored in teardown.
96
+ document.body.style.userSelect = 'none';
97
+ // Signal to non-drag-aware components that a drag is in progress,
98
+ // so they can suppress misleading hover states via CSS.
99
+ document.body.dataset.dragging = '';
100
+ }
101
+ function removeGlobalListeners() {
102
+ window.removeEventListener('pointermove', onPointerMove, true);
103
+ window.removeEventListener('pointerup', onPointerUp, true);
104
+ window.removeEventListener('pointercancel', onPointerCancel, true);
105
+ document.body.style.userSelect = '';
106
+ delete document.body.dataset.dragging;
107
+ }
108
+ function onPointerMove(e) {
109
+ dragState.pointerX = e.clientX;
110
+ dragState.pointerY = e.clientY;
111
+ if (dragState.phase === 'pending') {
112
+ const dx = e.clientX - pendingStartX;
113
+ const dy = e.clientY - pendingStartY;
114
+ if (dx * dx + dy * dy >= DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) {
115
+ dragState.phase = 'dragging';
116
+ }
117
+ }
118
+ }
119
+ function onPointerUp(_e) {
120
+ const wasDragging = dragState.phase === 'dragging';
121
+ if (wasDragging) {
122
+ commit();
123
+ // Swallow the synthetic click that fires on the source tab
124
+ // immediately after pointerup. A few ms is plenty — the click
125
+ // is dispatched synchronously or on the next microtask.
126
+ clickSuppressedUntil = performance.now() + 50;
127
+ }
128
+ teardown();
129
+ }
130
+ function onPointerCancel(_e) {
131
+ teardown();
132
+ }
133
+ function commit() {
134
+ const { source, target } = dragState;
135
+ if (!source || !target)
136
+ return;
137
+ const root = layoutStore.root;
138
+ // Pulling the tab out of its source group. Must happen before the
139
+ // insertion so the source and destination can be the same group
140
+ // (reorder within a strip).
141
+ const removed = removeTabBySlotId(root, source.slotId);
142
+ if (!removed)
143
+ return;
144
+ if (target.kind === 'strip') {
145
+ // If the strip target is the same group we just removed from and
146
+ // the removal shifted subsequent indices, fix insertIndex: the
147
+ // engine recorded insertIndex in the pre-removal coordinate space
148
+ // (because the drop indicator is computed live from the DOM). If
149
+ // the source index is before the insertion index, subtract one.
150
+ // We can detect this by walking the tree to see if this tabs node
151
+ // is the one we mutated, but the simpler signal is: the engine
152
+ // sees the tab was removed from target.tabsNode iff tabsNode no
153
+ // longer contains the source slotId AND its length decreased. We
154
+ // know it decreased if the tab was in it; the ops.findTabBySlotId
155
+ // returned the group. Rather than re-searching, trust the caller
156
+ // of setDropTarget to normalize: the strip hit-test converts a
157
+ // same-strip hit into an index that is already in post-removal
158
+ // coordinates. See TabStrip.svelte.
159
+ insertTabIntoTabs(target.tabsNode, removed, target.insertIndex);
160
+ }
161
+ else {
162
+ splitNodeAtPath(root, target.path, removed, target.side);
163
+ }
164
+ cleanupTree(root);
165
+ }
166
+ function teardown() {
167
+ dragState.phase = 'idle';
168
+ dragState.source = null;
169
+ dragState.target = null;
170
+ removeGlobalListeners();
171
+ }
172
+ /**
173
+ * Called by drop zone components when the pointer is over them. The
174
+ * last call wins, so innermost / most-specific zones should call this
175
+ * on pointermove over their geometry. `clearDropTarget` is called when
176
+ * the pointer leaves.
177
+ */
178
+ export function setDropTarget(target) {
179
+ if (dragState.phase !== 'dragging')
180
+ return;
181
+ dragState.target = target;
182
+ }
183
+ export function clearDropTarget(match) {
184
+ if (dragState.phase !== 'dragging')
185
+ return;
186
+ if (!dragState.target)
187
+ return;
188
+ if (match && !match(dragState.target))
189
+ return;
190
+ dragState.target = null;
191
+ }
@@ -0,0 +1,52 @@
1
+ import type { LayoutNode, TabEntry } from './types';
2
+ /**
3
+ * Read-only snapshot of the currently-rendered layout tree. The return
4
+ * value is the live object — callers MUST NOT mutate it directly;
5
+ * mutations go through `spliceIntoActiveLayout`. Returning the live
6
+ * object (not a deep clone) means reactive consumers can re-read on
7
+ * updates without manual subscription wiring.
8
+ */
9
+ export declare function inspectActiveLayout(): {
10
+ root: LayoutNode;
11
+ source: 'home' | 'app';
12
+ };
13
+ /**
14
+ * Add a new tab at the end of the first tabs group found in the
15
+ * currently-rendered layout. If no tabs group exists (e.g. the active
16
+ * root is the single-slot home layout), throws. Phase 8 scope — richer
17
+ * variants arrive later.
18
+ */
19
+ export declare function spliceIntoActiveLayout(entry: TabEntry): void;
20
+ /**
21
+ * Activate the tab whose slot matches `slotId` in the currently-rendered
22
+ * layout. Returns `true` if a matching tab was found and activated.
23
+ */
24
+ export declare function focusTab(slotId: string): boolean;
25
+ /**
26
+ * Activate the first tab whose `viewId` matches in the currently-rendered
27
+ * layout. Returns `true` if a matching tab was found and activated.
28
+ */
29
+ export declare function focusView(viewId: string): boolean;
30
+ /**
31
+ * Collapse a child of a split node at the given path. Returns true if
32
+ * the split was found and the child was collapsed.
33
+ */
34
+ export declare function collapseChild(splitPath: number[], childIndex: number): boolean;
35
+ /**
36
+ * Expand a previously collapsed child of a split node. Returns true if
37
+ * the split was found and the child was expanded.
38
+ */
39
+ export declare function expandChild(splitPath: number[], childIndex: number): boolean;
40
+ /**
41
+ * Request to close a tab by its slot ID. Respects the view's closable
42
+ * policy:
43
+ * - Non-closable (closable undefined/false): returns false immediately.
44
+ * - Pure (closable true): removes the tab, returns true.
45
+ * - Guarded (closable { canClose }): awaits canClose(); removes if
46
+ * resolved true, returns false if resolved false.
47
+ *
48
+ * This is the single entry point for closing tabs — the tab strip's X
49
+ * button and programmatic callers both use it. The layout engine remains
50
+ * the sole authority on tree mutations.
51
+ */
52
+ export declare function closeTab(slotId: string): Promise<boolean>;
@@ -0,0 +1,157 @@
1
+ /*
2
+ * Layout inspection / mutation — public API for advanced shards.
3
+ *
4
+ * These helpers are how shards like `diagnostic` inject themselves into
5
+ * the currently-rendered layout. The mutation primitives delegate to
6
+ * ops.ts for the actual tree work; inspection reads through the layout
7
+ * manager so diagnostic content stays live as the user rearranges things.
8
+ *
9
+ * Scope note: `spliceIntoActiveLayout` targets the CURRENTLY-RENDERED
10
+ * root only. Mutating a held-but-not-active app tree (e.g. while the
11
+ * user is on home) is not supported in phase 8. If an advanced shard
12
+ * wants to reach the held tree, it has to do so while the app is
13
+ * rendered.
14
+ */
15
+ import { activeLayout, getActiveRoot } from './store.svelte';
16
+ import { nodeAtPath, findTabBySlotId, removeTabBySlotId, cleanupTree } from './ops';
17
+ import { getSlotHandle } from './slotHostPool.svelte';
18
+ /**
19
+ * Read-only snapshot of the currently-rendered layout tree. The return
20
+ * value is the live object — callers MUST NOT mutate it directly;
21
+ * mutations go through `spliceIntoActiveLayout`. Returning the live
22
+ * object (not a deep clone) means reactive consumers can re-read on
23
+ * updates without manual subscription wiring.
24
+ */
25
+ export function inspectActiveLayout() {
26
+ return { root: activeLayout(), source: getActiveRoot() };
27
+ }
28
+ /**
29
+ * Add a new tab at the end of the first tabs group found in the
30
+ * currently-rendered layout. If no tabs group exists (e.g. the active
31
+ * root is the single-slot home layout), throws. Phase 8 scope — richer
32
+ * variants arrive later.
33
+ */
34
+ export function spliceIntoActiveLayout(entry) {
35
+ const root = activeLayout();
36
+ const target = findFirstTabsNode(root);
37
+ if (!target) {
38
+ throw new Error('spliceIntoActiveLayout: no tabs group found in the active layout; ' +
39
+ 'phase 8 only supports splicing into an existing tab group.');
40
+ }
41
+ target.tabs.push(entry);
42
+ target.activeTab = target.tabs.length - 1;
43
+ }
44
+ /**
45
+ * Activate the tab whose slot matches `slotId` in the currently-rendered
46
+ * layout. Returns `true` if a matching tab was found and activated.
47
+ */
48
+ export function focusTab(slotId) {
49
+ const root = activeLayout();
50
+ return focusTabWhere(root, (entry) => entry.slotId === slotId);
51
+ }
52
+ /**
53
+ * Activate the first tab whose `viewId` matches in the currently-rendered
54
+ * layout. Returns `true` if a matching tab was found and activated.
55
+ */
56
+ export function focusView(viewId) {
57
+ const root = activeLayout();
58
+ return focusTabWhere(root, (entry) => entry.viewId === viewId);
59
+ }
60
+ /** Walk the tree looking for a tab entry that satisfies `pred`, activate it. */
61
+ function focusTabWhere(node, pred) {
62
+ if (node.type === 'tabs') {
63
+ const idx = node.tabs.findIndex(pred);
64
+ if (idx >= 0) {
65
+ node.activeTab = idx;
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ if (node.type === 'split') {
71
+ for (const child of node.children) {
72
+ if (focusTabWhere(child, pred))
73
+ return true;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+ /**
79
+ * Collapse a child of a split node at the given path. Returns true if
80
+ * the split was found and the child was collapsed.
81
+ */
82
+ export function collapseChild(splitPath, childIndex) {
83
+ return setCollapsed(splitPath, childIndex, true);
84
+ }
85
+ /**
86
+ * Expand a previously collapsed child of a split node. Returns true if
87
+ * the split was found and the child was expanded.
88
+ */
89
+ export function expandChild(splitPath, childIndex) {
90
+ return setCollapsed(splitPath, childIndex, false);
91
+ }
92
+ function setCollapsed(splitPath, childIndex, value) {
93
+ const root = activeLayout();
94
+ const node = nodeAtPath(root, splitPath);
95
+ if (!node || node.type !== 'split')
96
+ return false;
97
+ const split = node;
98
+ if (childIndex < 0 || childIndex >= split.children.length)
99
+ return false;
100
+ if (!split.collapsed)
101
+ split.collapsed = split.children.map(() => false);
102
+ split.collapsed[childIndex] = value;
103
+ return true;
104
+ }
105
+ /**
106
+ * Request to close a tab by its slot ID. Respects the view's closable
107
+ * policy:
108
+ * - Non-closable (closable undefined/false): returns false immediately.
109
+ * - Pure (closable true): removes the tab, returns true.
110
+ * - Guarded (closable { canClose }): awaits canClose(); removes if
111
+ * resolved true, returns false if resolved false.
112
+ *
113
+ * This is the single entry point for closing tabs — the tab strip's X
114
+ * button and programmatic callers both use it. The layout engine remains
115
+ * the sole authority on tree mutations.
116
+ */
117
+ export async function closeTab(slotId) {
118
+ const root = activeLayout();
119
+ const located = findTabBySlotId(root, slotId);
120
+ if (!located)
121
+ return false;
122
+ const handle = getSlotHandle(slotId);
123
+ const closable = handle === null || handle === void 0 ? void 0 : handle.closable;
124
+ // Non-closable: no action.
125
+ if (!closable)
126
+ return false;
127
+ // Guarded: ask the shard.
128
+ if (typeof closable === 'object') {
129
+ const allowed = await closable.canClose();
130
+ if (!allowed)
131
+ return false;
132
+ // Re-verify the tab still exists after the async gap — another close
133
+ // request or a layout mutation may have removed it while we awaited.
134
+ if (!findTabBySlotId(root, slotId))
135
+ return false;
136
+ }
137
+ // Remove the tab from the tree. This causes Svelte to tear down the
138
+ // SlotContainer, which calls releaseSlotHost() — the pool's deferred
139
+ // destroy microtask then fires handle.unmount(). We do NOT call
140
+ // releaseSlotHost here; the existing component lifecycle handles it.
141
+ removeTabBySlotId(root, slotId);
142
+ // Cleanup: prune empty (non-persistent) tab groups, collapse single-child splits.
143
+ cleanupTree(root);
144
+ return true;
145
+ }
146
+ function findFirstTabsNode(node) {
147
+ if (node.type === 'tabs')
148
+ return node;
149
+ if (node.type === 'split') {
150
+ for (const c of node.children) {
151
+ const hit = findFirstTabsNode(c);
152
+ if (hit)
153
+ return hit;
154
+ }
155
+ }
156
+ return null;
157
+ }
@@ -0,0 +1,78 @@
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
+ /** Which side of a split the dropped tab should land on. */
39
+ export type SplitSide = 'left' | 'right' | 'top' | 'bottom';
40
+ /**
41
+ * Split a slot-leaf (or tabs-leaf) into a new SplitNode, placing a
42
+ * single-tab group containing `entry` on the requested side and the
43
+ * existing node on the other side. The existing node is preserved
44
+ * as-is (it keeps its slotId, viewId, etc. — critical for re-parenting
45
+ * of the untouched side).
46
+ *
47
+ * Sides map to split direction:
48
+ * left/right → horizontal split, new tab goes left or right
49
+ * top/bottom → vertical split, new tab goes top or bottom
50
+ *
51
+ * The target is identified by a LayoutPath from the root, because
52
+ * splitting requires rewriting the parent's child array and we need
53
+ * the parent reference. If the target IS the root, the root itself is
54
+ * replaced — callers that pass the root by reference need the wrapper
55
+ * form, so this function returns the new subtree and the caller
56
+ * reassigns parent.children[i] = returned value (or reassigns root).
57
+ */
58
+ export declare function makeSplitWithNewTab(existing: LayoutNode, entry: TabEntry, side: SplitSide): SplitNode;
59
+ /**
60
+ * Apply a slot-split as a tree mutation: find the target node at the
61
+ * given path and replace it with a new split. Handles the root case
62
+ * (path = []) by mutating the root's fields in place. The root object
63
+ * identity is preserved so Svelte's $state proxy keeps its reactivity.
64
+ */
65
+ export declare function splitNodeAtPath(root: LayoutNode, path: LayoutPath, entry: TabEntry, side: SplitSide): void;
66
+ /** Walk a LayoutPath and return the node at its tip, or null. */
67
+ export declare function nodeAtPath(root: LayoutNode, path: LayoutPath): LayoutNode | null;
68
+ /**
69
+ * Post-mutation cleanup pass. Removes empty tabs groups from their
70
+ * parents and collapses single-child splits. Runs until the tree
71
+ * stabilizes (a collapse can reveal another single-child split one
72
+ * level up).
73
+ *
74
+ * Called once at the end of every drag commit. Called separately from
75
+ * the mutation primitives so callers can do "remove then insert into
76
+ * the same group" without the group being collapsed between calls.
77
+ */
78
+ export declare function cleanupTree(root: LayoutNode): void;