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,333 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* ResizableSplitter — a generic N-pane split container with drag handles.
|
|
4
|
+
*
|
|
5
|
+
* Used by <LayoutRenderer> to implement `split` nodes. Also available as a
|
|
6
|
+
* standalone primitive for shards that need internal split UI.
|
|
7
|
+
*
|
|
8
|
+
* Sizing model mirrors docs/design/layout.md:
|
|
9
|
+
* - Each pane has a size value + a mode ('fr' | 'px').
|
|
10
|
+
* - 'fr' panes are proportional flex-grow children and absorb window
|
|
11
|
+
* resize deltas and drag deltas against each other.
|
|
12
|
+
* - 'px' panes are pixel-pinned (flex: 0 0 Npx) and stay fixed during
|
|
13
|
+
* window resize; dragging a handle adjacent to a px pane resizes the
|
|
14
|
+
* px pane in absolute pixels.
|
|
15
|
+
*
|
|
16
|
+
* `sizes` is bindable so the parent (layout tree) can observe user drags.
|
|
17
|
+
* Phase 2 does not yet persist sizes — phase 7 wires that up.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Snippet } from 'svelte';
|
|
21
|
+
import type { SizeMode, SplitDirection } from '../layout/types';
|
|
22
|
+
|
|
23
|
+
const MIN_PX = 40;
|
|
24
|
+
const COLLAPSED_PX = 28;
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
direction,
|
|
28
|
+
sizes,
|
|
29
|
+
pinned,
|
|
30
|
+
collapsed,
|
|
31
|
+
count,
|
|
32
|
+
pane,
|
|
33
|
+
onResize,
|
|
34
|
+
onCollapseToggle,
|
|
35
|
+
}: {
|
|
36
|
+
direction: SplitDirection;
|
|
37
|
+
/**
|
|
38
|
+
* Per-pane sizes, read-only. The splitter computes flex bases
|
|
39
|
+
* from this array but does not mutate it — writes go out through
|
|
40
|
+
* `onResize`. Treating sizes as read-only keeps the primitive
|
|
41
|
+
* decoupled from how the caller stores its layout, and sidesteps
|
|
42
|
+
* Svelte 5's `ownership_invalid_mutation` warning that fires when
|
|
43
|
+
* a child component writes into a prop it didn't own.
|
|
44
|
+
*/
|
|
45
|
+
sizes: number[];
|
|
46
|
+
pinned?: SizeMode[];
|
|
47
|
+
/** Per-pane collapsed state. Omitted entries default to false. */
|
|
48
|
+
collapsed?: boolean[];
|
|
49
|
+
/** Number of panes — `sizes.length` should match. */
|
|
50
|
+
count: number;
|
|
51
|
+
/** Snippet invoked once per pane with the pane index. */
|
|
52
|
+
pane: Snippet<[number]>;
|
|
53
|
+
/**
|
|
54
|
+
* Called whenever the splitter wants to update a pane's size.
|
|
55
|
+
* The parent is expected to write the value back into whatever
|
|
56
|
+
* it stores sizes in. A single per-index callback (rather than a
|
|
57
|
+
* whole-array setter) matches the drag math, which updates at
|
|
58
|
+
* most two panes per move, and avoids allocating a new array on
|
|
59
|
+
* every pointermove frame.
|
|
60
|
+
*/
|
|
61
|
+
onResize?: (index: number, value: number) => void;
|
|
62
|
+
/** Called when a collapsed pane's header is clicked to toggle. */
|
|
63
|
+
onCollapseToggle?: (index: number, collapsed: boolean) => void;
|
|
64
|
+
} = $props();
|
|
65
|
+
|
|
66
|
+
let container: HTMLDivElement;
|
|
67
|
+
|
|
68
|
+
const modeOf = (i: number): SizeMode => pinned?.[i] ?? 'fr';
|
|
69
|
+
const isCollapsed = (i: number): boolean => collapsed?.[i] ?? false;
|
|
70
|
+
|
|
71
|
+
/** CSS `flex` shorthand for pane i. */
|
|
72
|
+
function flexFor(i: number): string {
|
|
73
|
+
if (isCollapsed(i)) return `0 0 ${COLLAPSED_PX}px`;
|
|
74
|
+
if (modeOf(i) === 'px') return `0 0 ${Math.max(MIN_PX, sizes[i])}px`;
|
|
75
|
+
// Proportional: grow = sizes[i], shrink = 1, basis = 0
|
|
76
|
+
return `${Math.max(0.0001, sizes[i])} 1 0`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type DragState = {
|
|
80
|
+
handleIndex: number; // boundary between child handleIndex and handleIndex+1
|
|
81
|
+
startClient: number; // pointer x/y at drag start
|
|
82
|
+
startSizes: number[]; // sizes snapshot at drag start
|
|
83
|
+
containerPx: number; // container length along split axis
|
|
84
|
+
totalFr: number; // sum of 'fr' sizes at drag start
|
|
85
|
+
frAvailPx: number; // pixels available to fr children at drag start
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let drag: DragState | null = $state(null);
|
|
89
|
+
|
|
90
|
+
function beginDrag(e: PointerEvent, handleIndex: number) {
|
|
91
|
+
// Disable resize handles adjacent to collapsed panes.
|
|
92
|
+
if (isCollapsed(handleIndex) || isCollapsed(handleIndex + 1)) return;
|
|
93
|
+
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
96
|
+
|
|
97
|
+
const rect = container.getBoundingClientRect();
|
|
98
|
+
const containerPx = direction === 'horizontal' ? rect.width : rect.height;
|
|
99
|
+
|
|
100
|
+
// Sum fr weights and subtract pixel-pinned pane sizes to get the pixel
|
|
101
|
+
// space the fr children collectively occupy.
|
|
102
|
+
let totalFr = 0;
|
|
103
|
+
let pxUsed = 0;
|
|
104
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
105
|
+
if (modeOf(i) === 'fr') totalFr += sizes[i];
|
|
106
|
+
else pxUsed += Math.max(MIN_PX, sizes[i]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
drag = {
|
|
110
|
+
handleIndex,
|
|
111
|
+
startClient: direction === 'horizontal' ? e.clientX : e.clientY,
|
|
112
|
+
startSizes: sizes.slice(),
|
|
113
|
+
containerPx,
|
|
114
|
+
totalFr,
|
|
115
|
+
frAvailPx: Math.max(1, containerPx - pxUsed),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function moveDrag(e: PointerEvent) {
|
|
120
|
+
if (!drag) return;
|
|
121
|
+
const client = direction === 'horizontal' ? e.clientX : e.clientY;
|
|
122
|
+
const deltaPx = client - drag.startClient;
|
|
123
|
+
|
|
124
|
+
const a = drag.handleIndex;
|
|
125
|
+
const b = a + 1;
|
|
126
|
+
const modeA = modeOf(a);
|
|
127
|
+
const modeB = modeOf(b);
|
|
128
|
+
|
|
129
|
+
// Send updates through onResize rather than mutating the prop
|
|
130
|
+
// directly. Writing into `sizes` would trip Svelte 5's ownership
|
|
131
|
+
// warning; the parent owns the array and re-derives it for us.
|
|
132
|
+
const frPerPx = drag.totalFr / drag.frAvailPx;
|
|
133
|
+
|
|
134
|
+
if (modeA === 'fr' && modeB === 'fr') {
|
|
135
|
+
// Convert delta px to fr; clamp both sides to MIN_PX worth of fr.
|
|
136
|
+
// Positive deltaFr grows pane a and shrinks pane b, so:
|
|
137
|
+
// - upper bound: deltaFr ≤ startSizes[b] - minFr (stop when b hits min)
|
|
138
|
+
// - lower bound: deltaFr ≥ -(startSizes[a] - minFr) (stop when a hits min)
|
|
139
|
+
const minFr = MIN_PX * frPerPx;
|
|
140
|
+
const deltaFr = deltaPx * frPerPx;
|
|
141
|
+
const maxDelta = drag.startSizes[b] - minFr;
|
|
142
|
+
const minDelta = -(drag.startSizes[a] - minFr);
|
|
143
|
+
const clamped = Math.min(Math.max(deltaFr, minDelta), maxDelta);
|
|
144
|
+
onResize?.(a, drag.startSizes[a] + clamped);
|
|
145
|
+
onResize?.(b, drag.startSizes[b] - clamped);
|
|
146
|
+
} else if (modeA === 'px' && modeB === 'fr') {
|
|
147
|
+
const maxDelta = drag.frAvailPx - MIN_PX; // fr side must keep MIN_PX
|
|
148
|
+
const minDelta = MIN_PX - drag.startSizes[a];
|
|
149
|
+
onResize?.(
|
|
150
|
+
a,
|
|
151
|
+
drag.startSizes[a] + Math.min(Math.max(deltaPx, minDelta), maxDelta),
|
|
152
|
+
);
|
|
153
|
+
} else if (modeA === 'fr' && modeB === 'px') {
|
|
154
|
+
const maxDelta = drag.startSizes[b] - MIN_PX;
|
|
155
|
+
const minDelta = -(drag.frAvailPx - MIN_PX);
|
|
156
|
+
onResize?.(
|
|
157
|
+
b,
|
|
158
|
+
drag.startSizes[b] - Math.min(Math.max(deltaPx, minDelta), maxDelta),
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
// both px
|
|
162
|
+
const maxDelta = drag.startSizes[b] - MIN_PX;
|
|
163
|
+
const minDelta = -(drag.startSizes[a] - MIN_PX);
|
|
164
|
+
const clamped = Math.min(Math.max(deltaPx, minDelta), maxDelta);
|
|
165
|
+
onResize?.(a, drag.startSizes[a] + clamped);
|
|
166
|
+
onResize?.(b, drag.startSizes[b] - clamped);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function endDrag(e: PointerEvent) {
|
|
171
|
+
if (!drag) return;
|
|
172
|
+
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
|
173
|
+
drag = null;
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<div
|
|
178
|
+
class="splitter"
|
|
179
|
+
class:horizontal={direction === 'horizontal'}
|
|
180
|
+
class:vertical={direction === 'vertical'}
|
|
181
|
+
bind:this={container}
|
|
182
|
+
>
|
|
183
|
+
{#each Array(count) as _, i (i)}
|
|
184
|
+
<div
|
|
185
|
+
class="splitter-pane"
|
|
186
|
+
class:collapsed={isCollapsed(i)}
|
|
187
|
+
style="flex: {flexFor(i)};"
|
|
188
|
+
>
|
|
189
|
+
{#if isCollapsed(i)}
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
class="collapse-header"
|
|
193
|
+
class:horizontal={direction === 'horizontal'}
|
|
194
|
+
class:vertical={direction === 'vertical'}
|
|
195
|
+
onclick={() => onCollapseToggle?.(i, false)}
|
|
196
|
+
aria-label="Expand pane"
|
|
197
|
+
>
|
|
198
|
+
<span class="collapse-icon">{direction === 'horizontal' ? '▸' : '▾'}</span>
|
|
199
|
+
</button>
|
|
200
|
+
{:else}
|
|
201
|
+
{#if onCollapseToggle}
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
class="collapse-header expanded"
|
|
205
|
+
class:horizontal={direction === 'horizontal'}
|
|
206
|
+
class:vertical={direction === 'vertical'}
|
|
207
|
+
onclick={() => onCollapseToggle?.(i, true)}
|
|
208
|
+
aria-label="Collapse pane"
|
|
209
|
+
>
|
|
210
|
+
<span class="collapse-icon">{direction === 'horizontal' ? '◂' : '▴'}</span>
|
|
211
|
+
</button>
|
|
212
|
+
{/if}
|
|
213
|
+
<div class="pane-content">
|
|
214
|
+
{@render pane(i)}
|
|
215
|
+
</div>
|
|
216
|
+
{/if}
|
|
217
|
+
</div>
|
|
218
|
+
{#if i < count - 1}
|
|
219
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
220
|
+
<div
|
|
221
|
+
class="splitter-handle"
|
|
222
|
+
class:dragging={drag?.handleIndex === i}
|
|
223
|
+
class:disabled={isCollapsed(i) || isCollapsed(i + 1)}
|
|
224
|
+
onpointerdown={(e) => beginDrag(e, i)}
|
|
225
|
+
onpointermove={moveDrag}
|
|
226
|
+
onpointerup={endDrag}
|
|
227
|
+
onpointercancel={endDrag}
|
|
228
|
+
role="separator"
|
|
229
|
+
aria-orientation={direction === 'horizontal' ? 'vertical' : 'horizontal'}
|
|
230
|
+
></div>
|
|
231
|
+
{/if}
|
|
232
|
+
{/each}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<style>
|
|
236
|
+
.splitter {
|
|
237
|
+
display: flex;
|
|
238
|
+
width: 100%;
|
|
239
|
+
height: 100%;
|
|
240
|
+
min-width: 0;
|
|
241
|
+
min-height: 0;
|
|
242
|
+
}
|
|
243
|
+
.splitter.horizontal { flex-direction: row; }
|
|
244
|
+
.splitter.vertical { flex-direction: column; }
|
|
245
|
+
|
|
246
|
+
.splitter-pane {
|
|
247
|
+
position: relative;
|
|
248
|
+
min-width: 0;
|
|
249
|
+
min-height: 0;
|
|
250
|
+
overflow: hidden;
|
|
251
|
+
display: flex;
|
|
252
|
+
}
|
|
253
|
+
.horizontal > .splitter-pane { flex-direction: row; }
|
|
254
|
+
.vertical > .splitter-pane { flex-direction: column; }
|
|
255
|
+
.splitter-pane.collapsed {
|
|
256
|
+
overflow: visible;
|
|
257
|
+
}
|
|
258
|
+
.pane-content {
|
|
259
|
+
flex: 1 1 0;
|
|
260
|
+
position: relative;
|
|
261
|
+
min-width: 0;
|
|
262
|
+
min-height: 0;
|
|
263
|
+
overflow: hidden;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.collapse-header {
|
|
267
|
+
appearance: none;
|
|
268
|
+
flex: 0 0 auto;
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
justify-content: center;
|
|
272
|
+
background: var(--shell-bg-elevated);
|
|
273
|
+
border: none;
|
|
274
|
+
color: var(--shell-fg-muted);
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
padding: 0;
|
|
277
|
+
font-size: 10px;
|
|
278
|
+
}
|
|
279
|
+
.collapse-header:hover {
|
|
280
|
+
color: var(--shell-fg);
|
|
281
|
+
background: var(--shell-accent-muted);
|
|
282
|
+
}
|
|
283
|
+
/* Suppress misleading hover feedback during drag-reorganize. */
|
|
284
|
+
:global(body[data-dragging]) .collapse-header {
|
|
285
|
+
pointer-events: none;
|
|
286
|
+
}
|
|
287
|
+
.collapse-header.horizontal {
|
|
288
|
+
width: 100%;
|
|
289
|
+
height: 100%;
|
|
290
|
+
writing-mode: vertical-rl;
|
|
291
|
+
}
|
|
292
|
+
.collapse-header.vertical {
|
|
293
|
+
width: 100%;
|
|
294
|
+
height: 100%;
|
|
295
|
+
}
|
|
296
|
+
.collapse-header.expanded.horizontal {
|
|
297
|
+
width: 16px;
|
|
298
|
+
height: 100%;
|
|
299
|
+
border-right: 1px solid var(--shell-border);
|
|
300
|
+
}
|
|
301
|
+
.collapse-header.expanded.vertical {
|
|
302
|
+
width: 100%;
|
|
303
|
+
height: 16px;
|
|
304
|
+
border-bottom: 1px solid var(--shell-border);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.splitter-handle {
|
|
308
|
+
flex: 0 0 auto;
|
|
309
|
+
background: var(--shell-border);
|
|
310
|
+
transition: background-color 120ms ease;
|
|
311
|
+
touch-action: none;
|
|
312
|
+
}
|
|
313
|
+
.splitter-handle:hover,
|
|
314
|
+
.splitter-handle.dragging {
|
|
315
|
+
background: var(--shell-accent);
|
|
316
|
+
}
|
|
317
|
+
:global(body[data-dragging]) .splitter-handle {
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
}
|
|
320
|
+
.splitter-handle.disabled {
|
|
321
|
+
cursor: default;
|
|
322
|
+
pointer-events: none;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.horizontal > .splitter-handle {
|
|
326
|
+
width: 4px;
|
|
327
|
+
cursor: col-resize;
|
|
328
|
+
}
|
|
329
|
+
.vertical > .splitter-handle {
|
|
330
|
+
height: 4px;
|
|
331
|
+
cursor: row-resize;
|
|
332
|
+
}
|
|
333
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { SizeMode, SplitDirection } from '../layout/types';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
direction: SplitDirection;
|
|
5
|
+
/**
|
|
6
|
+
* Per-pane sizes, read-only. The splitter computes flex bases
|
|
7
|
+
* from this array but does not mutate it — writes go out through
|
|
8
|
+
* `onResize`. Treating sizes as read-only keeps the primitive
|
|
9
|
+
* decoupled from how the caller stores its layout, and sidesteps
|
|
10
|
+
* Svelte 5's `ownership_invalid_mutation` warning that fires when
|
|
11
|
+
* a child component writes into a prop it didn't own.
|
|
12
|
+
*/
|
|
13
|
+
sizes: number[];
|
|
14
|
+
pinned?: SizeMode[];
|
|
15
|
+
/** Per-pane collapsed state. Omitted entries default to false. */
|
|
16
|
+
collapsed?: boolean[];
|
|
17
|
+
/** Number of panes — `sizes.length` should match. */
|
|
18
|
+
count: number;
|
|
19
|
+
/** Snippet invoked once per pane with the pane index. */
|
|
20
|
+
pane: Snippet<[number]>;
|
|
21
|
+
/**
|
|
22
|
+
* Called whenever the splitter wants to update a pane's size.
|
|
23
|
+
* The parent is expected to write the value back into whatever
|
|
24
|
+
* it stores sizes in. A single per-index callback (rather than a
|
|
25
|
+
* whole-array setter) matches the drag math, which updates at
|
|
26
|
+
* most two panes per move, and avoids allocating a new array on
|
|
27
|
+
* every pointermove frame.
|
|
28
|
+
*/
|
|
29
|
+
onResize?: (index: number, value: number) => void;
|
|
30
|
+
/** Called when a collapsed pane's header is clicked to toggle. */
|
|
31
|
+
onCollapseToggle?: (index: number, collapsed: boolean) => void;
|
|
32
|
+
};
|
|
33
|
+
declare const ResizableSplitter: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
34
|
+
type ResizableSplitter = ReturnType<typeof ResizableSplitter>;
|
|
35
|
+
export default ResizableSplitter;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Controller plugged in by a layout-aware parent to turn tab drags
|
|
4
|
+
* into layout mutations. The primitive itself is layout-agnostic:
|
|
5
|
+
* it just calls `onPointerDown` when a tab is grabbed, and asks the
|
|
6
|
+
* controller to hit-test the strip during a drag via `onStripHover`.
|
|
7
|
+
*/
|
|
8
|
+
export interface TabDragController {
|
|
9
|
+
onPointerDown(index: number, event: PointerEvent, element: HTMLElement): void;
|
|
10
|
+
onStripHover(
|
|
11
|
+
stripRect: DOMRect,
|
|
12
|
+
pointerX: number,
|
|
13
|
+
pointerY: number,
|
|
14
|
+
tabRects: DOMRect[],
|
|
15
|
+
): number | null;
|
|
16
|
+
onStripLeave(): void;
|
|
17
|
+
readonly isDragging: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pure helper: given an insert index and the tab button rects,
|
|
22
|
+
* compute the indicator's position in strip-local coordinates.
|
|
23
|
+
*/
|
|
24
|
+
export function computeIndicatorRect(
|
|
25
|
+
insertIndex: number,
|
|
26
|
+
tabEls: (HTMLButtonElement | undefined)[],
|
|
27
|
+
stripEl: HTMLDivElement | undefined,
|
|
28
|
+
): { left: number; top: number; height: number } | null {
|
|
29
|
+
if (!stripEl) return null;
|
|
30
|
+
const stripRect = stripEl.getBoundingClientRect();
|
|
31
|
+
const els = tabEls.filter((el): el is HTMLButtonElement => !!el);
|
|
32
|
+
if (els.length === 0) {
|
|
33
|
+
return { left: 4, top: 2, height: stripRect.height - 4 };
|
|
34
|
+
}
|
|
35
|
+
const clamped = Math.max(0, Math.min(insertIndex, els.length));
|
|
36
|
+
let leftViewport: number;
|
|
37
|
+
if (clamped === els.length) {
|
|
38
|
+
const last = els[els.length - 1].getBoundingClientRect();
|
|
39
|
+
leftViewport = last.right;
|
|
40
|
+
} else {
|
|
41
|
+
const at = els[clamped].getBoundingClientRect();
|
|
42
|
+
leftViewport = at.left;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
left: leftViewport - stripRect.left - 1,
|
|
46
|
+
top: 2,
|
|
47
|
+
height: stripRect.height - 4,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<script lang="ts">
|
|
53
|
+
/*
|
|
54
|
+
* TabbedPanel — a tab strip over a single active body.
|
|
55
|
+
*
|
|
56
|
+
* Scope: render a strip of tab labels, click to switch active tab,
|
|
57
|
+
* render every tab's body (hiding inactive ones via `display: none`).
|
|
58
|
+
* If a `dragController` is provided, tab pointerdown starts a drag
|
|
59
|
+
* and the strip becomes a drop zone with an insertion indicator.
|
|
60
|
+
*
|
|
61
|
+
* All body snippets are rendered concurrently so every tab's
|
|
62
|
+
* SlotContainer stays alive while the tab is inactive. The
|
|
63
|
+
* re-parenting contract relies on this — see slotHostPool.ts.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
import type { Snippet } from 'svelte';
|
|
67
|
+
|
|
68
|
+
let {
|
|
69
|
+
labels,
|
|
70
|
+
icons,
|
|
71
|
+
body,
|
|
72
|
+
activeTab,
|
|
73
|
+
onActiveChange,
|
|
74
|
+
dragController,
|
|
75
|
+
clickGuard,
|
|
76
|
+
closable,
|
|
77
|
+
dirty,
|
|
78
|
+
onClose,
|
|
79
|
+
}: {
|
|
80
|
+
labels: string[];
|
|
81
|
+
icons?: (string | undefined)[];
|
|
82
|
+
/** Snippet invoked once per tab with its index. */
|
|
83
|
+
body: Snippet<[number]>;
|
|
84
|
+
activeTab: number;
|
|
85
|
+
/** Called when the user picks a different tab. The parent is
|
|
86
|
+
* expected to write the new value back into whatever it is
|
|
87
|
+
* storing activeTab in. We use a callback rather than a
|
|
88
|
+
* $bindable prop because the parent's activeTab typically lives
|
|
89
|
+
* inside a larger $state object (a LayoutNode), and `bind:` on a
|
|
90
|
+
* sub-property trips Svelte 5's ownership warning. */
|
|
91
|
+
onActiveChange?: (index: number) => void;
|
|
92
|
+
dragController?: TabDragController;
|
|
93
|
+
/** Optional: called by the tab click handler; if it returns true,
|
|
94
|
+
* the click is ignored. Used to swallow the synthetic click that
|
|
95
|
+
* fires on the source tab after a drag commit. */
|
|
96
|
+
clickGuard?: () => boolean;
|
|
97
|
+
/** Per-tab closability. True if the tab can be closed. */
|
|
98
|
+
closable?: (boolean | undefined)[];
|
|
99
|
+
/** Per-tab dirty state. True if the tab has unsaved changes. */
|
|
100
|
+
dirty?: (boolean | undefined)[];
|
|
101
|
+
/** Called when the user clicks a tab's close button. */
|
|
102
|
+
onClose?: (index: number) => void;
|
|
103
|
+
} = $props();
|
|
104
|
+
|
|
105
|
+
function select(i: number) {
|
|
106
|
+
if (clickGuard?.()) return;
|
|
107
|
+
onActiveChange?.(i);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleClose(i: number, e: Event) {
|
|
111
|
+
e.stopPropagation(); // Don't also trigger tab selection.
|
|
112
|
+
onClose?.(i);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let stripEl: HTMLDivElement | undefined = $state();
|
|
116
|
+
const tabEls: (HTMLButtonElement | undefined)[] = $state([]);
|
|
117
|
+
let hoverInsertIndex: number | null = $state(null);
|
|
118
|
+
|
|
119
|
+
function onTabPointerDown(i: number, e: PointerEvent) {
|
|
120
|
+
if (!dragController) return;
|
|
121
|
+
const el = tabEls[i];
|
|
122
|
+
if (!el) return;
|
|
123
|
+
dragController.onPointerDown(i, e, el);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function onStripPointerMove(e: PointerEvent) {
|
|
127
|
+
if (!dragController || !dragController.isDragging || !stripEl) return;
|
|
128
|
+
const stripRect = stripEl.getBoundingClientRect();
|
|
129
|
+
const rects = tabEls
|
|
130
|
+
.filter((el): el is HTMLButtonElement => !!el)
|
|
131
|
+
.map((el) => el.getBoundingClientRect());
|
|
132
|
+
hoverInsertIndex = dragController.onStripHover(stripRect, e.clientX, e.clientY, rects);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function onStripPointerLeave() {
|
|
136
|
+
if (!dragController) return;
|
|
137
|
+
hoverInsertIndex = null;
|
|
138
|
+
dragController.onStripLeave();
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<div class="tabbed-panel">
|
|
143
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
144
|
+
<div
|
|
145
|
+
class="tab-strip"
|
|
146
|
+
role="tablist"
|
|
147
|
+
tabindex="-1"
|
|
148
|
+
bind:this={stripEl}
|
|
149
|
+
onpointermove={onStripPointerMove}
|
|
150
|
+
onpointerleave={onStripPointerLeave}
|
|
151
|
+
>
|
|
152
|
+
{#each labels as label, i (i)}
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
class="tab"
|
|
156
|
+
class:active={activeTab === i}
|
|
157
|
+
role="tab"
|
|
158
|
+
aria-selected={activeTab === i}
|
|
159
|
+
bind:this={tabEls[i]}
|
|
160
|
+
onclick={() => select(i)}
|
|
161
|
+
onpointerdown={(e) => onTabPointerDown(i, e)}
|
|
162
|
+
onauxclick={(e) => { if (e.button === 1 && closable?.[i]) handleClose(i, e); }}
|
|
163
|
+
>
|
|
164
|
+
{#if dirty?.[i]}
|
|
165
|
+
<span class="tab-dirty" title="Unsaved changes"></span>
|
|
166
|
+
{/if}
|
|
167
|
+
{#if icons?.[i]}<span class="tab-icon">{icons[i]}</span>{/if}
|
|
168
|
+
<span class="tab-label">{label}</span>
|
|
169
|
+
{#if closable?.[i]}
|
|
170
|
+
<span
|
|
171
|
+
class="tab-close"
|
|
172
|
+
role="button"
|
|
173
|
+
tabindex="-1"
|
|
174
|
+
title="Close"
|
|
175
|
+
onclick={(e) => handleClose(i, e)}
|
|
176
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleClose(i, e); }}
|
|
177
|
+
>✕</span>
|
|
178
|
+
{/if}
|
|
179
|
+
</button>
|
|
180
|
+
{/each}
|
|
181
|
+
{#if hoverInsertIndex !== null && dragController?.isDragging}
|
|
182
|
+
{@const rect = computeIndicatorRect(hoverInsertIndex, tabEls, stripEl)}
|
|
183
|
+
{#if rect}
|
|
184
|
+
<div
|
|
185
|
+
class="drop-indicator"
|
|
186
|
+
style="left: {rect.left}px; height: {rect.height}px; top: {rect.top}px;"
|
|
187
|
+
></div>
|
|
188
|
+
{/if}
|
|
189
|
+
{/if}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="tab-body" role="tabpanel">
|
|
193
|
+
{#each labels as _label, i (i)}
|
|
194
|
+
<div class="tab-body-pane" class:active={activeTab === i}>
|
|
195
|
+
{@render body(i)}
|
|
196
|
+
</div>
|
|
197
|
+
{/each}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<style>
|
|
202
|
+
.tabbed-panel {
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-direction: column;
|
|
205
|
+
width: 100%;
|
|
206
|
+
height: 100%;
|
|
207
|
+
min-width: 0;
|
|
208
|
+
min-height: 0;
|
|
209
|
+
background: var(--shell-bg);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.tab-strip {
|
|
213
|
+
position: relative;
|
|
214
|
+
flex: 0 0 auto;
|
|
215
|
+
display: flex;
|
|
216
|
+
gap: 1px;
|
|
217
|
+
background: var(--shell-bg-sunken);
|
|
218
|
+
border-bottom: 1px solid var(--shell-border);
|
|
219
|
+
padding: 0 var(--shell-pad-sm);
|
|
220
|
+
user-select: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.tab {
|
|
224
|
+
appearance: none;
|
|
225
|
+
background: transparent;
|
|
226
|
+
border: none;
|
|
227
|
+
color: var(--shell-fg-muted);
|
|
228
|
+
font: inherit;
|
|
229
|
+
font-size: 12px;
|
|
230
|
+
padding: var(--shell-pad-sm) var(--shell-pad-md);
|
|
231
|
+
margin-top: 2px;
|
|
232
|
+
display: inline-flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: var(--shell-pad-sm);
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
border-top: 2px solid transparent;
|
|
237
|
+
border-radius: 2px 2px 0 0;
|
|
238
|
+
/* While dragging we still need pointerdown on tabs, but we want
|
|
239
|
+
the browser's native drag image suppressed — PointerEvent path
|
|
240
|
+
doesn't start an HTML5 drag, but preventing text selection here
|
|
241
|
+
avoids spurious selection rectangles during a drag. */
|
|
242
|
+
touch-action: none;
|
|
243
|
+
}
|
|
244
|
+
.tab:hover {
|
|
245
|
+
color: var(--shell-fg);
|
|
246
|
+
background: var(--shell-bg-elevated);
|
|
247
|
+
}
|
|
248
|
+
.tab.active {
|
|
249
|
+
color: var(--shell-fg);
|
|
250
|
+
background: var(--shell-bg);
|
|
251
|
+
border-top-color: var(--shell-accent);
|
|
252
|
+
}
|
|
253
|
+
.tab-icon { font-size: 11px; }
|
|
254
|
+
.tab-label { white-space: nowrap; }
|
|
255
|
+
|
|
256
|
+
.tab-dirty {
|
|
257
|
+
width: 8px;
|
|
258
|
+
height: 8px;
|
|
259
|
+
border-radius: 50%;
|
|
260
|
+
background: var(--shell-accent);
|
|
261
|
+
flex-shrink: 0;
|
|
262
|
+
}
|
|
263
|
+
.tab-close {
|
|
264
|
+
display: inline-flex;
|
|
265
|
+
font-size: 10px;
|
|
266
|
+
line-height: 1;
|
|
267
|
+
padding: 2px;
|
|
268
|
+
border-radius: 3px;
|
|
269
|
+
color: var(--shell-fg-muted);
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
flex-shrink: 0;
|
|
272
|
+
margin-left: auto;
|
|
273
|
+
}
|
|
274
|
+
.tab-close:hover {
|
|
275
|
+
color: var(--shell-fg);
|
|
276
|
+
background: var(--shell-bg-sunken);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.drop-indicator {
|
|
280
|
+
position: absolute;
|
|
281
|
+
width: 2px;
|
|
282
|
+
background: var(--shell-accent);
|
|
283
|
+
box-shadow: 0 0 6px var(--shell-accent);
|
|
284
|
+
pointer-events: none;
|
|
285
|
+
border-radius: 1px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.tab-body {
|
|
289
|
+
flex: 1 1 auto;
|
|
290
|
+
position: relative;
|
|
291
|
+
min-width: 0;
|
|
292
|
+
min-height: 0;
|
|
293
|
+
overflow: hidden;
|
|
294
|
+
}
|
|
295
|
+
.tab-body-pane {
|
|
296
|
+
position: absolute;
|
|
297
|
+
inset: 0;
|
|
298
|
+
min-width: 0;
|
|
299
|
+
min-height: 0;
|
|
300
|
+
display: none;
|
|
301
|
+
}
|
|
302
|
+
.tab-body-pane.active {
|
|
303
|
+
display: block;
|
|
304
|
+
}
|
|
305
|
+
</style>
|