sh3-core 0.6.0 → 0.7.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 +20 -14
- package/dist/api.d.ts +5 -3
- package/dist/app/admin/adminApp.js +2 -1
- package/dist/app/admin/adminShard.svelte.js +2 -1
- package/dist/app/store/StoreView.svelte +11 -5
- package/dist/app/store/storeApp.js +2 -1
- package/dist/app/store/storeShard.svelte.js +9 -4
- package/dist/apps/terminal/manifest.js +2 -1
- package/dist/apps/types.d.ts +28 -7
- package/dist/build.d.ts +5 -2
- package/dist/build.js +21 -10
- package/dist/env/client.d.ts +10 -2
- package/dist/env/client.js +13 -2
- package/dist/layout/LayoutRenderer.svelte +21 -9
- package/dist/layout/LayoutRenderer.svelte.d.ts +2 -0
- package/dist/layout/SlotDropZone.svelte +4 -1
- package/dist/layout/SlotDropZone.svelte.d.ts +2 -0
- package/dist/layout/drag.svelte.d.ts +5 -2
- package/dist/layout/drag.svelte.js +43 -11
- package/dist/layout/floats.d.ts +35 -0
- package/dist/layout/floats.js +73 -0
- package/dist/layout/floats.test.d.ts +1 -0
- package/dist/layout/floats.test.js +114 -0
- package/dist/layout/inspection.d.ts +2 -2
- package/dist/layout/inspection.js +6 -6
- package/dist/layout/ops.d.ts +14 -1
- package/dist/layout/ops.js +17 -0
- package/dist/layout/ops.test.d.ts +1 -0
- package/dist/layout/ops.test.js +36 -0
- package/dist/layout/presets.d.ts +2 -0
- package/dist/layout/presets.js +49 -0
- package/dist/layout/presets.test.d.ts +1 -0
- package/dist/layout/presets.test.js +71 -0
- package/dist/layout/store.svelte.d.ts +17 -13
- package/dist/layout/store.svelte.js +98 -36
- package/dist/layout/tree-walk.d.ts +12 -1
- package/dist/layout/tree-walk.js +13 -0
- package/dist/layout/tree-walk.test.d.ts +1 -0
- package/dist/layout/tree-walk.test.js +41 -0
- package/dist/layout/types.d.ts +96 -6
- package/dist/layout/types.js +1 -1
- package/dist/overlays/FloatFrame.svelte +141 -0
- package/dist/overlays/FloatFrame.svelte.d.ts +7 -0
- package/dist/overlays/FloatLayer.svelte +28 -0
- package/dist/overlays/FloatLayer.svelte.d.ts +3 -0
- package/dist/overlays/float.d.ts +29 -0
- package/dist/overlays/float.js +119 -0
- package/dist/overlays/float.test.d.ts +1 -0
- package/dist/overlays/float.test.js +37 -0
- package/dist/overlays/presets.d.ts +21 -0
- package/dist/overlays/presets.js +63 -0
- package/dist/overlays/presets.test.d.ts +1 -0
- package/dist/overlays/presets.test.js +40 -0
- package/dist/registry/client.d.ts +14 -0
- package/dist/registry/client.js +37 -0
- package/dist/registry/client.test.d.ts +1 -0
- package/dist/registry/client.test.js +54 -0
- package/dist/registry/installer.js +18 -5
- package/dist/registry/schema.js +5 -0
- package/dist/registry/types.d.ts +9 -0
- package/dist/shards/types.d.ts +27 -4
- package/dist/shell-shard/Terminal.svelte +14 -8
- package/dist/shell-shard/manifest.js +2 -1
- package/dist/shell-shard/shellShard.svelte.js +2 -1
- package/dist/shellRuntime.svelte.d.ts +6 -0
- package/dist/shellRuntime.svelte.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -3
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* them without tearing down the held tree.
|
|
5
5
|
*
|
|
6
6
|
* The manager is the sole owner of "which layout root is being rendered
|
|
7
|
-
* right now". LayoutRenderer reads `layoutStore.root` (
|
|
8
|
-
*
|
|
9
|
-
* Neither needs to know whether the active
|
|
7
|
+
* right now". LayoutRenderer reads `layoutStore.root` (an alias for
|
|
8
|
+
* `tree.docked`) and `layoutStore.tree`; drag.svelte.ts and any other
|
|
9
|
+
* mutation site do the same. Neither needs to know whether the active
|
|
10
|
+
* tree is home or an app.
|
|
10
11
|
*
|
|
11
12
|
* Refcount-hold discipline:
|
|
12
13
|
* The slot host pool is refcount-based with a microtask-deferred
|
|
@@ -30,7 +31,9 @@
|
|
|
30
31
|
*/
|
|
31
32
|
import { createStateZones, peekZone, clearZone } from '../state/zones.svelte';
|
|
32
33
|
import { acquireSlotHost, releaseSlotHost } from './slotHostPool.svelte';
|
|
33
|
-
import {
|
|
34
|
+
import { normalizeInitialLayout } from './presets';
|
|
35
|
+
import { collectTreeSlotRefs } from './tree-walk';
|
|
36
|
+
import { bindPresetBlob, unbindPresetBlob } from '../overlays/presets';
|
|
34
37
|
// ---------- orphan cleanup of pre-phase-8 shell layout key ----------------
|
|
35
38
|
// Legacy pre-phase-8 orphan cleanup. The literal '__shell__' here is
|
|
36
39
|
// intentional — it clears data written under the old reserved id before
|
|
@@ -47,8 +50,50 @@ const HOME_LAYOUT = {
|
|
|
47
50
|
slotId: 'sh3core.home',
|
|
48
51
|
viewId: 'sh3core:home',
|
|
49
52
|
};
|
|
53
|
+
const HOME_TREE = {
|
|
54
|
+
docked: HOME_LAYOUT,
|
|
55
|
+
floats: [],
|
|
56
|
+
};
|
|
50
57
|
let appEntry = $state(null);
|
|
51
58
|
let activeRoot = $state('home');
|
|
59
|
+
// ---------- read-side adapter helpers -------------------------------------
|
|
60
|
+
/**
|
|
61
|
+
* Read-side adapter from the legacy phase-7/phase-8 blob shape
|
|
62
|
+
* ({ layoutVersion, root: LayoutNode }) to the new preset-map shape.
|
|
63
|
+
* Returns a normalized AppLayoutBlob. Used only when loading a stored
|
|
64
|
+
* blob that lacks the new `presets` field — fresh writes always use the
|
|
65
|
+
* new shape directly.
|
|
66
|
+
*/
|
|
67
|
+
function adaptLegacyBlob(stored) {
|
|
68
|
+
if (!stored || typeof stored !== 'object')
|
|
69
|
+
return null;
|
|
70
|
+
const obj = stored;
|
|
71
|
+
// If new shape, pass through unchanged.
|
|
72
|
+
if (obj.presets && typeof obj.presets === 'object' && typeof obj.activePreset === 'string') {
|
|
73
|
+
return obj;
|
|
74
|
+
}
|
|
75
|
+
// Legacy shape: wrap .root into { default: { default: { docked: root, floats: [] } } }.
|
|
76
|
+
if (obj.root && typeof obj.layoutVersion === 'number') {
|
|
77
|
+
const tree = { docked: obj.root, floats: [] };
|
|
78
|
+
return {
|
|
79
|
+
layoutVersion: obj.layoutVersion,
|
|
80
|
+
activePreset: 'default',
|
|
81
|
+
presets: {
|
|
82
|
+
default: { default: tree },
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** Helper: read the active preset's default-variant tree out of a blob. */
|
|
89
|
+
function currentTree(blob) {
|
|
90
|
+
const preset = blob.presets[blob.activePreset];
|
|
91
|
+
if (!preset) {
|
|
92
|
+
throw new Error(`AppLayoutBlob active preset "${blob.activePreset}" not found in presets map`);
|
|
93
|
+
}
|
|
94
|
+
// v1 always uses 'default' variant
|
|
95
|
+
return preset.default;
|
|
96
|
+
}
|
|
52
97
|
// ---------- public (within-framework) API ---------------------------------
|
|
53
98
|
/**
|
|
54
99
|
* Attach an app: create or hydrate its workspace-zone layout proxy,
|
|
@@ -61,36 +106,44 @@ export function attachApp(app) {
|
|
|
61
106
|
throw new Error(`Layout manager cannot attach app "${app.manifest.id}": app "${appEntry.appId}" is still attached`);
|
|
62
107
|
}
|
|
63
108
|
const shardId = `__sh3core__:app:${app.manifest.id}`;
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
109
|
+
// Normalize the app's initialLayout into canonical presets.
|
|
110
|
+
const canonical = normalizeInitialLayout(app.initialLayout);
|
|
111
|
+
if (canonical.length === 0) {
|
|
112
|
+
throw new Error(`App "${app.manifest.id}" normalized to zero presets`);
|
|
113
|
+
}
|
|
114
|
+
// Build the default blob (used as fallback when nothing is stored or
|
|
115
|
+
// the stored blob's version doesn't match).
|
|
116
|
+
const defaultBlob = {
|
|
117
|
+
layoutVersion: app.manifest.layoutVersion,
|
|
118
|
+
activePreset: canonical[0].name,
|
|
119
|
+
presets: Object.fromEntries(canonical.map((p) => [p.name, Object.assign({}, p.variants)])),
|
|
120
|
+
};
|
|
121
|
+
// Version gate: if a stored blob's layoutVersion doesn't match, clear
|
|
122
|
+
// and fall back to defaults.
|
|
67
123
|
const stored = peekZone('workspace', shardId);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
124
|
+
const adapted = adaptLegacyBlob(stored);
|
|
125
|
+
if (adapted && adapted.layoutVersion !== app.manifest.layoutVersion) {
|
|
126
|
+
clearZone('workspace', shardId);
|
|
127
|
+
}
|
|
128
|
+
else if (stored && !adapted) {
|
|
129
|
+
// Unknown/corrupt shape — clear it so createStateZones takes defaults.
|
|
130
|
+
clearZone('workspace', shardId);
|
|
73
131
|
}
|
|
74
132
|
const state = createStateZones(shardId, {
|
|
75
|
-
workspace:
|
|
76
|
-
layoutVersion: app.manifest.layoutVersion,
|
|
77
|
-
root: app.initialLayout,
|
|
78
|
-
},
|
|
133
|
+
workspace: defaultBlob,
|
|
79
134
|
});
|
|
80
135
|
const proxy = state.workspace;
|
|
81
|
-
// Take
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// tree, so the active rendering doesn't double-hold harmfully (the
|
|
86
|
-
// pool's destroy logic just sees refcount 2, then 1 on release).
|
|
87
|
-
const refs = collectSlotRefs(proxy.root);
|
|
136
|
+
// Take refcount holds on every slot in the active preset's tree
|
|
137
|
+
// (including floats). See the refcount-hold discipline comment at top.
|
|
138
|
+
const tree = currentTree(proxy);
|
|
139
|
+
const refs = collectTreeSlotRefs(tree);
|
|
88
140
|
const heldSlotIds = [];
|
|
89
141
|
for (const { slotId, viewId, label } of refs) {
|
|
90
142
|
acquireSlotHost(slotId, viewId, label);
|
|
91
143
|
heldSlotIds.push(slotId);
|
|
92
144
|
}
|
|
93
145
|
appEntry = { appId: app.manifest.id, proxy, heldSlotIds };
|
|
146
|
+
bindPresetBlob(proxy);
|
|
94
147
|
}
|
|
95
148
|
/**
|
|
96
149
|
* Detach the currently-attached app. Releases its refcount holds; the
|
|
@@ -100,6 +153,7 @@ export function attachApp(app) {
|
|
|
100
153
|
export function detachApp() {
|
|
101
154
|
if (!appEntry)
|
|
102
155
|
return;
|
|
156
|
+
unbindPresetBlob();
|
|
103
157
|
for (const slotId of appEntry.heldSlotIds) {
|
|
104
158
|
releaseSlotHost(slotId);
|
|
105
159
|
}
|
|
@@ -118,15 +172,15 @@ export function switchToApp() {
|
|
|
118
172
|
activeRoot = 'app';
|
|
119
173
|
}
|
|
120
174
|
/**
|
|
121
|
-
* The currently-rendered
|
|
122
|
-
*
|
|
123
|
-
*
|
|
175
|
+
* The currently-rendered LayoutTree. LayoutRenderer reads this via
|
|
176
|
+
* layoutStore.tree. Home uses a framework constant; app reads the
|
|
177
|
+
* currently-active preset from the workspace-zone proxy (reactive, so
|
|
124
178
|
* mutations from splitter/drag/ops reach the renderer unchanged).
|
|
125
179
|
*/
|
|
126
180
|
export function activeLayout() {
|
|
127
181
|
if (activeRoot === 'app' && appEntry)
|
|
128
|
-
return appEntry.proxy
|
|
129
|
-
return
|
|
182
|
+
return currentTree(appEntry.proxy);
|
|
183
|
+
return HOME_TREE;
|
|
130
184
|
}
|
|
131
185
|
export function getActiveRoot() {
|
|
132
186
|
return activeRoot;
|
|
@@ -137,17 +191,25 @@ export function getAttachedAppId() {
|
|
|
137
191
|
}
|
|
138
192
|
// ---------- `layoutStore` back-compat shim -------------------------------
|
|
139
193
|
/**
|
|
140
|
-
* Preserved for callers that still read `layoutStore.root`. The
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
194
|
+
* Preserved for callers that still read `layoutStore.root`. The `root`
|
|
195
|
+
* getter is an alias for `tree.docked` so existing callers still compile
|
|
196
|
+
* without changes. The `tree` getter exposes the full LayoutTree for
|
|
197
|
+
* new consumers that also need floats. The `floats` getter is a
|
|
198
|
+
* convenience alias for `tree.floats`.
|
|
199
|
+
*
|
|
200
|
+
* Writes to any of these properties are disallowed (mutation happens on
|
|
201
|
+
* the returned tree's nodes in place — splitter drags mutate `sizes[i]`,
|
|
202
|
+
* tab clicks mutate `activeTab`, drag-commit calls ops.ts functions that
|
|
203
|
+
* mutate children arrays).
|
|
148
204
|
*/
|
|
149
205
|
export const layoutStore = {
|
|
150
206
|
get root() {
|
|
207
|
+
return activeLayout().docked;
|
|
208
|
+
},
|
|
209
|
+
get tree() {
|
|
151
210
|
return activeLayout();
|
|
152
211
|
},
|
|
212
|
+
get floats() {
|
|
213
|
+
return activeLayout().floats;
|
|
214
|
+
},
|
|
153
215
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LayoutNode } from './types';
|
|
1
|
+
import type { LayoutNode, LayoutTree } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Collect the slot id / view id pairs of every slot leaf (including the
|
|
4
4
|
* slots embedded inside tabs entries) in a layout tree. Used by the
|
|
@@ -13,3 +13,14 @@ export declare function collectSlotRefs(tree: LayoutNode): {
|
|
|
13
13
|
viewId: string | null;
|
|
14
14
|
label: string;
|
|
15
15
|
}[];
|
|
16
|
+
/**
|
|
17
|
+
* Multi-root version of `collectSlotRefs`: walks the docked tree first
|
|
18
|
+
* and then each float's content in order. Used by the layout manager to
|
|
19
|
+
* take refcount holds on slot ids across every tree-owned view when
|
|
20
|
+
* attaching an app whose preset includes floats.
|
|
21
|
+
*/
|
|
22
|
+
export declare function collectTreeSlotRefs(tree: LayoutTree): {
|
|
23
|
+
slotId: string;
|
|
24
|
+
viewId: string | null;
|
|
25
|
+
label: string;
|
|
26
|
+
}[];
|
package/dist/layout/tree-walk.js
CHANGED
|
@@ -31,3 +31,16 @@ export function collectSlotRefs(tree) {
|
|
|
31
31
|
walk(tree);
|
|
32
32
|
return out;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Multi-root version of `collectSlotRefs`: walks the docked tree first
|
|
36
|
+
* and then each float's content in order. Used by the layout manager to
|
|
37
|
+
* take refcount holds on slot ids across every tree-owned view when
|
|
38
|
+
* attaching an app whose preset includes floats.
|
|
39
|
+
*/
|
|
40
|
+
export function collectTreeSlotRefs(tree) {
|
|
41
|
+
const out = collectSlotRefs(tree.docked);
|
|
42
|
+
for (const f of tree.floats) {
|
|
43
|
+
out.push(...collectSlotRefs(f.content));
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { collectSlotRefs, collectTreeSlotRefs } from './tree-walk';
|
|
3
|
+
const slot = (slotId, viewId) => ({
|
|
4
|
+
type: 'slot',
|
|
5
|
+
slotId,
|
|
6
|
+
viewId,
|
|
7
|
+
});
|
|
8
|
+
describe('collectSlotRefs (single LayoutNode — existing behavior)', () => {
|
|
9
|
+
it('returns a single entry for a lone slot', () => {
|
|
10
|
+
expect(collectSlotRefs(slot('s1', 'v1'))).toEqual([
|
|
11
|
+
{ slotId: 's1', viewId: 'v1', label: 'v1' },
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe('collectTreeSlotRefs (LayoutTree — new)', () => {
|
|
16
|
+
it('returns docked slots followed by float content slots in order', () => {
|
|
17
|
+
const tree = {
|
|
18
|
+
docked: slot('docked1', 'v-docked'),
|
|
19
|
+
floats: [
|
|
20
|
+
{
|
|
21
|
+
id: 'f1',
|
|
22
|
+
content: slot('float1', 'v-float1'),
|
|
23
|
+
position: { x: 0, y: 0 },
|
|
24
|
+
size: { w: 600, h: 400 },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'f2',
|
|
28
|
+
content: slot('float2', 'v-float2'),
|
|
29
|
+
position: { x: 32, y: 32 },
|
|
30
|
+
size: { w: 600, h: 400 },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
const refs = collectTreeSlotRefs(tree);
|
|
35
|
+
expect(refs.map((r) => r.slotId)).toEqual(['docked1', 'float1', 'float2']);
|
|
36
|
+
});
|
|
37
|
+
it('returns only docked slots when floats is empty', () => {
|
|
38
|
+
const tree = { docked: slot('only', 'v'), floats: [] };
|
|
39
|
+
expect(collectTreeSlotRefs(tree).map((r) => r.slotId)).toEqual(['only']);
|
|
40
|
+
});
|
|
41
|
+
});
|
package/dist/layout/types.d.ts
CHANGED
|
@@ -79,6 +79,80 @@ export interface SlotNode {
|
|
|
79
79
|
* these three types: `split` → children, `tabs` → slots, `slot` → leaf.
|
|
80
80
|
*/
|
|
81
81
|
export type LayoutNode = SplitNode | TabsNode | SlotNode;
|
|
82
|
+
/**
|
|
83
|
+
* A detached panel that floats above the docked tree. Each float owns a
|
|
84
|
+
* `LayoutNode` subtree (so it can itself contain splits and tabs) plus
|
|
85
|
+
* positioning metadata. Floats cannot contain other floats — the type
|
|
86
|
+
* system enforces this by using `LayoutNode`, not `LayoutNode | FloatEntry`.
|
|
87
|
+
*/
|
|
88
|
+
export interface FloatEntry {
|
|
89
|
+
/** Stable identifier for this float. Survives re-parents and reloads. */
|
|
90
|
+
id: string;
|
|
91
|
+
/** The float's content subtree. Split | tabs | slot — no nested floats. */
|
|
92
|
+
content: LayoutNode;
|
|
93
|
+
/** Top-left position of the float frame, relative to the tree-allocated area. */
|
|
94
|
+
position: {
|
|
95
|
+
x: number;
|
|
96
|
+
y: number;
|
|
97
|
+
};
|
|
98
|
+
/** Size of the float frame in pixels. */
|
|
99
|
+
size: {
|
|
100
|
+
w: number;
|
|
101
|
+
h: number;
|
|
102
|
+
};
|
|
103
|
+
/** Optional human-readable title; defaults to the active view's label. */
|
|
104
|
+
title?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Root shape of a workspace layout. The docked tree is the primary
|
|
108
|
+
* topology; floats are a parallel collection that share the same area
|
|
109
|
+
* visually but live outside the recursive-tree invariant. Persisted to
|
|
110
|
+
* the workspace state zone as part of an `AppLayoutBlob` preset entry.
|
|
111
|
+
*/
|
|
112
|
+
export interface LayoutTree {
|
|
113
|
+
docked: LayoutNode;
|
|
114
|
+
floats: FloatEntry[];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A named layout blueprint. Apps ship one or more presets in their
|
|
118
|
+
* manifest; users switch between them at runtime. The ergonomic `tree`
|
|
119
|
+
* field is shorthand for `variants.default`; the normalizer canonicalizes
|
|
120
|
+
* every preset into a variants-only shape on load. v1 always uses the
|
|
121
|
+
* `default` variant; other variant keys (e.g. `companion`) are reserved
|
|
122
|
+
* for the rescoped DF10 selection policy and are persisted but inert.
|
|
123
|
+
*/
|
|
124
|
+
export interface LayoutPreset {
|
|
125
|
+
name: string;
|
|
126
|
+
/** Ergonomic shortcut — discarded after normalization in favor of `variants.default`. */
|
|
127
|
+
tree?: LayoutTree;
|
|
128
|
+
/** Variant map. After normalization always contains at least `default`. */
|
|
129
|
+
variants?: {
|
|
130
|
+
[variant: string]: LayoutTree;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Canonical form of a preset after normalization. Used internally by
|
|
135
|
+
* the framework; authors write `LayoutPreset` in their manifests.
|
|
136
|
+
*/
|
|
137
|
+
export interface CanonicalPreset {
|
|
138
|
+
name: string;
|
|
139
|
+
variants: {
|
|
140
|
+
[variant: string]: LayoutTree;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Reference to a root within the current `LayoutTree`. Either the
|
|
145
|
+
* primary docked tree, or the content subtree of a specific float by id.
|
|
146
|
+
* Used by `LayoutRenderer` to target a subtree without passing a `node`
|
|
147
|
+
* prop (which would break the ownership-by-path contract — see the
|
|
148
|
+
* component's header comment for rationale).
|
|
149
|
+
*/
|
|
150
|
+
export type TreeRootRef = {
|
|
151
|
+
kind: 'docked';
|
|
152
|
+
} | {
|
|
153
|
+
kind: 'float';
|
|
154
|
+
floatId: string;
|
|
155
|
+
};
|
|
82
156
|
/**
|
|
83
157
|
* Schema version for persisted layouts. Bump this when the shape of
|
|
84
158
|
* `LayoutNode` (or anything reachable from it) changes incompatibly.
|
|
@@ -86,7 +160,7 @@ export type LayoutNode = SplitNode | TabsNode | SlotNode;
|
|
|
86
160
|
* the default tree takes over — phase 7 deliberately does not ship a
|
|
87
161
|
* migration framework, only the hook for one.
|
|
88
162
|
*/
|
|
89
|
-
export declare const LAYOUT_SCHEMA_VERSION =
|
|
163
|
+
export declare const LAYOUT_SCHEMA_VERSION = 4;
|
|
90
164
|
/**
|
|
91
165
|
* The wire shape of a persisted layout in the workspace state zone.
|
|
92
166
|
* One blob per shell (or per program, once per-program layouts exist);
|
|
@@ -94,15 +168,31 @@ export declare const LAYOUT_SCHEMA_VERSION = 3;
|
|
|
94
168
|
*/
|
|
95
169
|
export interface PersistedLayout {
|
|
96
170
|
version: typeof LAYOUT_SCHEMA_VERSION;
|
|
97
|
-
|
|
171
|
+
tree: LayoutTree;
|
|
98
172
|
}
|
|
99
173
|
/**
|
|
100
174
|
* Per-app layout blob written to the workspace state zone under
|
|
101
|
-
* `__sh3core__:app:<appId>`.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
175
|
+
* `__sh3core__:app:<appId>`. Holds the full multi-preset, multi-variant
|
|
176
|
+
* tree collection. On launch, a stored blob whose `layoutVersion` does
|
|
177
|
+
* not match the app's current `AppManifest.layoutVersion` is discarded
|
|
178
|
+
* and the app's normalized `initialLayout` is used in its place.
|
|
179
|
+
*
|
|
180
|
+
* Legacy phase-7/phase-8 blobs stored `{ layoutVersion, root: LayoutNode }`.
|
|
181
|
+
* The `attachApp` read path wraps that shape into the new form rather
|
|
182
|
+
* than discarding it; see `layout/store.svelte.ts`.
|
|
104
183
|
*/
|
|
105
184
|
export interface AppLayoutBlob {
|
|
106
185
|
layoutVersion: number;
|
|
107
|
-
|
|
186
|
+
/** Name of the currently-active preset. Must be a key of `presets`. */
|
|
187
|
+
activePreset: string;
|
|
188
|
+
/**
|
|
189
|
+
* Preset map. Each entry holds a variant map; v1 only reads/writes
|
|
190
|
+
* the `default` variant, other keys are reserved for the rescoped
|
|
191
|
+
* DF10 selection policy and pass through untouched.
|
|
192
|
+
*/
|
|
193
|
+
presets: {
|
|
194
|
+
[presetName: string]: {
|
|
195
|
+
[variantName: string]: LayoutTree;
|
|
196
|
+
};
|
|
197
|
+
};
|
|
108
198
|
}
|
package/dist/layout/types.js
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Single floating panel frame.
|
|
3
|
+
|
|
4
|
+
Renders:
|
|
5
|
+
- Header bar (title + close button, receives pointerdown for drag).
|
|
6
|
+
- Body that mounts the float's content subtree via LayoutRenderer
|
|
7
|
+
using rootRef={{ kind: 'float', floatId: entry.id }} so the
|
|
8
|
+
renderer reads from layoutStore.tree.floats[...].content instead
|
|
9
|
+
of layoutStore.root.
|
|
10
|
+
|
|
11
|
+
Behavior:
|
|
12
|
+
- Pointer drag on header mutates entry.position in place. The entry
|
|
13
|
+
is a live reference from layoutStore.tree.floats, so mutation
|
|
14
|
+
reactivity flows through the workspace-zone proxy.
|
|
15
|
+
- Click anywhere on the frame raises it (calls floatManager.focus).
|
|
16
|
+
- Close button calls floatManager.close.
|
|
17
|
+
-->
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import LayoutRenderer from '../layout/LayoutRenderer.svelte';
|
|
20
|
+
import { floatManager } from './float';
|
|
21
|
+
import type { FloatEntry } from '../layout/types';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
entry: FloatEntry;
|
|
25
|
+
}
|
|
26
|
+
const { entry }: Props = $props();
|
|
27
|
+
|
|
28
|
+
let dragging = $state(false);
|
|
29
|
+
let dragOffset = { x: 0, y: 0 };
|
|
30
|
+
|
|
31
|
+
function onHeaderPointerDown(e: PointerEvent): void {
|
|
32
|
+
if (e.button !== 0) return;
|
|
33
|
+
const target = e.currentTarget as HTMLElement;
|
|
34
|
+
target.setPointerCapture(e.pointerId);
|
|
35
|
+
dragging = true;
|
|
36
|
+
dragOffset = { x: e.clientX - entry.position.x, y: e.clientY - entry.position.y };
|
|
37
|
+
floatManager.focus(entry.id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function onHeaderPointerMove(e: PointerEvent): void {
|
|
41
|
+
if (!dragging) return;
|
|
42
|
+
entry.position.x = e.clientX - dragOffset.x;
|
|
43
|
+
entry.position.y = e.clientY - dragOffset.y;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onHeaderPointerUp(e: PointerEvent): void {
|
|
47
|
+
if (!dragging) return;
|
|
48
|
+
dragging = false;
|
|
49
|
+
const target = e.currentTarget as HTMLElement;
|
|
50
|
+
if (target.hasPointerCapture(e.pointerId)) {
|
|
51
|
+
target.releasePointerCapture(e.pointerId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onFrameClick(): void {
|
|
56
|
+
floatManager.focus(entry.id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function onClose(e: MouseEvent): void {
|
|
60
|
+
e.stopPropagation();
|
|
61
|
+
floatManager.close(entry.id);
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
66
|
+
<div
|
|
67
|
+
class="sh3-float-frame"
|
|
68
|
+
style:left="{entry.position.x}px"
|
|
69
|
+
style:top="{entry.position.y}px"
|
|
70
|
+
style:width="{entry.size.w}px"
|
|
71
|
+
style:height="{entry.size.h}px"
|
|
72
|
+
onclick={onFrameClick}
|
|
73
|
+
role="dialog"
|
|
74
|
+
aria-label={entry.title ?? 'Float panel'}
|
|
75
|
+
tabindex="-1"
|
|
76
|
+
>
|
|
77
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
78
|
+
<header
|
|
79
|
+
class="sh3-float-header"
|
|
80
|
+
onpointerdown={onHeaderPointerDown}
|
|
81
|
+
onpointermove={onHeaderPointerMove}
|
|
82
|
+
onpointerup={onHeaderPointerUp}
|
|
83
|
+
onpointercancel={onHeaderPointerUp}
|
|
84
|
+
>
|
|
85
|
+
<span class="sh3-float-title">{entry.title ?? entry.content.type}</span>
|
|
86
|
+
<button class="sh3-float-close" onclick={onClose} aria-label="Close float">×</button>
|
|
87
|
+
</header>
|
|
88
|
+
<div class="sh3-float-body">
|
|
89
|
+
<LayoutRenderer rootRef={{ kind: 'float', floatId: entry.id }} path={[]} />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<style>
|
|
94
|
+
.sh3-float-frame {
|
|
95
|
+
position: absolute;
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated, #1e1e1e));
|
|
99
|
+
color: var(--shell-fg);
|
|
100
|
+
border: 1px solid var(--shell-border-strong);
|
|
101
|
+
border-radius: var(--shell-radius);
|
|
102
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
103
|
+
pointer-events: auto;
|
|
104
|
+
}
|
|
105
|
+
.sh3-float-header {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
padding: 4px 8px;
|
|
110
|
+
background: var(--shell-bg, #111);
|
|
111
|
+
cursor: move;
|
|
112
|
+
user-select: none;
|
|
113
|
+
border-bottom: 1px solid var(--shell-border-strong);
|
|
114
|
+
border-top-left-radius: var(--shell-radius);
|
|
115
|
+
border-top-right-radius: var(--shell-radius);
|
|
116
|
+
flex-shrink: 0;
|
|
117
|
+
}
|
|
118
|
+
.sh3-float-title {
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
color: var(--shell-fg);
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
text-overflow: ellipsis;
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
}
|
|
125
|
+
.sh3-float-close {
|
|
126
|
+
background: transparent;
|
|
127
|
+
border: none;
|
|
128
|
+
color: var(--shell-fg);
|
|
129
|
+
font-size: 16px;
|
|
130
|
+
line-height: 1;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
padding: 0 4px;
|
|
133
|
+
flex-shrink: 0;
|
|
134
|
+
}
|
|
135
|
+
.sh3-float-body {
|
|
136
|
+
flex: 1;
|
|
137
|
+
position: relative;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
min-height: 0;
|
|
140
|
+
}
|
|
141
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Layer 1 overlay root — iterates the active LayoutTree's floats and
|
|
3
|
+
renders a FloatFrame for each. Mounted into the layer-1 DOM root by
|
|
4
|
+
Shell.svelte. Reactivity flows from the workspace-zone proxy through
|
|
5
|
+
layoutStore.floats into this component, so mutations (open, close,
|
|
6
|
+
position changes, reorder) re-render automatically.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { layoutStore } from '../layout/store.svelte';
|
|
10
|
+
import FloatFrame from './FloatFrame.svelte';
|
|
11
|
+
|
|
12
|
+
const floats = $derived(layoutStore.floats);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="sh3-float-layer">
|
|
16
|
+
{#each floats as entry (entry.id)}
|
|
17
|
+
<FloatFrame {entry} />
|
|
18
|
+
{/each}
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
.sh3-float-layer {
|
|
23
|
+
position: absolute;
|
|
24
|
+
inset: 0;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
/* Children are pointer-events: auto so the layer itself passes through. */
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FloatEntry } from '../layout/types';
|
|
2
|
+
import type { Size } from '../layout/floats';
|
|
3
|
+
export interface FloatOptions {
|
|
4
|
+
title?: string;
|
|
5
|
+
position?: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
};
|
|
9
|
+
size?: Size;
|
|
10
|
+
}
|
|
11
|
+
export interface FloatManager {
|
|
12
|
+
open(viewId: string, options?: FloatOptions): string;
|
|
13
|
+
close(floatId: string): void;
|
|
14
|
+
list(): FloatEntry[];
|
|
15
|
+
focus(floatId: string): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Bind the manager to the active LayoutTree's `floats` array. Called
|
|
19
|
+
* from Shell.svelte during boot. `getBounds` returns the current
|
|
20
|
+
* tree-allocated area for cascade-position wraparound.
|
|
21
|
+
*/
|
|
22
|
+
export declare function bindFloatStore(floats: FloatEntry[], getBounds: () => {
|
|
23
|
+
w: number;
|
|
24
|
+
h: number;
|
|
25
|
+
}): void;
|
|
26
|
+
export declare function unbindFloatStore(): void;
|
|
27
|
+
/** Test-only reset. Clears in-memory fallback and unbinds any store. */
|
|
28
|
+
export declare function __resetFloatManagerForTest(): void;
|
|
29
|
+
export declare const floatManager: FloatManager;
|