sh3-core 0.16.0 → 0.17.0

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 (68) hide show
  1. package/dist/Sh3.svelte +2 -73
  2. package/dist/actions/ctx-actions.svelte.test.js +4 -4
  3. package/dist/api.d.ts +2 -0
  4. package/dist/api.js +1 -0
  5. package/dist/build.d.ts +27 -0
  6. package/dist/build.js +59 -1
  7. package/dist/build.test.d.ts +1 -0
  8. package/dist/build.test.js +31 -0
  9. package/dist/contributions/index.d.ts +1 -1
  10. package/dist/contributions/index.js +1 -1
  11. package/dist/contributions/registry.d.ts +17 -1
  12. package/dist/contributions/registry.js +50 -2
  13. package/dist/contributions/scope.test.d.ts +1 -0
  14. package/dist/contributions/scope.test.js +52 -0
  15. package/dist/contributions/types.d.ts +11 -3
  16. package/dist/createShell.js +7 -1
  17. package/dist/fields/address.d.ts +3 -0
  18. package/dist/fields/address.js +36 -0
  19. package/dist/fields/address.test.d.ts +1 -0
  20. package/dist/fields/address.test.js +34 -0
  21. package/dist/fields/decoration.d.ts +7 -0
  22. package/dist/fields/decoration.js +199 -0
  23. package/dist/fields/decoration.svelte.test.d.ts +1 -0
  24. package/dist/fields/decoration.svelte.test.js +177 -0
  25. package/dist/fields/dispatch.d.ts +22 -0
  26. package/dist/fields/dispatch.js +254 -0
  27. package/dist/fields/dispatch.test.d.ts +1 -0
  28. package/dist/fields/dispatch.test.js +175 -0
  29. package/dist/fields/types.d.ts +101 -0
  30. package/dist/fields/types.js +16 -0
  31. package/dist/fields/walker.svelte.test.d.ts +1 -0
  32. package/dist/fields/walker.svelte.test.js +138 -0
  33. package/dist/host.js +27 -2
  34. package/dist/host.svelte.test.d.ts +1 -0
  35. package/dist/host.svelte.test.js +92 -0
  36. package/dist/layout/slotHostPool.svelte.d.ts +8 -0
  37. package/dist/layout/slotHostPool.svelte.js +14 -1
  38. package/dist/overlays/OverlayRoots.svelte +86 -0
  39. package/dist/overlays/OverlayRoots.svelte.d.ts +3 -0
  40. package/dist/platform/tauri-backend.d.ts +3 -3
  41. package/dist/platform/tauri-backend.js +24 -3
  42. package/dist/projects/session-state.svelte.d.ts +3 -3
  43. package/dist/projects/session-state.svelte.js +5 -4
  44. package/dist/runtime/runVerb.js +2 -2
  45. package/dist/satellite/SatelliteShell.svelte +58 -11
  46. package/dist/satellite/SatelliteShell.svelte.test.d.ts +1 -0
  47. package/dist/satellite/SatelliteShell.svelte.test.js +61 -0
  48. package/dist/sh3Api/fields-walker.svelte.test.d.ts +1 -0
  49. package/dist/sh3Api/fields-walker.svelte.test.js +75 -0
  50. package/dist/sh3Api/headless.d.ts +9 -0
  51. package/dist/sh3Api/headless.js +163 -16
  52. package/dist/sh3Api/headless.svelte.test.js +9 -9
  53. package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -2
  54. package/dist/shards/activate-fields.svelte.test.d.ts +1 -0
  55. package/dist/shards/activate-fields.svelte.test.js +121 -0
  56. package/dist/shards/activate-runtime.test.js +8 -8
  57. package/dist/shards/activate.svelte.js +29 -35
  58. package/dist/shards/types.d.ts +14 -75
  59. package/dist/shell-shard/ScrollbackView.svelte +55 -9
  60. package/dist/shell-shard/Terminal.svelte +1 -1
  61. package/dist/shell-shard/scrollback-stick.d.ts +9 -0
  62. package/dist/shell-shard/scrollback-stick.js +21 -0
  63. package/dist/shell-shard/scrollback-stick.test.d.ts +1 -0
  64. package/dist/shell-shard/scrollback-stick.test.js +25 -0
  65. package/dist/verbs/types.d.ts +56 -1
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ /*
3
+ * OverlayRoots — the six overlay-layer roots and the float-store binding,
4
+ * extracted so both the main shell (`Sh3.svelte`) and satellite shell
5
+ * (`SatelliteShell.svelte`) can mount them.
6
+ *
7
+ * Without this, satellite views can't open modals/popups/floats: layer
8
+ * managers call `getLayerRoot('modal')` and throw because no root was
9
+ * registered. Same situation for the float store binding the
10
+ * `FloatLayer` reads to render detached frames.
11
+ *
12
+ * Each layer is an absolutely-positioned full-window div with
13
+ * pointer-events: none; managers enable pointer events on the surfaces
14
+ * they portal in.
15
+ */
16
+
17
+ import DragPreview from '../layout/DragPreview.svelte';
18
+ import FloatLayer from './FloatLayer.svelte';
19
+ import { registerLayerRoot, unregisterLayerRoot } from './roots';
20
+ import { bindFloatStore, unbindFloatStore } from './float';
21
+ import { layoutStore } from '../layout/store.svelte';
22
+ import type { OverlayLayer } from './types';
23
+
24
+ // Layer metadata — order matches the stack in docs/design/layout.md.
25
+ // Index 0 here is layer 1 (floating panels); layer 0 is the content area.
26
+ const overlayLayers: { layer: number; name: OverlayLayer }[] = [
27
+ { layer: 1, name: 'floating' },
28
+ { layer: 2, name: 'drag-preview' },
29
+ { layer: 3, name: 'popup' },
30
+ { layer: 4, name: 'modal' },
31
+ { layer: 5, name: 'toast' },
32
+ { layer: 6, name: 'command' },
33
+ ];
34
+
35
+ const overlayRoots: Partial<Record<OverlayLayer, HTMLDivElement>> = $state({});
36
+
37
+ $effect(() => {
38
+ for (const { name } of overlayLayers) {
39
+ const el = overlayRoots[name];
40
+ if (el) registerLayerRoot(name, el);
41
+ }
42
+ return () => {
43
+ for (const { name } of overlayLayers) unregisterLayerRoot(name);
44
+ };
45
+ });
46
+
47
+ $effect(() => {
48
+ const tree = layoutStore.tree;
49
+ bindFloatStore(tree.floats, () => ({
50
+ w: window.innerWidth,
51
+ h: window.innerHeight,
52
+ }));
53
+ return () => unbindFloatStore();
54
+ });
55
+ </script>
56
+
57
+ <div class="sh3-overlays" aria-hidden="true">
58
+ {#each overlayLayers as { layer, name } (layer)}
59
+ <div
60
+ class="sh3-overlay-root"
61
+ data-sh3-overlay={name}
62
+ data-sh3-layer={layer}
63
+ style="z-index: var(--sh3-z-layer-{layer});"
64
+ bind:this={overlayRoots[name]}
65
+ >
66
+ {#if name === 'floating'}
67
+ <FloatLayer />
68
+ {:else if name === 'drag-preview'}
69
+ <DragPreview />
70
+ {/if}
71
+ </div>
72
+ {/each}
73
+ </div>
74
+
75
+ <style>
76
+ .sh3-overlays {
77
+ position: absolute;
78
+ inset: 0;
79
+ pointer-events: none;
80
+ }
81
+ .sh3-overlay-root {
82
+ position: absolute;
83
+ inset: 0;
84
+ pointer-events: none;
85
+ }
86
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const OverlayRoots: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type OverlayRoots = ReturnType<typeof OverlayRoots>;
3
+ export default OverlayRoots;
@@ -7,9 +7,9 @@ export declare class TauriStoreBackend implements Backend {
7
7
  delete(shardId: string): void;
8
8
  list(): string[];
9
9
  /**
10
- * Load the store from disk into the local cache. Must be called once
11
- * before read/list return meaningful data. Called by the platform
12
- * resolver at boot.
10
+ * Load the store from disk into the local cache and start mirroring
11
+ * cross-window writes. Must be called once before read/list return
12
+ * meaningful data. Called by the platform resolver at boot.
13
13
  */
14
14
  init(): Promise<void>;
15
15
  }
@@ -8,6 +8,15 @@
8
8
  * init(), then serve reads/lists synchronously from that cache —
9
9
  * matching the Backend interface contract. Writes go to both the
10
10
  * local cache and the Tauri store (fire-and-forget async).
11
+ *
12
+ * Multi-window coherence: each Tauri WebviewWindow runs its own JS
13
+ * context, so each gets its own TauriStoreBackend instance with an
14
+ * independent #cache. Without coordination, a satellite window booted
15
+ * from the host would never see writes made in the host (or vice
16
+ * versa). init() subscribes to plugin-store's onChange event so any
17
+ * cross-window write is mirrored into the local cache. The same event
18
+ * fires for this window's own writes too, but the cache.set there is
19
+ * idempotent.
11
20
  */
12
21
  var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
13
22
  if (kind === "m") throw new TypeError("Private method is not writable");
@@ -44,15 +53,27 @@ export class TauriStoreBackend {
44
53
  return [...__classPrivateFieldGet(this, _TauriStoreBackend_cache, "f").keys()];
45
54
  }
46
55
  /**
47
- * Load the store from disk into the local cache. Must be called once
48
- * before read/list return meaningful data. Called by the platform
49
- * resolver at boot.
56
+ * Load the store from disk into the local cache and start mirroring
57
+ * cross-window writes. Must be called once before read/list return
58
+ * meaningful data. Called by the platform resolver at boot.
50
59
  */
51
60
  async init() {
52
61
  const entries = await __classPrivateFieldGet(this, _TauriStoreBackend_store, "f").entries();
53
62
  for (const [key, value] of entries) {
54
63
  __classPrivateFieldGet(this, _TauriStoreBackend_cache, "f").set(key, value);
55
64
  }
65
+ // Mirror writes from other WebviewWindows (and our own) into the
66
+ // local cache so reads stay coherent across the host + satellites.
67
+ // The unlisten fn is intentionally not retained — the backend lives
68
+ // for the page session and the subscription tears down with it.
69
+ await __classPrivateFieldGet(this, _TauriStoreBackend_store, "f").onChange((key, value) => {
70
+ if (value === undefined) {
71
+ __classPrivateFieldGet(this, _TauriStoreBackend_cache, "f").delete(key);
72
+ }
73
+ else {
74
+ __classPrivateFieldGet(this, _TauriStoreBackend_cache, "f").set(key, value);
75
+ }
76
+ });
56
77
  }
57
78
  }
58
79
  _TauriStoreBackend_store = new WeakMap(), _TauriStoreBackend_cache = new WeakMap();
@@ -10,8 +10,8 @@ export declare const sessionState: {
10
10
  *
11
11
  * Setting the same value is a no-op (no app unload, no breadcrumb clear).
12
12
  *
13
- * The unloadApp call is deferred to a dynamic import so this module stays
14
- * importable from circular-dependency hot-paths (lifecycle imports
15
- * sessionState; sessionState here would otherwise import lifecycle eagerly).
13
+ * Note: lifecycle session-state is a circular import. Safe because both
14
+ * sides only read the imported binding inside function bodies, after module
15
+ * init completes.
16
16
  */
17
17
  export declare function setActiveProjectId(id: string | null): void;
@@ -11,6 +11,7 @@
11
11
  * active app (whose scopeId is bound to the old scope).
12
12
  */
13
13
  import { activeApp, breadcrumbApp } from '../apps/registry.svelte';
14
+ import { unloadApp } from '../apps/lifecycle';
14
15
  export const sessionState = $state({
15
16
  activeProjectId: null,
16
17
  });
@@ -23,9 +24,9 @@ export const sessionState = $state({
23
24
  *
24
25
  * Setting the same value is a no-op (no app unload, no breadcrumb clear).
25
26
  *
26
- * The unloadApp call is deferred to a dynamic import so this module stays
27
- * importable from circular-dependency hot-paths (lifecycle imports
28
- * sessionState; sessionState here would otherwise import lifecycle eagerly).
27
+ * Note: lifecycle session-state is a circular import. Safe because both
28
+ * sides only read the imported binding inside function bodies, after module
29
+ * init completes.
29
30
  */
30
31
  export function setActiveProjectId(id) {
31
32
  if (sessionState.activeProjectId === id)
@@ -34,6 +35,6 @@ export function setActiveProjectId(id) {
34
35
  sessionState.activeProjectId = id;
35
36
  breadcrumbApp.id = null;
36
37
  if (previousActive) {
37
- void import('../apps/lifecycle').then((m) => m.unloadApp(previousActive));
38
+ unloadApp(previousActive);
38
39
  }
39
40
  }
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { activeShards } from '../shards/activate.svelte';
19
19
  import { getVerb, listVerbsWithShard } from '../shards/registry';
20
- import { makeSh3ApiHeadless } from '../sh3Api/headless';
20
+ import { makeSh3Api } from '../sh3Api/headless';
21
21
  import { Scrollback } from '../shell-shard/scrollback.svelte';
22
22
  import { SessionClient } from '../shell-shard/session-client.svelte';
23
23
  import { TenantFsClient } from '../shell-shard/tenant-fs-client';
@@ -53,7 +53,7 @@ export async function runVerbProgrammatic(shardId, name, args, opts) {
53
53
  async function buildProgrammaticContext(b) {
54
54
  var _a, _b;
55
55
  const ctx = {
56
- sh3: makeSh3ApiHeadless(),
56
+ sh3: makeSh3Api({ callerKind: 'verb' }),
57
57
  scrollback: b.sinkScrollback,
58
58
  session: makeStubSession(),
59
59
  cwd: '/',
@@ -3,22 +3,23 @@
3
3
  * SatelliteShell — top-level root component mounted by createShell() when
4
4
  * detectSatelliteMode() returns a payload.
5
5
  *
6
- * Renders only LayoutRenderer against the seeded in-memory layout. No brand
7
- * bar, no home button, no overlay chrome. The workspace zone backend has
8
- * already been forced to MemoryBackend by createShell() before this mounts.
9
- *
10
- * Float payloads: seeds HOME_TREE's docked node directly via
11
- * seedSatelliteLayout(), so LayoutRenderer immediately shows the detached
6
+ * Float payloads: chromeless. Seeds HOME_TREE's docked node directly via
7
+ * seedSatelliteLayout() so LayoutRenderer immediately shows the detached
12
8
  * float content.
13
9
  *
14
- * App payloads: kicks off launchApp(appId, { skipLastApp, skipSwitchToHome })
15
- * which handles the full attach/switchToApp lifecycle. The seed call is
16
- * skipped because launchApp replaces the active root to 'app' on its own.
10
+ * App payloads: renders a slim tabbar (BrandSlot + MenuBar + reserved
11
+ * minimized-floats slot) above LayoutRenderer, then kicks off
12
+ * launchApp(appId, { skipLastApp, skipSwitchToHome }) which handles the
13
+ * full attach + required-shard activation lifecycle. Home button is
14
+ * intentionally omitted — the OS window close is the satellite's "home".
17
15
  */
18
16
 
19
17
  import '../tokens.css';
20
18
  import '../primitives/base.css';
21
19
  import LayoutRenderer from '../layout/LayoutRenderer.svelte';
20
+ import OverlayRoots from '../overlays/OverlayRoots.svelte';
21
+ import BrandSlot from '../BrandSlot.svelte';
22
+ import MenuBar from '../actions/MenuBar.svelte';
22
23
  import { seedSatelliteLayout } from '../layout/store.svelte';
23
24
  import { seedLayoutFromPayload } from './seed';
24
25
  import { launchApp } from '../apps/lifecycle';
@@ -45,8 +46,20 @@
45
46
  });
46
47
  </script>
47
48
 
48
- <div class="sh3-satellite-root">
49
- <LayoutRenderer />
49
+ <div class="sh3-satellite-root" class:sh3-satellite-app={payload.kind === 'app'}>
50
+ {#if payload.kind === 'app'}
51
+ <header class="sh3-satellite-tabbar" data-sh3-region="tabbar">
52
+ <BrandSlot />
53
+ <MenuBar />
54
+ <div class="sh3-satellite-floats-slot" aria-hidden="true"></div>
55
+ </header>
56
+ <main class="sh3-satellite-content" data-sh3-region="content" data-sh3-layer="0">
57
+ <LayoutRenderer />
58
+ </main>
59
+ {:else}
60
+ <LayoutRenderer />
61
+ {/if}
62
+ <OverlayRoots />
50
63
  </div>
51
64
 
52
65
  <style>
@@ -57,4 +70,38 @@
57
70
  background: var(--sh3-grad-bg, var(--sh3-bg));
58
71
  color: var(--sh3-fg);
59
72
  }
73
+
74
+ /*
75
+ * App-payload satellites get a slim chrome row above the content. Float
76
+ * payloads keep the original full-bleed layout (no grid, content fills
77
+ * the root), so the .sh3-satellite-app modifier is required for the grid.
78
+ */
79
+ .sh3-satellite-root.sh3-satellite-app {
80
+ display: grid;
81
+ grid-template-rows: var(--sh3-tabbar-height) 1fr;
82
+ }
83
+
84
+ .sh3-satellite-tabbar {
85
+ display: grid;
86
+ grid-template-columns: auto 1fr auto;
87
+ align-items: center;
88
+ gap: var(--sh3-pad-md);
89
+ padding: 0 var(--sh3-pad-md);
90
+ background: var(--sh3-grad-bg-elevated, var(--sh3-bg-elevated));
91
+ border-bottom: 1px solid var(--sh3-border);
92
+ user-select: none;
93
+ }
94
+
95
+ .sh3-satellite-content {
96
+ position: relative;
97
+ overflow: hidden;
98
+ background: var(--sh3-grad-bg, var(--sh3-bg));
99
+ min-width: 0;
100
+ min-height: 0;
101
+ }
102
+
103
+ .sh3-satellite-floats-slot {
104
+ /* Reserved cell for future minimized-float chips; empty for now. */
105
+ min-width: 0;
106
+ }
60
107
  </style>
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { tick } from 'svelte';
3
+ import SatelliteShell from './SatelliteShell.svelte';
4
+ import { resetFramework } from '../__test__/reset';
5
+ import { renderWithShell } from '../__test__/render';
6
+ import { makeApp, makeAppManifest, makeSlotNode, makeTree, } from '../__test__/fixtures';
7
+ import { registerApp } from '../apps/registry.svelte';
8
+ import { registerView } from '../shards/registry';
9
+ function registerStubView() {
10
+ registerView('test:view', {
11
+ mount(container, ctx) {
12
+ const node = document.createElement('div');
13
+ node.dataset.viewFor = ctx.slotId;
14
+ container.appendChild(node);
15
+ return { unmount: () => node.remove() };
16
+ },
17
+ });
18
+ }
19
+ describe('SatelliteShell — chrome rendering', () => {
20
+ beforeEach(resetFramework);
21
+ it('renders BrandSlot + MenuBar + reserved floats slot for app payloads', async () => {
22
+ registerStubView();
23
+ registerApp(makeApp({
24
+ manifest: makeAppManifest({
25
+ id: 'sat-app',
26
+ label: 'Sat App',
27
+ }),
28
+ initialLayout: [
29
+ { name: 'default', tree: makeTree(makeSlotNode('s', 'test:view')) },
30
+ ],
31
+ }));
32
+ const payload = {
33
+ kind: 'app',
34
+ appId: 'sat-app',
35
+ activateShards: [],
36
+ };
37
+ const { container } = renderWithShell(SatelliteShell, { payload });
38
+ // launchApp runs in queueMicrotask inside the $effect; flush both.
39
+ await Promise.resolve();
40
+ await tick();
41
+ await tick();
42
+ expect(container.querySelector('[data-sh3-region="tabbar"]')).toBeTruthy();
43
+ expect(container.querySelector('.sh3-brand-slot')).toBeTruthy();
44
+ expect(container.querySelector('[role="menubar"]')).toBeTruthy();
45
+ expect(container.querySelector('.sh3-satellite-floats-slot')).toBeTruthy();
46
+ });
47
+ it('renders no tabbar / chrome for float payloads', async () => {
48
+ const payload = {
49
+ kind: 'float',
50
+ content: makeSlotNode('s', 'test:view'),
51
+ size: { w: 800, h: 600 },
52
+ activateShards: [],
53
+ };
54
+ const { container } = renderWithShell(SatelliteShell, { payload });
55
+ await tick();
56
+ expect(container.querySelector('[data-sh3-region="tabbar"]')).toBeFalsy();
57
+ expect(container.querySelector('.sh3-brand-slot')).toBeFalsy();
58
+ expect(container.querySelector('[role="menubar"]')).toBeFalsy();
59
+ expect(container.querySelector('.sh3-satellite-floats-slot')).toBeFalsy();
60
+ });
61
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { makeSh3Api } from './headless';
3
+ import { acquireSlotHost, resetSlotHostPool, } from '../layout/slotHostPool.svelte';
4
+ import { __resetContributionsForTest } from '../contributions/registry';
5
+ /*
6
+ * Walker dispatch through the Sh3Api facade.
7
+ *
8
+ * Locks in that ctx.sh3.fields.get/set work with walker-produced addresses
9
+ * (shardId === 'sh3.walker'). The walker synthesizes FieldViews on every
10
+ * call — facade re-walks the slot's container to resolve the target element,
11
+ * then routes through the same writeElement helper as element-ref descriptors.
12
+ */
13
+ describe('Sh3Api fields walker dispatch', () => {
14
+ beforeEach(() => {
15
+ __resetContributionsForTest();
16
+ resetSlotHostPool();
17
+ document.body.innerHTML = '';
18
+ });
19
+ it('set on a walker addr writes the underlying input and dispatches input+change', async () => {
20
+ const slotId = 'sw1';
21
+ const host = acquireSlotHost(slotId, 'view', 'lbl');
22
+ document.body.appendChild(host);
23
+ host.innerHTML = `<select name="color">
24
+ <option value="red">Red</option>
25
+ <option value="blue">Blue</option>
26
+ </select>`;
27
+ const select = host.querySelector('select');
28
+ let inputs = 0;
29
+ let changes = 0;
30
+ select.addEventListener('input', () => inputs++);
31
+ select.addEventListener('change', () => changes++);
32
+ const api = makeSh3Api({ callerKind: 'verb' });
33
+ await api.fields.set({ shardId: 'sh3.walker', slotId, fieldId: 'color' }, 'blue');
34
+ expect(select.value).toBe('blue');
35
+ expect(inputs).toBe(1);
36
+ expect(changes).toBe(1);
37
+ });
38
+ it('get on a walker addr returns the live value', () => {
39
+ const slotId = 'sw2';
40
+ const host = acquireSlotHost(slotId, 'view', 'lbl');
41
+ document.body.appendChild(host);
42
+ host.innerHTML = `<input name="title" value="hello" />`;
43
+ const api = makeSh3Api({ callerKind: 'verb' });
44
+ expect(api.fields.get({ shardId: 'sh3.walker', slotId, fieldId: 'title' })).toBe('hello');
45
+ });
46
+ it('get on a walker addr reads .checked for checkbox', () => {
47
+ const slotId = 'sw3';
48
+ const host = acquireSlotHost(slotId, 'view', 'lbl');
49
+ document.body.appendChild(host);
50
+ host.innerHTML = `<input type="checkbox" name="published" />`;
51
+ const cb = host.querySelector('input');
52
+ cb.checked = true;
53
+ const api = makeSh3Api({ callerKind: 'verb' });
54
+ expect(api.fields.get({ shardId: 'sh3.walker', slotId, fieldId: 'published' })).toBe(true);
55
+ });
56
+ it('rejects unknown walker addr (slot has no matching field)', () => {
57
+ const slotId = 'sw4';
58
+ const host = acquireSlotHost(slotId, 'view', 'lbl');
59
+ document.body.appendChild(host);
60
+ const api = makeSh3Api({ callerKind: 'verb' });
61
+ expect(() => api.fields.get({ shardId: 'sh3.walker', slotId, fieldId: 'absent' })).toThrow(/unknown field/);
62
+ });
63
+ it('rejects walker addr without slotId', async () => {
64
+ const api = makeSh3Api({ callerKind: 'verb' });
65
+ await expect(api.fields.set({ shardId: 'sh3.walker', fieldId: 'whatever' }, 'x')).rejects.toThrow(/requires slotId/);
66
+ });
67
+ it('rejects writing to a [data-sh3-field] custom element', async () => {
68
+ const slotId = 'sw5';
69
+ const host = acquireSlotHost(slotId, 'view', 'lbl');
70
+ document.body.appendChild(host);
71
+ host.innerHTML = `<div data-sh3-field="custom">x</div>`;
72
+ const api = makeSh3Api({ callerKind: 'verb' });
73
+ await expect(api.fields.set({ shardId: 'sh3.walker', slotId, fieldId: 'custom' }, 'y')).rejects.toThrow(/cannot write/);
74
+ });
75
+ });
@@ -1,4 +1,13 @@
1
1
  import type { Sh3Api } from '../shell-shard/registry';
2
2
  import type { ZoneManager } from '../state/types';
3
+ export interface MakeSh3ApiOpts {
4
+ callerKind: 'shard' | 'verb';
5
+ /** Present when callerKind === 'shard' (used by future permission gates). */
6
+ callerShardId?: string;
7
+ /** Cross-shard zone manager — passed in by ShardContext when permitted. */
8
+ zones?: ZoneManager;
9
+ }
10
+ export declare function makeSh3Api(opts?: MakeSh3ApiOpts): Sh3Api;
11
+ /** @deprecated Renamed to makeSh3Api(opts?). Kept for one minor cycle. */
3
12
  export declare function makeSh3ApiHeadless(zones?: ZoneManager): Sh3Api;
4
13
  export declare function makeSh3ApiForTest(): Sh3Api;