sh3-core 0.16.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/Sh3.svelte +50 -108
  2. package/dist/__screenshots__/handheld.browser.test.ts/handheld-viewport-flip-e2e-viewport-override-flips-chrome-and-body-branches-1.png +0 -0
  3. package/dist/actions/ctx-actions.svelte.test.js +4 -4
  4. package/dist/actions/listActionsFromEntries.test.js +29 -0
  5. package/dist/actions/listActive.js +2 -0
  6. package/dist/actions/listeners.js +4 -0
  7. package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
  8. package/dist/actions/types.d.ts +8 -0
  9. package/dist/api.d.ts +6 -1
  10. package/dist/api.js +1 -0
  11. package/dist/chrome/CompactChrome.svelte +96 -0
  12. package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
  13. package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
  14. package/dist/chrome/CompactChrome.svelte.test.js +67 -0
  15. package/dist/chrome/MenuSheet.svelte +224 -0
  16. package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
  17. package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
  18. package/dist/chrome/MenuSheet.svelte.test.js +46 -0
  19. package/dist/contributions/index.d.ts +1 -1
  20. package/dist/contributions/index.js +1 -1
  21. package/dist/contributions/registry.d.ts +17 -1
  22. package/dist/contributions/registry.js +50 -2
  23. package/dist/contributions/scope.test.d.ts +1 -0
  24. package/dist/contributions/scope.test.js +52 -0
  25. package/dist/contributions/types.d.ts +11 -3
  26. package/dist/createShell.js +7 -1
  27. package/dist/fields/address.d.ts +3 -0
  28. package/dist/fields/address.js +36 -0
  29. package/dist/fields/address.test.d.ts +1 -0
  30. package/dist/fields/address.test.js +34 -0
  31. package/dist/fields/decoration.d.ts +7 -0
  32. package/dist/fields/decoration.js +199 -0
  33. package/dist/fields/decoration.svelte.test.d.ts +1 -0
  34. package/dist/fields/decoration.svelte.test.js +177 -0
  35. package/dist/fields/dispatch.d.ts +22 -0
  36. package/dist/fields/dispatch.js +254 -0
  37. package/dist/fields/dispatch.test.d.ts +1 -0
  38. package/dist/fields/dispatch.test.js +175 -0
  39. package/dist/fields/types.d.ts +101 -0
  40. package/dist/fields/types.js +16 -0
  41. package/dist/fields/walker.svelte.test.d.ts +1 -0
  42. package/dist/fields/walker.svelte.test.js +138 -0
  43. package/dist/handheld.browser.test.d.ts +1 -0
  44. package/dist/handheld.browser.test.js +90 -0
  45. package/dist/host.js +27 -2
  46. package/dist/host.svelte.test.d.ts +1 -0
  47. package/dist/host.svelte.test.js +92 -0
  48. package/dist/layout/LayoutRenderer.svelte +12 -1
  49. package/dist/layout/LayoutRenderer.svelte.d.ts +2 -1
  50. package/dist/layout/compact/CompactRenderer.svelte +53 -0
  51. package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
  52. package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
  53. package/dist/layout/compact/CompactRenderer.svelte.test.js +76 -0
  54. package/dist/layout/compact/derive.d.ts +3 -0
  55. package/dist/layout/compact/derive.js +155 -0
  56. package/dist/layout/compact/derive.test.d.ts +1 -0
  57. package/dist/layout/compact/derive.test.js +160 -0
  58. package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
  59. package/dist/layout/compact/drawerStore.svelte.js +75 -0
  60. package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
  61. package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
  62. package/dist/layout/compact/resolveRole.d.ts +6 -0
  63. package/dist/layout/compact/resolveRole.js +13 -0
  64. package/dist/layout/compact/resolveRole.test.d.ts +1 -0
  65. package/dist/layout/compact/resolveRole.test.js +18 -0
  66. package/dist/layout/compact/types.d.ts +27 -0
  67. package/dist/layout/compact/types.js +15 -0
  68. package/dist/layout/presets.compactVariant.test.d.ts +1 -0
  69. package/dist/layout/presets.compactVariant.test.js +27 -0
  70. package/dist/layout/presets.d.ts +12 -0
  71. package/dist/layout/presets.js +16 -0
  72. package/dist/layout/slotHostPool.svelte.d.ts +8 -0
  73. package/dist/layout/slotHostPool.svelte.js +14 -1
  74. package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
  75. package/dist/layout/store.drawers.svelte.test.js +49 -0
  76. package/dist/layout/store.schemaVersion.test.d.ts +1 -0
  77. package/dist/layout/store.schemaVersion.test.js +35 -0
  78. package/dist/layout/store.svelte.js +52 -2
  79. package/dist/layout/types.d.ts +43 -1
  80. package/dist/layout/types.js +1 -1
  81. package/dist/overlays/DrawerSurface.svelte +141 -0
  82. package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
  83. package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
  84. package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
  85. package/dist/overlays/OverlayRoots.svelte +89 -0
  86. package/dist/overlays/OverlayRoots.svelte.d.ts +3 -0
  87. package/dist/overlays/types.d.ts +1 -1
  88. package/dist/platform/tauri-backend.d.ts +3 -3
  89. package/dist/platform/tauri-backend.js +24 -3
  90. package/dist/projects/session-state.svelte.d.ts +3 -3
  91. package/dist/projects/session-state.svelte.js +5 -4
  92. package/dist/runtime/runVerb.js +2 -2
  93. package/dist/satellite/SatelliteShell.svelte +58 -11
  94. package/dist/satellite/SatelliteShell.svelte.test.d.ts +1 -0
  95. package/dist/satellite/SatelliteShell.svelte.test.js +61 -0
  96. package/dist/sh3Api/fields-walker.svelte.test.d.ts +1 -0
  97. package/dist/sh3Api/fields-walker.svelte.test.js +75 -0
  98. package/dist/sh3Api/headless.d.ts +9 -0
  99. package/dist/sh3Api/headless.js +171 -16
  100. package/dist/sh3Api/headless.svelte.test.js +54 -10
  101. package/dist/sh3Runtime.svelte.d.ts +36 -0
  102. package/dist/sh3Runtime.svelte.js +33 -0
  103. package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -2
  104. package/dist/shards/activate-fields.svelte.test.d.ts +1 -0
  105. package/dist/shards/activate-fields.svelte.test.js +121 -0
  106. package/dist/shards/activate-runtime.test.js +8 -8
  107. package/dist/shards/activate.svelte.js +29 -35
  108. package/dist/shards/types.d.ts +23 -76
  109. package/dist/shell-shard/ScrollbackView.svelte +55 -9
  110. package/dist/shell-shard/Terminal.svelte +1 -1
  111. package/dist/shell-shard/scrollback-stick.d.ts +9 -0
  112. package/dist/shell-shard/scrollback-stick.js +21 -0
  113. package/dist/shell-shard/scrollback-stick.test.d.ts +1 -0
  114. package/dist/shell-shard/scrollback-stick.test.js +25 -0
  115. package/dist/tokens.css +3 -2
  116. package/dist/verbs/types.d.ts +59 -1
  117. package/dist/version.d.ts +1 -1
  118. package/dist/version.js +1 -1
  119. package/dist/viewport/classify.d.ts +8 -0
  120. package/dist/viewport/classify.js +20 -0
  121. package/dist/viewport/classify.test.d.ts +1 -0
  122. package/dist/viewport/classify.test.js +32 -0
  123. package/dist/viewport/store.browser.test.d.ts +1 -0
  124. package/dist/viewport/store.browser.test.js +33 -0
  125. package/dist/viewport/store.svelte.d.ts +9 -0
  126. package/dist/viewport/store.svelte.js +71 -0
  127. package/dist/viewport/store.svelte.test.d.ts +1 -0
  128. package/dist/viewport/store.svelte.test.js +54 -0
  129. package/dist/viewport/types.d.ts +9 -0
  130. package/dist/viewport/types.js +6 -0
  131. package/package.json +1 -1
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { resetFramework } from './__test__/reset';
3
+ import { makeShard, makeShardManifest } from './__test__/fixtures';
4
+ import { bootstrapSatellite } from './host';
5
+ import { registerShard, activeShards, erroredShards, } from './shards/activate.svelte';
6
+ import { getLiveDispatcherState } from './actions/state.svelte';
7
+ import { listActions } from './actions/registry';
8
+ // loadInstalledPackages reads IndexedDB; neutralize it so the satellite
9
+ // boot path can run in happy-dom without a real storage backend.
10
+ vi.mock('./registry/installer', async (orig) => {
11
+ const real = await orig();
12
+ return Object.assign(Object.assign({}, real), { loadInstalledPackages: vi.fn(async () => { }) });
13
+ });
14
+ describe('bootstrapSatellite — autostart sweep', () => {
15
+ beforeEach(resetFramework);
16
+ it('activates a registered autostart shard even when not in activateShardIds', async () => {
17
+ const activate = vi.fn();
18
+ const autostart = vi.fn();
19
+ const svc = makeShard({
20
+ manifest: makeShardManifest({ id: 'svc-llm' }),
21
+ activate,
22
+ autostart,
23
+ });
24
+ registerShard(svc);
25
+ await bootstrapSatellite({ activateShardIds: [] });
26
+ expect(activate).toHaveBeenCalledTimes(1);
27
+ expect(autostart).toHaveBeenCalledTimes(1);
28
+ expect(activeShards.has('svc-llm')).toBe(true);
29
+ });
30
+ it('adds every activated autostart shard to the dispatcher autostart set', async () => {
31
+ const svc = makeShard({
32
+ manifest: makeShardManifest({ id: 'svc-llm' }),
33
+ autostart: () => { },
34
+ });
35
+ registerShard(svc);
36
+ await bootstrapSatellite({ activateShardIds: [] });
37
+ expect(getLiveDispatcherState().autostartShards.has('svc-llm')).toBe(true);
38
+ });
39
+ it('records a throwing autostart shard with phase "autostart" and continues activating the rest', async () => {
40
+ var _a;
41
+ const goodActivate = vi.fn();
42
+ const bad = makeShard({
43
+ manifest: makeShardManifest({ id: 'svc-bad' }),
44
+ activate: () => {
45
+ throw new Error('boom');
46
+ },
47
+ autostart: () => { },
48
+ });
49
+ const good = makeShard({
50
+ manifest: makeShardManifest({ id: 'svc-good' }),
51
+ activate: goodActivate,
52
+ autostart: () => { },
53
+ });
54
+ registerShard(bad);
55
+ registerShard(good);
56
+ await bootstrapSatellite({ activateShardIds: [] });
57
+ expect((_a = erroredShards.get('svc-bad')) === null || _a === void 0 ? void 0 : _a.phase).toBe('autostart');
58
+ expect(activeShards.has('svc-bad')).toBe(false);
59
+ expect(goodActivate).toHaveBeenCalledTimes(1);
60
+ expect(activeShards.has('svc-good')).toBe(true);
61
+ });
62
+ it('dedupes — an autostart shard listed in activateShardIds activates exactly once', async () => {
63
+ const activate = vi.fn();
64
+ const svc = makeShard({
65
+ manifest: makeShardManifest({ id: 'svc-llm' }),
66
+ activate,
67
+ autostart: () => { },
68
+ });
69
+ registerShard(svc);
70
+ await bootstrapSatellite({ activateShardIds: ['svc-llm'] });
71
+ expect(activate).toHaveBeenCalledTimes(1);
72
+ });
73
+ it('registers sh3.palette.open after bootstrapSatellite completes (palette reachable)', async () => {
74
+ await bootstrapSatellite({ activateShardIds: [] });
75
+ const ids = listActions().map((entry) => entry.action.id);
76
+ expect(ids).toContain('sh3.palette.open');
77
+ });
78
+ it('still activates non-autostart shards passed in activateShardIds with phase "satellite"', async () => {
79
+ var _a;
80
+ const activate = vi.fn(() => {
81
+ throw new Error('explicit-fail');
82
+ });
83
+ const view = makeShard({
84
+ manifest: makeShardManifest({ id: 'view-only' }),
85
+ activate,
86
+ });
87
+ registerShard(view);
88
+ await bootstrapSatellite({ activateShardIds: ['view-only'] });
89
+ expect(activate).toHaveBeenCalledTimes(1);
90
+ expect((_a = erroredShards.get('view-only')) === null || _a === void 0 ? void 0 : _a.phase).toBe('satellite');
91
+ });
92
+ });
@@ -47,7 +47,8 @@
47
47
  let {
48
48
  path = [],
49
49
  rootRef = { kind: 'docked' } as TreeRootRef,
50
- }: { path?: number[]; rootRef?: TreeRootRef } = $props();
50
+ rootOverride,
51
+ }: { path?: number[]; rootRef?: TreeRootRef; rootOverride?: LayoutNode } = $props();
51
52
 
