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.
Files changed (115) hide show
  1. package/dist/Shell.svelte +12 -31
  2. package/dist/__test__/reset.js +6 -0
  3. package/dist/actions/CommandPalette.svelte +68 -0
  4. package/dist/actions/CommandPalette.svelte.d.ts +11 -0
  5. package/dist/actions/ContextMenu.svelte +97 -0
  6. package/dist/actions/ContextMenu.svelte.d.ts +9 -0
  7. package/dist/actions/bindings-store.d.ts +8 -0
  8. package/dist/actions/bindings-store.js +27 -0
  9. package/dist/actions/bindings-store.test.d.ts +1 -0
  10. package/dist/actions/bindings-store.test.js +25 -0
  11. package/dist/actions/bindings.d.ts +4 -0
  12. package/dist/actions/bindings.js +17 -0
  13. package/dist/actions/bindings.test.d.ts +1 -0
  14. package/dist/actions/bindings.test.js +30 -0
  15. package/dist/actions/contextMenuModel.d.ts +16 -0
  16. package/dist/actions/contextMenuModel.js +71 -0
  17. package/dist/actions/contextMenuModel.test.d.ts +1 -0
  18. package/dist/actions/contextMenuModel.test.js +44 -0
  19. package/dist/actions/dispatcher.svelte.d.ts +34 -0
  20. package/dist/actions/dispatcher.svelte.js +117 -0
  21. package/dist/actions/dispatcher.test.d.ts +1 -0
  22. package/dist/actions/dispatcher.test.js +155 -0
  23. package/dist/actions/listeners.d.ts +11 -0
  24. package/dist/actions/listeners.js +180 -0
  25. package/dist/actions/listeners.test.d.ts +1 -0
  26. package/dist/actions/listeners.test.js +149 -0
  27. package/dist/actions/palette-scorer.d.ts +11 -0
  28. package/dist/actions/palette-scorer.js +49 -0
  29. package/dist/actions/palette-scorer.test.d.ts +1 -0
  30. package/dist/actions/palette-scorer.test.js +40 -0
  31. package/dist/actions/paletteModel.d.ts +4 -0
  32. package/dist/actions/paletteModel.js +40 -0
  33. package/dist/actions/paletteModel.test.d.ts +1 -0
  34. package/dist/actions/paletteModel.test.js +33 -0
  35. package/dist/actions/registry.d.ts +10 -0
  36. package/dist/actions/registry.js +36 -0
  37. package/dist/actions/registry.test.d.ts +1 -0
  38. package/dist/actions/registry.test.js +49 -0
  39. package/dist/actions/selection.svelte.d.ts +8 -0
  40. package/dist/actions/selection.svelte.js +44 -0
  41. package/dist/actions/selection.test.d.ts +1 -0
  42. package/dist/actions/selection.test.js +51 -0
  43. package/dist/actions/shardContext.test.d.ts +1 -0
  44. package/dist/actions/shardContext.test.js +41 -0
  45. package/dist/actions/shellActions.test.d.ts +1 -0
  46. package/dist/actions/shellActions.test.js +22 -0
  47. package/dist/actions/shortcuts.d.ts +5 -0
  48. package/dist/actions/shortcuts.js +87 -0
  49. package/dist/actions/shortcuts.test.d.ts +1 -0
  50. package/dist/actions/shortcuts.test.js +49 -0
  51. package/dist/actions/state.svelte.d.ts +16 -0
  52. package/dist/actions/state.svelte.js +76 -0
  53. package/dist/actions/state.test.d.ts +1 -0
  54. package/dist/actions/state.test.js +40 -0
  55. package/dist/actions/syncMountedViewIds.test.d.ts +1 -0
  56. package/dist/actions/syncMountedViewIds.test.js +97 -0
  57. package/dist/actions/types.d.ts +56 -0
  58. package/dist/actions/types.js +7 -0
  59. package/dist/api.d.ts +2 -2
  60. package/dist/api.js +1 -1
  61. package/dist/apps/lifecycle.js +13 -3
  62. package/dist/createShell.js +4 -1
  63. package/dist/host.js +6 -3
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.js +2 -0
  66. package/dist/layout/inspection.d.ts +11 -1
  67. package/dist/layout/inspection.js +13 -1
  68. package/dist/layout/ops-locate.test.d.ts +1 -0
  69. package/dist/layout/ops-locate.test.js +103 -0
  70. package/dist/layout/ops.d.ts +8 -0
  71. package/dist/layout/ops.js +27 -0
  72. package/dist/layout/slotHostPool.svelte.js +24 -0
  73. package/dist/layout/slotHostPool.test.js +14 -0
  74. package/dist/layout/types.d.ts +7 -0
  75. package/dist/overlays/FloatFrame.svelte +23 -11
  76. package/dist/overlays/ModalFrame.svelte +9 -1
  77. package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
  78. package/dist/overlays/__test__/DummyFrame.svelte +6 -0
  79. package/dist/overlays/__test__/DummyFrame.svelte.d.ts +6 -0
  80. package/dist/overlays/float.d.ts +6 -0
  81. package/dist/overlays/float.js +24 -9
  82. package/dist/overlays/float.test.js +175 -0
  83. package/dist/overlays/floatDismiss.d.ts +8 -0
  84. package/dist/overlays/floatDismiss.js +68 -0
  85. package/dist/overlays/modal.js +5 -1
  86. package/dist/overlays/modal.test.d.ts +1 -0
  87. package/dist/overlays/modal.test.js +55 -0
  88. package/dist/overlays/popup.d.ts +2 -0
  89. package/dist/overlays/popup.js +24 -4
  90. package/dist/overlays/popup.test.d.ts +1 -0
  91. package/dist/overlays/popup.test.js +95 -0
  92. package/dist/overlays/types.d.ts +17 -1
  93. package/dist/primitives/Button.svelte +144 -0
  94. package/dist/primitives/Button.svelte.d.ts +18 -0
  95. package/dist/primitives/icon-context.d.ts +15 -0
  96. package/dist/primitives/icon-context.js +29 -0
  97. package/dist/sh3core-shard/sh3coreShard.svelte.js +50 -0
  98. package/dist/shards/activate.svelte.js +14 -0
  99. package/dist/shards/types.d.ts +19 -0
  100. package/dist/shards/types.js +5 -4
  101. package/dist/shell-shard/locateSlot.test.d.ts +1 -0
  102. package/dist/shell-shard/locateSlot.test.js +101 -0
  103. package/dist/shell-shard/shellShard.svelte.d.ts +7 -0
  104. package/dist/shell-shard/shellShard.svelte.js +34 -1
  105. package/dist/shellRuntime.svelte.d.ts +19 -0
  106. package/dist/shellRuntime.svelte.js +30 -0
  107. package/dist/tokens.css +11 -1
  108. package/dist/verbs/types.d.ts +9 -0
  109. package/dist/version.d.ts +1 -1
  110. package/dist/version.js +1 -1
  111. package/package.json +1 -1
  112. package/dist/apps/terminal/manifest.d.ts +0 -8
  113. package/dist/apps/terminal/manifest.js +0 -14
  114. package/dist/apps/terminal/terminal-app.d.ts +0 -7
  115. 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';
@@ -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
@@ -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
+ });
@@ -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