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,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;
@@ -8,6 +8,10 @@
8
8
  *
9
9
  * Mode-related methods (`setMode`, `listModes`) stub to false / [];
10
10
  * Terminal.svelte wraps this with mode-aware closures.
11
+ *
12
+ * Caller identity (callerKind, callerShardId?) is plumbed through but not
13
+ * yet consulted — it exists so a future permission gate can be a one-liner
14
+ * inside this factory without changing the public Sh3Api type.
11
15
  */
12
16
  import { listRegisteredApps, getActiveApp } from '../apps/registry.svelte';
13
17
  import { launchApp } from '../apps/lifecycle';
@@ -16,6 +20,17 @@ import { inspectActiveLayout, focusView, closeTab, popoutView, dockFloat, dockIn
16
20
  import { floatManager } from '../overlays/float';
17
21
  import { getUser, isAdmin } from '../auth/index';
18
22
  import { makeDispatchToTerminal } from '../shell-shard/dispatch-to-terminal';
23
+ import { listVerbsWithShard } from '../shards/registry';
24
+ import { listActionsFromEntries } from '../actions/listActive';
25
+ import { listActions as listActionEntriesFromRegistry } from '../actions/registry';
26
+ import { dispatchActionProgrammatic } from '../actions/listeners';
27
+ import { getLiveDispatcherState } from '../actions/state.svelte';
28
+ import { runVerbProgrammatic } from '../runtime/runVerb';
29
+ import { peekSlotHost } from '../layout/slotHostPool.svelte';
30
+ import { listFields as listFieldsImpl, getField as getFieldImpl, setField as setFieldImpl, walkSlotContainer, writeElement, } from '../fields/dispatch';
31
+ import { attachDecoration as attachDecorationImpl } from '../fields/decoration';
32
+ import { onChange as onContributionsChange } from '../contributions';
33
+ import { FIELD_POINT_ID, WALKER_SHARD_ID } from '../fields/types';
19
34
  const KNOWN_ZONES = ['ephemeral', 'session', 'workspace', 'user'];
20
35
  function collectTabEntries(node) {
21
36
  if (node.type === 'tabs') {
@@ -29,7 +44,123 @@ function collectTabEntries(node) {
29
44
  }
30
45
  return [];
31
46
  }
32
- export function makeSh3ApiHeadless(zones) {
47
+ export function makeSh3Api(opts) {
48
+ // Caller identity is plumbed but not yet used by any gate. The arg exists
49
+ // so future gates can be one-liners inside this factory.
50
+ void (opts === null || opts === void 0 ? void 0 : opts.callerKind);
51
+ void (opts === null || opts === void 0 ? void 0 : opts.callerShardId);
52
+ const zones = opts === null || opts === void 0 ? void 0 : opts.zones;
53
+ function listViewsImpl() {
54
+ try {
55
+ const { root } = inspectActiveLayout();
56
+ return collectTabEntries(root.docked).map((t) => {
57
+ var _a;
58
+ return ({
59
+ slotId: t.slotId,
60
+ viewId: (_a = t.viewId) !== null && _a !== void 0 ? _a : '',
61
+ label: t.label,
62
+ });
63
+ });
64
+ }
65
+ catch (_a) {
66
+ return [];
67
+ }
68
+ }
69
+ function resolveWalkerField(addr) {
70
+ if (!addr.slotId) {
71
+ throw new Error(`walker field requires slotId: ${addr.shardId}::::${addr.fieldId}`);
72
+ }
73
+ const host = peekSlotHost(addr.slotId);
74
+ if (!(host === null || host === void 0 ? void 0 : host.host)) {
75
+ throw new Error(`unknown field: ${addr.shardId}::${addr.slotId}::${addr.fieldId}`);
76
+ }
77
+ const found = walkSlotContainer(addr.slotId, host.host)
78
+ .find((f) => f.fieldId === addr.fieldId);
79
+ if (!found || !found.element) {
80
+ throw new Error(`unknown field: ${addr.shardId}::${addr.slotId}::${addr.fieldId}`);
81
+ }
82
+ return found;
83
+ }
84
+ function readWalkerField(addr) {
85
+ var _a;
86
+ const field = resolveWalkerField(addr);
87
+ const el = field.element;
88
+ if (el instanceof HTMLInputElement && el.type === 'checkbox')
89
+ return el.checked;
90
+ if (el instanceof HTMLInputElement ||
91
+ el instanceof HTMLTextAreaElement ||
92
+ el instanceof HTMLSelectElement) {
93
+ return el.value;
94
+ }
95
+ // [data-sh3-field] custom element — text content is the closest analogue.
96
+ return (_a = el.textContent) !== null && _a !== void 0 ? _a : '';
97
+ }
98
+ function writeWalkerField(addr, value) {
99
+ const field = resolveWalkerField(addr);
100
+ const el = field.element;
101
+ if (el instanceof HTMLInputElement ||
102
+ el instanceof HTMLTextAreaElement ||
103
+ el instanceof HTMLSelectElement) {
104
+ writeElement(el, value);
105
+ return;
106
+ }
107
+ throw new Error(`walker field "${addr.fieldId}" is a [data-sh3-field] custom element; ` +
108
+ `the framework cannot write into it. Use an explicit imperative registration with set().`);
109
+ }
110
+ const fields = {
111
+ list(listOpts) {
112
+ var _a;
113
+ const all = listFieldsImpl({
114
+ slotId: listOpts === null || listOpts === void 0 ? void 0 : listOpts.slotId,
115
+ shardId: listOpts === null || listOpts === void 0 ? void 0 : listOpts.shardId,
116
+ kind: listOpts === null || listOpts === void 0 ? void 0 : listOpts.kind,
117
+ });
118
+ const walkerMode = (_a = listOpts === null || listOpts === void 0 ? void 0 : listOpts.walker) !== null && _a !== void 0 ? _a : 'off';
119
+ if (walkerMode === 'off')
120
+ return all;
121
+ const targetSlots = (listOpts === null || listOpts === void 0 ? void 0 : listOpts.slotId)
122
+ ? [listOpts.slotId]
123
+ : Array.from(new Set(all.map((f) => f.slotId).filter((s) => !!s)));
124
+ let result = all;
125
+ for (const sid of targetSlots) {
126
+ const host = peekSlotHost(sid);
127
+ if (!(host === null || host === void 0 ? void 0 : host.host))
128
+ continue;
129
+ const slotHasContributions = all.some((f) => f.slotId === sid);
130
+ if (walkerMode === 'fallback' && slotHasContributions)
131
+ continue;
132
+ result = result.concat(walkSlotContainer(sid, host.host));
133
+ }
134
+ if (listOpts === null || listOpts === void 0 ? void 0 : listOpts.kind)
135
+ result = result.filter((f) => f.kind === listOpts.kind);
136
+ return result;
137
+ },
138
+ get(addr) {
139
+ if (addr.shardId === WALKER_SHARD_ID) {
140
+ return readWalkerField(addr);
141
+ }
142
+ return getFieldImpl(addr);
143
+ },
144
+ async set(addr, value) {
145
+ if (addr.shardId === WALKER_SHARD_ID) {
146
+ writeWalkerField(addr, value);
147
+ return;
148
+ }
149
+ return setFieldImpl(addr, value);
150
+ },
151
+ walk(slotId) {
152
+ const host = peekSlotHost(slotId);
153
+ if (!(host === null || host === void 0 ? void 0 : host.host))
154
+ throw new Error(`unknown slot: ${slotId}`);
155
+ return walkSlotContainer(slotId, host.host);
156
+ },
157
+ onChange(cb) {
158
+ return onContributionsChange(FIELD_POINT_ID, cb);
159
+ },
160
+ attachDecoration(addr, factory) {
161
+ return attachDecorationImpl(addr, factory);
162
+ },
163
+ };
33
164
  return {
34
165
  listApps() {
35
166
  return listRegisteredApps().map((m) => ({ id: m.id, label: m.label }));
@@ -49,20 +180,7 @@ export function makeSh3ApiHeadless(zones) {
49
180
  }));
50
181
  },
51
182
  listViewsInCurrentLayout() {
52
- try {
53
- const { root } = inspectActiveLayout();
54
- return collectTabEntries(root.docked).map((t) => {
55
- var _a;
56
- return ({
57
- slotId: t.slotId,
58
- viewId: (_a = t.viewId) !== null && _a !== void 0 ? _a : '',
59
- label: t.label,
60
- });
61
- });
62
- }
63
- catch (_a) {
64
- return [];
65
- }
183
+ return listViewsImpl();
66
184
  },
67
185
  openViewInCurrentLayout(viewId) {
68
186
  try {
@@ -166,8 +284,45 @@ export function makeSh3ApiHeadless(zones) {
166
284
  listModes() { return []; },
167
285
  getMode() { return { id: 'sh3', label: 'sh3' }; },
168
286
  dispatchToTerminal: makeDispatchToTerminal({ headless: true }),
287
+ listVerbs(verbOpts) {
288
+ const programmaticOnly = (verbOpts === null || verbOpts === void 0 ? void 0 : verbOpts.programmaticOnly) === true;
289
+ const out = listVerbsWithShard().map(({ verb, shardId }) => ({
290
+ shardId,
291
+ name: verb.name,
292
+ summary: verb.summary,
293
+ programmatic: verb.programmatic,
294
+ schema: verb.schema,
295
+ }));
296
+ return programmaticOnly ? out.filter((v) => v.programmatic === true) : out;
297
+ },
298
+ async runVerb(shardId, name, args, runOpts) {
299
+ return runVerbProgrammatic(shardId, name, args, runOpts);
300
+ },
301
+ listActions(actionOpts) {
302
+ const all = listActionsFromEntries(listActionEntriesFromRegistry(), getLiveDispatcherState());
303
+ let out = all;
304
+ if ((actionOpts === null || actionOpts === void 0 ? void 0 : actionOpts.submenuOf) !== undefined) {
305
+ const parent = actionOpts.submenuOf;
306
+ out = out.filter((a) => a.submenuOf === parent);
307
+ }
308
+ if (actionOpts === null || actionOpts === void 0 ? void 0 : actionOpts.activeOnly) {
309
+ out = out.filter((a) => a.active);
310
+ }
311
+ return out;
312
+ },
313
+ runAction(id, runOpts) {
314
+ return dispatchActionProgrammatic(id, runOpts);
315
+ },
316
+ listViews() {
317
+ return listViewsImpl();
318
+ },
319
+ fields,
169
320
  };
170
321
  }
322
+ /** @deprecated Renamed to makeSh3Api(opts?). Kept for one minor cycle. */
323
+ export function makeSh3ApiHeadless(zones) {
324
+ return makeSh3Api({ callerKind: 'verb', zones });
325
+ }
171
326
  export function makeSh3ApiForTest() {
172
- return makeSh3ApiHeadless();
327
+ return makeSh3Api({ callerKind: 'verb' });
173
328
  }
@@ -1,5 +1,8 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { makeSh3ApiHeadless } from './headless';
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { makeSh3Api } from './headless';
3
+ import { registerAction, __resetActionsRegistryForTest, } from '../actions/registry';
4
+ import { __resetContributionsForTest } from '../contributions/registry';
5
+ import { __resetDispatcherStateForTest } from '../actions/state.svelte';
3
6
  function makeMockZoneManager() {
4
7
  const data = {
5
8
  ephemeral: {},
@@ -16,44 +19,85 @@ function makeMockZoneManager() {
16
19
  }
17
20
  describe('sh3Api listZones', () => {
18
21
  it('returns empty when no ZoneManager provided (stub mode)', () => {
19
- const api = makeSh3ApiHeadless();
22
+ const api = makeSh3Api({ callerKind: 'verb' });
20
23
  expect(api.listZones()).toEqual([]);
21
24
  });
22
25
  it('returns one row per shard with the zones it has data in', () => {
23
26
  var _a, _b;
24
- const api = makeSh3ApiHeadless(makeMockZoneManager());
27
+ const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
25
28
  const rows = api.listZones();
26
29
  const byShard = new Map(rows.map((r) => [r.shardId, r.zones]));
27
30
  expect((_a = byShard.get('shard-a')) === null || _a === void 0 ? void 0 : _a.sort()).toEqual(['session', 'workspace']);
28
31
  expect((_b = byShard.get('shard-b')) === null || _b === void 0 ? void 0 : _b.sort()).toEqual(['user', 'workspace']);
29
32
  });
30
33
  it('filters by shardId when provided', () => {
31
- const api = makeSh3ApiHeadless(makeMockZoneManager());
34
+ const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
32
35
  const rows = api.listZones('shard-a');
33
36
  expect(rows).toHaveLength(1);
34
37
  expect(rows[0].shardId).toBe('shard-a');
35
38
  expect(rows[0].zones.sort()).toEqual(['session', 'workspace']);
36
39
  });
37
40
  it('returns empty array for unknown shardId', () => {
38
- const api = makeSh3ApiHeadless(makeMockZoneManager());
41
+ const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
39
42
  expect(api.listZones('not-a-shard')).toEqual([]);
40
43
  });
41
44
  });
42
45
  describe('sh3Api readZone', () => {
43
46
  it('returns null when no ZoneManager provided', () => {
44
- const api = makeSh3ApiHeadless();
47
+ const api = makeSh3Api({ callerKind: 'verb' });
45
48
  expect(api.readZone('shard-a', 'workspace')).toBe(null);
46
49
  });
47
50
  it('returns peeked value via ZoneManager', () => {
48
- const api = makeSh3ApiHeadless(makeMockZoneManager());
51
+ const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
49
52
  expect(api.readZone('shard-a', 'workspace')).toEqual({ bar: 2 });
50
53
  });
51
54
  it('returns null for unknown zone name', () => {
52
- const api = makeSh3ApiHeadless(makeMockZoneManager());
55
+ const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
53
56
  expect(api.readZone('shard-a', 'bogus')).toBe(null);
54
57
  });
55
58
  it('returns null for missing shard entry', () => {
56
- const api = makeSh3ApiHeadless(makeMockZoneManager());
59
+ const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
57
60
  expect(api.readZone('not-a-shard', 'workspace')).toBe(null);
58
61
  });
59
62
  });
63
+ describe('sh3Api listActions submenu filter', () => {
64
+ beforeEach(() => {
65
+ __resetContributionsForTest();
66
+ __resetActionsRegistryForTest();
67
+ __resetDispatcherStateForTest();
68
+ });
69
+ it('returns only children of the named parent when { submenuOf } is set', () => {
70
+ registerAction({ id: 'theme.set', label: 'Theme', scope: 'home', submenu: true }, 'shard.x');
71
+ registerAction({
72
+ id: 'theme.set:dark', label: 'Dark', scope: 'home',
73
+ submenuOf: 'theme.set', run: () => { },
74
+ }, 'shard.x');
75
+ registerAction({
76
+ id: 'theme.set:light', label: 'Light', scope: 'home',
77
+ submenuOf: 'theme.set', run: () => { },
78
+ }, 'shard.x');
79
+ registerAction({ id: 'unrelated', label: 'U', scope: 'home', run: () => { } }, 'shard.x');
80
+ const api = makeSh3Api({ callerKind: 'verb' });
81
+ const ids = api.listActions({ submenuOf: 'theme.set' }).map((d) => d.id);
82
+ expect(ids.sort()).toEqual(['theme.set:dark', 'theme.set:light']);
83
+ });
84
+ it('returns [] when no children match the parent id', () => {
85
+ registerAction({ id: 'home.go', label: 'Go', scope: 'home', run: () => { } }, 'shard.x');
86
+ const api = makeSh3Api({ callerKind: 'verb' });
87
+ expect(api.listActions({ submenuOf: 'nope' })).toEqual([]);
88
+ });
89
+ it('combines with { activeOnly } — both predicates must hold', () => {
90
+ registerAction({ id: 'p', label: 'P', scope: 'home', submenu: true }, 'shard.x');
91
+ // active child (home is active by default in the test state)
92
+ registerAction({ id: 'p:a', label: 'A', scope: 'home',
93
+ submenuOf: 'p', run: () => { } }, 'shard.x');
94
+ // inactive child (app scope, no active app)
95
+ registerAction({ id: 'p:b', label: 'B', scope: 'app',
96
+ submenuOf: 'p', run: () => { } }, 'shard.x');
97
+ const api = makeSh3Api({ callerKind: 'verb' });
98
+ const ids = api
99
+ .listActions({ submenuOf: 'p', activeOnly: true })
100
+ .map((d) => d.id);
101
+ expect(ids).toEqual(['p:a']);
102
+ });
103
+ });
@@ -10,6 +10,8 @@ import type { ColorApi } from './color/api';
10
10
  import { type OpenContextMenuOpts, type OpenPaletteOpts } from './actions/listeners';
11
11
  import type { ActiveActionDescriptor } from './actions/types';
12
12
  import { type DispatchToTerminalResult } from './shell-shard/dispatch-to-terminal';
13
+ import type { ViewportInfo, ViewportClass } from './viewport/types';
14
+ import type { DrawerAnchor, DrawerStateMap } from './layout/compact/types';
13
15
  /**
14
16
  * The process-wide sh3 singleton exposed to shards and the sh3's own
15
17
  * internal code. Provides state zone creation and overlay managers.
@@ -39,6 +41,17 @@ export interface Sh3 {
39
41
  color: ColorApi;
40
42
  /** Actions facade — rebind keys, query bindings, open menus/palette. */
41
43
  actions: Sh3ActionsApi;
44
+ /**
45
+ * Reactive viewport classification. Subscribers fire on class change
46
+ * (desktop ↔ compact). Use `override(cls)` to pin a class for
47
+ * playgrounds and debug; pass null to restore auto-derivation.
48
+ */
49
+ readonly viewport: Sh3Viewport;
50
+ /**
51
+ * Compact-mode drawer surface controls. Inert on desktop — mutating
52
+ * methods throw rather than silently no-op so misuse is caught early.
53
+ */
54
+ readonly drawers: Sh3Drawers;
42
55
  /**
43
56
  * Dispatch `line` through a Terminal view's normal submit path. Used by
44
57
  * views outside a verb context (floating pickers, dialogs) to drive a
@@ -51,6 +64,29 @@ export interface Sh3 {
51
64
  */
52
65
  dispatchToTerminal(line: string): DispatchToTerminalResult;
53
66
  }
67
+ /**
68
+ * Compact-mode drawer surface controls. Mutating methods throw on desktop
69
+ * so misuse surfaces as a loud error instead of a silent no-op.
70
+ */
71
+ export interface Sh3Drawers {
72
+ readonly state: DrawerStateMap;
73
+ open(anchor: DrawerAnchor): void;
74
+ close(anchor: DrawerAnchor): void;
75
+ toggle(anchor: DrawerAnchor): void;
76
+ activate(anchor: DrawerAnchor, slotId: string): void;
77
+ }
78
+ /**
79
+ * Reactive viewport classification surface. See viewport/store.svelte.ts.
80
+ */
81
+ export interface Sh3Viewport {
82
+ /** Reactive — read directly inside an effect, or use `subscribe()`. */
83
+ readonly current: ViewportInfo;
84
+ subscribe(cb: (i: ViewportInfo) => void): () => void;
85
+ /** Pin the class. Pass null to restore auto. Debug/playground only. */
86
+ override(cls: ViewportClass | null): void;
87
+ /** Currently-pinned override (null = auto). */
88
+ readonly pinned: ViewportClass | null;
89
+ }
54
90
  /**
55
91
  * API for managing action bindings and triggering menus/palette
56
92
  * programmatically (e.g. from a future settings UI shard).
@@ -28,6 +28,8 @@ import { setUserBindings, getLiveDispatcherState, onActiveChange as onActiveChan
28
28
  import { listActions, onActionsChange } from './actions/registry';
29
29
  import { listActiveFromEntries } from './actions/listActive';
30
30
  import { makeDispatchToTerminal } from './shell-shard/dispatch-to-terminal';
31
+ import { viewportStore } from './viewport/store.svelte';
32
+ import { drawerStore } from './layout/compact/drawerStore.svelte';
31
33
  const sh3Actions = {
32
34
  async rebind(appId, actionId, shortcut) {
33
35
  await saveUserBinding(appId, actionId, shortcut);
@@ -77,4 +79,35 @@ export const sh3 = {
77
79
  color: colorApi,
78
80
  actions: sh3Actions,
79
81
  dispatchToTerminal: makeDispatchToTerminal({ headless: false }),
82
+ viewport: {
83
+ get current() { return viewportStore.current; },
84
+ subscribe: (cb) => viewportStore.subscribe(cb),
85
+ override: (cls) => viewportStore.override(cls),
86
+ get pinned() { return viewportStore.pinned; },
87
+ },
88
+ drawers: {
89
+ get state() { return drawerStore.state; },
90
+ open: (anchor) => {
91
+ assertCompact('open');
92
+ drawerStore.open(anchor);
93
+ },
94
+ close: (anchor) => {
95
+ assertCompact('close');
96
+ drawerStore.close(anchor);
97
+ },
98
+ toggle: (anchor) => {
99
+ assertCompact('toggle');
100
+ drawerStore.toggle(anchor);
101
+ },
102
+ activate: (anchor, slotId) => {
103
+ assertCompact('activate');
104
+ drawerStore.activate(anchor, slotId);
105
+ },
106
+ },
80
107
  };
108
+ function assertCompact(method) {
109
+ const cls = viewportStore.current.class;
110
+ if (cls !== 'compact') {
111
+ throw new Error(`Sh3.drawers.${method}: viewport class is "${cls}"; drawers exist only on compact`);
112
+ }
113
+ }
@@ -33,6 +33,7 @@ import { resetActivePresetToDefault } from '../layout/store.svelte';
33
33
  import { modalManager } from '../overlays/modal';
34
34
  import { floatManager } from '../overlays/float';
35
35
  import { registerAppActions } from './appActions';
36
+ import { openPalette } from '../actions/listeners';
36
37
  /**
37
38
  * Build the palette-only float-maximize toggle action. Targets the topmost
38
39
  * float (last entry in `floatManager.list()`); disabled when no floats are
@@ -79,8 +80,7 @@ export const sh3coreShard = {
79
80
  contextItem: false,
80
81
  paletteItem: false,
81
82
  run(_dispatchCtx) {
82
- // Lazy import to avoid circular module load at boot.
83
- import('../actions/listeners').then(({ openPalette }) => openPalette());
83
+ openPalette();
84
84
  },
85
85
  });
86
86
  ctx.actions.register(buildToggleMaximizeAction());
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
3
+ import { __resetViewRegistryForTest } from './registry';
4
+ import { __resetContributionsForTest } from '../contributions/registry';
5
+ import { __resetDecorationLayerForTest } from '../fields/decoration';
6
+ const makeShard = (overrides) => (Object.assign({ activate: () => { } }, overrides));
7
+ describe('ctx.sh3.fields integration', () => {
8
+ beforeEach(() => {
9
+ __resetShardRegistryForTest();
10
+ __resetViewRegistryForTest();
11
+ __resetContributionsForTest();
12
+ __resetDecorationLayerForTest();
13
+ document.body.innerHTML = '';
14
+ });
15
+ it('shard-scoped field is visible to a consumer via ctx.sh3.fields.list', async () => {
16
+ let value = 'dark';
17
+ registerShard(makeShard({
18
+ manifest: { id: 'producer', label: 'P', version: '0.0.0', views: [] },
19
+ activate(ctx) {
20
+ ctx.contributions.register('sh3.controllable-field', {
21
+ shape: 'imperative',
22
+ fieldId: 'theme',
23
+ label: 'Theme',
24
+ kind: 'enum',
25
+ enumValues: ['light', 'dark'],
26
+ get: () => value,
27
+ set: (v) => { value = v; },
28
+ });
29
+ },
30
+ }));
31
+ let consumerCtx = null;
32
+ registerShard(makeShard({
33
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
34
+ activate(ctx) { consumerCtx = ctx; },
35
+ }));
36
+ await activateShard('producer');
37
+ await activateShard('consumer');
38
+ const list = consumerCtx.sh3.fields.list();
39
+ expect(list).toHaveLength(1);
40
+ expect(list[0]).toMatchObject({
41
+ shardId: 'producer',
42
+ fieldId: 'theme',
43
+ kind: 'enum',
44
+ readonly: false,
45
+ source: 'contributed',
46
+ });
47
+ expect(consumerCtx.sh3.fields.get({ shardId: 'producer', fieldId: 'theme' })).toBe('dark');
48
+ await consumerCtx.sh3.fields.set({ shardId: 'producer', fieldId: 'theme' }, 'light');
49
+ expect(value).toBe('light');
50
+ });
51
+ it('contenteditable-style imperative+element pattern works end-to-end (set, decorate, unmount)', async () => {
52
+ const editorEl = document.createElement('div');
53
+ editorEl.contentEditable = 'true';
54
+ editorEl.textContent = 'initial';
55
+ document.body.appendChild(editorEl);
56
+ editorEl.getBoundingClientRect = () => ({ x: 5, y: 5, width: 100, height: 50, top: 5, left: 5, right: 105, bottom: 55 });
57
+ const set = vi.fn().mockImplementation((v) => { editorEl.textContent = String(v); });
58
+ registerShard(makeShard({
59
+ manifest: { id: 'producer', label: 'P', version: '0.0.0', views: [] },
60
+ activate(ctx) {
61
+ ctx.contributions.register('sh3.controllable-field', {
62
+ shape: 'imperative',
63
+ fieldId: 'body',
64
+ label: 'Body',
65
+ kind: 'string',
66
+ get: () => { var _a; return (_a = editorEl.textContent) !== null && _a !== void 0 ? _a : ''; },
67
+ set,
68
+ element: editorEl,
69
+ });
70
+ },
71
+ }));
72
+ let consumerCtx = null;
73
+ registerShard(makeShard({
74
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
75
+ activate(ctx) { consumerCtx = ctx; },
76
+ }));
77
+ await activateShard('producer');
78
+ await activateShard('consumer');
79
+ await consumerCtx.sh3.fields.set({ shardId: 'producer', fieldId: 'body' }, 'hello');
80
+ expect(set).toHaveBeenCalledWith('hello');
81
+ expect(editorEl.textContent).toBe('hello');
82
+ const off = consumerCtx.sh3.fields.attachDecoration({ shardId: 'producer', fieldId: 'body' }, () => {
83
+ const badge = document.createElement('span');
84
+ badge.dataset.kind = 'sh3-ai-badge';
85
+ return badge;
86
+ });
87
+ expect(document.querySelector('[data-kind="sh3-ai-badge"]')).not.toBeNull();
88
+ off();
89
+ expect(document.querySelector('[data-kind="sh3-ai-badge"]')).toBeNull();
90
+ });
91
+ it('listFields filters by shardId', async () => {
92
+ registerShard(makeShard({
93
+ manifest: { id: 'a', label: 'A', version: '0.0.0', views: [] },
94
+ activate(ctx) {
95
+ ctx.contributions.register('sh3.controllable-field', {
96
+ shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string',
97
+ get: () => '1',
98
+ });
99
+ },
100
+ }));
101
+ registerShard(makeShard({
102
+ manifest: { id: 'b', label: 'B', version: '0.0.0', views: [] },
103
+ activate(ctx) {
104
+ ctx.contributions.register('sh3.controllable-field', {
105
+ shape: 'imperative', fieldId: 'y', label: 'Y', kind: 'string',
106
+ get: () => '2',
107
+ });
108
+ },
109
+ }));
110
+ let consumerCtx = null;
111
+ registerShard(makeShard({
112
+ manifest: { id: 'c', label: 'C', version: '0.0.0', views: [] },
113
+ activate(ctx) { consumerCtx = ctx; },
114
+ }));
115
+ await activateShard('a');
116
+ await activateShard('b');
117
+ await activateShard('c');
118
+ expect(consumerCtx.sh3.fields.list({ shardId: 'a' }).map((f) => f.fieldId)).toEqual(['x']);
119
+ expect(consumerCtx.sh3.fields.list({ shardId: 'b' }).map((f) => f.fieldId)).toEqual(['y']);
120
+ });
121
+ });