sh3-core 0.10.4 → 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.
Files changed (122) hide show
  1. package/dist/Shell.svelte +12 -31
  2. package/dist/__test__/fixtures.js +1 -0
  3. package/dist/__test__/reset.js +6 -0
  4. package/dist/actions/CommandPalette.svelte +68 -0
  5. package/dist/actions/CommandPalette.svelte.d.ts +11 -0
  6. package/dist/actions/ContextMenu.svelte +97 -0
  7. package/dist/actions/ContextMenu.svelte.d.ts +9 -0
  8. package/dist/actions/bindings-store.d.ts +8 -0
  9. package/dist/actions/bindings-store.js +27 -0
  10. package/dist/actions/bindings-store.test.d.ts +1 -0
  11. package/dist/actions/bindings-store.test.js +25 -0
  12. package/dist/actions/bindings.d.ts +4 -0
  13. package/dist/actions/bindings.js +17 -0
  14. package/dist/actions/bindings.test.d.ts +1 -0
  15. package/dist/actions/bindings.test.js +30 -0
  16. package/dist/actions/contextMenuModel.d.ts +16 -0
  17. package/dist/actions/contextMenuModel.js +71 -0
  18. package/dist/actions/contextMenuModel.test.d.ts +1 -0
  19. package/dist/actions/contextMenuModel.test.js +44 -0
  20. package/dist/actions/dispatcher.svelte.d.ts +34 -0
  21. package/dist/actions/dispatcher.svelte.js +117 -0
  22. package/dist/actions/dispatcher.test.d.ts +1 -0
  23. package/dist/actions/dispatcher.test.js +155 -0
  24. package/dist/actions/listeners.d.ts +11 -0
  25. package/dist/actions/listeners.js +180 -0
  26. package/dist/actions/listeners.test.d.ts +1 -0
  27. package/dist/actions/listeners.test.js +149 -0
  28. package/dist/actions/palette-scorer.d.ts +11 -0
  29. package/dist/actions/palette-scorer.js +49 -0
  30. package/dist/actions/palette-scorer.test.d.ts +1 -0
  31. package/dist/actions/palette-scorer.test.js +40 -0
  32. package/dist/actions/paletteModel.d.ts +4 -0
  33. package/dist/actions/paletteModel.js +40 -0
  34. package/dist/actions/paletteModel.test.d.ts +1 -0
  35. package/dist/actions/paletteModel.test.js +33 -0
  36. package/dist/actions/registry.d.ts +10 -0
  37. package/dist/actions/registry.js +36 -0
  38. package/dist/actions/registry.test.d.ts +1 -0
  39. package/dist/actions/registry.test.js +49 -0
  40. package/dist/actions/selection.svelte.d.ts +8 -0
  41. package/dist/actions/selection.svelte.js +44 -0
  42. package/dist/actions/selection.test.d.ts +1 -0
  43. package/dist/actions/selection.test.js +51 -0
  44. package/dist/actions/shardContext.test.d.ts +1 -0
  45. package/dist/actions/shardContext.test.js +41 -0
  46. package/dist/actions/shellActions.test.d.ts +1 -0
  47. package/dist/actions/shellActions.test.js +22 -0
  48. package/dist/actions/shortcuts.d.ts +5 -0
  49. package/dist/actions/shortcuts.js +87 -0
  50. package/dist/actions/shortcuts.test.d.ts +1 -0
  51. package/dist/actions/shortcuts.test.js +49 -0
  52. package/dist/actions/state.svelte.d.ts +16 -0
  53. package/dist/actions/state.svelte.js +76 -0
  54. package/dist/actions/state.test.d.ts +1 -0
  55. package/dist/actions/state.test.js +40 -0
  56. package/dist/actions/syncMountedViewIds.test.d.ts +1 -0
  57. package/dist/actions/syncMountedViewIds.test.js +97 -0
  58. package/dist/actions/types.d.ts +56 -0
  59. package/dist/actions/types.js +7 -0
  60. package/dist/api.d.ts +2 -2
  61. package/dist/api.js +1 -1
  62. package/dist/apps/lifecycle.js +13 -3
  63. package/dist/createShell.js +4 -1
  64. package/dist/host.js +6 -3
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.js +2 -0
  67. package/dist/layout/LayoutRenderer.browser.test.js +78 -0
  68. package/dist/layout/LayoutRenderer.svelte +1 -0
  69. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-freezes-the-handle-adjacent-to-a-fixed-pane--dblclick-does-not-collapse-1.png +0 -0
  70. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-hides-the-collapse-widget-on-a-fixed-pane-1.png +0 -0
  71. package/dist/layout/inspection.d.ts +11 -1
  72. package/dist/layout/inspection.js +13 -1
  73. package/dist/layout/ops-locate.test.d.ts +1 -0
  74. package/dist/layout/ops-locate.test.js +103 -0
  75. package/dist/layout/ops.d.ts +8 -0
  76. package/dist/layout/ops.js +27 -0
  77. package/dist/layout/slotHostPool.svelte.js +24 -0
  78. package/dist/layout/slotHostPool.test.js +14 -0
  79. package/dist/layout/types.d.ts +15 -0
  80. package/dist/overlays/FloatFrame.svelte +23 -11
  81. package/dist/overlays/ModalFrame.svelte +9 -1
  82. package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
  83. package/dist/overlays/__test__/DummyFrame.svelte +6 -0
  84. package/dist/overlays/__test__/DummyFrame.svelte.d.ts +6 -0
  85. package/dist/overlays/float.d.ts +6 -0
  86. package/dist/overlays/float.js +24 -9
  87. package/dist/overlays/float.test.js +175 -0
  88. package/dist/overlays/floatDismiss.d.ts +8 -0
  89. package/dist/overlays/floatDismiss.js +68 -0
  90. package/dist/overlays/modal.js +5 -1
  91. package/dist/overlays/modal.test.d.ts +1 -0
  92. package/dist/overlays/modal.test.js +55 -0
  93. package/dist/overlays/popup.d.ts +2 -0
  94. package/dist/overlays/popup.js +24 -4
  95. package/dist/overlays/popup.test.d.ts +1 -0
  96. package/dist/overlays/popup.test.js +95 -0
  97. package/dist/overlays/types.d.ts +17 -1
  98. package/dist/primitives/Button.svelte +144 -0
  99. package/dist/primitives/Button.svelte.d.ts +18 -0
  100. package/dist/primitives/ResizableSplitter.svelte +38 -3
  101. package/dist/primitives/ResizableSplitter.svelte.d.ts +7 -0
  102. package/dist/primitives/icon-context.d.ts +15 -0
  103. package/dist/primitives/icon-context.js +29 -0
  104. package/dist/sh3core-shard/sh3coreShard.svelte.js +50 -0
  105. package/dist/shards/activate.svelte.js +14 -0
  106. package/dist/shards/types.d.ts +19 -0
  107. package/dist/shards/types.js +5 -4
  108. package/dist/shell-shard/locateSlot.test.d.ts +1 -0
  109. package/dist/shell-shard/locateSlot.test.js +101 -0
  110. package/dist/shell-shard/shellShard.svelte.d.ts +7 -0
  111. package/dist/shell-shard/shellShard.svelte.js +34 -1
  112. package/dist/shellRuntime.svelte.d.ts +19 -0
  113. package/dist/shellRuntime.svelte.js +30 -0
  114. package/dist/tokens.css +11 -1
  115. package/dist/verbs/types.d.ts +9 -0
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.js +1 -1
  118. package/package.json +1 -1
  119. package/dist/apps/terminal/manifest.d.ts +0 -8
  120. package/dist/apps/terminal/manifest.js +0 -14
  121. package/dist/apps/terminal/terminal-app.d.ts +0 -7
  122. 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
