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,260 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* LayoutRenderer — recursive walker for a LayoutNode tree.
|
|
4
|
+
*
|
|
5
|
+
* Dispatches on the node kind:
|
|
6
|
+
* split → <ResizableSplitter> with one recursive <Self> per pane
|
|
7
|
+
* tabs → <TabbedPanel> with one <SlotContainer> per tab, a
|
|
8
|
+
* SlotDropZone overlay, and a drag controller wired to
|
|
9
|
+
* the drag engine
|
|
10
|
+
* slot → <SlotContainer> wrapped in a SlotDropZone
|
|
11
|
+
*
|
|
12
|
+
* Props: only `path` — a list of child indices from the root. The
|
|
13
|
+
* node itself is resolved as a $derived from `layoutStore.root` by
|
|
14
|
+
* walking the path. This is how we sidestep Svelte 5's
|
|
15
|
+
* `ownership_invalid_mutation` warning for a recursive component:
|
|
16
|
+
* - Passing `node` as a prop would make every mutation (e.g.
|
|
17
|
+
* `node.activeTab = i`) a write to a child-received prop, which
|
|
18
|
+
* the ownership tracker flags regardless of whether the
|
|
19
|
+
* underlying $state lives in a component or in a module.
|
|
20
|
+
* - Passing only `path` and deriving `node` locally means
|
|
21
|
+
* mutations go through a $derived of module state — no prop is
|
|
22
|
+
* involved, no ownership warning fires.
|
|
23
|
+
*
|
|
24
|
+
* `path` is a plain array, not reactive state, so there is no
|
|
25
|
+
* ownership concern on it. Each recursive call builds a new child
|
|
26
|
+
* path via `[...path, i]`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { TabsNode, LayoutNode } from './types';
|
|
30
|
+
import ResizableSplitter from '../primitives/ResizableSplitter.svelte';
|
|
31
|
+
import TabbedPanel, { type TabDragController } from '../primitives/TabbedPanel.svelte';
|
|
32
|
+
import SlotContainer from './SlotContainer.svelte';
|
|
33
|
+
import SlotDropZone from './SlotDropZone.svelte';
|
|
34
|
+
import Self from './LayoutRenderer.svelte';
|
|
35
|
+
import { layoutStore } from './store.svelte';
|
|
36
|
+
import { nodeAtPath } from './ops';
|
|
37
|
+
import { isSlotClosable, isSlotDirty } from './slotHostPool.svelte';
|
|
38
|
+
import { closeTab } from './inspection';
|
|
39
|
+
import {
|
|
40
|
+
dragState,
|
|
41
|
+
beginTabDrag,
|
|
42
|
+
setDropTarget,
|
|
43
|
+
clearDropTarget,
|
|
44
|
+
suppressNextClick,
|
|
45
|
+
} from './drag.svelte';
|
|
46
|
+
|
|
47
|
+
let { path = [] }: { path?: number[] } = $props();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the current node by walking `layoutStore.root` along the
|
|
51
|
+
* path. $derived tracks the reads so Svelte re-runs this when the
|
|
52
|
+
* layout mutates. If the path becomes invalid mid-mutation (a
|
|
53
|
+
* cleanup pass can collapse nodes out from under a recursive
|
|
54
|
+
* renderer), we render null.
|
|
55
|
+
*/
|
|
56
|
+
const node = $derived(nodeAtPath(layoutStore.root, path));
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build a TabDragController bound to the current tabs node.
|
|
60
|
+
* Rebuilt whenever `node` changes identity (mutation can replace
|
|
61
|
+
* the node at this path with a new one during cleanup).
|
|
62
|
+
*/
|
|
63
|
+
function makeController(tabsNode: TabsNode): TabDragController {
|
|
64
|
+
return {
|
|
65
|
+
get isDragging() {
|
|
66
|
+
return dragState.phase === 'dragging';
|
|
67
|
+
},
|
|
68
|
+
onPointerDown(index, event, element) {
|
|
69
|
+
const entry = tabsNode.tabs[index];
|
|
70
|
+
if (!entry) return;
|
|
71
|
+
beginTabDrag(entry.slotId, entry, event, element);
|
|
72
|
+
},
|
|
73
|
+
onStripHover(stripRect, pointerX, pointerY, tabRects) {
|
|
74
|
+
if (pointerY < stripRect.top || pointerY > stripRect.bottom) {
|
|
75
|
+
clearDropTarget((t) => t.kind === 'strip' && t.tabsNode === tabsNode);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
let insertIndex = tabRects.length;
|
|
79
|
+
for (let i = 0; i < tabRects.length; i++) {
|
|
80
|
+
const r = tabRects[i];
|
|
81
|
+
const mid = r.left + r.width / 2;
|
|
82
|
+
if (pointerX < mid) {
|
|
83
|
+
insertIndex = i;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Normalize for same-strip reorder so the engine's commit
|
|
88
|
+
// doesn't double-count the removal shift: if the source tab
|
|
89
|
+
// lives in this strip at index < insertIndex, subtract one.
|
|
90
|
+
const source = dragState.source;
|
|
91
|
+
if (source) {
|
|
92
|
+
const srcIdx = tabsNode.tabs.findIndex((t) => t.slotId === source.slotId);
|
|
93
|
+
if (srcIdx >= 0 && srcIdx < insertIndex) insertIndex -= 1;
|
|
94
|
+
}
|
|
95
|
+
setDropTarget({ kind: 'strip', tabsNode, insertIndex });
|
|
96
|
+
return insertIndex;
|
|
97
|
+
},
|
|
98
|
+
onStripLeave() {
|
|
99
|
+
clearDropTarget((t) => t.kind === 'strip' && t.tabsNode === tabsNode);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Narrowing helpers — Svelte templates can't narrow a $derived
|
|
105
|
+
// across block boundaries, so we re-cast inside each branch below.
|
|
106
|
+
// These getters are just there to make the template readable.
|
|
107
|
+
function asSplit(n: LayoutNode) {
|
|
108
|
+
return n.type === 'split' ? n : null;
|
|
109
|
+
}
|
|
110
|
+
function asTabs(n: LayoutNode) {
|
|
111
|
+
return n.type === 'tabs' ? n : null;
|
|
112
|
+
}
|
|
113
|
+
function asSlot(n: LayoutNode) {
|
|
114
|
+
return n.type === 'slot' ? n : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Build per-tab closable flags from reactive pool state. */
|
|
118
|
+
function tabClosable(tabs: import('./types').TabEntry[]): (boolean | undefined)[] {
|
|
119
|
+
return tabs.map((t) => isSlotClosable(t.slotId) || undefined);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Build per-tab dirty flags for TabbedPanel from live pool state. */
|
|
123
|
+
function tabDirty(tabs: import('./types').TabEntry[]): (boolean | undefined)[] {
|
|
124
|
+
return tabs.map((t) => isSlotDirty(t.slotId) || undefined);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Handle close button click from TabbedPanel. */
|
|
128
|
+
function handleTabClose(tabs: import('./types').TabEntry[], index: number) {
|
|
129
|
+
const entry = tabs[index];
|
|
130
|
+
if (entry) closeTab(entry.slotId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Svelte action: mount a custom empty renderer into the element. */
|
|
134
|
+
function mountEmptyRenderer(node: HTMLElement, renderer: (el: HTMLElement) => void) {
|
|
135
|
+
renderer(node);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Drop handler for empty persistent tab groups. The whole area acts as
|
|
140
|
+
* a single "insert as first tab" target — no quadrant splits.
|
|
141
|
+
*/
|
|
142
|
+
function onEmptyTabsDrop(_e: PointerEvent, tabsNode: import('./types').TabsNode) {
|
|
143
|
+
if (dragState.phase !== 'dragging') return;
|
|
144
|
+
setDropTarget({ kind: 'strip', tabsNode, insertIndex: 0 });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onEmptyTabsLeave() {
|
|
148
|
+
clearDropTarget((t) => t.kind === 'strip' && t.insertIndex === 0);
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
{#if node}
|
|
153
|
+
{#if node.type === 'split'}
|
|
154
|
+
{@const split = asSplit(node)!}
|
|
155
|
+
<ResizableSplitter
|
|
156
|
+
direction={split.direction}
|
|
157
|
+
sizes={split.sizes}
|
|
158
|
+
pinned={split.pinned}
|
|
159
|
+
collapsed={split.collapsed}
|
|
160
|
+
count={split.children.length}
|
|
161
|
+
pane={splitPane}
|
|
162
|
+
onResize={(i, v) => (split.sizes[i] = v)}
|
|
163
|
+
onCollapseToggle={(i, v) => {
|
|
164
|
+
if (!split.collapsed) split.collapsed = split.children.map(() => false);
|
|
165
|
+
split.collapsed[i] = v;
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
{#snippet splitPane(i: number)}
|
|
169
|
+
<Self path={[...path, i]} />
|
|
170
|
+
{/snippet}
|
|
171
|
+
{:else if node.type === 'tabs'}
|
|
172
|
+
{@const tabs = asTabs(node)}
|
|
173
|
+
{#if tabs && tabs.tabs.length > 0}
|
|
174
|
+
{@const controller = makeController(tabs)}
|
|
175
|
+
<TabbedPanel
|
|
176
|
+
labels={tabs.tabs.map((t) => t.label)}
|
|
177
|
+
icons={tabs.tabs.map((t) => t.icon)}
|
|
178
|
+
activeTab={tabs.activeTab}
|
|
179
|
+
onActiveChange={(i) => (tabs.activeTab = i)}
|
|
180
|
+
body={tabBody}
|
|
181
|
+
dragController={controller}
|
|
182
|
+
clickGuard={suppressNextClick}
|
|
183
|
+
closable={tabClosable(tabs.tabs)}
|
|
184
|
+
dirty={tabDirty(tabs.tabs)}
|
|
185
|
+
onClose={(i) => handleTabClose(tabs.tabs, i)}
|
|
186
|
+
/>
|
|
187
|
+
{#snippet tabBody(i: number)}
|
|
188
|
+
{@const entry = tabs.tabs[i]}
|
|
189
|
+
<div class="tab-slot-wrapper">
|
|
190
|
+
<SlotContainer node={{ type: 'slot', slotId: entry.slotId, viewId: entry.viewId }} label={entry.label} />
|
|
191
|
+
<SlotDropZone path={path} />
|
|
192
|
+
</div>
|
|
193
|
+
{/snippet}
|
|
194
|
+
{:else if tabs?.persistent}
|
|
195
|
+
<div class="empty-tabs-placeholder">
|
|
196
|
+
{#if tabs.emptyRenderer}
|
|
197
|
+
<div class="empty-tabs-custom" use:mountEmptyRenderer={tabs.emptyRenderer}></div>
|
|
198
|
+
{:else}
|
|
199
|
+
<div class="empty-tabs-default">
|
|
200
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
201
|
+
<div
|
|
202
|
+
class="empty-tabs-drop"
|
|
203
|
+
onpointermove={(e) => onEmptyTabsDrop(e, tabs)}
|
|
204
|
+
onpointerleave={onEmptyTabsLeave}
|
|
205
|
+
></div>
|
|
206
|
+
</div>
|
|
207
|
+
{/if}
|
|
208
|
+
</div>
|
|
209
|
+
{/if}
|
|
210
|
+
{:else}
|
|
211
|
+
{@const slot = asSlot(node)!}
|
|
212
|
+
<div class="leaf-slot-wrapper">
|
|
213
|
+
<SlotContainer node={slot} />
|
|
214
|
+
<SlotDropZone path={path} />
|
|
215
|
+
</div>
|
|
216
|
+
{/if}
|
|
217
|
+
{/if}
|
|
218
|
+
|
|
219
|
+
<style>
|
|
220
|
+
.tab-slot-wrapper,
|
|
221
|
+
.leaf-slot-wrapper {
|
|
222
|
+
position: absolute;
|
|
223
|
+
inset: 0;
|
|
224
|
+
min-width: 0;
|
|
225
|
+
min-height: 0;
|
|
226
|
+
}
|
|
227
|
+
.empty-tabs-placeholder {
|
|
228
|
+
width: 100%;
|
|
229
|
+
height: 100%;
|
|
230
|
+
min-width: 0;
|
|
231
|
+
min-height: 0;
|
|
232
|
+
position: relative;
|
|
233
|
+
}
|
|
234
|
+
.empty-tabs-default {
|
|
235
|
+
position: absolute;
|
|
236
|
+
inset: 0;
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-direction: column;
|
|
239
|
+
align-items: center;
|
|
240
|
+
justify-content: center;
|
|
241
|
+
color: var(--shell-fg-muted);
|
|
242
|
+
font-size: 12px;
|
|
243
|
+
background:
|
|
244
|
+
repeating-linear-gradient(
|
|
245
|
+
45deg,
|
|
246
|
+
var(--shell-bg) 0 10px,
|
|
247
|
+
var(--shell-bg-elevated) 10px 20px
|
|
248
|
+
);
|
|
249
|
+
border: 1px dashed var(--shell-border-strong);
|
|
250
|
+
}
|
|
251
|
+
.empty-tabs-custom {
|
|
252
|
+
position: absolute;
|
|
253
|
+
inset: 0;
|
|
254
|
+
}
|
|
255
|
+
.empty-tabs-drop {
|
|
256
|
+
position: absolute;
|
|
257
|
+
inset: 0;
|
|
258
|
+
pointer-events: auto;
|
|
259
|
+
}
|
|
260
|
+
</style>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* SlotContainer — the leaf of the layout tree and the hand-off point
|
|
4
|
+
* between the framework and shard-contributed views.
|
|
5
|
+
*
|
|
6
|
+
* Phase 6 change: the mounted view no longer lives inside this
|
|
7
|
+
* component's own DOM. Instead, the slot host is owned by the
|
|
8
|
+
* `slotHostPool` module and SlotContainer merely attaches (and later
|
|
9
|
+
* releases) the pooled host to its own wrapper. This is what makes
|
|
10
|
+
* drag-to-reorganize survive: when a tab moves, the old SlotContainer
|
|
11
|
+
* tears down and a new one mounts, but the pooled host (and the view
|
|
12
|
+
* mounted into it) is re-parented to the new wrapper without being
|
|
13
|
+
* destroyed. See slotHostPool.ts for the refcount / deferred-destroy
|
|
14
|
+
* details.
|
|
15
|
+
*
|
|
16
|
+
* Responsibilities:
|
|
17
|
+
* 1. Acquire the pooled host for `node.slotId` on mount and append
|
|
18
|
+
* it to the wrapper.
|
|
19
|
+
* 2. Release the pooled host on unmount. The pool decides whether
|
|
20
|
+
* that's a genuine destroy or the first half of a re-parent.
|
|
21
|
+
* 3. If no factory is registered for the viewId (empty slot or the
|
|
22
|
+
* shard providing it hasn't activated yet), render a placeholder
|
|
23
|
+
* in the wrapper alongside the empty host. A local ResizeObserver
|
|
24
|
+
* feeds the placeholder's dimensions readout.
|
|
25
|
+
*
|
|
26
|
+
* The view's own onResize delivery is NOT SlotContainer's job — the
|
|
27
|
+
* pool owns a ResizeObserver on each host that outlives this component
|
|
28
|
+
* across re-parents. See slotHostPool.ts.
|
|
29
|
+
*
|
|
30
|
+
* Note on the placeholder: the pool creates a host even when there is
|
|
31
|
+
* no factory, so the placeholder is layered *on top* of the empty
|
|
32
|
+
* host. That keeps the acquire/release path uniform — phase 7's
|
|
33
|
+
* "factory registered after layout render" case will just replace the
|
|
34
|
+
* host's contents without rewriting SlotContainer.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { SlotNode } from './types';
|
|
38
|
+
import { getView } from '../shards/registry';
|
|
39
|
+
import { acquireSlotHost, releaseSlotHost } from './slotHostPool.svelte';
|
|
40
|
+
|
|
41
|
+
let { node, label = '' }: { node: SlotNode; label?: string } = $props();
|
|
42
|
+
|
|
43
|
+
let wrapper: HTMLDivElement | undefined = $state();
|
|
44
|
+
let width = $state(0);
|
|
45
|
+
let height = $state(0);
|
|
46
|
+
// Whether a factory is registered — drives the placeholder. The pool
|
|
47
|
+
// owns the actual mount call; we mirror the registry lookup here just
|
|
48
|
+
// to decide whether to show the "no factory" hint.
|
|
49
|
+
const hasFactory = $derived(node.viewId ? !!getView(node.viewId) : false);
|
|
50
|
+
|
|
51
|
+
$effect(() => {
|
|
52
|
+
if (!wrapper) return;
|
|
53
|
+
|
|
54
|
+
const host = acquireSlotHost(node.slotId, node.viewId, label || node.viewId || node.slotId);
|
|
55
|
+
wrapper.appendChild(host);
|
|
56
|
+
|
|
57
|
+
// Local observer exists only to drive the placeholder's dims text;
|
|
58
|
+
// the view's own onResize is delivered by the pool.
|
|
59
|
+
const ro = new ResizeObserver((entries) => {
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const box = entry.contentRect;
|
|
62
|
+
width = Math.round(box.width);
|
|
63
|
+
height = Math.round(box.height);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
ro.observe(wrapper);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
ro.disconnect();
|
|
70
|
+
releaseSlotHost(node.slotId);
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<div
|
|
76
|
+
class="slot"
|
|
77
|
+
data-slot-id={node.slotId}
|
|
78
|
+
data-view-id={node.viewId ?? ''}
|
|
79
|
+
bind:this={wrapper}
|
|
80
|
+
>
|
|
81
|
+
{#if !hasFactory}
|
|
82
|
+
<div class="slot-placeholder">
|
|
83
|
+
<div class="slot-id">{node.slotId}</div>
|
|
84
|
+
<div class="slot-meta">
|
|
85
|
+
{#if node.viewId}
|
|
86
|
+
no factory for <code>{node.viewId}</code>
|
|
87
|
+
{:else}
|
|
88
|
+
<em>empty slot</em>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
<div class="slot-dims">{width} × {height}</div>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
.slot {
|
|
98
|
+
position: relative;
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
min-width: 0;
|
|
102
|
+
min-height: 0;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
}
|
|
105
|
+
.slot-placeholder {
|
|
106
|
+
position: absolute;
|
|
107
|
+
inset: 0;
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
gap: var(--shell-pad-sm);
|
|
113
|
+
color: var(--shell-fg-muted);
|
|
114
|
+
font-size: 12px;
|
|
115
|
+
text-align: center;
|
|
116
|
+
padding: var(--shell-pad-md);
|
|
117
|
+
background:
|
|
118
|
+
repeating-linear-gradient(
|
|
119
|
+
45deg,
|
|
120
|
+
var(--shell-bg) 0 10px,
|
|
121
|
+
var(--shell-bg-elevated) 10px 20px
|
|
122
|
+
);
|
|
123
|
+
border: 1px dashed var(--shell-border-strong);
|
|
124
|
+
pointer-events: none;
|
|
125
|
+
}
|
|
126
|
+
.slot-id {
|
|
127
|
+
color: var(--shell-fg);
|
|
128
|
+
font-family: var(--shell-font-mono);
|
|
129
|
+
font-size: 13px;
|
|
130
|
+
}
|
|
131
|
+
.slot-meta code {
|
|
132
|
+
font-family: var(--shell-font-mono);
|
|
133
|
+
color: var(--shell-accent);
|
|
134
|
+
}
|
|
135
|
+
.slot-dims {
|
|
136
|
+
font-family: var(--shell-font-mono);
|
|
137
|
+
color: var(--shell-fg-subtle);
|
|
138
|
+
font-size: 11px;
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SlotNode } from './types';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
node: SlotNode;
|
|
4
|
+
label?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const SlotContainer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type SlotContainer = ReturnType<typeof SlotContainer>;
|
|
8
|
+
export default SlotContainer;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* SlotDropZone — an overlay covering a slot's body that, during a
|
|
4
|
+
* drag, reports 4-quadrant split-drop targets to the drag engine
|
|
5
|
+
* and draws a colored quadrant highlight matching the hovered side.
|
|
6
|
+
*
|
|
7
|
+
* Sits in the tab-body pane (for tab leaves) or directly over a
|
|
8
|
+
* standalone slot container. Does not intercept pointer events when
|
|
9
|
+
* no drag is active; during a drag, it captures pointermove to
|
|
10
|
+
* compute the hovered quadrant.
|
|
11
|
+
*
|
|
12
|
+
* Quadrant math:
|
|
13
|
+
* The body is divided into 4 triangles meeting at the center, so
|
|
14
|
+
* each triangle maps the nearest edge to a split side. Top → top
|
|
15
|
+
* split (vertical, new tab above). Same for bottom / left / right.
|
|
16
|
+
* The inner "center" region is deliberately absent in phase 6 —
|
|
17
|
+
* we don't support "merge into same tabs group" via body drop,
|
|
18
|
+
* only via strip drop. This keeps the UX unambiguous: body = split,
|
|
19
|
+
* strip = merge.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { dragState, setDropTarget, clearDropTarget, type DropTarget } from './drag.svelte';
|
|
23
|
+
import type { LayoutPath, SplitSide } from './ops';
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
path,
|
|
27
|
+
}: {
|
|
28
|
+
/** Path of the node this zone covers, used when reporting the drop. */
|
|
29
|
+
path: LayoutPath;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
let zoneEl: HTMLDivElement | undefined = $state();
|
|
33
|
+
let hoveredSide: SplitSide | null = $state(null);
|
|
34
|
+
|
|
35
|
+
// Don't capture pointer events unless a drag is in progress — otherwise
|
|
36
|
+
// the zone would shadow the slot's own interactions.
|
|
37
|
+
const active = $derived(dragState.phase === 'dragging');
|
|
38
|
+
|
|
39
|
+
function quadrantFor(x: number, y: number, rect: DOMRect): SplitSide {
|
|
40
|
+
const cx = rect.left + rect.width / 2;
|
|
41
|
+
const cy = rect.top + rect.height / 2;
|
|
42
|
+
const dx = x - cx;
|
|
43
|
+
const dy = y - cy;
|
|
44
|
+
// Triangles: compare which signed half-plane the point falls in,
|
|
45
|
+
// determined by the cell aspect ratio so the diagonals meet at the
|
|
46
|
+
// center regardless of shape.
|
|
47
|
+
const nx = dx / rect.width;
|
|
48
|
+
const ny = dy / rect.height;
|
|
49
|
+
if (Math.abs(nx) > Math.abs(ny)) {
|
|
50
|
+
return nx < 0 ? 'left' : 'right';
|
|
51
|
+
}
|
|
52
|
+
return ny < 0 ? 'top' : 'bottom';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onMove(e: PointerEvent) {
|
|
56
|
+
if (!zoneEl) return;
|
|
57
|
+
const rect = zoneEl.getBoundingClientRect();
|
|
58
|
+
// If pointer is outside the zone (pointercapture from elsewhere),
|
|
59
|
+
// clear.
|
|
60
|
+
if (
|
|
61
|
+
e.clientX < rect.left ||
|
|
62
|
+
e.clientX > rect.right ||
|
|
63
|
+
e.clientY < rect.top ||
|
|
64
|
+
e.clientY > rect.bottom
|
|
65
|
+
) {
|
|
66
|
+
if (hoveredSide !== null) {
|
|
67
|
+
hoveredSide = null;
|
|
68
|
+
clearDropTarget((t) => t.kind === 'split' && t.path.join('/') === path.join('/'));
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const side = quadrantFor(e.clientX, e.clientY, rect);
|
|
73
|
+
if (side !== hoveredSide) {
|
|
74
|
+
hoveredSide = side;
|
|
75
|
+
const target: DropTarget = { kind: 'split', path: [...path], side };
|
|
76
|
+
setDropTarget(target);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onLeave() {
|
|
81
|
+
if (hoveredSide !== null) {
|
|
82
|
+
hoveredSide = null;
|
|
83
|
+
clearDropTarget((t) => t.kind === 'split' && t.path.join('/') === path.join('/'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
89
|
+
<div
|
|
90
|
+
class="slot-drop-zone"
|
|
91
|
+
class:active
|
|
92
|
+
bind:this={zoneEl}
|
|
93
|
+
onpointermove={onMove}
|
|
94
|
+
onpointerleave={onLeave}
|
|
95
|
+
>
|
|
96
|
+
{#if hoveredSide}
|
|
97
|
+
<div class="quad-highlight quad-{hoveredSide}"></div>
|
|
98
|
+
{/if}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<style>
|
|
102
|
+
.slot-drop-zone {
|
|
103
|
+
position: absolute;
|
|
104
|
+
inset: 0;
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
}
|
|
107
|
+
.slot-drop-zone.active {
|
|
108
|
+
pointer-events: auto;
|
|
109
|
+
}
|
|
110
|
+
.quad-highlight {
|
|
111
|
+
position: absolute;
|
|
112
|
+
background: var(--shell-accent);
|
|
113
|
+
opacity: 0.18;
|
|
114
|
+
border: 1px dashed var(--shell-accent);
|
|
115
|
+
pointer-events: none;
|
|
116
|
+
transition: inset 80ms ease;
|
|
117
|
+
}
|
|
118
|
+
.quad-highlight.quad-left { top: 0; bottom: 0; left: 0; right: 50%; }
|
|
119
|
+
.quad-highlight.quad-right { top: 0; bottom: 0; left: 50%; right: 0; }
|
|
120
|
+
.quad-highlight.quad-top { left: 0; right: 0; top: 0; bottom: 50%; }
|
|
121
|
+
.quad-highlight.quad-bottom { left: 0; right: 0; top: 50%; bottom: 0; }
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LayoutPath } from './ops';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Path of the node this zone covers, used when reporting the drop. */
|
|
4
|
+
path: LayoutPath;
|
|
5
|
+
};
|
|
6
|
+
declare const SlotDropZone: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type SlotDropZone = ReturnType<typeof SlotDropZone>;
|
|
8
|
+
export default SlotDropZone;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TabEntry, TabsNode } from './types';
|
|
2
|
+
import { type LayoutPath, type SplitSide } from './ops';
|
|
3
|
+
export type DropTarget = {
|
|
4
|
+
kind: 'strip';
|
|
5
|
+
tabsNode: TabsNode;
|
|
6
|
+
/** Insertion index within tabsNode.tabs (0..length). */
|
|
7
|
+
insertIndex: number;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'split';
|
|
10
|
+
path: LayoutPath;
|
|
11
|
+
side: SplitSide;
|
|
12
|
+
};
|
|
13
|
+
interface DragSource {
|
|
14
|
+
slotId: string;
|
|
15
|
+
entry: TabEntry;
|
|
16
|
+
/** The tab's viewport rect at drag start — used to offset the ghost. */
|
|
17
|
+
startRect: DOMRect;
|
|
18
|
+
/** Pointer offset inside the tab at drag start. */
|
|
19
|
+
offsetX: number;
|
|
20
|
+
offsetY: number;
|
|
21
|
+
}
|
|
22
|
+
interface DragState {
|
|
23
|
+
phase: 'idle' | 'pending' | 'dragging';
|
|
24
|
+
source: DragSource | null;
|
|
25
|
+
pointerX: number;
|
|
26
|
+
pointerY: number;
|
|
27
|
+
target: DropTarget | null;
|
|
28
|
+
}
|
|
29
|
+
export declare const dragState: DragState;
|
|
30
|
+
export declare function suppressNextClick(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Begin a potential tab drag. Call from pointerdown on a tab element.
|
|
33
|
+
* This does not yet enter the dragging phase — movement past the
|
|
34
|
+
* threshold is required.
|
|
35
|
+
*/
|
|
36
|
+
export declare function beginTabDrag(slotId: string, entry: TabEntry, event: PointerEvent, tabElement: HTMLElement): void;
|
|
37
|
+
/**
|
|
38
|
+
* Called by drop zone components when the pointer is over them. The
|
|
39
|
+
* last call wins, so innermost / most-specific zones should call this
|
|
40
|
+
* on pointermove over their geometry. `clearDropTarget` is called when
|
|
41
|
+
* the pointer leaves.
|
|
42
|
+
*/
|
|
43
|
+
export declare function setDropTarget(target: DropTarget): void;
|
|
44
|
+
export declare function clearDropTarget(match?: (target: DropTarget) => boolean): void;
|
|
45
|
+
export {};
|