52
53
  /**
53
54
  * Resolve the current node by walking `layoutStore.root` along the
@@ -55,8 +56,18 @@
55
56
  * layout mutates. If the path becomes invalid mid-mutation (a
56
57
  * cleanup pass can collapse nodes out from under a recursive
57
58
  * renderer), we render null.
59
+ *
60
+ * `rootOverride`: compact-mode entry point. CompactRenderer derives a
61
+ * body-only sub-tree from the canonical layout and passes it here as
62
+ * the root. The recursive `<Self>` calls below DO NOT propagate the
63
+ * override — they keep the path-only contract so structural mutations
64
+ * still go through `layoutStore.root` for the (rare) cases where the
65
+ * derived bodyRoot shares object identity with the source.
58
66
  */
59
67
  const node = $derived.by(() => {
68
+ if (rootOverride !== undefined) {
69
+ return nodeAtPath(rootOverride, path);
70
+ }
60
71
  let rootNode: LayoutNode | null;
61
72
  if (rootRef.kind === 'docked') {
62
73
  rootNode = layoutStore.root;
@@ -1,7 +1,8 @@
1
- import type { TreeRootRef } from './types';
1
+ import type { LayoutNode, TreeRootRef } from './types';
2
2
  type $$ComponentProps = {
3
3
  path?: number[];
4
4
  rootRef?: TreeRootRef;
5
+ rootOverride?: LayoutNode;
5
6
  };
6
7
  declare const LayoutRenderer: import("svelte").Component<$$ComponentProps, {}, "">;
7
8
  type LayoutRenderer = ReturnType<typeof LayoutRenderer>;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Compact rendering wrapper. Reads the active layout tree from
4
+ * layoutStore, runs derive() to get the CompactRendering, renders the
5
+ * bodyRoot via LayoutRenderer (with rootOverride), and mounts a
6
+ * DrawerSurface for each non-null anchor.
7
+ *
8
+ * Drawer state lives in drawerStore (Sh3.drawers backing). The surfaces
9
+ * paint above the body content via their own absolute-positioned
10
+ * frames; the drawer overlay layer (--sh3-z-layer-drawers) sits above
11
+ * the docked content so the surfaces stack correctly.
12
+ *
13
+ * View-default role lookup is intentionally omitted in v1 — derive()
14
+ * reads slot.role / tab.role directly. Apps that want non-body slots
15
+ * tag them at authoring time. View-default fall-through ships when the
16
+ * registry exposes a pre-mount lookup (deferred from this PR).
17
+ */
18
+ import { layoutStore } from '../store.svelte';
19
+ import { drawerStore } from './drawerStore.svelte';
20
+ import { derive } from './derive';
21
+ import LayoutRenderer from '../LayoutRenderer.svelte';
22
+ import DrawerSurface from '../../overlays/DrawerSurface.svelte';
23
+ import type { DrawerAnchor } from './types';
24
+
25
+ const rendering = $derived(derive(layoutStore.root));
26
+
27
+ const anchors: DrawerAnchor[] = ['left', 'right', 'top'];
28
+ </script>
29
+
30
+ <div class="compact-body" data-sh3-region="compact-body">
31
+ <LayoutRenderer rootOverride={rendering.bodyRoot} />
32
+ </div>
33
+
34
+ {#each anchors as anchor (anchor)}
35
+ {@const spec = rendering.drawers[anchor]}
36
+ {#if spec}
37
+ <DrawerSurface
38
+ {anchor}
39
+ {spec}
40
+ open={drawerStore.state[anchor].open}
41
+ activeSlotId={drawerStore.state[anchor].activeSlotId}
42
+ onClose={() => drawerStore.close(anchor)}
43
+ onActivate={(slotId) => drawerStore.activate(anchor, slotId)}
44
+ />
45
+ {/if}
46
+ {/each}
47
+
48
+ <style>
49
+ .compact-body {
50
+ position: absolute;
51
+ inset: 0;
52
+ }
53
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const CompactRenderer: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type CompactRenderer = ReturnType<typeof CompactRenderer>;
3
+ export default CompactRenderer;
@@ -0,0 +1,76 @@
1
+ /*
2
+ * DOM smoke for CompactRenderer. With a sidebar+body+inspector tree
3
+ * attached as the active layout, the wrapper renders one drawer surface
4
+ * per anchor (open=false, but the drawer-region lookup happens on open
5
+ * — here we just verify the wrapper accepts the tree and renders the
6
+ * body container plus the LayoutRenderer for bodyRoot).
7
+ *
8
+ * Slot-survival on viewport flip is covered by the browser e2e in T21.
9
+ */
10
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
11
+ import { mount, unmount, flushSync } from 'svelte';
12
+ import CompactRenderer from './CompactRenderer.svelte';
13
+ import { drawerStore } from './drawerStore.svelte';
14
+ import { __resetLayoutStoreForTest, attachApp, detachApp, switchToApp } from '../store.svelte';
15
+ function fakeApp() {
16
+ return {
17
+ manifest: { id: 'compact-test-app', layoutVersion: 5 },
18
+ initialLayout: {
19
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.6, 0.2],
20
+ children: [
21
+ { type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
22
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
23
+ { type: 'slot', slotId: 'ins', viewId: 'v:ins', role: 'inspector' },
24
+ ],
25
+ },
26
+ };
27
+ }
28
+ let mounted = null;
29
+ let host = null;
30
+ afterEach(() => {
31
+ if (mounted) {
32
+ unmount(mounted);
33
+ mounted = null;
34
+ }
35
+ if (host) {
36
+ host.remove();
37
+ host = null;
38
+ }
39
+ detachApp();
40
+ });
41
+ beforeEach(() => {
42
+ __resetLayoutStoreForTest();
43
+ drawerStore.__reset();
44
+ });
45
+ describe('CompactRenderer (dom)', () => {
46
+ it('renders the compact-body region for the bodyRoot', () => {
47
+ attachApp(fakeApp());
48
+ switchToApp();
49
+ flushSync();
50
+ host = document.createElement('div');
51
+ host.style.width = '400px';
52
+ host.style.height = '600px';
53
+ host.style.position = 'relative';
54
+ document.body.appendChild(host);
55
+ mounted = mount(CompactRenderer, { target: host });
56
+ flushSync();
57
+ expect(host.querySelector('[data-sh3-region="compact-body"]')).not.toBeNull();
58
+ });
59
+ it('opening left drawer renders one drawer-region with sidebar label', () => {
60
+ attachApp(fakeApp());
61
+ switchToApp();
62
+ flushSync();
63
+ host = document.createElement('div');
64
+ host.style.width = '400px';
65
+ host.style.height = '600px';
66
+ host.style.position = 'relative';
67
+ document.body.appendChild(host);
68
+ mounted = mount(CompactRenderer, { target: host });
69
+ flushSync();
70
+ drawerStore.open('left');
71
+ flushSync();
72
+ const drawer = host.querySelector('[data-sh3-region="drawer"][data-sh3-anchor="left"]');
73
+ expect(drawer).not.toBeNull();
74
+ expect(drawer.querySelector('header').textContent).toContain('sb');
75
+ });
76
+ });
@@ -0,0 +1,3 @@
1
+ import type { LayoutNode } from '../types';
2
+ import type { CompactRendering } from './types';
3
+ export declare function derive(tree: LayoutNode): CompactRendering;
@@ -0,0 +1,155 @@
1
+ /*
2
+ * derive — pure transform from a canonical LayoutNode tree to a
3
+ * CompactRendering. See ./types.ts for the output shape.
4
+ *
5
+ * Anchor inference (from spec §4):
6
+ * - Horizontal split: first non-body subtree → 'left', last → 'right'.
7
+ * - Vertical split: first non-body subtree → 'top'.
8
+ * - Otherwise: sidebar → 'left', inspector → 'right'.
9
+ *
10
+ * Once an outer split has assigned an anchor for a subtree, descent
11
+ * locks that anchor — inner splits don't retag (a vertical split
12
+ * inside a left-anchored subtree keeps both children on the left).
13
+ *
14
+ * Note: this transform reads slot.role / tab.role only. View-level
15
+ * defaultRole resolution happens at the call site via resolveRole(),
16
+ * which materializes a tree with effective roles before passing to
17
+ * derive(). See layout/compact/CompactRenderer.svelte.
18
+ */
19
+ import { EMPTY_BODY } from './types';
20
+ function effectiveRole(role) {
21
+ return role !== null && role !== void 0 ? role : 'body';
22
+ }
23
+ function collectSlots(node) {
24
+ if (node.type === 'slot') {
25
+ return [{
26
+ slotId: node.slotId,
27
+ viewId: node.viewId,
28
+ label: node.slotId,
29
+ role: effectiveRole(node.role),
30
+ }];
31
+ }
32
+ if (node.type === 'tabs') {
33
+ return node.tabs.map((t) => ({
34
+ slotId: t.slotId,
35
+ viewId: t.viewId,
36
+ label: t.label,
37
+ icon: t.icon,
38
+ role: effectiveRole(t.role),
39
+ }));
40
+ }
41
+ return node.children.flatMap(collectSlots);
42
+ }
43
+ function hasAnyBody(node) {
44
+ return collectSlots(node).some((s) => s.role === 'body');
45
+ }
46
+ function stripNonBody(node) {
47
+ if (node.type === 'slot') {
48
+ return effectiveRole(node.role) === 'body' ? node : null;
49
+ }
50
+ if (node.type === 'tabs') {
51
+ const bodyTabs = node.tabs.filter((t) => effectiveRole(t.role) === 'body');
52
+ if (bodyTabs.length === 0)
53
+ return null;
54
+ if (bodyTabs.length === node.tabs.length)
55
+ return node;
56
+ return {
57
+ type: 'tabs',
58
+ activeTab: Math.min(node.activeTab, bodyTabs.length - 1),
59
+ tabs: bodyTabs,
60
+ };
61
+ }
62
+ // split
63
+ const survivors = [];
64
+ for (let i = 0; i < node.children.length; i++) {
65
+ const stripped = stripNonBody(node.children[i]);
66
+ if (stripped)
67
+ survivors.push({ child: stripped, size: node.sizes[i] });
68
+ }
69
+ if (survivors.length === 0)
70
+ return null;
71
+ if (survivors.length === 1)
72
+ return survivors[0].child;
73
+ return {
74
+ type: 'split',
75
+ direction: node.direction,
76
+ sizes: survivors.map((s) => s.size),
77
+ children: survivors.map((s) => s.child),
78
+ };
79
+ }
80
+ function defaultAnchor(role) {
81
+ return role === 'inspector' ? 'right' : 'left';
82
+ }
83
+ function partitionDrawers(node) {
84
+ const buckets = { left: [], right: [], top: [] };
85
+ function walk(n, hint) {
86
+ if (n.type === 'slot') {
87
+ const role = effectiveRole(n.role);
88
+ if (role === 'body')
89
+ return;
90
+ const anchor = hint !== null && hint !== void 0 ? hint : defaultAnchor(role);
91
+ buckets[anchor].push({
92
+ slotId: n.slotId, viewId: n.viewId, label: n.slotId, role,
93
+ });
94
+ return;
95
+ }
96
+ if (n.type === 'tabs') {
97
+ for (const t of n.tabs) {
98
+ const role = effectiveRole(t.role);
99
+ if (role === 'body')
100
+ continue;
101
+ const anchor = hint !== null && hint !== void 0 ? hint : defaultAnchor(role);
102
+ buckets[anchor].push({
103
+ slotId: t.slotId, viewId: t.viewId, label: t.label, icon: t.icon, role,
104
+ });
105
+ }
106
+ return;
107
+ }
108
+ if (n.direction === 'horizontal') {
109
+ for (let i = 0; i < n.children.length; i++) {
110
+ const child = n.children[i];
111
+ let nextHint = hint;
112
+ if (hint === null && !hasAnyBody(child)) {
113
+ if (i === 0)
114
+ nextHint = 'left';
115
+ else if (i === n.children.length - 1)
116
+ nextHint = 'right';
117
+ }
118
+ walk(child, nextHint);
119
+ }
120
+ }
121
+ else {
122
+ for (let i = 0; i < n.children.length; i++) {
123
+ const child = n.children[i];
124
+ let nextHint = hint;
125
+ if (hint === null && i === 0 && !hasAnyBody(child))
126
+ nextHint = 'top';
127
+ walk(child, nextHint);
128
+ }
129
+ }
130
+ }
131
+ walk(node, null);
132
+ return buckets;
133
+ }
134
+ function toSpec(slots) {
135
+ if (slots.length === 0)
136
+ return null;
137
+ return {
138
+ slots: slots.map((s) => ({
139
+ slotId: s.slotId, viewId: s.viewId, label: s.label, icon: s.icon, role: s.role,
140
+ })),
141
+ };
142
+ }
143
+ export function derive(tree) {
144
+ const stripped = stripNonBody(tree);
145
+ const bodyRoot = stripped !== null && stripped !== void 0 ? stripped : EMPTY_BODY;
146
+ const buckets = partitionDrawers(tree);
147
+ return {
148
+ bodyRoot,
149
+ drawers: {
150
+ left: toSpec(buckets.left),
151
+ right: toSpec(buckets.right),
152
+ top: toSpec(buckets.top),
153
+ },
154
+ };
155
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { derive } from './derive';
3
+ describe('derive', () => {
4
+ describe('body partition', () => {
5
+ it('all-body tree → bodyRoot identical, all drawers null', () => {
6
+ const tree = {
7
+ type: 'split',
8
+ direction: 'horizontal',
9
+ sizes: [1, 1],
10
+ children: [
11
+ { type: 'slot', slotId: 'a', viewId: 'view:a', role: 'body' },
12
+ { type: 'slot', slotId: 'b', viewId: 'view:b', role: 'body' },
13
+ ],
14
+ };
15
+ const result = derive(tree);
16
+ expect(result.bodyRoot).toEqual(tree);
17
+ expect(result.drawers.left).toBeNull();
18
+ expect(result.drawers.right).toBeNull();
19
+ expect(result.drawers.top).toBeNull();
20
+ });
21
+ it('zero-body tree → bodyRoot is empty placeholder', () => {
22
+ var _a, _b;
23
+ const tree = {
24
+ type: 'split', direction: 'horizontal', sizes: [1, 1],
25
+ children: [
26
+ { type: 'slot', slotId: 'a', viewId: 'view:a', role: 'sidebar' },
27
+ { type: 'slot', slotId: 'b', viewId: 'view:b', role: 'inspector' },
28
+ ],
29
+ };
30
+ const result = derive(tree);
31
+ expect(result.bodyRoot.type).toBe('slot');
32
+ expect(result.bodyRoot.slotId).toBe('__sh3core__:compact:empty');
33
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['a']);
34
+ expect((_b = result.drawers.right) === null || _b === void 0 ? void 0 : _b.slots.map((s) => s.slotId)).toEqual(['b']);
35
+ });
36
+ it('single body wrapped in split collapses to bare body slot', () => {
37
+ const tree = {
38
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.8],
39
+ children: [
40
+ { type: 'slot', slotId: 'side', viewId: 'view:side', role: 'sidebar' },
41
+ { type: 'slot', slotId: 'body', viewId: 'view:body', role: 'body' },
42
+ ],
43
+ };
44
+ const result = derive(tree);
45
+ expect(result.bodyRoot.type).toBe('slot');
46
+ expect(result.bodyRoot.slotId).toBe('body');
47
+ });
48
+ });
49
+ describe('anchor inference', () => {
50
+ it('first horizontal child → left, last → right', () => {
51
+ var _a, _b;
52
+ const tree = {
53
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.6, 0.2],
54
+ children: [
55
+ { type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
56
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
57
+ { type: 'slot', slotId: 'ins', viewId: 'v:ins', role: 'inspector' },
58
+ ],
59
+ };
60
+ const result = derive(tree);
61
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['sb']);
62
+ expect((_b = result.drawers.right) === null || _b === void 0 ? void 0 : _b.slots.map((s) => s.slotId)).toEqual(['ins']);
63
+ expect(result.drawers.top).toBeNull();
64
+ });
65
+ it('top of vertical split → top anchor', () => {
66
+ var _a;
67
+ const tree = {
68
+ type: 'split', direction: 'vertical', sizes: [0.3, 0.7],
69
+ children: [
70
+ { type: 'slot', slotId: 'tools', viewId: 'v:tools', role: 'sidebar' },
71
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
72
+ ],
73
+ };
74
+ const result = derive(tree);
75
+ expect((_a = result.drawers.top) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['tools']);
76
+ expect(result.drawers.left).toBeNull();
77
+ expect(result.drawers.right).toBeNull();
78
+ });
79
+ it('inspector with no horizontal anchor defaults to right', () => {
80
+ var _a;
81
+ const tree = {
82
+ type: 'slot', slotId: 'lone', viewId: 'v:lone', role: 'inspector',
83
+ };
84
+ const result = derive(tree);
85
+ expect((_a = result.drawers.right) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['lone']);
86
+ expect(result.drawers.left).toBeNull();
87
+ });
88
+ it('sidebar with no horizontal anchor defaults to left', () => {
89
+ var _a;
90
+ const tree = {
91
+ type: 'slot', slotId: 'lone', viewId: 'v:lone', role: 'sidebar',
92
+ };
93
+ const result = derive(tree);
94
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['lone']);
95
+ expect(result.drawers.right).toBeNull();
96
+ });
97
+ });
98
+ describe('multi-slot drawers', () => {
99
+ it('two sidebars on the left render as one drawer with two slots', () => {
100
+ var _a;
101
+ const tree = {
102
+ type: 'split', direction: 'horizontal', sizes: [0.3, 0.7],
103
+ children: [
104
+ {
105
+ type: 'split', direction: 'vertical', sizes: [0.5, 0.5],
106
+ children: [
107
+ { type: 'slot', slotId: 'sb-top', viewId: 'v:sb-top', role: 'sidebar' },
108
+ { type: 'slot', slotId: 'sb-bot', viewId: 'v:sb-bot', role: 'sidebar' },
109
+ ],
110
+ },
111
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
112
+ ],
113
+ };
114
+ const result = derive(tree);
115
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['sb-top', 'sb-bot']);
116
+ });
117
+ });
118
+ describe('default role', () => {
119
+ it('untagged slot defaults to body', () => {
120
+ const tree = { type: 'slot', slotId: 'x', viewId: 'v:x' };
121
+ const result = derive(tree);
122
+ expect(result.bodyRoot).toEqual(tree);
123
+ expect(result.drawers.left).toBeNull();
124
+ expect(result.drawers.right).toBeNull();
125
+ expect(result.drawers.top).toBeNull();
126
+ });
127
+ });
128
+ describe('tabs nodes', () => {
129
+ it('tabs of body slots stay in body root', () => {
130
+ const tree = {
131
+ type: 'tabs', activeTab: 0,
132
+ tabs: [
133
+ { slotId: 't1', viewId: 'v:t1', label: 'Tab 1', role: 'body' },
134
+ { slotId: 't2', viewId: 'v:t2', label: 'Tab 2', role: 'body' },
135
+ ],
136
+ };
137
+ const result = derive(tree);
138
+ expect(result.bodyRoot.type).toBe('tabs');
139
+ });
140
+ it('tabs with mixed roles split body tabs from sidebar slots', () => {
141
+ var _a;
142
+ const tree = {
143
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.8],
144
+ children: [
145
+ { type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
146
+ {
147
+ type: 'tabs', activeTab: 0,
148
+ tabs: [
149
+ { slotId: 't1', viewId: 'v:t1', label: 'Tab 1', role: 'body' },
150
+ { slotId: 't2', viewId: 'v:t2', label: 'Tab 2', role: 'body' },
151
+ ],
152
+ },
153
+ ],
154
+ };
155
+ const result = derive(tree);
156
+ expect(result.bodyRoot.type).toBe('tabs');
157
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['sb']);
158
+ });
159
+ });
160
+ });
@@ -0,0 +1,21 @@
1
+ import type { DrawerAnchor, DrawerStateMap } from './types';
2
+ export declare const drawerStore: {
3
+ readonly state: DrawerStateMap;
4
+ open(anchor: DrawerAnchor): void;
5
+ close(anchor: DrawerAnchor): void;
6
+ toggle(anchor: DrawerAnchor): void;
7
+ activate(anchor: DrawerAnchor, slotId: string): void;
8
+ /**
9
+ * Bind a write-through callback. layoutStore calls this when the active
10
+ * preset/viewport changes; the callback persists state into the
11
+ * AppLayoutBlob.drawers field. Pass null to unbind.
12
+ */
13
+ __setWriteThrough(cb: ((next: DrawerStateMap) => void) | null): void;
14
+ /**
15
+ * Replace the current state from a persisted snapshot. Used by
16
+ * layoutStore on preset/viewport-class change.
17
+ */
18
+ __hydrate(snapshot: DrawerStateMap): void;
19
+ /** Test-only reset. */
20
+ __reset(): void;
21
+ };