+ }
@@ -0,0 +1,7 @@
1
+ // packages/sh3-core/src/actions/types.ts
2
+ /*
3
+ * Action primitive — scoped UI operations dispatched by keyboard,
4
+ * context menu, or command palette. See the spec at
5
+ * docs/superpowers/specs/2026-04-22-actions-contexts-design.md.
6
+ */
7
+ export {};
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';
@@ -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 ((_d = app.activate) === null || _d === void 0 ? void 0 : _d.call(app, getOrCreateAppContext(id)));
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 ((_e = app.onAppReady) === null || _e === void 0 ? void 0 : _e.call(app, getOrCreateAppContext(id)));
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 -------------------------------------------------------
@@ -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. Mount the shell
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, terminalApp];
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';
@@ -272,3 +272,81 @@ describe('LayoutRenderer browser — E.5 splitter collapse toggle', () => {
272
272
  expect((_a = root.collapsed) === null || _a === void 0 ? void 0 : _a[0]).toBe(true);
273
273
  });
274
274
  });
275
+ // ---------------------------------------------------------------------------
276
+ // E.6 — fixed[] slots: no collapse widget, frozen handles
277
+ // ---------------------------------------------------------------------------
278
+ describe('LayoutRenderer browser — E.6 fixed slots', () => {
279
+ beforeEach(() => { cleanupDOM(); resetFramework(); });
280
+ it('hides the collapse widget on a fixed pane but keeps it on panes with a non-fixed neighbor', async () => {
281
+ stubView();
282
+ registerApp(makeApp({
283
+ manifest: makeAppManifest({ id: 'e6a' }),
284
+ initialLayout: [
285
+ {
286
+ name: 'default',
287
+ tree: makeTree(makeSplitNode([
288
+ makeSlotNode('a', 'test:view'),
289
+ makeSlotNode('b', 'test:view'),
290
+ makeSlotNode('c', 'test:view'),
291
+ ], { fixed: [true, false, false] })),
292
+ },
293
+ ],
294
+ }));
295
+ await launchApp('e6a');
296
+ renderWithShell(LayoutRenderer, { path: [] });
297
+ await settle(30);
298
+ expect(document.querySelector('[data-testid="collapse-toggle-0"]')).toBeNull();
299
+ expect(document.querySelector('[data-testid="collapse-toggle-1"]')).not.toBeNull();
300
+ expect(document.querySelector('[data-testid="collapse-toggle-2"]')).not.toBeNull();
301
+ });
302
+ it('freezes the handle adjacent to a fixed pane: dblclick does not collapse', async () => {
303
+ var _a, _b, _c, _d;
304
+ stubView();
305
+ registerApp(makeApp({
306
+ manifest: makeAppManifest({ id: 'e6b' }),
307
+ initialLayout: [
308
+ {
309
+ name: 'default',
310
+ tree: makeTree(makeSplitNode([makeSlotNode('a', 'test:view'), makeSlotNode('b', 'test:view')], { fixed: [true, false] })),
311
+ },
312
+ ],
313
+ }));
314
+ await launchApp('e6b');
315
+ renderWithShell(LayoutRenderer, { path: [] });
316
+ await settle(30);
317
+ const handle = document.querySelector('[data-testid="splitter-handle-0"]');
318
+ expect(handle).not.toBeNull();
319
+ expect(handle.classList.contains('frozen')).toBe(true);
320
+ // pointer-events: none blocks Playwright clicks, so dispatch the event
321
+ // directly to verify the handler itself refuses to toggle.
322
+ handle.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
323
+ await settle(50);
324
+ const root = layoutStore.root;
325
+ if ((root === null || root === void 0 ? void 0 : root.type) !== 'split')
326
+ throw new Error('expected split root');
327
+ expect((_b = (_a = root.collapsed) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : false).toBe(false);
328
+ expect((_d = (_c = root.collapsed) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : false).toBe(false);
329
+ });
330
+ it('hides the collapse widget on a middle pane whose neighbors are both fixed', async () => {
331
+ stubView();
332
+ registerApp(makeApp({
333
+ manifest: makeAppManifest({ id: 'e6c' }),
334
+ initialLayout: [
335
+ {
336
+ name: 'default',
337
+ tree: makeTree(makeSplitNode([
338
+ makeSlotNode('a', 'test:view'),
339
+ makeSlotNode('b', 'test:view'),
340
+ makeSlotNode('c', 'test:view'),
341
+ ], { fixed: [true, false, true] })),
342
+ },
343
+ ],
344
+ }));
345
+ await launchApp('e6c');
346
+ renderWithShell(LayoutRenderer, { path: [] });
347
+ await settle(30);
348
+ expect(document.querySelector('[data-testid="collapse-toggle-0"]')).toBeNull();
349
+ expect(document.querySelector('[data-testid="collapse-toggle-1"]')).toBeNull();
350
+ expect(document.querySelector('[data-testid="collapse-toggle-2"]')).toBeNull();
351
+ });
352
+ });
@@ -169,6 +169,7 @@
169
169
  sizes={split.sizes}
170
170
  pinned={split.pinned}
171
171
  collapsed={split.collapsed}
172
+ fixed={split.fixed}
172
173
  count={split.children.length}
173
174
  pane={splitPane}
174
175
  onResize={(i, v) => (split.sizes[i] = v)}
@@ -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
+ });
@@ -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