sh3-core 0.10.5 → 0.11.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/Shell.svelte +12 -31
- package/dist/__test__/reset.js +6 -0
- package/dist/actions/CommandPalette.svelte +68 -0
- package/dist/actions/CommandPalette.svelte.d.ts +11 -0
- package/dist/actions/ContextMenu.svelte +97 -0
- package/dist/actions/ContextMenu.svelte.d.ts +9 -0
- package/dist/actions/bindings-store.d.ts +8 -0
- package/dist/actions/bindings-store.js +27 -0
- package/dist/actions/bindings-store.test.d.ts +1 -0
- package/dist/actions/bindings-store.test.js +25 -0
- package/dist/actions/bindings.d.ts +4 -0
- package/dist/actions/bindings.js +17 -0
- package/dist/actions/bindings.test.d.ts +1 -0
- package/dist/actions/bindings.test.js +30 -0
- package/dist/actions/contextMenuModel.d.ts +16 -0
- package/dist/actions/contextMenuModel.js +71 -0
- package/dist/actions/contextMenuModel.test.d.ts +1 -0
- package/dist/actions/contextMenuModel.test.js +44 -0
- package/dist/actions/dispatcher.svelte.d.ts +34 -0
- package/dist/actions/dispatcher.svelte.js +117 -0
- package/dist/actions/dispatcher.test.d.ts +1 -0
- package/dist/actions/dispatcher.test.js +155 -0
- package/dist/actions/listeners.d.ts +11 -0
- package/dist/actions/listeners.js +180 -0
- package/dist/actions/listeners.test.d.ts +1 -0
- package/dist/actions/listeners.test.js +149 -0
- package/dist/actions/palette-scorer.d.ts +11 -0
- package/dist/actions/palette-scorer.js +49 -0
- package/dist/actions/palette-scorer.test.d.ts +1 -0
- package/dist/actions/palette-scorer.test.js +40 -0
- package/dist/actions/paletteModel.d.ts +4 -0
- package/dist/actions/paletteModel.js +40 -0
- package/dist/actions/paletteModel.test.d.ts +1 -0
- package/dist/actions/paletteModel.test.js +33 -0
- package/dist/actions/registry.d.ts +10 -0
- package/dist/actions/registry.js +36 -0
- package/dist/actions/registry.test.d.ts +1 -0
- package/dist/actions/registry.test.js +49 -0
- package/dist/actions/selection.svelte.d.ts +8 -0
- package/dist/actions/selection.svelte.js +44 -0
- package/dist/actions/selection.test.d.ts +1 -0
- package/dist/actions/selection.test.js +51 -0
- package/dist/actions/shardContext.test.d.ts +1 -0
- package/dist/actions/shardContext.test.js +41 -0
- package/dist/actions/shellActions.test.d.ts +1 -0
- package/dist/actions/shellActions.test.js +22 -0
- package/dist/actions/shortcuts.d.ts +5 -0
- package/dist/actions/shortcuts.js +87 -0
- package/dist/actions/shortcuts.test.d.ts +1 -0
- package/dist/actions/shortcuts.test.js +49 -0
- package/dist/actions/state.svelte.d.ts +16 -0
- package/dist/actions/state.svelte.js +76 -0
- package/dist/actions/state.test.d.ts +1 -0
- package/dist/actions/state.test.js +40 -0
- package/dist/actions/syncMountedViewIds.test.d.ts +1 -0
- package/dist/actions/syncMountedViewIds.test.js +97 -0
- package/dist/actions/types.d.ts +56 -0
- package/dist/actions/types.js +7 -0
- package/dist/api.d.ts +2 -2
- package/dist/api.js +1 -1
- package/dist/apps/lifecycle.js +13 -3
- package/dist/createShell.js +4 -1
- package/dist/host.js +6 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/layout/inspection.d.ts +11 -1
- package/dist/layout/inspection.js +13 -1
- package/dist/layout/ops-locate.test.d.ts +1 -0
- package/dist/layout/ops-locate.test.js +103 -0
- package/dist/layout/ops.d.ts +8 -0
- package/dist/layout/ops.js +27 -0
- package/dist/layout/slotHostPool.svelte.js +24 -0
- package/dist/layout/slotHostPool.test.js +14 -0
- package/dist/layout/types.d.ts +7 -0
- package/dist/overlays/FloatFrame.svelte +23 -11
- package/dist/overlays/ModalFrame.svelte +9 -1
- package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
- package/dist/overlays/__test__/DummyFrame.svelte +6 -0
- package/dist/overlays/__test__/DummyFrame.svelte.d.ts +6 -0
- package/dist/overlays/float.d.ts +6 -0
- package/dist/overlays/float.js +24 -9
- package/dist/overlays/float.test.js +175 -0
- package/dist/overlays/floatDismiss.d.ts +8 -0
- package/dist/overlays/floatDismiss.js +68 -0
- package/dist/overlays/modal.js +5 -1
- package/dist/overlays/modal.test.d.ts +1 -0
- package/dist/overlays/modal.test.js +55 -0
- package/dist/overlays/popup.d.ts +2 -0
- package/dist/overlays/popup.js +24 -4
- package/dist/overlays/popup.test.d.ts +1 -0
- package/dist/overlays/popup.test.js +95 -0
- package/dist/overlays/types.d.ts +17 -1
- package/dist/primitives/Button.svelte +144 -0
- package/dist/primitives/Button.svelte.d.ts +18 -0
- package/dist/primitives/icon-context.d.ts +15 -0
- package/dist/primitives/icon-context.js +29 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +50 -0
- package/dist/shards/activate.svelte.js +14 -0
- package/dist/shards/types.d.ts +19 -0
- package/dist/shards/types.js +5 -4
- package/dist/shell-shard/locateSlot.test.d.ts +1 -0
- package/dist/shell-shard/locateSlot.test.js +101 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +7 -0
- package/dist/shell-shard/shellShard.svelte.js +34 -1
- package/dist/shellRuntime.svelte.d.ts +19 -0
- package/dist/shellRuntime.svelte.js +30 -0
- package/dist/tokens.css +11 -1
- package/dist/verbs/types.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/apps/terminal/manifest.d.ts +0 -8
- package/dist/apps/terminal/manifest.js +0 -14
- package/dist/apps/terminal/terminal-app.d.ts +0 -7
- package/dist/apps/terminal/terminal-app.js +0 -14
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { setActiveApp, setFocusedViewId, setMountedViewIds, setUserBindings, getLiveDispatcherState, __resetDispatcherStateForTest, } from './state.svelte';
|
|
3
|
+
import { __resetSelectionForTest, makeSelectionApi } from './selection.svelte';
|
|
4
|
+
describe('live dispatcher state', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
__resetDispatcherStateForTest();
|
|
7
|
+
__resetSelectionForTest();
|
|
8
|
+
});
|
|
9
|
+
it('starts in home state with empty sets', () => {
|
|
10
|
+
const s = getLiveDispatcherState();
|
|
11
|
+
expect(s.activeAppId).toBeNull();
|
|
12
|
+
expect(s.mountedViewIds.size).toBe(0);
|
|
13
|
+
expect(s.focusedViewId).toBeNull();
|
|
14
|
+
expect(s.selection).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
it('setActiveApp updates activeAppId and requiredShards', () => {
|
|
17
|
+
setActiveApp('app.a', new Set(['shard.x']));
|
|
18
|
+
const s = getLiveDispatcherState();
|
|
19
|
+
expect(s.activeAppId).toBe('app.a');
|
|
20
|
+
expect(s.activeAppRequiredShards.has('shard.x')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('setFocusedViewId updates focus', () => {
|
|
23
|
+
setFocusedViewId('editor');
|
|
24
|
+
expect(getLiveDispatcherState().focusedViewId).toBe('editor');
|
|
25
|
+
});
|
|
26
|
+
it('setMountedViewIds updates mounted', () => {
|
|
27
|
+
setMountedViewIds(new Set(['a', 'b']));
|
|
28
|
+
expect(getLiveDispatcherState().mountedViewIds.has('a')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('setUserBindings updates bindings', () => {
|
|
31
|
+
setUserBindings({ 'shard.x.save': 'Ctrl+Alt+S' });
|
|
32
|
+
expect(getLiveDispatcherState().bindings['shard.x.save']).toBe('Ctrl+Alt+S');
|
|
33
|
+
});
|
|
34
|
+
it('reads selection reactively', () => {
|
|
35
|
+
var _a;
|
|
36
|
+
const api = makeSelectionApi('shard.a');
|
|
37
|
+
api.set({ type: 'orb', ref: 42 });
|
|
38
|
+
expect((_a = getLiveDispatcherState().selection) === null || _a === void 0 ? void 0 : _a.type).toBe('orb');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { resetFramework } from '../__test__/reset';
|
|
3
|
+
import { makeApp, makeAppManifest, makeTabEntry, makeTabsNode, makeTree, makeSlotNode, makeSplitNode } from '../__test__/fixtures';
|
|
4
|
+
import { registerApp } from '../apps/registry.svelte';
|
|
5
|
+
import { launchApp } from '../apps/lifecycle';
|
|
6
|
+
import { syncMountedViewIdsFromLayout, getLiveDispatcherState } from './state.svelte';
|
|
7
|
+
import { bindFloatStore } from '../overlays/float';
|
|
8
|
+
import { layoutStore } from '../layout/store.svelte';
|
|
9
|
+
import { popoutView } from '../layout/inspection';
|
|
10
|
+
describe('syncMountedViewIdsFromLayout', () => {
|
|
11
|
+
beforeEach(resetFramework);
|
|
12
|
+
it('collects viewIds from the docked tree after an app launches', async () => {
|
|
13
|
+
registerApp(makeApp({
|
|
14
|
+
manifest: makeAppManifest({ id: 'sync-app-docked' }),
|
|
15
|
+
initialLayout: [
|
|
16
|
+
{
|
|
17
|
+
name: 'default',
|
|
18
|
+
tree: makeTree(makeTabsNode([
|
|
19
|
+
makeTabEntry({ slotId: 's1', viewId: 'view:a' }),
|
|
20
|
+
makeTabEntry({ slotId: 's2', viewId: 'view:b' }),
|
|
21
|
+
])),
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
}));
|
|
25
|
+
await launchApp('sync-app-docked');
|
|
26
|
+
syncMountedViewIdsFromLayout();
|
|
27
|
+
const mounted = getLiveDispatcherState().mountedViewIds;
|
|
28
|
+
expect(mounted.has('view:a')).toBe(true);
|
|
29
|
+
expect(mounted.has('view:b')).toBe(true);
|
|
30
|
+
expect(mounted.size).toBe(2);
|
|
31
|
+
});
|
|
32
|
+
it('collects viewIds from splits and bare slot leaves', async () => {
|
|
33
|
+
registerApp(makeApp({
|
|
34
|
+
manifest: makeAppManifest({ id: 'sync-app-split' }),
|
|
35
|
+
initialLayout: [
|
|
36
|
+
{
|
|
37
|
+
name: 'default',
|
|
38
|
+
tree: makeTree(makeSplitNode([
|
|
39
|
+
makeSlotNode('leaf-s', 'view:leaf'),
|
|
40
|
+
makeTabsNode([makeTabEntry({ slotId: 'tab-s', viewId: 'view:tabbed' })]),
|
|
41
|
+
])),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}));
|
|
45
|
+
await launchApp('sync-app-split');
|
|
46
|
+
syncMountedViewIdsFromLayout();
|
|
47
|
+
const mounted = getLiveDispatcherState().mountedViewIds;
|
|
48
|
+
expect(mounted.has('view:leaf')).toBe(true);
|
|
49
|
+
expect(mounted.has('view:tabbed')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('includes viewIds inside floats', async () => {
|
|
52
|
+
registerApp(makeApp({
|
|
53
|
+
manifest: makeAppManifest({ id: 'sync-app-float' }),
|
|
54
|
+
initialLayout: [
|
|
55
|
+
{
|
|
56
|
+
name: 'default',
|
|
57
|
+
tree: makeTree(makeTabsNode([makeTabEntry({ slotId: 'anchor', viewId: 'view:anchor' })])),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}));
|
|
61
|
+
await launchApp('sync-app-float');
|
|
62
|
+
// Bind the float manager to the live tree (Shell.svelte does this at boot).
|
|
63
|
+
bindFloatStore(layoutStore.floats, () => ({ w: 1024, h: 768 }));
|
|
64
|
+
// Popout the anchor → creates a float with view:anchor inside.
|
|
65
|
+
popoutView('anchor');
|
|
66
|
+
syncMountedViewIdsFromLayout();
|
|
67
|
+
const mounted = getLiveDispatcherState().mountedViewIds;
|
|
68
|
+
// Anchor was removed from docked and re-added inside a float.
|
|
69
|
+
expect(mounted.has('view:anchor')).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it('drops viewIds that leave the tree', async () => {
|
|
72
|
+
registerApp(makeApp({
|
|
73
|
+
manifest: makeAppManifest({ id: 'sync-app-remove' }),
|
|
74
|
+
initialLayout: [
|
|
75
|
+
{
|
|
76
|
+
name: 'default',
|
|
77
|
+
tree: makeTree(makeTabsNode([
|
|
78
|
+
makeTabEntry({ slotId: 'keep', viewId: 'view:keep' }),
|
|
79
|
+
makeTabEntry({ slotId: 'drop', viewId: 'view:drop' }),
|
|
80
|
+
])),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
}));
|
|
84
|
+
await launchApp('sync-app-remove');
|
|
85
|
+
syncMountedViewIdsFromLayout();
|
|
86
|
+
expect(getLiveDispatcherState().mountedViewIds.has('view:drop')).toBe(true);
|
|
87
|
+
// Mutate the tree directly — remove the 'drop' tab.
|
|
88
|
+
const tabs = layoutStore.tree.docked.type === 'tabs' ? layoutStore.tree.docked : null;
|
|
89
|
+
if (tabs) {
|
|
90
|
+
tabs.tabs = tabs.tabs.filter((t) => t.slotId !== 'drop');
|
|
91
|
+
}
|
|
92
|
+
syncMountedViewIdsFromLayout();
|
|
93
|
+
const mounted = getLiveDispatcherState().mountedViewIds;
|
|
94
|
+
expect(mounted.has('view:keep')).toBe(true);
|
|
95
|
+
expect(mounted.has('view:drop')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type AtomicScope = 'home' | 'app' | `view:${string}` | `focus:${string}` | {
|
|
2
|
+
element: string;
|
|
3
|
+
};
|
|
4
|
+
export type ActionScope = AtomicScope | AtomicScope[];
|
|
5
|
+
export interface Action {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
scope: ActionScope;
|
|
9
|
+
contextItem?: boolean;
|
|
10
|
+
paletteItem?: boolean;
|
|
11
|
+
defaultShortcut?: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
group?: string;
|
|
14
|
+
allowInInputs?: boolean;
|
|
15
|
+
run(ctx: ActionDispatchContext): void | Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface Selection {
|
|
18
|
+
type: string;
|
|
19
|
+
ref: unknown;
|
|
20
|
+
ownerShardId: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ActionDispatchContext {
|
|
23
|
+
action: {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
};
|
|
27
|
+
appId: string | null;
|
|
28
|
+
viewId?: string;
|
|
29
|
+
selection?: Selection;
|
|
30
|
+
invokedVia: 'keyboard' | 'context-menu' | 'palette' | 'programmatic';
|
|
31
|
+
dispatch(actionId: string): void;
|
|
32
|
+
}
|
|
33
|
+
export interface SelectionApi {
|
|
34
|
+
get(): Selection | null;
|
|
35
|
+
set(sel: {
|
|
36
|
+
type: string;
|
|
37
|
+
ref: unknown;
|
|
38
|
+
}): void;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
export interface ActionsApi {
|
|
42
|
+
register(action: Action): () => void;
|
|
43
|
+
selection: SelectionApi;
|
|
44
|
+
openContextMenu(opts: {
|
|
45
|
+
x: number;
|
|
46
|
+
y: number;
|
|
47
|
+
}): void;
|
|
48
|
+
openPalette(opts?: {
|
|
49
|
+
prefill?: string;
|
|
50
|
+
}): void;
|
|
51
|
+
}
|
|
52
|
+
export interface ResolvedAction {
|
|
53
|
+
action: Action;
|
|
54
|
+
ownerShardId: string;
|
|
55
|
+
effectiveShortcut: string | null;
|
|
56
|
+
}
|
package/dist/api.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { shell } from './shellRuntime.svelte';
|
|
2
2
|
export type { Shell } from './shellRuntime.svelte';
|
|
3
3
|
export type { Shard, ShardManifest, SourceShard, SourceShardManifest, ShardContext, ViewDeclaration, ViewFactory, ViewHandle, MountContext, } from './shards/types';
|
|
4
|
-
export type { LayoutNode, SplitNode, TabsNode, SlotNode, TabEntry, SplitDirection, SizeMode, LayoutTree, FloatEntry, LayoutPreset, CanonicalPreset, } from './layout/types';
|
|
4
|
+
export type { LayoutNode, SplitNode, TabsNode, SlotNode, TabEntry, SplitDirection, SizeMode, LayoutTree, FloatEntry, LayoutPreset, CanonicalPreset, TreeRootRef as SlotLocation, } from './layout/types';
|
|
5
5
|
export type { FloatManager, FloatOptions } from './overlays/float';
|
|
6
6
|
export type { ModalManager } from './overlays/modal';
|
|
7
7
|
export type { PopupManager } from './overlays/popup';
|
|
@@ -15,7 +15,7 @@ export type { EnvState } from './env/types';
|
|
|
15
15
|
export type { App, AppManifest, SourceApp, SourceAppManifest, AppContext, } from './apps/types';
|
|
16
16
|
export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
|
|
17
17
|
export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
|
|
18
|
-
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, } from './layout/inspection';
|
|
18
|
+
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, popoutView, dockFloat, locateSlot, } from './layout/inspection';
|
|
19
19
|
export type { DocumentHandle, DocumentHandleOptions, DocumentFormat, DocumentMeta, DocumentChange, AutosaveController, } from './documents/types';
|
|
20
20
|
export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
|
|
21
21
|
export type { BrowseCapability } from './documents/browse';
|
package/dist/api.js
CHANGED
|
@@ -28,7 +28,7 @@ export { PERMISSION_STATE_MANAGE } from './state/types';
|
|
|
28
28
|
export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
|
|
29
29
|
export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
|
|
30
30
|
// Layout inspection / mutation for advanced shards (diagnostic, etc.).
|
|
31
|
-
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, } from './layout/inspection';
|
|
31
|
+
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, popoutView, dockFloat, locateSlot, } from './layout/inspection';
|
|
32
32
|
export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
|
|
33
33
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
|
|
34
34
|
export { CONFLICT_RENDERER_POINT, ConflictPermissionError, ConflictSessionOrphanedError, } from './conflicts/api';
|
package/dist/apps/lifecycle.js
CHANGED
|
@@ -17,6 +17,9 @@ import { attachApp, acquireAppSlotHolds, detachApp, switchToApp, switchToHome, }
|
|
|
17
17
|
import { activeApp, getRegisteredApp, registeredApps } from './registry.svelte';
|
|
18
18
|
import { createZoneManager } from '../state/manage';
|
|
19
19
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
20
|
+
import { setActiveApp, setUserBindings } from '../actions/state.svelte';
|
|
21
|
+
import { clearSelectionUnconditional } from '../actions/selection.svelte';
|
|
22
|
+
import { loadUserBindings } from '../actions/bindings-store';
|
|
20
23
|
// ---------- last-active-app user zone ------------------------------------
|
|
21
24
|
/**
|
|
22
25
|
* Framework-reserved user-zone slot storing which app to boot into on
|
|
@@ -69,7 +72,7 @@ function getOrCreateAppContext(appId) {
|
|
|
69
72
|
* @throws If the app is not registered or a required shard is not registered.
|
|
70
73
|
*/
|
|
71
74
|
export async function launchApp(id) {
|
|
72
|
-
var _a, _b, _c, _d, _e;
|
|
75
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
73
76
|
const app = getRegisteredApp(id);
|
|
74
77
|
if (!app) {
|
|
75
78
|
throw new Error(`Cannot launch app "${id}": not registered`);
|
|
@@ -93,6 +96,8 @@ export async function launchApp(id) {
|
|
|
93
96
|
switchToApp();
|
|
94
97
|
void ((_c = app.onAppReady) === null || _c === void 0 ? void 0 : _c.call(app, getOrCreateAppContext(id)));
|
|
95
98
|
writeLastApp(id);
|
|
99
|
+
setActiveApp(id, new Set((_d = app.manifest.requiredShards) !== null && _d !== void 0 ? _d : []));
|
|
100
|
+
void loadUserBindings(id).then(setUserBindings);
|
|
96
101
|
return;
|
|
97
102
|
}
|
|
98
103
|
// Validate required shards are registered before attaching anything,
|
|
@@ -123,10 +128,12 @@ export async function launchApp(id) {
|
|
|
123
128
|
// refcount holds on the app's slots now (pool's factory lookup
|
|
124
129
|
// happens in a microtask from this call).
|
|
125
130
|
acquireAppSlotHolds();
|
|
126
|
-
void ((
|
|
131
|
+
void ((_e = app.activate) === null || _e === void 0 ? void 0 : _e.call(app, getOrCreateAppContext(id)));
|
|
127
132
|
activeApp.id = id;
|
|
133
|
+
setActiveApp(id, new Set((_f = app.manifest.requiredShards) !== null && _f !== void 0 ? _f : []));
|
|
134
|
+
void loadUserBindings(id).then(setUserBindings);
|
|
128
135
|
switchToApp();
|
|
129
|
-
void ((
|
|
136
|
+
void ((_g = app.onAppReady) === null || _g === void 0 ? void 0 : _g.call(app, getOrCreateAppContext(id)));
|
|
130
137
|
writeLastApp(id);
|
|
131
138
|
}
|
|
132
139
|
// ---------- unload --------------------------------------------------------
|
|
@@ -167,6 +174,9 @@ export function unloadApp(id) {
|
|
|
167
174
|
deactivateShard(shardId);
|
|
168
175
|
}
|
|
169
176
|
activeApp.id = null;
|
|
177
|
+
setActiveApp(null, new Set());
|
|
178
|
+
clearSelectionUnconditional();
|
|
179
|
+
void loadUserBindings('sh3.home').then(setUserBindings);
|
|
170
180
|
appContexts.delete(id);
|
|
171
181
|
}
|
|
172
182
|
// ---------- unregister -------------------------------------------------------
|
package/dist/createShell.js
CHANGED
|
@@ -17,6 +17,7 @@ import { initFromBoot } from './auth/index';
|
|
|
17
17
|
import SignInWall from './auth/SignInWall.svelte';
|
|
18
18
|
import { loadBundleModule } from './registry/loader';
|
|
19
19
|
import { registerLoadedBundle } from './registry/register';
|
|
20
|
+
import { attachGlobalListeners } from './actions/listeners';
|
|
20
21
|
export async function createShell(config) {
|
|
21
22
|
var _a, _b, _c, _d, _e;
|
|
22
23
|
const sUrl = (_a = config === null || config === void 0 ? void 0 : config.serverUrl) !== null && _a !== void 0 ? _a : '';
|
|
@@ -110,7 +111,9 @@ export async function createShell(config) {
|
|
|
110
111
|
if (config === null || config === void 0 ? void 0 : config.excludeShards)
|
|
111
112
|
bootstrapConfig.excludeShards = config.excludeShards;
|
|
112
113
|
await bootstrap(bootstrapConfig);
|
|
113
|
-
// 8.
|
|
114
|
+
// 8. Attach document-level keyboard / focus listeners
|
|
115
|
+
attachGlobalListeners();
|
|
116
|
+
// 9. Mount the shell
|
|
114
117
|
mount(Shell, { target });
|
|
115
118
|
}
|
|
116
119
|
/**
|
package/dist/host.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* imports from `host.ts`.
|
|
17
17
|
*/
|
|
18
18
|
import { registerShard as registerShardInternal, activateShard, registeredShards, } from './shards/activate.svelte';
|
|
19
|
+
import { addAutostartShard } from './actions/state.svelte';
|
|
19
20
|
import { registerApp, registeredApps } from './apps/registry.svelte';
|
|
20
21
|
import { launchApp, readLastApp } from './apps/lifecycle';
|
|
21
22
|
import { sh3coreShard } from './sh3core-shard/sh3coreShard.svelte';
|
|
@@ -27,7 +28,6 @@ import { setLocalOwner } from './auth/index';
|
|
|
27
28
|
import { storeApp } from './app/store/storeApp';
|
|
28
29
|
import { adminShard } from './app/admin/adminShard.svelte';
|
|
29
30
|
import { adminApp } from './app/admin/adminApp';
|
|
30
|
-
import { terminalApp } from './apps/terminal/terminal-app';
|
|
31
31
|
import { runShellRenameMigration, } from './migrations/shell-rename';
|
|
32
32
|
export { __setBackend };
|
|
33
33
|
export { setLocalOwner };
|
|
@@ -65,15 +65,18 @@ export async function bootstrap(config) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
// 2. Framework-shipped apps
|
|
68
|
-
const frameworkApps = [storeApp, adminApp
|
|
68
|
+
const frameworkApps = [storeApp, adminApp];
|
|
69
69
|
for (const app of frameworkApps) {
|
|
70
70
|
registerApp(app);
|
|
71
71
|
}
|
|
72
72
|
// 3. Load any packages installed in a previous session from IndexedDB
|
|
73
73
|
await loadInstalledPackages();
|
|
74
|
-
// 4. Activate every self-starting shard
|
|
74
|
+
// 4. Activate every self-starting shard. Track them in the dispatcher's
|
|
75
|
+
// autostartShards set so the `'app'` action scope treats their actions as
|
|
76
|
+
// ambient (active even inside apps that don't list them as required).
|
|
75
77
|
for (const [id, shard] of registeredShards) {
|
|
76
78
|
if (shard.autostart) {
|
|
79
|
+
addAutostartShard(id);
|
|
77
80
|
await activateShard(id);
|
|
78
81
|
}
|
|
79
82
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from './api';
|
|
2
2
|
export { default as Shell } from './Shell.svelte';
|
|
3
|
+
export { default as Button } from './primitives/Button.svelte';
|
|
4
|
+
export { provideIcons, getIconSprite, type ButtonVariant } from './primitives/icon-context';
|
|
3
5
|
export type { ArtifactManifest } from './artifact';
|
|
4
6
|
export * from './shell-shard/protocol';
|
package/dist/index.js
CHANGED
|
@@ -11,4 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export * from './api';
|
|
13
13
|
export { default as Shell } from './Shell.svelte';
|
|
14
|
+
export { default as Button } from './primitives/Button.svelte';
|
|
15
|
+
export { provideIcons, getIconSprite } from './primitives/icon-context';
|
|
14
16
|
export * from './shell-shard/protocol';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TabEntry, LayoutTree } from './types';
|
|
1
|
+
import type { TabEntry, LayoutTree, TreeRootRef } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Read-only snapshot of the currently-rendered layout tree. The return
|
|
4
4
|
* value is the live object — callers MUST NOT mutate it directly;
|
|
@@ -87,3 +87,13 @@ export declare function dockFloat(floatId: string): boolean;
|
|
|
87
87
|
* was empty or otherwise un-dockable.
|
|
88
88
|
*/
|
|
89
89
|
export declare function dockIntoActiveLayout(entry: TabEntry): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Find which root a slot currently lives under in the active layout.
|
|
92
|
+
* Returns `{ kind: 'docked' }` when the slot is anywhere in the docked
|
|
93
|
+
* tree, `{ kind: 'float', floatId }` when it lives inside a float, or
|
|
94
|
+
* `null` when the slot is not present (stale id, post-unmount, or
|
|
95
|
+
* held-but-not-active app tree). Thin wrapper over the pure
|
|
96
|
+
* `locateSlotIn(tree, slotId)` from `ops.ts` — use this when you only
|
|
97
|
+
* have a slotId in hand; use `locateSlotIn` if you already hold the tree.
|
|
98
|
+
*/
|
|
99
|
+
export declare function locateSlot(slotId: string): TreeRootRef | null;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* rendered.
|
|
14
14
|
*/
|
|
15
15
|
import { activeLayout, getActiveRoot } from './store.svelte';
|
|
16
|
-
import { nodeAtPath, findTabBySlotId, removeTabBySlotId, cleanupTree, splitNodeAtPath, } from './ops';
|
|
16
|
+
import { nodeAtPath, findTabBySlotId, removeTabBySlotId, cleanupTree, splitNodeAtPath, locateSlotIn, } from './ops';
|
|
17
17
|
import { getSlotHandle } from './slotHostPool.svelte';
|
|
18
18
|
import { floatManager } from '../overlays/float';
|
|
19
19
|
/**
|
|
@@ -311,3 +311,15 @@ export function dockIntoActiveLayout(entry) {
|
|
|
311
311
|
splitNodeAtPath(root, slotPath, entry, 'right');
|
|
312
312
|
return true;
|
|
313
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Find which root a slot currently lives under in the active layout.
|
|
316
|
+
* Returns `{ kind: 'docked' }` when the slot is anywhere in the docked
|
|
317
|
+
* tree, `{ kind: 'float', floatId }` when it lives inside a float, or
|
|
318
|
+
* `null` when the slot is not present (stale id, post-unmount, or
|
|
319
|
+
* held-but-not-active app tree). Thin wrapper over the pure
|
|
320
|
+
* `locateSlotIn(tree, slotId)` from `ops.ts` — use this when you only
|
|
321
|
+
* have a slotId in hand; use `locateSlotIn` if you already hold the tree.
|
|
322
|
+
*/
|
|
323
|
+
export function locateSlot(slotId) {
|
|
324
|
+
return locateSlotIn(activeLayout(), slotId);
|
|
325
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { locateSlotIn } from './ops';
|
|
3
|
+
describe('locateSlotIn', () => {
|
|
4
|
+
it('finds a tab-entry slot in the docked tree', () => {
|
|
5
|
+
const tree = {
|
|
6
|
+
docked: {
|
|
7
|
+
type: 'tabs',
|
|
8
|
+
tabs: [{ slotId: 'dock-a', viewId: 'v', label: 'A' }],
|
|
9
|
+
activeTab: 0,
|
|
10
|
+
},
|
|
11
|
+
floats: [],
|
|
12
|
+
};
|
|
13
|
+
expect(locateSlotIn(tree, 'dock-a')).toEqual({ kind: 'docked' });
|
|
14
|
+
});
|
|
15
|
+
it('finds a bare slot leaf in the docked tree', () => {
|
|
16
|
+
const tree = {
|
|
17
|
+
docked: { type: 'slot', slotId: 'dock-leaf', viewId: 'v' },
|
|
18
|
+
floats: [],
|
|
19
|
+
};
|
|
20
|
+
expect(locateSlotIn(tree, 'dock-leaf')).toEqual({ kind: 'docked' });
|
|
21
|
+
});
|
|
22
|
+
it('finds a slot nested inside a split', () => {
|
|
23
|
+
const tree = {
|
|
24
|
+
docked: {
|
|
25
|
+
type: 'split',
|
|
26
|
+
direction: 'horizontal',
|
|
27
|
+
sizes: [0.5, 0.5],
|
|
28
|
+
children: [
|
|
29
|
+
{ type: 'slot', slotId: 'left', viewId: 'v' },
|
|
30
|
+
{
|
|
31
|
+
type: 'tabs',
|
|
32
|
+
tabs: [{ slotId: 'right', viewId: 'v', label: 'R' }],
|
|
33
|
+
activeTab: 0,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
floats: [],
|
|
38
|
+
};
|
|
39
|
+
expect(locateSlotIn(tree, 'left')).toEqual({ kind: 'docked' });
|
|
40
|
+
expect(locateSlotIn(tree, 'right')).toEqual({ kind: 'docked' });
|
|
41
|
+
});
|
|
42
|
+
it('finds a tab-entry slot inside a float', () => {
|
|
43
|
+
const tree = {
|
|
44
|
+
docked: { type: 'tabs', tabs: [], activeTab: 0 },
|
|
45
|
+
floats: [
|
|
46
|
+
{
|
|
47
|
+
id: 'float-1',
|
|
48
|
+
content: {
|
|
49
|
+
type: 'tabs',
|
|
50
|
+
tabs: [{ slotId: 'float-a', viewId: 'v', label: 'A' }],
|
|
51
|
+
activeTab: 0,
|
|
52
|
+
},
|
|
53
|
+
position: { x: 0, y: 0 },
|
|
54
|
+
size: { w: 600, h: 400 },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
expect(locateSlotIn(tree, 'float-a')).toEqual({ kind: 'float', floatId: 'float-1' });
|
|
59
|
+
});
|
|
60
|
+
it('finds a bare slot leaf inside a float', () => {
|
|
61
|
+
const tree = {
|
|
62
|
+
docked: { type: 'tabs', tabs: [], activeTab: 0 },
|
|
63
|
+
floats: [
|
|
64
|
+
{
|
|
65
|
+
id: 'float-2',
|
|
66
|
+
content: { type: 'slot', slotId: 'float-leaf', viewId: 'v' },
|
|
67
|
+
position: { x: 0, y: 0 },
|
|
68
|
+
size: { w: 600, h: 400 },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
expect(locateSlotIn(tree, 'float-leaf')).toEqual({ kind: 'float', floatId: 'float-2' });
|
|
73
|
+
});
|
|
74
|
+
it('returns null for an absent slot', () => {
|
|
75
|
+
const tree = {
|
|
76
|
+
docked: { type: 'tabs', tabs: [], activeTab: 0 },
|
|
77
|
+
floats: [],
|
|
78
|
+
};
|
|
79
|
+
expect(locateSlotIn(tree, 'nope')).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
it('prefers docked when an id is (illegally) present in both', () => {
|
|
82
|
+
const tree = {
|
|
83
|
+
docked: {
|
|
84
|
+
type: 'tabs',
|
|
85
|
+
tabs: [{ slotId: 'dup', viewId: 'v', label: 'D' }],
|
|
86
|
+
activeTab: 0,
|
|
87
|
+
},
|
|
88
|
+
floats: [
|
|
89
|
+
{
|
|
90
|
+
id: 'float-3',
|
|
91
|
+
content: {
|
|
92
|
+
type: 'tabs',
|
|
93
|
+
tabs: [{ slotId: 'dup', viewId: 'v', label: 'D' }],
|
|
94
|
+
activeTab: 0,
|
|
95
|
+
},
|
|
96
|
+
position: { x: 0, y: 0 },
|
|
97
|
+
size: { w: 600, h: 400 },
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
expect(locateSlotIn(tree, 'dup')).toEqual({ kind: 'docked' });
|
|
102
|
+
});
|
|
103
|
+
});
|
package/dist/layout/ops.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface LocatedTabInTree {
|
|
|
33
33
|
* Returns null if the slot id is not present in any root.
|
|
34
34
|
*/
|
|
35
35
|
export declare function findTabInTree(tree: LayoutTree, slotId: string): LocatedTabInTree | null;
|
|
36
|
+
/**
|
|
37
|
+
* Locate the root a slot currently lives under in the given tree. Handles
|
|
38
|
+
* both tab entries and bare slot leaves, docked first then each float's
|
|
39
|
+
* content. Returns null when the slot is not present anywhere. Pure —
|
|
40
|
+
* takes the tree as input so callers with an in-hand tree (and tests)
|
|
41
|
+
* don't need the layout store.
|
|
42
|
+
*/
|
|
43
|
+
export declare function locateSlotIn(tree: LayoutTree, slotId: string): TreeRootRef | null;
|
|
36
44
|
/**
|
|
37
45
|
* Remove a tab from its current location, returning the removed entry
|
|
38
46
|
* (or null if not found). The tabs group it was in may become empty
|
package/dist/layout/ops.js
CHANGED
|
@@ -93,6 +93,33 @@ export function findTabInTree(tree, slotId) {
|
|
|
93
93
|
}
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Locate the root a slot currently lives under in the given tree. Handles
|
|
98
|
+
* both tab entries and bare slot leaves, docked first then each float's
|
|
99
|
+
* content. Returns null when the slot is not present anywhere. Pure —
|
|
100
|
+
* takes the tree as input so callers with an in-hand tree (and tests)
|
|
101
|
+
* don't need the layout store.
|
|
102
|
+
*/
|
|
103
|
+
export function locateSlotIn(tree, slotId) {
|
|
104
|
+
if (containsSlot(tree.docked, slotId))
|
|
105
|
+
return { kind: 'docked' };
|
|
106
|
+
for (const f of tree.floats) {
|
|
107
|
+
if (containsSlot(f.content, slotId))
|
|
108
|
+
return { kind: 'float', floatId: f.id };
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function containsSlot(node, slotId) {
|
|
113
|
+
if (node.type === 'slot')
|
|
114
|
+
return node.slotId === slotId;
|
|
115
|
+
if (node.type === 'tabs')
|
|
116
|
+
return node.tabs.some((t) => t.slotId === slotId);
|
|
117
|
+
for (const child of node.children) {
|
|
118
|
+
if (containsSlot(child, slotId))
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
96
123
|
// ---------- Tab removal ----------------------------------------------------
|
|
97
124
|
/**
|
|
98
125
|
* Remove a tab from its current location, returning the removed entry
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
* phase 6 has no view that needs it.
|
|
34
34
|
*/
|
|
35
35
|
import { getView, __addViewRegistrationListener } from '../shards/registry';
|
|
36
|
+
import { locateSlotIn } from './ops';
|
|
37
|
+
import { activeLayout } from './store.svelte';
|
|
36
38
|
const pool = new Map();
|
|
37
39
|
const pendingDestroy = new Set();
|
|
38
40
|
/**
|
|
@@ -59,6 +61,9 @@ function onViewRegistered(viewId, factory) {
|
|
|
59
61
|
setDirty(dirty) {
|
|
60
62
|
dirtyState[slotId] = dirty;
|
|
61
63
|
},
|
|
64
|
+
location() {
|
|
65
|
+
return locateSlotIn(activeLayout(), slotId);
|
|
66
|
+
},
|
|
62
67
|
};
|
|
63
68
|
queueMicrotask(() => {
|
|
64
69
|
var _a, _b;
|
|
@@ -129,6 +134,8 @@ function createHost(slotId, viewId, label, meta) {
|
|
|
129
134
|
const host = document.createElement('div');
|
|
130
135
|
host.className = 'slot-host';
|
|
131
136
|
host.dataset.slotId = slotId;
|
|
137
|
+
if (viewId)
|
|
138
|
+
host.setAttribute('data-sh3-view', viewId);
|
|
132
139
|
// Position:absolute inset:0 so the host fills whichever wrapper it is
|
|
133
140
|
// attached to. The wrapper is what the layout engine sizes; the host
|
|
134
141
|
// just tracks it. Styles are set inline (not in a class) so consumers
|
|
@@ -163,6 +170,9 @@ function createHost(slotId, viewId, label, meta) {
|
|
|
163
170
|
setDirty(dirty) {
|
|
164
171
|
dirtyState[slotId] = dirty;
|
|
165
172
|
},
|
|
173
|
+
location() {
|
|
174
|
+
return locateSlotIn(activeLayout(), slotId);
|
|
175
|
+
},
|
|
166
176
|
};
|
|
167
177
|
entry.handle = factory === null || factory === void 0 ? void 0 : factory.mount(host, ctx);
|
|
168
178
|
if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:')) {
|
|
@@ -201,6 +211,20 @@ export function acquireSlotHost(slotId, viewId, label, meta) {
|
|
|
201
211
|
entry = createHost(slotId, viewId, label, meta);
|
|
202
212
|
pool.set(slotId, entry);
|
|
203
213
|
}
|
|
214
|
+
else if (entry.viewId !== viewId) {
|
|
215
|
+
// viewId on an existing slot should be stable — the pool does not support
|
|
216
|
+
// swapping views for the same slotId. Keep the focus-tracking attribute
|
|
217
|
+
// in sync with whatever we have and warn; the underlying view handle will
|
|
218
|
+
// not be reconstructed.
|
|
219
|
+
console.warn(`[sh3] acquireSlotHost("${slotId}") called with viewId "${viewId}" ` +
|
|
220
|
+
`but existing pooled entry has viewId "${entry.viewId}". Attribute synced; ` +
|
|
221
|
+
`view handle unchanged.`);
|
|
222
|
+
entry.viewId = viewId;
|
|
223
|
+
if (viewId)
|
|
224
|
+
entry.host.setAttribute('data-sh3-view', viewId);
|
|
225
|
+
else
|
|
226
|
+
entry.host.removeAttribute('data-sh3-view');
|
|
227
|
+
}
|
|
204
228
|
entry.refcount++;
|
|
205
229
|
return entry.host;
|
|
206
230
|
}
|
|
@@ -102,3 +102,17 @@ describe('slotHostPool — D.5 root swap preserves app slots', () => {
|
|
|
102
102
|
expect(teardown).not.toHaveBeenCalled();
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
|
+
// ─── D.6 ─────────────────────────────────────────────────────────────────────
|
|
106
|
+
describe('slotHostPool — D.6 data-sh3-view attribute', () => {
|
|
107
|
+
beforeEach(resetFramework);
|
|
108
|
+
it('pooled host has data-sh3-view when viewId is set', () => {
|
|
109
|
+
const host = acquireSlotHost('slot-1', 'editor', 'Editor');
|
|
110
|
+
expect(host.getAttribute('data-sh3-view')).toBe('editor');
|
|
111
|
+
releaseSlotHost('slot-1');
|
|
112
|
+
});
|
|
113
|
+
it('pooled host has no data-sh3-view when viewId is null', () => {
|
|
114
|
+
const host = acquireSlotHost('slot-2', null, 'Empty');
|
|
115
|
+
expect(host.hasAttribute('data-sh3-view')).toBe(false);
|
|
116
|
+
releaseSlotHost('slot-2');
|
|
117
|
+
});
|
|
118
|
+
});
|
package/dist/layout/types.d.ts
CHANGED
|
@@ -115,6 +115,13 @@ export interface FloatEntry {
|
|
|
115
115
|
};
|
|
116
116
|
/** Optional human-readable title; defaults to the active view's label. */
|
|
117
117
|
title?: string;
|
|
118
|
+
/**
|
|
119
|
+
* When true, this float dismisses on any pointerdown outside its frame,
|
|
120
|
+
* renders its content as a raw slot (no tab-strip handle, not dockable),
|
|
121
|
+
* and hides chrome when `title` is unset. See
|
|
122
|
+
* docs/superpowers/specs/2026-04-21-dismissable-float-design.md.
|
|
123
|
+
*/
|
|
124
|
+
dismissable?: boolean;
|
|
118
125
|
}
|
|
119
126
|
/**
|
|
120
127
|
* Root shape of a workspace layout. The docked tree is the primary
|