sh3-core 0.16.1 → 0.17.2
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/Sh3.svelte +50 -108
- package/dist/__screenshots__/handheld.browser.test.ts/handheld-viewport-flip-e2e-viewport-override-flips-chrome-and-body-branches-1.png +0 -0
- package/dist/actions/ctx-actions.svelte.test.js +4 -4
- package/dist/actions/listActionsFromEntries.test.js +29 -0
- package/dist/actions/listActive.js +2 -0
- package/dist/actions/listeners.js +4 -0
- package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
- package/dist/actions/types.d.ts +8 -0
- package/dist/api.d.ts +6 -1
- package/dist/api.js +1 -0
- package/dist/chrome/CompactChrome.svelte +96 -0
- package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
- package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
- package/dist/chrome/CompactChrome.svelte.test.js +67 -0
- package/dist/chrome/MenuSheet.svelte +224 -0
- package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
- package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
- package/dist/chrome/MenuSheet.svelte.test.js +46 -0
- package/dist/contributions/index.d.ts +1 -1
- package/dist/contributions/index.js +1 -1
- package/dist/contributions/registry.d.ts +17 -1
- package/dist/contributions/registry.js +50 -2
- package/dist/contributions/scope.test.d.ts +1 -0
- package/dist/contributions/scope.test.js +52 -0
- package/dist/contributions/types.d.ts +11 -3
- package/dist/createShell.js +7 -1
- package/dist/fields/address.d.ts +3 -0
- package/dist/fields/address.js +36 -0
- package/dist/fields/address.test.d.ts +1 -0
- package/dist/fields/address.test.js +34 -0
- package/dist/fields/decoration.d.ts +7 -0
- package/dist/fields/decoration.js +199 -0
- package/dist/fields/decoration.svelte.test.d.ts +1 -0
- package/dist/fields/decoration.svelte.test.js +177 -0
- package/dist/fields/dispatch.d.ts +22 -0
- package/dist/fields/dispatch.js +254 -0
- package/dist/fields/dispatch.test.d.ts +1 -0
- package/dist/fields/dispatch.test.js +175 -0
- package/dist/fields/types.d.ts +101 -0
- package/dist/fields/types.js +16 -0
- package/dist/fields/walker.svelte.test.d.ts +1 -0
- package/dist/fields/walker.svelte.test.js +138 -0
- package/dist/handheld.browser.test.d.ts +1 -0
- package/dist/handheld.browser.test.js +90 -0
- package/dist/host.js +27 -2
- package/dist/host.svelte.test.d.ts +1 -0
- package/dist/host.svelte.test.js +92 -0
- package/dist/layout/LayoutRenderer.svelte +12 -1
- package/dist/layout/LayoutRenderer.svelte.d.ts +2 -1
- package/dist/layout/compact/CompactRenderer.svelte +53 -0
- package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.js +76 -0
- package/dist/layout/compact/derive.d.ts +3 -0
- package/dist/layout/compact/derive.js +155 -0
- package/dist/layout/compact/derive.test.d.ts +1 -0
- package/dist/layout/compact/derive.test.js +160 -0
- package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
- package/dist/layout/compact/drawerStore.svelte.js +75 -0
- package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
- package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
- package/dist/layout/compact/resolveRole.d.ts +6 -0
- package/dist/layout/compact/resolveRole.js +13 -0
- package/dist/layout/compact/resolveRole.test.d.ts +1 -0
- package/dist/layout/compact/resolveRole.test.js +18 -0
- package/dist/layout/compact/types.d.ts +27 -0
- package/dist/layout/compact/types.js +15 -0
- package/dist/layout/presets.compactVariant.test.d.ts +1 -0
- package/dist/layout/presets.compactVariant.test.js +27 -0
- package/dist/layout/presets.d.ts +12 -0
- package/dist/layout/presets.js +16 -0
- package/dist/layout/slotHostPool.svelte.d.ts +8 -0
- package/dist/layout/slotHostPool.svelte.js +14 -1
- package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
- package/dist/layout/store.drawers.svelte.test.js +49 -0
- package/dist/layout/store.schemaVersion.test.d.ts +1 -0
- package/dist/layout/store.schemaVersion.test.js +35 -0
- package/dist/layout/store.svelte.js +52 -2
- package/dist/layout/types.d.ts +43 -1
- package/dist/layout/types.js +1 -1
- package/dist/overlays/DrawerSurface.svelte +141 -0
- package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
- package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
- package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
- package/dist/overlays/OverlayRoots.svelte +89 -0
- package/dist/overlays/OverlayRoots.svelte.d.ts +3 -0
- package/dist/overlays/types.d.ts +1 -1
- package/dist/platform/tauri-backend.d.ts +3 -3
- package/dist/platform/tauri-backend.js +24 -3
- package/dist/projects/session-state.svelte.d.ts +3 -3
- package/dist/projects/session-state.svelte.js +5 -4
- package/dist/runtime/runVerb.js +2 -2
- package/dist/satellite/SatelliteShell.svelte +58 -11
- package/dist/satellite/SatelliteShell.svelte.test.d.ts +1 -0
- package/dist/satellite/SatelliteShell.svelte.test.js +61 -0
- package/dist/sh3Api/fields-walker.svelte.test.d.ts +1 -0
- package/dist/sh3Api/fields-walker.svelte.test.js +75 -0
- package/dist/sh3Api/headless.d.ts +9 -0
- package/dist/sh3Api/headless.js +171 -16
- package/dist/sh3Api/headless.svelte.test.js +54 -10
- package/dist/sh3Runtime.svelte.d.ts +36 -0
- package/dist/sh3Runtime.svelte.js +33 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -2
- package/dist/shards/activate-fields.svelte.test.d.ts +1 -0
- package/dist/shards/activate-fields.svelte.test.js +121 -0
- package/dist/shards/activate-runtime.test.js +8 -8
- package/dist/shards/activate.svelte.js +29 -35
- package/dist/shards/types.d.ts +23 -76
- package/dist/shell-shard/ScrollbackView.svelte +55 -9
- package/dist/shell-shard/Terminal.svelte +1 -1
- package/dist/shell-shard/scrollback-stick.d.ts +9 -0
- package/dist/shell-shard/scrollback-stick.js +21 -0
- package/dist/shell-shard/scrollback-stick.test.d.ts +1 -0
- package/dist/shell-shard/scrollback-stick.test.js +25 -0
- package/dist/tokens.css +3 -2
- package/dist/verbs/types.d.ts +59 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewport/classify.d.ts +8 -0
- package/dist/viewport/classify.js +20 -0
- package/dist/viewport/classify.test.d.ts +1 -0
- package/dist/viewport/classify.test.js +32 -0
- package/dist/viewport/store.browser.test.d.ts +1 -0
- package/dist/viewport/store.browser.test.js +33 -0
- package/dist/viewport/store.svelte.d.ts +9 -0
- package/dist/viewport/store.svelte.js +71 -0
- package/dist/viewport/store.svelte.test.d.ts +1 -0
- package/dist/viewport/store.svelte.test.js +54 -0
- package/dist/viewport/types.d.ts +9 -0
- package/dist/viewport/types.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* drawerStore — backing store for Sh3.drawers.
|
|
3
|
+
*
|
|
4
|
+
* In-memory state mirror; persistence integration (read/write
|
|
5
|
+
* AppLayoutBlob.drawers) lives in layout/store.svelte.ts and is wired
|
|
6
|
+
* via __setWriteThrough(...). When no blob is bound (home layout, satellite
|
|
7
|
+
* mode, tests), mutations stay in-memory only.
|
|
8
|
+
*
|
|
9
|
+
* The state object is keyed by anchor only; the per-(preset, viewport)
|
|
10
|
+
* dimension lives in the blob, not here. The bind helper in layoutStore
|
|
11
|
+
* rehydrates this store when the active preset or viewport class changes.
|
|
12
|
+
*
|
|
13
|
+
* File is .svelte.ts so the Svelte compiler processes the runes.
|
|
14
|
+
*/
|
|
15
|
+
function initial() {
|
|
16
|
+
return {
|
|
17
|
+
left: { open: false, activeSlotId: null },
|
|
18
|
+
right: { open: false, activeSlotId: null },
|
|
19
|
+
top: { open: false, activeSlotId: null },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const state = $state(initial());
|
|
23
|
+
let writeThrough = null;
|
|
24
|
+
function flush() {
|
|
25
|
+
if (writeThrough)
|
|
26
|
+
writeThrough(state);
|
|
27
|
+
}
|
|
28
|
+
export const drawerStore = {
|
|
29
|
+
get state() {
|
|
30
|
+
return state;
|
|
31
|
+
},
|
|
32
|
+
open(anchor) {
|
|
33
|
+
state[anchor].open = true;
|
|
34
|
+
flush();
|
|
35
|
+
},
|
|
36
|
+
close(anchor) {
|
|
37
|
+
state[anchor].open = false;
|
|
38
|
+
flush();
|
|
39
|
+
},
|
|
40
|
+
toggle(anchor) {
|
|
41
|
+
state[anchor].open = !state[anchor].open;
|
|
42
|
+
flush();
|
|
43
|
+
},
|
|
44
|
+
activate(anchor, slotId) {
|
|
45
|
+
state[anchor].activeSlotId = slotId;
|
|
46
|
+
flush();
|
|
47
|
+
},
|
|
48
|
+
/**
|
|
49
|
+
* Bind a write-through callback. layoutStore calls this when the active
|
|
50
|
+
* preset/viewport changes; the callback persists state into the
|
|
51
|
+
* AppLayoutBlob.drawers field. Pass null to unbind.
|
|
52
|
+
*/
|
|
53
|
+
__setWriteThrough(cb) {
|
|
54
|
+
writeThrough = cb;
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* Replace the current state from a persisted snapshot. Used by
|
|
58
|
+
* layoutStore on preset/viewport-class change.
|
|
59
|
+
*/
|
|
60
|
+
__hydrate(snapshot) {
|
|
61
|
+
for (const anchor of ['left', 'right', 'top']) {
|
|
62
|
+
state[anchor].open = snapshot[anchor].open;
|
|
63
|
+
state[anchor].activeSlotId = snapshot[anchor].activeSlotId;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
/** Test-only reset. */
|
|
67
|
+
__reset() {
|
|
68
|
+
writeThrough = null;
|
|
69
|
+
const fresh = initial();
|
|
70
|
+
for (const anchor of ['left', 'right', 'top']) {
|
|
71
|
+
state[anchor].open = fresh[anchor].open;
|
|
72
|
+
state[anchor].activeSlotId = fresh[anchor].activeSlotId;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Drawer store unit tests. These exercise the in-memory state machine
|
|
3
|
+
* only; persistence integration is covered in Task 11's blob
|
|
4
|
+
* round-trip test.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
7
|
+
import { drawerStore } from './drawerStore.svelte';
|
|
8
|
+
describe('drawerStore', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
drawerStore.__reset();
|
|
11
|
+
});
|
|
12
|
+
it('starts with all drawers closed', () => {
|
|
13
|
+
expect(drawerStore.state.left).toEqual({ open: false, activeSlotId: null });
|
|
14
|
+
expect(drawerStore.state.right).toEqual({ open: false, activeSlotId: null });
|
|
15
|
+
expect(drawerStore.state.top).toEqual({ open: false, activeSlotId: null });
|
|
16
|
+
});
|
|
17
|
+
it('open(anchor) sets open true', () => {
|
|
18
|
+
drawerStore.open('left');
|
|
19
|
+
expect(drawerStore.state.left.open).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it('close(anchor) sets open false', () => {
|
|
22
|
+
drawerStore.open('left');
|
|
23
|
+
drawerStore.close('left');
|
|
24
|
+
expect(drawerStore.state.left.open).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it('toggle(anchor) flips open state', () => {
|
|
27
|
+
drawerStore.toggle('right');
|
|
28
|
+
expect(drawerStore.state.right.open).toBe(true);
|
|
29
|
+
drawerStore.toggle('right');
|
|
30
|
+
expect(drawerStore.state.right.open).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
it('activate(anchor, slotId) sets activeSlotId', () => {
|
|
33
|
+
drawerStore.activate('left', 'sidebar-1');
|
|
34
|
+
expect(drawerStore.state.left.activeSlotId).toBe('sidebar-1');
|
|
35
|
+
});
|
|
36
|
+
it('reset returns to initial state', () => {
|
|
37
|
+
drawerStore.open('left');
|
|
38
|
+
drawerStore.activate('right', 'x');
|
|
39
|
+
drawerStore.__reset();
|
|
40
|
+
expect(drawerStore.state.left.open).toBe(false);
|
|
41
|
+
expect(drawerStore.state.right.activeSlotId).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Role resolution: slot-level wins, view-level fills in, default 'body'.
|
|
3
|
+
*
|
|
4
|
+
* Why this matters: authoring a layout shouldn't require knowing every
|
|
5
|
+
* view's natural role. The view's author knows best ('graphlive:hierarchy'
|
|
6
|
+
* is a sidebar by nature), so they declare `defaultRole: 'sidebar'` on
|
|
7
|
+
* the ViewHandle. The app retains override authority by writing `role`
|
|
8
|
+
* on the slot itself.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveRole(slot, viewDefault) {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
return (_b = (_a = slot.role) !== null && _a !== void 0 ? _a : viewDefault) !== null && _b !== void 0 ? _b : 'body';
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { resolveRole } from './resolveRole';
|
|
3
|
+
describe('resolveRole', () => {
|
|
4
|
+
it('slot-level role wins over view default', () => {
|
|
5
|
+
expect(resolveRole({ role: 'body' }, 'sidebar')).toBe('body');
|
|
6
|
+
expect(resolveRole({ role: 'inspector' }, 'sidebar')).toBe('inspector');
|
|
7
|
+
});
|
|
8
|
+
it('view default fills in when slot role is unset', () => {
|
|
9
|
+
expect(resolveRole({}, 'sidebar')).toBe('sidebar');
|
|
10
|
+
expect(resolveRole({}, 'inspector')).toBe('inspector');
|
|
11
|
+
});
|
|
12
|
+
it('defaults to body when both unset', () => {
|
|
13
|
+
expect(resolveRole({}, undefined)).toBe('body');
|
|
14
|
+
});
|
|
15
|
+
it('treats undefined slot role same as missing field', () => {
|
|
16
|
+
expect(resolveRole({ role: undefined }, 'sidebar')).toBe('sidebar');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LayoutNode, SlotNode, SlotRole } from '../types';
|
|
2
|
+
export type DrawerAnchor = 'left' | 'right' | 'top';
|
|
3
|
+
export interface DrawerSpec {
|
|
4
|
+
slots: Array<{
|
|
5
|
+
slotId: string;
|
|
6
|
+
viewId: string | null;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
role: SlotRole;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export interface CompactRendering {
|
|
13
|
+
bodyRoot: LayoutNode;
|
|
14
|
+
drawers: {
|
|
15
|
+
left: DrawerSpec | null;
|
|
16
|
+
right: DrawerSpec | null;
|
|
17
|
+
top: DrawerSpec | null;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface DrawerState {
|
|
21
|
+
open: boolean;
|
|
22
|
+
activeSlotId: string | null;
|
|
23
|
+
}
|
|
24
|
+
export type DrawerStateMap = {
|
|
25
|
+
[anchor in DrawerAnchor]: DrawerState;
|
|
26
|
+
};
|
|
27
|
+
export declare const EMPTY_BODY: SlotNode;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Compact-rendering types. The derivation (./derive.ts) walks the
|
|
3
|
+
* canonical LayoutNode tree and emits a CompactRendering: a body root
|
|
4
|
+
* (the body-only sub-tree) plus per-anchor drawer specs.
|
|
5
|
+
*
|
|
6
|
+
* EMPTY_BODY is the placeholder used when an input tree has zero body
|
|
7
|
+
* slots — likely an authoring bug, but we render the chrome correctly
|
|
8
|
+
* rather than crashing.
|
|
9
|
+
*/
|
|
10
|
+
export const EMPTY_BODY = {
|
|
11
|
+
type: 'slot',
|
|
12
|
+
slotId: '__sh3core__:compact:empty',
|
|
13
|
+
viewId: null,
|
|
14
|
+
role: 'body',
|
|
15
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { resolveActiveTree } from './presets';
|
|
3
|
+
describe('compact variant selection', () => {
|
|
4
|
+
const preset = {
|
|
5
|
+
name: 'main',
|
|
6
|
+
variants: {
|
|
7
|
+
default: { docked: { type: 'slot', slotId: 'a', viewId: 'v:a' }, floats: [] },
|
|
8
|
+
compact: { docked: { type: 'slot', slotId: 'b', viewId: 'v:b' }, floats: [] },
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
it('returns default variant when class is desktop', () => {
|
|
12
|
+
const tree = resolveActiveTree(preset, 'desktop');
|
|
13
|
+
expect(tree.docked.slotId).toBe('a');
|
|
14
|
+
});
|
|
15
|
+
it('returns compact variant when class is compact and present', () => {
|
|
16
|
+
const tree = resolveActiveTree(preset, 'compact');
|
|
17
|
+
expect(tree.docked.slotId).toBe('b');
|
|
18
|
+
});
|
|
19
|
+
it('falls back to default when compact variant is absent', () => {
|
|
20
|
+
const onlyDefault = {
|
|
21
|
+
name: 'main',
|
|
22
|
+
variants: { default: preset.variants.default },
|
|
23
|
+
};
|
|
24
|
+
const tree = resolveActiveTree(onlyDefault, 'compact');
|
|
25
|
+
expect(tree.docked.slotId).toBe('a');
|
|
26
|
+
});
|
|
27
|
+
});
|
package/dist/layout/presets.d.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import type { LayoutNode, LayoutTree, LayoutPreset, CanonicalPreset } from './types';
|
|
2
|
+
import type { ViewportClass } from '../viewport/types';
|
|
3
|
+
/**
|
|
4
|
+
* Pick the active LayoutTree for a preset given the current viewport
|
|
5
|
+
* class. Compact viewport uses `variants.compact` if authored, else
|
|
6
|
+
* falls back to `variants.default`. Desktop always uses `variants.default`.
|
|
7
|
+
*
|
|
8
|
+
* Per spec §4 (Override path): when the compact variant is taken as-is,
|
|
9
|
+
* any role-tagged sidebar/inspector slots in *it* still get extracted
|
|
10
|
+
* into drawers by the derive() transform. So an explicit override
|
|
11
|
+
* doesn't have to author drawer chrome — only the docked structure.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveActiveTree(preset: CanonicalPreset, cls: ViewportClass): LayoutTree;
|
|
2
14
|
export declare function normalizeInitialLayout(input: LayoutNode | LayoutTree | LayoutPreset[]): CanonicalPreset[];
|
package/dist/layout/presets.js
CHANGED
|
@@ -35,6 +35,22 @@ function canonicalizePreset(p) {
|
|
|
35
35
|
}
|
|
36
36
|
return { name: p.name, variants };
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Pick the active LayoutTree for a preset given the current viewport
|
|
40
|
+
* class. Compact viewport uses `variants.compact` if authored, else
|
|
41
|
+
* falls back to `variants.default`. Desktop always uses `variants.default`.
|
|
42
|
+
*
|
|
43
|
+
* Per spec §4 (Override path): when the compact variant is taken as-is,
|
|
44
|
+
* any role-tagged sidebar/inspector slots in *it* still get extracted
|
|
45
|
+
* into drawers by the derive() transform. So an explicit override
|
|
46
|
+
* doesn't have to author drawer chrome — only the docked structure.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveActiveTree(preset, cls) {
|
|
49
|
+
if (cls === 'compact' && preset.variants.compact) {
|
|
50
|
+
return preset.variants.compact;
|
|
51
|
+
}
|
|
52
|
+
return preset.variants.default;
|
|
53
|
+
}
|
|
38
54
|
export function normalizeInitialLayout(input) {
|
|
39
55
|
if (Array.isArray(input)) {
|
|
40
56
|
return input.map(canonicalizePreset);
|
|
@@ -23,6 +23,14 @@ export declare function releaseSlotHost(slotId: string, fromWrapper?: HTMLElemen
|
|
|
23
23
|
* by HMR boundaries and tests; not part of normal runtime flow.
|
|
24
24
|
*/
|
|
25
25
|
export declare function resetSlotHostPool(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Read-only peek at a pooled host. Returns the live entry without
|
|
28
|
+
* affecting refcounts. Used by Sh3Api.fields.walk and friends.
|
|
29
|
+
*/
|
|
30
|
+
export declare function peekSlotHost(slotId: string): {
|
|
31
|
+
host: HTMLElement;
|
|
32
|
+
refcount: number;
|
|
33
|
+
} | undefined;
|
|
26
34
|
/**
|
|
27
35
|
* Read the current ViewHandle for a slot. Returns undefined if the slot
|
|
28
36
|
* is not in the pool or hasn't finished mounting yet. Used by the close
|
|
@@ -36,6 +36,7 @@ import { getView, __addViewRegistrationListener } from '../shards/registry';
|
|
|
36
36
|
import { locateSlotIn } from './ops';
|
|
37
37
|
import { activeLayout } from './store.svelte';
|
|
38
38
|
import { scopeToString } from '../actions/scope-helpers';
|
|
39
|
+
import { __disposeSlotContributions } from '../contributions';
|
|
39
40
|
const pool = new Map();
|
|
40
41
|
const pendingDestroy = new Set();
|
|
41
42
|
/**
|
|
@@ -284,6 +285,7 @@ export function releaseSlotHost(slotId, fromWrapper) {
|
|
|
284
285
|
if (!current || current.refcount > 0)
|
|
285
286
|
return; // re-acquired, keep
|
|
286
287
|
(_a = current.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
288
|
+
__disposeSlotContributions(slotId);
|
|
287
289
|
(_b = current.handle) === null || _b === void 0 ? void 0 : _b.unmount();
|
|
288
290
|
current.cancelPendingMount();
|
|
289
291
|
current.host.remove();
|
|
@@ -299,8 +301,9 @@ export function releaseSlotHost(slotId, fromWrapper) {
|
|
|
299
301
|
export function resetSlotHostPool() {
|
|
300
302
|
var _a, _b;
|
|
301
303
|
pendingDestroy.clear();
|
|
302
|
-
for (const entry of pool.
|
|
304
|
+
for (const [slotId, entry] of pool.entries()) {
|
|
303
305
|
(_a = entry.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
306
|
+
__disposeSlotContributions(slotId);
|
|
304
307
|
(_b = entry.handle) === null || _b === void 0 ? void 0 : _b.unmount();
|
|
305
308
|
entry.cancelPendingMount();
|
|
306
309
|
entry.host.remove();
|
|
@@ -312,6 +315,16 @@ export function resetSlotHostPool() {
|
|
|
312
315
|
delete closableState[key];
|
|
313
316
|
handleOverrides.clear();
|
|
314
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Read-only peek at a pooled host. Returns the live entry without
|
|
320
|
+
* affecting refcounts. Used by Sh3Api.fields.walk and friends.
|
|
321
|
+
*/
|
|
322
|
+
export function peekSlotHost(slotId) {
|
|
323
|
+
const entry = pool.get(slotId);
|
|
324
|
+
if (!entry)
|
|
325
|
+
return undefined;
|
|
326
|
+
return { host: entry.host, refcount: entry.refcount };
|
|
327
|
+
}
|
|
315
328
|
/**
|
|
316
329
|
* Read the current ViewHandle for a slot. Returns undefined if the slot
|
|
317
330
|
* is not in the pool or hasn't finished mounting yet. Used by the close
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Drawer persistence round-trip — open the right drawer, simulate a
|
|
3
|
+
* remount (call attachApp twice on the same blob), assert the open
|
|
4
|
+
* state survived.
|
|
5
|
+
*
|
|
6
|
+
* The test uses the layoutStore's app-attach machinery directly rather
|
|
7
|
+
* than going through createShell, since the drawerStore <→ blob binding
|
|
8
|
+
* is the unit under test.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
|
+
import { flushSync } from 'svelte';
|
|
12
|
+
import { attachApp, detachApp, __resetLayoutStoreForTest } from './store.svelte';
|
|
13
|
+
import { drawerStore } from './compact/drawerStore.svelte';
|
|
14
|
+
function fakeApp() {
|
|
15
|
+
// Minimal App shape — only fields attachApp reads.
|
|
16
|
+
return {
|
|
17
|
+
manifest: { id: 'test-app', layoutVersion: 5 },
|
|
18
|
+
initialLayout: {
|
|
19
|
+
type: 'split', direction: 'horizontal', sizes: [0.3, 0.7],
|
|
20
|
+
children: [
|
|
21
|
+
{ type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
|
|
22
|
+
{ type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
describe('drawer persistence round-trip', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
__resetLayoutStoreForTest();
|
|
30
|
+
drawerStore.__reset();
|
|
31
|
+
});
|
|
32
|
+
it('drawer state survives detach + re-attach', async () => {
|
|
33
|
+
const app = fakeApp();
|
|
34
|
+
attachApp(app);
|
|
35
|
+
// Run the proxy's initial $effect so the "first run skip" is consumed
|
|
36
|
+
// before our mutations. Subsequent mutations will trigger real flushes.
|
|
37
|
+
flushSync();
|
|
38
|
+
drawerStore.open('right');
|
|
39
|
+
drawerStore.activate('right', 'sb');
|
|
40
|
+
flushSync();
|
|
41
|
+
await new Promise((r) => queueMicrotask(r));
|
|
42
|
+
detachApp();
|
|
43
|
+
drawerStore.__reset();
|
|
44
|
+
expect(drawerStore.state.right.open).toBe(false);
|
|
45
|
+
attachApp(app);
|
|
46
|
+
expect(drawerStore.state.right.open).toBe(true);
|
|
47
|
+
expect(drawerStore.state.right.activeSlotId).toBe('sb');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Schema-version regression — a stored AppLayoutBlob written under
|
|
3
|
+
* LAYOUT_SCHEMA_VERSION 4 (no role hints, no drawers) must load cleanly
|
|
4
|
+
* under v5. The bump is purely additive; if this test breaks, the
|
|
5
|
+
* additive promise is broken.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { LAYOUT_SCHEMA_VERSION } from './types';
|
|
9
|
+
describe('layout schema v4 → v5 backward compatibility', () => {
|
|
10
|
+
it('a v4 blob loads with all roles undefined and drawers absent', () => {
|
|
11
|
+
const tree = {
|
|
12
|
+
docked: {
|
|
13
|
+
type: 'split',
|
|
14
|
+
direction: 'horizontal',
|
|
15
|
+
sizes: [0.3, 0.7],
|
|
16
|
+
children: [
|
|
17
|
+
{ type: 'slot', slotId: 'a', viewId: 'view:a' },
|
|
18
|
+
{ type: 'slot', slotId: 'b', viewId: 'view:b' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
floats: [],
|
|
22
|
+
};
|
|
23
|
+
const v4Blob = {
|
|
24
|
+
layoutVersion: 4,
|
|
25
|
+
activePreset: 'default',
|
|
26
|
+
presets: { default: { default: tree } },
|
|
27
|
+
};
|
|
28
|
+
const slotA = v4Blob.presets.default.default.docked.children[0];
|
|
29
|
+
expect(slotA.role).toBeUndefined();
|
|
30
|
+
expect(v4Blob.drawers).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
it('LAYOUT_SCHEMA_VERSION is 5', () => {
|
|
33
|
+
expect(LAYOUT_SCHEMA_VERSION).toBe(5);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -31,10 +31,12 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import { createStateZones, peekZone, clearZone } from '../state/zones.svelte';
|
|
33
33
|
import { acquireSlotHost, releaseSlotHost } from './slotHostPool.svelte';
|
|
34
|
-
import { normalizeInitialLayout } from './presets';
|
|
34
|
+
import { normalizeInitialLayout, resolveActiveTree } from './presets';
|
|
35
|
+
import { viewportStore } from '../viewport/store.svelte';
|
|
35
36
|
import { collectTreeSlotRefs } from './tree-walk';
|
|
36
37
|
import { bindPresetBlob, unbindPresetBlob } from '../overlays/presets';
|
|
37
38
|
import { getRegisteredApp } from '../apps/registry.svelte';
|
|
39
|
+
import { drawerStore } from './compact/drawerStore.svelte';
|
|
38
40
|
// ---------- orphan cleanup of pre-phase-8 sh3 layout key ----------------
|
|
39
41
|
// Legacy pre-phase-8 orphan cleanup. The literal '__shell__' here is
|
|
40
42
|
// intentional — it clears data written under the old reserved id before
|
|
@@ -140,6 +142,44 @@ export function attachApp(app) {
|
|
|
140
142
|
// so shards can read/switch presets from their activate() hook.
|
|
141
143
|
appEntry = { appId: app.manifest.id, proxy, heldSlotIds: [] };
|
|
142
144
|
bindPresetBlob(proxy);
|
|
145
|
+
bindDrawerStoreToBlob(proxy);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sync drawerStore <→ AppLayoutBlob.drawers.
|
|
149
|
+
*
|
|
150
|
+
* On attach (or preset/viewport change): read the persisted snapshot for
|
|
151
|
+
* (activePreset, 'compact') and hydrate drawerStore. Bind a
|
|
152
|
+
* write-through callback so subsequent mutations persist.
|
|
153
|
+
*
|
|
154
|
+
* v1 only persists the 'compact' viewport class; future viewport classes
|
|
155
|
+
* (e.g. 'phone') would extend this map without schema work.
|
|
156
|
+
*/
|
|
157
|
+
function bindDrawerStoreToBlob(blob) {
|
|
158
|
+
const presetName = blob.activePreset;
|
|
159
|
+
const ensure = () => {
|
|
160
|
+
if (!blob.drawers)
|
|
161
|
+
blob.drawers = {};
|
|
162
|
+
if (!blob.drawers[presetName])
|
|
163
|
+
blob.drawers[presetName] = {};
|
|
164
|
+
if (!blob.drawers[presetName].compact) {
|
|
165
|
+
blob.drawers[presetName].compact = {
|
|
166
|
+
left: { open: false, activeSlotId: null },
|
|
167
|
+
right: { open: false, activeSlotId: null },
|
|
168
|
+
top: { open: false, activeSlotId: null },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return blob.drawers;
|
|
172
|
+
};
|
|
173
|
+
const persisted = ensure();
|
|
174
|
+
drawerStore.__hydrate(persisted[presetName].compact);
|
|
175
|
+
drawerStore.__setWriteThrough((next) => {
|
|
176
|
+
const drawers = ensure();
|
|
177
|
+
drawers[presetName].compact = {
|
|
178
|
+
left: Object.assign({}, next.left),
|
|
179
|
+
right: Object.assign({}, next.right),
|
|
180
|
+
top: Object.assign({}, next.top),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
143
183
|
}
|
|
144
184
|
/**
|
|
145
185
|
* Second-phase attach: take refcount holds on every slot in the active
|
|
@@ -231,6 +271,12 @@ export function detachApp() {
|
|
|
231
271
|
if (!appEntry)
|
|
232
272
|
return;
|
|
233
273
|
unbindPresetBlob();
|
|
274
|
+
drawerStore.__setWriteThrough(null);
|
|
275
|
+
drawerStore.__hydrate({
|
|
276
|
+
left: { open: false, activeSlotId: null },
|
|
277
|
+
right: { open: false, activeSlotId: null },
|
|
278
|
+
top: { open: false, activeSlotId: null },
|
|
279
|
+
});
|
|
234
280
|
for (const slotId of appEntry.heldSlotIds) {
|
|
235
281
|
releaseSlotHost(slotId);
|
|
236
282
|
}
|
|
@@ -271,7 +317,11 @@ const activeTree = $derived.by(() => {
|
|
|
271
317
|
if (!preset) {
|
|
272
318
|
throw new Error(`AppLayoutBlob active preset "${presetName}" not found in presets map`);
|
|
273
319
|
}
|
|
274
|
-
|
|
320
|
+
// Per ADR-024: when the viewport is compact and the preset declares
|
|
321
|
+
// a `compact` variant, that variant wins. Otherwise the default
|
|
322
|
+
// variant is used and the framework derives drawer chrome from
|
|
323
|
+
// role-tagged slots in it.
|
|
324
|
+
return resolveActiveTree({ name: presetName, variants: preset }, viewportStore.current.class);
|
|
275
325
|
}
|
|
276
326
|
return HOME_TREE;
|
|
277
327
|
});
|
package/dist/layout/types.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/** Axis along which a split node divides its children. */
|
|
2
2
|
export type SplitDirection = 'horizontal' | 'vertical';
|
|
3
|
+
/**
|
|
4
|
+
* Slot role hint. Inert on desktop; the framework reads it on small
|
|
5
|
+
* viewports to derive a compact rendering (sidebars/inspectors lift into
|
|
6
|
+
* drawer surfaces, body slots fill the page).
|
|
7
|
+
*
|
|
8
|
+
* Default `'body'`. Authored on a slot or tab entry; if unset, falls back
|
|
9
|
+
* to the view's `defaultRole` (registered via the shard contract). See
|
|
10
|
+
* `layout/compact/resolveRole.ts`.
|
|
11
|
+
*/
|
|
12
|
+
export type SlotRole = 'body' | 'sidebar' | 'inspector';
|
|
3
13
|
/** How a child of a split node is sized. */
|
|
4
14
|
export type SizeMode = 'fr' | 'px';
|
|
5
15
|
/**
|
|
@@ -46,6 +56,11 @@ export interface TabEntry {
|
|
|
46
56
|
label: string;
|
|
47
57
|
/** Optional icon hint (not yet rendered in phase 8). */
|
|
48
58
|
icon?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Slot-role hint for compact rendering. Default `'body'` via
|
|
61
|
+
* `resolveRole(slot, viewDefault)`. Inert on desktop.
|
|
62
|
+
*/
|
|
63
|
+
role?: SlotRole;
|
|
49
64
|
/**
|
|
50
65
|
* Caller-supplied instance data, threaded to `MountContext.meta`.
|
|
51
66
|
* Ephemeral — not serialized with the layout tree.
|
|
@@ -86,6 +101,11 @@ export interface SlotNode {
|
|
|
86
101
|
slotId: string;
|
|
87
102
|
/** View id to mount into this slot, or null for an empty slot. */
|
|
88
103
|
viewId: string | null;
|
|
104
|
+
/**
|
|
105
|
+
* Slot-role hint for compact rendering. Default `'body'` via
|
|
106
|
+
* `resolveRole(slot, viewDefault)`. Inert on desktop.
|
|
107
|
+
*/
|
|
108
|
+
role?: SlotRole;
|
|
89
109
|
/**
|
|
90
110
|
* Caller-supplied instance data, threaded to `MountContext.meta`.
|
|
91
111
|
* Ephemeral — not serialized with the layout tree. Mirrors
|
|
@@ -187,7 +207,7 @@ export type TreeRootRef = {
|
|
|
187
207
|
* the default tree takes over — phase 7 deliberately does not ship a
|
|
188
208
|
* migration framework, only the hook for one.
|
|
189
209
|
*/
|
|
190
|
-
export declare const LAYOUT_SCHEMA_VERSION =
|
|
210
|
+
export declare const LAYOUT_SCHEMA_VERSION = 5;
|
|
191
211
|
/**
|
|
192
212
|
* The wire shape of a persisted layout in the workspace state zone.
|
|
193
213
|
* One blob per sh3 (or per program, once per-program layouts exist);
|
|
@@ -208,6 +228,14 @@ export interface PersistedLayout {
|
|
|
208
228
|
* The `attachApp` read path wraps that shape into the new form rather
|
|
209
229
|
* than discarding it; see `layout/store.svelte.ts`.
|
|
210
230
|
*/
|
|
231
|
+
/**
|
|
232
|
+
* Persisted per-anchor drawer state — open flag and active slot id.
|
|
233
|
+
* Lives on `AppLayoutBlob.drawers[presetName][viewportClass][anchor]`.
|
|
234
|
+
*/
|
|
235
|
+
export interface DrawerStateBlob {
|
|
236
|
+
open: boolean;
|
|
237
|
+
activeSlotId: string | null;
|
|
238
|
+
}
|
|
211
239
|
export interface AppLayoutBlob {
|
|
212
240
|
layoutVersion: number;
|
|
213
241
|
/** Name of the currently-active preset. Must be a key of `presets`. */
|
|
@@ -222,4 +250,18 @@ export interface AppLayoutBlob {
|
|
|
222
250
|
[variantName: string]: LayoutTree;
|
|
223
251
|
};
|
|
224
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* Drawer state keyed by `(presetName, viewportClass)`. Optional so
|
|
255
|
+
* legacy blobs without this field still load. v1 only writes the
|
|
256
|
+
* `'compact'` viewport-class key. See ADR-024.
|
|
257
|
+
*/
|
|
258
|
+
drawers?: {
|
|
259
|
+
[presetName: string]: {
|
|
260
|
+
[viewportClass: string]: {
|
|
261
|
+
left: DrawerStateBlob;
|
|
262
|
+
right: DrawerStateBlob;
|
|
263
|
+
top: DrawerStateBlob;
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
};
|
|
225
267
|
}
|
package/dist/layout/types.js
CHANGED