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,141 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* One drawer frame anchored to an edge. Renders nothing when closed.
|
|
4
|
+
* Open state slides the panel in over a backdrop. Tapping the backdrop
|
|
5
|
+
* fires onClose; tapping the close button does the same.
|
|
6
|
+
*
|
|
7
|
+
* Multi-slot drawers render a tab strip in the header. Single-slot
|
|
8
|
+
* drawers show the slot label only.
|
|
9
|
+
*
|
|
10
|
+
* Slot rendering goes through the standard SlotContainer path so the
|
|
11
|
+
* pooled host (and the mounted view) survives mount/unmount via the
|
|
12
|
+
* slot host pool — same mechanism as tab-drag re-parents.
|
|
13
|
+
*/
|
|
14
|
+
import type { DrawerAnchor, DrawerSpec } from '../layout/compact/types';
|
|
15
|
+
import type { SlotNode } from '../layout/types';
|
|
16
|
+
import SlotContainer from '../layout/SlotContainer.svelte';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
anchor,
|
|
20
|
+
spec,
|
|
21
|
+
open,
|
|
22
|
+
activeSlotId,
|
|
23
|
+
onClose,
|
|
24
|
+
onActivate,
|
|
25
|
+
}: {
|
|
26
|
+
anchor: DrawerAnchor;
|
|
27
|
+
spec: DrawerSpec;
|
|
28
|
+
open: boolean;
|
|
29
|
+
activeSlotId: string | null;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
onActivate: (slotId: string) => void;
|
|
32
|
+
} = $props();
|
|
33
|
+
|
|
34
|
+
const activeSlot = $derived(
|
|
35
|
+
spec.slots.find((s) => s.slotId === activeSlotId) ?? spec.slots[0],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const slotNode: SlotNode = $derived({
|
|
39
|
+
type: 'slot',
|
|
40
|
+
slotId: activeSlot.slotId,
|
|
41
|
+
viewId: activeSlot.viewId,
|
|
42
|
+
role: activeSlot.role,
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
{#if open}
|
|
47
|
+
<div
|
|
48
|
+
class="drawer-backdrop"
|
|
49
|
+
onclick={onClose}
|
|
50
|
+
onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
|
|
51
|
+
role="presentation"
|
|
52
|
+
></div>
|
|
53
|
+
<div
|
|
54
|
+
class="drawer drawer-{anchor}"
|
|
55
|
+
data-sh3-region="drawer"
|
|
56
|
+
data-sh3-anchor={anchor}
|
|
57
|
+
>
|
|
58
|
+
<header>
|
|
59
|
+
<span class="title">{activeSlot.label}</span>
|
|
60
|
+
<button
|
|
61
|
+
class="close"
|
|
62
|
+
onclick={onClose}
|
|
63
|
+
aria-label="Close drawer"
|
|
64
|
+
>×</button>
|
|
65
|
+
</header>
|
|
66
|
+
{#if spec.slots.length > 1}
|
|
67
|
+
<div class="tab-strip" role="tablist">
|
|
68
|
+
{#each spec.slots as s (s.slotId)}
|
|
69
|
+
<button
|
|
70
|
+
role="tab"
|
|
71
|
+
aria-selected={s.slotId === activeSlot.slotId}
|
|
72
|
+
class:active={s.slotId === activeSlot.slotId}
|
|
73
|
+
onclick={() => onActivate(s.slotId)}
|
|
74
|
+
>
|
|
75
|
+
{s.label}
|
|
76
|
+
</button>
|
|
77
|
+
{/each}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
<div class="body">
|
|
81
|
+
<SlotContainer node={slotNode} label={activeSlot.label} />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
86
|
+
<style>
|
|
87
|
+
.drawer-backdrop {
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset: 0;
|
|
90
|
+
background: var(--sh3-overlay-backdrop, rgba(0, 0, 0, 0.35));
|
|
91
|
+
pointer-events: auto;
|
|
92
|
+
}
|
|
93
|
+
.drawer {
|
|
94
|
+
position: absolute;
|
|
95
|
+
background: var(--sh3-bg);
|
|
96
|
+
color: var(--sh3-fg);
|
|
97
|
+
box-shadow: var(--sh3-shadow-md, 0 0 16px rgba(0, 0, 0, 0.2));
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
border: 1px solid var(--sh3-border);
|
|
101
|
+
pointer-events: auto;
|
|
102
|
+
}
|
|
103
|
+
.drawer-left { top: 0; bottom: 0; left: 0; width: min(360px, 80vw); }
|
|
104
|
+
.drawer-right { top: 0; bottom: 0; right: 0; width: min(360px, 80vw); }
|
|
105
|
+
.drawer-top { left: 0; right: 0; top: 0; height: min(50vh, 360px); }
|
|
106
|
+
header {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
padding: var(--sh3-pad-sm) var(--sh3-pad-md);
|
|
110
|
+
gap: var(--sh3-pad-md);
|
|
111
|
+
border-bottom: 1px solid var(--sh3-border);
|
|
112
|
+
background: var(--sh3-bg-elevated);
|
|
113
|
+
}
|
|
114
|
+
.title { font-weight: 600; }
|
|
115
|
+
.close {
|
|
116
|
+
margin-left: auto;
|
|
117
|
+
background: none;
|
|
118
|
+
border: none;
|
|
119
|
+
font-size: var(--sh3-font-lg);
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
color: var(--sh3-fg-muted);
|
|
122
|
+
padding: 0 var(--sh3-pad-sm);
|
|
123
|
+
}
|
|
124
|
+
.close:hover { color: var(--sh3-fg); }
|
|
125
|
+
.tab-strip {
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: 2px;
|
|
128
|
+
padding: var(--sh3-pad-xs) var(--sh3-pad-sm) 0;
|
|
129
|
+
background: var(--sh3-bg-sunken);
|
|
130
|
+
}
|
|
131
|
+
.tab-strip button {
|
|
132
|
+
padding: var(--sh3-pad-xs) var(--sh3-pad-sm);
|
|
133
|
+
border: 1px solid var(--sh3-border);
|
|
134
|
+
background: var(--sh3-bg-elevated);
|
|
135
|
+
border-bottom: none;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
color: var(--sh3-fg);
|
|
138
|
+
}
|
|
139
|
+
.tab-strip button.active { background: var(--sh3-bg); }
|
|
140
|
+
.body { flex: 1; min-height: 0; overflow: auto; }
|
|
141
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DrawerAnchor, DrawerSpec } from '../layout/compact/types';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
anchor: DrawerAnchor;
|
|
4
|
+
spec: DrawerSpec;
|
|
5
|
+
open: boolean;
|
|
6
|
+
activeSlotId: string | null;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onActivate: (slotId: string) => void;
|
|
9
|
+
};
|
|
10
|
+
declare const DrawerSurface: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type DrawerSurface = ReturnType<typeof DrawerSurface>;
|
|
12
|
+
export default DrawerSurface;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DOM smoke test for DrawerSurface — when open with a single-slot spec,
|
|
3
|
+
* the drawer renders the slot's label in its header. When closed, it
|
|
4
|
+
* renders nothing.
|
|
5
|
+
*
|
|
6
|
+
* Re-parent contract testing (slot DOM container actually moves, doesn't
|
|
7
|
+
* remount) is handled by the browser-mode handheld-flip test.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
10
|
+
import { mount, unmount, flushSync } from 'svelte';
|
|
11
|
+
import DrawerSurface from './DrawerSurface.svelte';
|
|
12
|
+
const DrawerSurfaceAny = DrawerSurface;
|
|
13
|
+
const spec = {
|
|
14
|
+
slots: [{ slotId: 'sb', viewId: 'v:sb', label: 'Files', role: 'sidebar' }],
|
|
15
|
+
};
|
|
16
|
+
const multiSpec = {
|
|
17
|
+
slots: [
|
|
18
|
+
{ slotId: 'sb', viewId: 'v:sb', label: 'Files', role: 'sidebar' },
|
|
19
|
+
{ slotId: 'pin', viewId: 'v:pin', label: 'Pinned', role: 'sidebar' },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
let mounted = null;
|
|
23
|
+
let host = null;
|
|
24
|
+
function renderHost(props) {
|
|
25
|
+
host = document.createElement('div');
|
|
26
|
+
host.style.position = 'relative';
|
|
27
|
+
document.body.appendChild(host);
|
|
28
|
+
mounted = mount(DrawerSurfaceAny, { target: host, props });
|
|
29
|
+
flushSync();
|
|
30
|
+
return host;
|
|
31
|
+
}
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (mounted) {
|
|
34
|
+
unmount(mounted);
|
|
35
|
+
mounted = null;
|
|
36
|
+
}
|
|
37
|
+
if (host) {
|
|
38
|
+
host.remove();
|
|
39
|
+
host = null;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
describe('DrawerSurface (dom)', () => {
|
|
43
|
+
it('renders nothing when closed', () => {
|
|
44
|
+
const el = renderHost({
|
|
45
|
+
anchor: 'left', spec, open: false, activeSlotId: null,
|
|
46
|
+
onClose: () => { }, onActivate: () => { },
|
|
47
|
+
});
|
|
48
|
+
expect(el.querySelector('[data-sh3-region="drawer"]')).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
it('renders header with slot label when open', () => {
|
|
51
|
+
const el = renderHost({
|
|
52
|
+
anchor: 'left', spec, open: true, activeSlotId: 'sb',
|
|
53
|
+
onClose: () => { }, onActivate: () => { },
|
|
54
|
+
});
|
|
55
|
+
const header = el.querySelector('[data-sh3-region="drawer"] header');
|
|
56
|
+
expect(header === null || header === void 0 ? void 0 : header.textContent).toContain('Files');
|
|
57
|
+
});
|
|
58
|
+
it('multi-slot drawer renders a tab strip with one button per slot', () => {
|
|
59
|
+
const el = renderHost({
|
|
60
|
+
anchor: 'left', spec: multiSpec, open: true, activeSlotId: 'pin',
|
|
61
|
+
onClose: () => { }, onActivate: () => { },
|
|
62
|
+
});
|
|
63
|
+
const tabs = el.querySelectorAll('[data-sh3-region="drawer"] .tab-strip button');
|
|
64
|
+
expect(tabs.length).toBe(2);
|
|
65
|
+
expect(tabs[1].classList.contains('active')).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* OverlayRoots — the six overlay-layer roots and the float-store binding,
|
|
4
|
+
* extracted so both the main shell (`Sh3.svelte`) and satellite shell
|
|
5
|
+
* (`SatelliteShell.svelte`) can mount them.
|
|
6
|
+
*
|
|
7
|
+
* Without this, satellite views can't open modals/popups/floats: layer
|
|
8
|
+
* managers call `getLayerRoot('modal')` and throw because no root was
|
|
9
|
+
* registered. Same situation for the float store binding the
|
|
10
|
+
* `FloatLayer` reads to render detached frames.
|
|
11
|
+
*
|
|
12
|
+
* Each layer is an absolutely-positioned full-window div with
|
|
13
|
+
* pointer-events: none; managers enable pointer events on the surfaces
|
|
14
|
+
* they portal in.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import DragPreview from '../layout/DragPreview.svelte';
|
|
18
|
+
import FloatLayer from './FloatLayer.svelte';
|
|
19
|
+
import { registerLayerRoot, unregisterLayerRoot } from './roots';
|
|
20
|
+
import { bindFloatStore, unbindFloatStore } from './float';
|
|
21
|
+
import { layoutStore } from '../layout/store.svelte';
|
|
22
|
+
import type { OverlayLayer } from './types';
|
|
23
|
+
|
|
24
|
+
// Layer metadata — order matches the stack in docs/design/layout.md.
|
|
25
|
+
// Index 0 here is layer 1 (floating panels); layer 0 is the content area.
|
|
26
|
+
// The 'drawers' layer (compact-mode side panels) sits between docked (0)
|
|
27
|
+
// and floating (1); its z-index comes from --sh3-z-layer-drawers.
|
|
28
|
+
const overlayLayers: { layer: number | string; name: OverlayLayer; zToken: string }[] = [
|
|
29
|
+
{ layer: 'drawers', name: 'drawers', zToken: '--sh3-z-layer-drawers' },
|
|
30
|
+
{ layer: 1, name: 'floating', zToken: '--sh3-z-layer-1' },
|
|
31
|
+
{ layer: 2, name: 'drag-preview', zToken: '--sh3-z-layer-2' },
|
|
32
|
+
{ layer: 3, name: 'popup', zToken: '--sh3-z-layer-3' },
|
|
33
|
+
{ layer: 4, name: 'modal', zToken: '--sh3-z-layer-4' },
|
|
34
|
+
{ layer: 5, name: 'toast', zToken: '--sh3-z-layer-5' },
|
|
35
|
+
{ layer: 6, name: 'command', zToken: '--sh3-z-layer-6' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const overlayRoots: Partial<Record<OverlayLayer, HTMLDivElement>> = $state({});
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
for (const { name } of overlayLayers) {
|
|
42
|
+
const el = overlayRoots[name];
|
|
43
|
+
if (el) registerLayerRoot(name, el);
|
|
44
|
+
}
|
|
45
|
+
return () => {
|
|
46
|
+
for (const { name } of overlayLayers) unregisterLayerRoot(name);
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
$effect(() => {
|
|
51
|
+
const tree = layoutStore.tree;
|
|
52
|
+
bindFloatStore(tree.floats, () => ({
|
|
53
|
+
w: window.innerWidth,
|
|
54
|
+
h: window.innerHeight,
|
|
55
|
+
}));
|
|
56
|
+
return () => unbindFloatStore();
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div class="sh3-overlays" aria-hidden="true">
|
|
61
|
+
{#each overlayLayers as { layer, name, zToken } (layer)}
|
|
62
|
+
<div
|
|
63
|
+
class="sh3-overlay-root"
|
|
64
|
+
data-sh3-overlay={name}
|
|
65
|
+
data-sh3-layer={layer}
|
|
66
|
+
style="z-index: var({zToken});"
|
|
67
|
+
bind:this={overlayRoots[name]}
|
|
68
|
+
>
|
|
69
|
+
{#if name === 'floating'}
|
|
70
|
+
<FloatLayer />
|
|
71
|
+
{:else if name === 'drag-preview'}
|
|
72
|
+
<DragPreview />
|
|
73
|
+
{/if}
|
|
74
|
+
</div>
|
|
75
|
+
{/each}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.sh3-overlays {
|
|
80
|
+
position: absolute;
|
|
81
|
+
inset: 0;
|
|
82
|
+
pointer-events: none;
|
|
83
|
+
}
|
|
84
|
+
.sh3-overlay-root {
|
|
85
|
+
position: absolute;
|
|
86
|
+
inset: 0;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
}
|
|
89
|
+
</style>
|
package/dist/overlays/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type OverlayLayer = 'floating' | 'drag-preview' | 'popup' | 'modal' | 'toast' | 'command';
|
|
1
|
+
export type OverlayLayer = 'drawers' | 'floating' | 'drag-preview' | 'popup' | 'modal' | 'toast' | 'command';
|
|
2
2
|
/** A handle returned by every overlay opener. Calling close() is idempotent. */
|
|
3
3
|
export interface OverlayHandle {
|
|
4
4
|
close(): void;
|
|
@@ -7,9 +7,9 @@ export declare class TauriStoreBackend implements Backend {
|
|
|
7
7
|
delete(shardId: string): void;
|
|
8
8
|
list(): string[];
|
|
9
9
|
/**
|
|
10
|
-
* Load the store from disk into the local cache
|
|
11
|
-
*
|
|
12
|
-
* resolver at boot.
|
|
10
|
+
* Load the store from disk into the local cache and start mirroring
|
|
11
|
+
* cross-window writes. Must be called once before read/list return
|
|
12
|
+
* meaningful data. Called by the platform resolver at boot.
|
|
13
13
|
*/
|
|
14
14
|
init(): Promise<void>;
|
|
15
15
|
}
|
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
* init(), then serve reads/lists synchronously from that cache —
|
|
9
9
|
* matching the Backend interface contract. Writes go to both the
|
|
10
10
|
* local cache and the Tauri store (fire-and-forget async).
|
|
11
|
+
*
|
|
12
|
+
* Multi-window coherence: each Tauri WebviewWindow runs its own JS
|
|
13
|
+
* context, so each gets its own TauriStoreBackend instance with an
|
|
14
|
+
* independent #cache. Without coordination, a satellite window booted
|
|
15
|
+
* from the host would never see writes made in the host (or vice
|
|
16
|
+
* versa). init() subscribes to plugin-store's onChange event so any
|
|
17
|
+
* cross-window write is mirrored into the local cache. The same event
|
|
18
|
+
* fires for this window's own writes too, but the cache.set there is
|
|
19
|
+
* idempotent.
|
|
11
20
|
*/
|
|
12
21
|
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
13
22
|
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
@@ -44,15 +53,27 @@ export class TauriStoreBackend {
|
|
|
44
53
|
return [...__classPrivateFieldGet(this, _TauriStoreBackend_cache, "f").keys()];
|
|
45
54
|
}
|
|
46
55
|
/**
|
|
47
|
-
* Load the store from disk into the local cache
|
|
48
|
-
*
|
|
49
|
-
* resolver at boot.
|
|
56
|
+
* Load the store from disk into the local cache and start mirroring
|
|
57
|
+
* cross-window writes. Must be called once before read/list return
|
|
58
|
+
* meaningful data. Called by the platform resolver at boot.
|
|
50
59
|
*/
|
|
51
60
|
async init() {
|
|
52
61
|
const entries = await __classPrivateFieldGet(this, _TauriStoreBackend_store, "f").entries();
|
|
53
62
|
for (const [key, value] of entries) {
|
|
54
63
|
__classPrivateFieldGet(this, _TauriStoreBackend_cache, "f").set(key, value);
|
|
55
64
|
}
|
|
65
|
+
// Mirror writes from other WebviewWindows (and our own) into the
|
|
66
|
+
// local cache so reads stay coherent across the host + satellites.
|
|
67
|
+
// The unlisten fn is intentionally not retained — the backend lives
|
|
68
|
+
// for the page session and the subscription tears down with it.
|
|
69
|
+
await __classPrivateFieldGet(this, _TauriStoreBackend_store, "f").onChange((key, value) => {
|
|
70
|
+
if (value === undefined) {
|
|
71
|
+
__classPrivateFieldGet(this, _TauriStoreBackend_cache, "f").delete(key);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
__classPrivateFieldGet(this, _TauriStoreBackend_cache, "f").set(key, value);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
56
77
|
}
|
|
57
78
|
}
|
|
58
79
|
_TauriStoreBackend_store = new WeakMap(), _TauriStoreBackend_cache = new WeakMap();
|
|
@@ -10,8 +10,8 @@ export declare const sessionState: {
|
|
|
10
10
|
*
|
|
11
11
|
* Setting the same value is a no-op (no app unload, no breadcrumb clear).
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Note: lifecycle ↔ session-state is a circular import. Safe because both
|
|
14
|
+
* sides only read the imported binding inside function bodies, after module
|
|
15
|
+
* init completes.
|
|
16
16
|
*/
|
|
17
17
|
export declare function setActiveProjectId(id: string | null): void;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* active app (whose scopeId is bound to the old scope).
|
|
12
12
|
*/
|
|
13
13
|
import { activeApp, breadcrumbApp } from '../apps/registry.svelte';
|
|
14
|
+
import { unloadApp } from '../apps/lifecycle';
|
|
14
15
|
export const sessionState = $state({
|
|
15
16
|
activeProjectId: null,
|
|
16
17
|
});
|
|
@@ -23,9 +24,9 @@ export const sessionState = $state({
|
|
|
23
24
|
*
|
|
24
25
|
* Setting the same value is a no-op (no app unload, no breadcrumb clear).
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Note: lifecycle ↔ session-state is a circular import. Safe because both
|
|
28
|
+
* sides only read the imported binding inside function bodies, after module
|
|
29
|
+
* init completes.
|
|
29
30
|
*/
|
|
30
31
|
export function setActiveProjectId(id) {
|
|
31
32
|
if (sessionState.activeProjectId === id)
|
|
@@ -34,6 +35,6 @@ export function setActiveProjectId(id) {
|
|
|
34
35
|
sessionState.activeProjectId = id;
|
|
35
36
|
breadcrumbApp.id = null;
|
|
36
37
|
if (previousActive) {
|
|
37
|
-
|
|
38
|
+
unloadApp(previousActive);
|
|
38
39
|
}
|
|
39
40
|
}
|
package/dist/runtime/runVerb.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { activeShards } from '../shards/activate.svelte';
|
|
19
19
|
import { getVerb, listVerbsWithShard } from '../shards/registry';
|
|
20
|
-
import {
|
|
20
|
+
import { makeSh3Api } from '../sh3Api/headless';
|
|
21
21
|
import { Scrollback } from '../shell-shard/scrollback.svelte';
|
|
22
22
|
import { SessionClient } from '../shell-shard/session-client.svelte';
|
|
23
23
|
import { TenantFsClient } from '../shell-shard/tenant-fs-client';
|
|
@@ -53,7 +53,7 @@ export async function runVerbProgrammatic(shardId, name, args, opts) {
|
|
|
53
53
|
async function buildProgrammaticContext(b) {
|
|
54
54
|
var _a, _b;
|
|
55
55
|
const ctx = {
|
|
56
|
-
sh3:
|
|
56
|
+
sh3: makeSh3Api({ callerKind: 'verb' }),
|
|
57
57
|
scrollback: b.sinkScrollback,
|
|
58
58
|
session: makeStubSession(),
|
|
59
59
|
cwd: '/',
|
|
@@ -3,22 +3,23 @@
|
|
|
3
3
|
* SatelliteShell — top-level root component mounted by createShell() when
|
|
4
4
|
* detectSatelliteMode() returns a payload.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* already been forced to MemoryBackend by createShell() before this mounts.
|
|
9
|
-
*
|
|
10
|
-
* Float payloads: seeds HOME_TREE's docked node directly via
|
|
11
|
-
* seedSatelliteLayout(), so LayoutRenderer immediately shows the detached
|
|
6
|
+
* Float payloads: chromeless. Seeds HOME_TREE's docked node directly via
|
|
7
|
+
* seedSatelliteLayout() so LayoutRenderer immediately shows the detached
|
|
12
8
|
* float content.
|
|
13
9
|
*
|
|
14
|
-
* App payloads:
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* App payloads: renders a slim tabbar (BrandSlot + MenuBar + reserved
|
|
11
|
+
* minimized-floats slot) above LayoutRenderer, then kicks off
|
|
12
|
+
* launchApp(appId, { skipLastApp, skipSwitchToHome }) which handles the
|
|
13
|
+
* full attach + required-shard activation lifecycle. Home button is
|
|
14
|
+
* intentionally omitted — the OS window close is the satellite's "home".
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
17
|
import '../tokens.css';
|
|
20
18
|
import '../primitives/base.css';
|
|
21
19
|
import LayoutRenderer from '../layout/LayoutRenderer.svelte';
|
|
20
|
+
import OverlayRoots from '../overlays/OverlayRoots.svelte';
|
|
21
|
+
import BrandSlot from '../BrandSlot.svelte';
|
|
22
|
+
import MenuBar from '../actions/MenuBar.svelte';
|
|
22
23
|
import { seedSatelliteLayout } from '../layout/store.svelte';
|
|
23
24
|
import { seedLayoutFromPayload } from './seed';
|
|
24
25
|
import { launchApp } from '../apps/lifecycle';
|
|
@@ -45,8 +46,20 @@
|
|
|
45
46
|
});
|
|
46
47
|
</script>
|
|
47
48
|
|
|
48
|
-
<div class="sh3-satellite-root">
|
|
49
|
-
|
|
49
|
+
<div class="sh3-satellite-root" class:sh3-satellite-app={payload.kind === 'app'}>
|
|
50
|
+
{#if payload.kind === 'app'}
|
|
51
|
+
<header class="sh3-satellite-tabbar" data-sh3-region="tabbar">
|
|
52
|
+
<BrandSlot />
|
|
53
|
+
<MenuBar />
|
|
54
|
+
<div class="sh3-satellite-floats-slot" aria-hidden="true"></div>
|
|
55
|
+
</header>
|
|
56
|
+
<main class="sh3-satellite-content" data-sh3-region="content" data-sh3-layer="0">
|
|
57
|
+
<LayoutRenderer />
|
|
58
|
+
</main>
|
|
59
|
+
{:else}
|
|
60
|
+
<LayoutRenderer />
|
|
61
|
+
{/if}
|
|
62
|
+
<OverlayRoots />
|
|
50
63
|
</div>
|
|
51
64
|
|
|
52
65
|
<style>
|
|
@@ -57,4 +70,38 @@
|
|
|
57
70
|
background: var(--sh3-grad-bg, var(--sh3-bg));
|
|
58
71
|
color: var(--sh3-fg);
|
|
59
72
|
}
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* App-payload satellites get a slim chrome row above the content. Float
|
|
76
|
+
* payloads keep the original full-bleed layout (no grid, content fills
|
|
77
|
+
* the root), so the .sh3-satellite-app modifier is required for the grid.
|
|
78
|
+
*/
|
|
79
|
+
.sh3-satellite-root.sh3-satellite-app {
|
|
80
|
+
display: grid;
|
|
81
|
+
grid-template-rows: var(--sh3-tabbar-height) 1fr;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.sh3-satellite-tabbar {
|
|
85
|
+
display: grid;
|
|
86
|
+
grid-template-columns: auto 1fr auto;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: var(--sh3-pad-md);
|
|
89
|
+
padding: 0 var(--sh3-pad-md);
|
|
90
|
+
background: var(--sh3-grad-bg-elevated, var(--sh3-bg-elevated));
|
|
91
|
+
border-bottom: 1px solid var(--sh3-border);
|
|
92
|
+
user-select: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.sh3-satellite-content {
|
|
96
|
+
position: relative;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
background: var(--sh3-grad-bg, var(--sh3-bg));
|
|
99
|
+
min-width: 0;
|
|
100
|
+
min-height: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.sh3-satellite-floats-slot {
|
|
104
|
+
/* Reserved cell for future minimized-float chips; empty for now. */
|
|
105
|
+
min-width: 0;
|
|
106
|
+
}
|
|
60
107
|
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import SatelliteShell from './SatelliteShell.svelte';
|
|
4
|
+
import { resetFramework } from '../__test__/reset';
|
|
5
|
+
import { renderWithShell } from '../__test__/render';
|
|
6
|
+
import { makeApp, makeAppManifest, makeSlotNode, makeTree, } from '../__test__/fixtures';
|
|
7
|
+
import { registerApp } from '../apps/registry.svelte';
|
|
8
|
+
import { registerView } from '../shards/registry';
|
|
9
|
+
function registerStubView() {
|
|
10
|
+
registerView('test:view', {
|
|
11
|
+
mount(container, ctx) {
|
|
12
|
+
const node = document.createElement('div');
|
|
13
|
+
node.dataset.viewFor = ctx.slotId;
|
|
14
|
+
container.appendChild(node);
|
|
15
|
+
return { unmount: () => node.remove() };
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
describe('SatelliteShell — chrome rendering', () => {
|
|
20
|
+
beforeEach(resetFramework);
|
|
21
|
+
it('renders BrandSlot + MenuBar + reserved floats slot for app payloads', async () => {
|
|
22
|
+
registerStubView();
|
|
23
|
+
registerApp(makeApp({
|
|
24
|
+
manifest: makeAppManifest({
|
|
25
|
+
id: 'sat-app',
|
|
26
|
+
label: 'Sat App',
|
|
27
|
+
}),
|
|
28
|
+
initialLayout: [
|
|
29
|
+
{ name: 'default', tree: makeTree(makeSlotNode('s', 'test:view')) },
|
|
30
|
+
],
|
|
31
|
+
}));
|
|
32
|
+
const payload = {
|
|
33
|
+
kind: 'app',
|
|
34
|
+
appId: 'sat-app',
|
|
35
|
+
activateShards: [],
|
|
36
|
+
};
|
|
37
|
+
const { container } = renderWithShell(SatelliteShell, { payload });
|
|
38
|
+
// launchApp runs in queueMicrotask inside the $effect; flush both.
|
|
39
|
+
await Promise.resolve();
|
|
40
|
+
await tick();
|
|
41
|
+
await tick();
|
|
42
|
+
expect(container.querySelector('[data-sh3-region="tabbar"]')).toBeTruthy();
|
|
43
|
+
expect(container.querySelector('.sh3-brand-slot')).toBeTruthy();
|
|
44
|
+
expect(container.querySelector('[role="menubar"]')).toBeTruthy();
|
|
45
|
+
expect(container.querySelector('.sh3-satellite-floats-slot')).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
it('renders no tabbar / chrome for float payloads', async () => {
|
|
48
|
+
const payload = {
|
|
49
|
+
kind: 'float',
|
|
50
|
+
content: makeSlotNode('s', 'test:view'),
|
|
51
|
+
size: { w: 800, h: 600 },
|
|
52
|
+
activateShards: [],
|
|
53
|
+
};
|
|
54
|
+
const { container } = renderWithShell(SatelliteShell, { payload });
|
|
55
|
+
await tick();
|
|
56
|
+
expect(container.querySelector('[data-sh3-region="tabbar"]')).toBeFalsy();
|
|
57
|
+
expect(container.querySelector('.sh3-brand-slot')).toBeFalsy();
|
|
58
|
+
expect(container.querySelector('[role="menubar"]')).toBeFalsy();
|
|
59
|
+
expect(container.querySelector('.sh3-satellite-floats-slot')).toBeFalsy();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|