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,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;
|