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,175 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { __resetContributionsForTest, register, } from '../contributions/registry';
3
+ import { listFields, getField, setField, } from './dispatch';
4
+ import { FIELD_POINT_ID } from './types';
5
+ function registerField(entry) {
6
+ return register(FIELD_POINT_ID, entry);
7
+ }
8
+ describe('fields dispatch', () => {
9
+ beforeEach(() => {
10
+ __resetContributionsForTest();
11
+ });
12
+ describe('listFields', () => {
13
+ it('returns [] when no fields are registered', () => {
14
+ expect(listFields()).toEqual([]);
15
+ });
16
+ it('lists imperative fields with readonly=false when set is present', () => {
17
+ registerField({
18
+ owner: { shardId: 'a' },
19
+ descriptor: {
20
+ shape: 'imperative',
21
+ fieldId: 'theme',
22
+ label: 'Theme',
23
+ kind: 'string',
24
+ get: () => 'dark',
25
+ set: () => undefined,
26
+ },
27
+ });
28
+ const out = listFields();
29
+ expect(out).toHaveLength(1);
30
+ expect(out[0]).toMatchObject({
31
+ shardId: 'a',
32
+ fieldId: 'theme',
33
+ label: 'Theme',
34
+ kind: 'string',
35
+ readonly: false,
36
+ source: 'contributed',
37
+ });
38
+ expect(out[0].slotId).toBeUndefined();
39
+ expect(out[0].element).toBeUndefined();
40
+ });
41
+ it('lists imperative fields without set as readonly=true', () => {
42
+ registerField({
43
+ owner: { shardId: 'a' },
44
+ descriptor: {
45
+ shape: 'imperative',
46
+ fieldId: 'count',
47
+ label: 'Count',
48
+ kind: 'integer',
49
+ get: () => 7,
50
+ },
51
+ });
52
+ expect(listFields()[0].readonly).toBe(true);
53
+ });
54
+ it('lists readonly fields as readonly=true', () => {
55
+ registerField({
56
+ owner: { shardId: 'a' },
57
+ descriptor: {
58
+ shape: 'readonly',
59
+ fieldId: 'now',
60
+ label: 'Now',
61
+ kind: 'integer',
62
+ get: () => Date.now(),
63
+ },
64
+ });
65
+ expect(listFields()[0].readonly).toBe(true);
66
+ });
67
+ it('passes the element ref through to FieldView when present', () => {
68
+ const fakeEl = { tagName: 'DIV' };
69
+ registerField({
70
+ owner: { shardId: 'a' },
71
+ descriptor: {
72
+ shape: 'imperative',
73
+ fieldId: 'rich',
74
+ label: 'Rich',
75
+ kind: 'string',
76
+ get: () => 'x',
77
+ element: fakeEl,
78
+ },
79
+ });
80
+ expect(listFields()[0].element).toBe(fakeEl);
81
+ });
82
+ it('filters by shardId', () => {
83
+ registerField({ owner: { shardId: 'a' }, descriptor: { shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string', get: () => 1 } });
84
+ registerField({ owner: { shardId: 'b' }, descriptor: { shape: 'imperative', fieldId: 'y', label: 'Y', kind: 'string', get: () => 2 } });
85
+ expect(listFields({ shardId: 'a' }).map((f) => f.fieldId)).toEqual(['x']);
86
+ });
87
+ it('filters by slotId', () => {
88
+ registerField({ owner: { shardId: 'a', slotId: 's1' }, descriptor: { shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string', get: () => 1 } });
89
+ registerField({ owner: { shardId: 'a', slotId: 's2' }, descriptor: { shape: 'imperative', fieldId: 'y', label: 'Y', kind: 'string', get: () => 2 } });
90
+ expect(listFields({ slotId: 's1' }).map((f) => f.fieldId)).toEqual(['x']);
91
+ });
92
+ it('filters by kind', () => {
93
+ registerField({ owner: { shardId: 'a' }, descriptor: { shape: 'imperative', fieldId: 'n', label: 'N', kind: 'number', get: () => 1 } });
94
+ registerField({ owner: { shardId: 'a' }, descriptor: { shape: 'imperative', fieldId: 's', label: 'S', kind: 'string', get: () => 'a' } });
95
+ expect(listFields({ kind: 'number' }).map((f) => f.fieldId)).toEqual(['n']);
96
+ });
97
+ });
98
+ describe('getField', () => {
99
+ it('throws on unknown address', () => {
100
+ expect(() => getField({ shardId: 'a', fieldId: 'x' })).toThrow(/unknown field/);
101
+ });
102
+ it('returns the imperative descriptor get() value', () => {
103
+ registerField({
104
+ owner: { shardId: 'a' },
105
+ descriptor: {
106
+ shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string', get: () => 'hello',
107
+ },
108
+ });
109
+ expect(getField({ shardId: 'a', fieldId: 'x' })).toBe('hello');
110
+ });
111
+ it('returns readonly descriptor get() value', () => {
112
+ registerField({
113
+ owner: { shardId: 'a' },
114
+ descriptor: {
115
+ shape: 'readonly', fieldId: 'r', label: 'R', kind: 'integer', get: () => 42,
116
+ },
117
+ });
118
+ expect(getField({ shardId: 'a', fieldId: 'r' })).toBe(42);
119
+ });
120
+ it('matches on slot-scoped addresses', () => {
121
+ registerField({
122
+ owner: { shardId: 'a', slotId: 's1' },
123
+ descriptor: {
124
+ shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string', get: () => 'slot',
125
+ },
126
+ });
127
+ expect(getField({ shardId: 'a', slotId: 's1', fieldId: 'x' })).toBe('slot');
128
+ });
129
+ });
130
+ describe('setField', () => {
131
+ it('rejects on unknown address', async () => {
132
+ await expect(setField({ shardId: 'a', fieldId: 'x' }, 'v')).rejects.toThrow(/unknown field/);
133
+ });
134
+ it('rejects when imperative field has no set', async () => {
135
+ registerField({
136
+ owner: { shardId: 'a' },
137
+ descriptor: {
138
+ shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string', get: () => 'r',
139
+ },
140
+ });
141
+ await expect(setField({ shardId: 'a', fieldId: 'x' }, 'v')).rejects.toThrow(/read-only/);
142
+ });
143
+ it('rejects when shape is readonly', async () => {
144
+ registerField({
145
+ owner: { shardId: 'a' },
146
+ descriptor: {
147
+ shape: 'readonly', fieldId: 'x', label: 'X', kind: 'string', get: () => 'r',
148
+ },
149
+ });
150
+ await expect(setField({ shardId: 'a', fieldId: 'x' }, 'v')).rejects.toThrow(/read-only/);
151
+ });
152
+ it('invokes imperative set and awaits a promise', async () => {
153
+ const set = vi.fn().mockResolvedValue(undefined);
154
+ registerField({
155
+ owner: { shardId: 'a' },
156
+ descriptor: {
157
+ shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string',
158
+ get: () => 'r', set,
159
+ },
160
+ });
161
+ await setField({ shardId: 'a', fieldId: 'x' }, 'v');
162
+ expect(set).toHaveBeenCalledWith('v');
163
+ });
164
+ it('propagates errors from the set callback', async () => {
165
+ registerField({
166
+ owner: { shardId: 'a' },
167
+ descriptor: {
168
+ shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string',
169
+ get: () => 'r', set: () => { throw new Error('boom'); },
170
+ },
171
+ });
172
+ await expect(setField({ shardId: 'a', fieldId: 'x' }, 'v')).rejects.toThrow('boom');
173
+ });
174
+ });
175
+ });
@@ -0,0 +1,101 @@
1
+ export type FieldKind = 'string' | 'number' | 'integer' | 'boolean' | 'enum' | 'json';
2
+ export interface FieldBase {
3
+ /** Stable, unique within the (shardId, slotId?) tuple. */
4
+ fieldId: string;
5
+ /** Human-readable label; passed to AI tool descriptions and debug UIs. */
6
+ label: string;
7
+ kind: FieldKind;
8
+ /** Optional one-line description (passed to AI tool callers). */
9
+ description?: string;
10
+ /** For kind: 'enum' — the allowed values. */
11
+ enumValues?: readonly unknown[];
12
+ }
13
+ export interface ImperativeFieldDescriptor extends FieldBase {
14
+ shape: 'imperative';
15
+ get(): unknown;
16
+ /** Omit to make the field read-only. */
17
+ set?: (value: unknown) => void | Promise<void>;
18
+ /** Optional value-change subscription. Caller passes a value-snapshot cb. */
19
+ onChange?: (cb: (value: unknown) => void) => () => void;
20
+ /**
21
+ * Optional visual locator. Set when the field has a DOM presence even
22
+ * though I/O goes through callbacks (contenteditable rich-text editors,
23
+ * ARIA-roled custom widgets, custom elements). The framework NEVER writes
24
+ * to this element directly — exposed only to visual decorators and to
25
+ * FieldView.element for highlighting / anchoring overlays.
26
+ */
27
+ element?: HTMLElement;
28
+ }
29
+ export interface ElementRefFieldDescriptor extends FieldBase {
30
+ shape: 'element';
31
+ /** Framework reads/writes .value and dispatches 'input' + 'change'. */
32
+ element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
33
+ }
34
+ export interface ReadonlyFieldDescriptor extends FieldBase {
35
+ shape: 'readonly';
36
+ get(): unknown;
37
+ /** Optional visual locator (same semantics as ImperativeFieldDescriptor.element). */
38
+ element?: HTMLElement;
39
+ }
40
+ export type ControllableFieldDescriptor = ImperativeFieldDescriptor | ElementRefFieldDescriptor | ReadonlyFieldDescriptor;
41
+ /**
42
+ * Address tuple for ctx.sh3.fields.get/set. shardId 'sh3.walker' identifies
43
+ * walker-produced fields. slotId is present iff the field is slot-scoped or
44
+ * walker-produced.
45
+ */
46
+ export interface FieldAddress {
47
+ shardId: string;
48
+ slotId?: string;
49
+ fieldId: string;
50
+ }
51
+ /**
52
+ * Read-shape returned by ctx.sh3.fields.list() / walk(). Intentionally omits
53
+ * I/O handlers (get/set/onChange) so the gating chokepoint at
54
+ * ctx.sh3.fields.get/set works. Direct DOM access via `element` is fine —
55
+ * any shard could already do document.querySelector.
56
+ */
57
+ export interface FieldView {
58
+ shardId: string;
59
+ slotId?: string;
60
+ fieldId: string;
61
+ label: string;
62
+ kind: FieldKind;
63
+ description?: string;
64
+ enumValues?: readonly unknown[];
65
+ readonly: boolean;
66
+ source: 'contributed' | 'walker';
67
+ element?: HTMLElement;
68
+ }
69
+ /**
70
+ * Returned (or constructed) by an attachDecoration factory.
71
+ * - element: the consumer's content; mounted into the SH3 overlay layer.
72
+ * - update?(rect): called when the field's bounding rect changes.
73
+ * - dispose?(): called at decoration teardown (field unregister or explicit dispose).
74
+ */
75
+ export interface DecorationHandle {
76
+ element: HTMLElement;
77
+ update?(rect: DOMRect): void;
78
+ dispose?(): void;
79
+ }
80
+ /**
81
+ * The Sh3Api.fields surface. See packages/sh3-core/src/fields/dispatch.ts and
82
+ * packages/sh3-core/src/fields/decoration.ts for implementations.
83
+ */
84
+ export interface FieldsApi {
85
+ list(opts?: {
86
+ slotId?: string;
87
+ shardId?: string;
88
+ walker?: 'off' | 'fallback' | 'always';
89
+ kind?: FieldKind;
90
+ }): FieldView[];
91
+ get(addr: FieldAddress): unknown;
92
+ set(addr: FieldAddress, value: unknown): Promise<void>;
93
+ walk(slotId: string): FieldView[];
94
+ onChange(cb: () => void): () => void;
95
+ attachDecoration(addr: FieldAddress, factory: (target: {
96
+ element: HTMLElement;
97
+ rect: DOMRect;
98
+ }) => HTMLElement | DecorationHandle): () => void;
99
+ }
100
+ export declare const FIELD_POINT_ID = "sh3.controllable-field";
101
+ export declare const WALKER_SHARD_ID = "sh3.walker";
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Controllable-field type definitions.
3
+ *
4
+ * Fields are stored as contributions under pointId 'sh3.controllable-field'.
5
+ * Three registration shapes:
6
+ * - 'imperative': caller-supplied get/set callbacks; optional element ref
7
+ * for visual locator (contenteditable / ARIA / custom widget).
8
+ * - 'element': framework drives an HTMLInputElement / HTMLTextAreaElement /
9
+ * HTMLSelectElement directly (.value + dispatched events).
10
+ * - 'readonly': caller-supplied get only; optional element ref.
11
+ *
12
+ * `Sh3Api.fields` exposes the read+invoke surface; FieldView is the
13
+ * read-shape returned by list() / walk().
14
+ */
15
+ export const FIELD_POINT_ID = 'sh3.controllable-field';
16
+ export const WALKER_SHARD_ID = 'sh3.walker';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { __resetContributionsForTest, register, } from '../contributions/registry';
3
+ import { walkSlotContainer, listFields } from './dispatch';
4
+ import { FIELD_POINT_ID } from './types';
5
+ function makeContainer(html) {
6
+ const c = document.createElement('div');
7
+ c.innerHTML = html;
8
+ document.body.appendChild(c);
9
+ return c;
10
+ }
11
+ describe('field walker', () => {
12
+ beforeEach(() => {
13
+ __resetContributionsForTest();
14
+ document.body.innerHTML = '';
15
+ });
16
+ it('returns [] for an empty container', () => {
17
+ const c = makeContainer('');
18
+ expect(walkSlotContainer('s1', c)).toEqual([]);
19
+ });
20
+ it('matches native input, textarea, select', () => {
21
+ const c = makeContainer(`
22
+ <input name="title" />
23
+ <textarea name="body"></textarea>
24
+ <select name="color"><option value="red">Red</option><option value="blue">Blue</option></select>
25
+ `);
26
+ const out = walkSlotContainer('s1', c);
27
+ expect(out.map((f) => f.fieldId).sort()).toEqual(['body', 'color', 'title']);
28
+ });
29
+ it('skips submit/button/reset/file/hidden inputs', () => {
30
+ const c = makeContainer(`
31
+ <input type="submit" name="x" />
32
+ <input type="button" name="y" />
33
+ <input type="reset" name="z" />
34
+ <input type="file" name="f" />
35
+ <input type="hidden" name="h" />
36
+ <input type="text" name="ok" />
37
+ `);
38
+ expect(walkSlotContainer('s1', c).map((f) => f.fieldId)).toEqual(['ok']);
39
+ });
40
+ it('matches [data-sh3-field] elements', () => {
41
+ const c = makeContainer(`<div data-sh3-field="custom">x</div>`);
42
+ expect(walkSlotContainer('s1', c).map((f) => f.fieldId)).toEqual(['custom']);
43
+ });
44
+ it('infers kind from input type', () => {
45
+ const c = makeContainer(`
46
+ <input type="number" name="n" />
47
+ <input type="checkbox" name="b" />
48
+ <input type="text" name="s" />
49
+ `);
50
+ const byId = Object.fromEntries(walkSlotContainer('s1', c).map((f) => [f.fieldId, f.kind]));
51
+ expect(byId).toEqual({ n: 'number', b: 'boolean', s: 'string' });
52
+ });
53
+ it('infers enum from select options', () => {
54
+ const c = makeContainer(`
55
+ <select name="c"><option value="red">Red</option><option value="blue">Blue</option></select>
56
+ `);
57
+ const out = walkSlotContainer('s1', c);
58
+ expect(out[0].kind).toBe('enum');
59
+ expect(out[0].enumValues).toEqual(['red', 'blue']);
60
+ });
61
+ it('id strategy: data-sh3-field > name > id > label > placeholder > field-N', () => {
62
+ const c = makeContainer(`
63
+ <input data-sh3-field="data-id" />
64
+ <input name="name-id" />
65
+ <input id="dom-id" />
66
+ <label for="lbl">My Label</label><input id="lbl" />
67
+ <input placeholder="The Hint" />
68
+ <input />
69
+ `);
70
+ const ids = walkSlotContainer('s1', c).map((f) => f.fieldId);
71
+ expect(ids).toContain('data-id');
72
+ expect(ids).toContain('name-id');
73
+ expect(ids).toContain('dom-id');
74
+ expect(ids).toContain('my-label');
75
+ expect(ids).toContain('the-hint');
76
+ expect(ids.some((i) => /^field-\d+$/.test(i))).toBe(true);
77
+ });
78
+ it('walker fields use shardId "sh3.walker" and slotId of the call', () => {
79
+ const c = makeContainer(`<input name="x" />`);
80
+ const out = walkSlotContainer('slotABC', c);
81
+ expect(out[0]).toMatchObject({
82
+ shardId: 'sh3.walker',
83
+ slotId: 'slotABC',
84
+ source: 'walker',
85
+ });
86
+ });
87
+ it('exposes the matched element on FieldView.element', () => {
88
+ const c = makeContainer(`<input name="x" />`);
89
+ const input = c.querySelector('input');
90
+ expect(walkSlotContainer('s1', c)[0].element).toBe(input);
91
+ });
92
+ it('writeElement on a native input dispatches input + change', async () => {
93
+ const { writeElement } = await import('./dispatch');
94
+ const input = document.createElement('input');
95
+ input.type = 'text';
96
+ document.body.appendChild(input);
97
+ let inputCount = 0, changeCount = 0;
98
+ input.addEventListener('input', () => inputCount++);
99
+ input.addEventListener('change', () => changeCount++);
100
+ writeElement(input, 'hello');
101
+ expect(input.value).toBe('hello');
102
+ expect(inputCount).toBe(1);
103
+ expect(changeCount).toBe(1);
104
+ });
105
+ it('writeElement on a checkbox toggles .checked', async () => {
106
+ const { writeElement } = await import('./dispatch');
107
+ const input = document.createElement('input');
108
+ input.type = 'checkbox';
109
+ document.body.appendChild(input);
110
+ writeElement(input, true);
111
+ expect(input.checked).toBe(true);
112
+ writeElement(input, false);
113
+ expect(input.checked).toBe(false);
114
+ });
115
+ describe('listFields walker integration', () => {
116
+ it('walker: "off" (default) ignores walker', () => {
117
+ makeContainer(`<input name="x" />`);
118
+ expect(listFields({ slotId: 's1' })).toEqual([]);
119
+ });
120
+ it('walker: "always" appends walker fields when a container is registered', () => {
121
+ const c = makeContainer(`<input name="x" />`);
122
+ expect(walkSlotContainer('s1', c)[0].fieldId).toBe('x');
123
+ });
124
+ it('walker dedup: skips elements already covered by an element-ref contribution', () => {
125
+ const c = makeContainer(`<input name="x" /><input name="y" />`);
126
+ const xInput = c.querySelector('input[name="x"]');
127
+ register(FIELD_POINT_ID, {
128
+ owner: { shardId: 'tester', slotId: 's1' },
129
+ descriptor: {
130
+ shape: 'element', fieldId: 'titled-x', label: 'Titled X', kind: 'string', element: xInput,
131
+ },
132
+ });
133
+ const out = walkSlotContainer('s1', c);
134
+ const ids = out.map((f) => f.fieldId);
135
+ expect(ids).toEqual(['y']);
136
+ });
137
+ });
138
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ /*
2
+ * Handheld flip e2e — verifies slot survival across viewport flips
3
+ * using the real browser's re-parent semantics + slot host pool.
4
+ *
5
+ * The proof rides on slot id stability: if the same slot id is requested
6
+ * by both the docked LayoutRenderer (desktop) and the compact-mode
7
+ * DrawerSurface, the host pool keeps the host alive across the swap and
8
+ * the same DOM element is reused. We stamp a unique data attribute on
9
+ * the mounted view DOM, flip viewport classes, and assert the same
10
+ * element is still findable after each flip.
11
+ */
12
+ import { describe, it, expect, beforeEach } from 'vitest';
13
+ import { tick } from 'svelte';
14
+ import { resetFramework } from './__test__/reset';
15
+ import { renderWithShell } from './__test__/render';
16
+ import { registerApp } from './apps/registry.svelte';
17
+ import { launchApp } from './apps/lifecycle';
18
+ import { registerView } from './shards/registry';
19
+ import { makeApp, makeAppManifest } from './__test__/fixtures';
20
+ import { sh3 } from './sh3Runtime.svelte';
21
+ import Sh3 from './Sh3.svelte';
22
+ function settle(ms = 50) {
23
+ return new Promise((r) => setTimeout(r, ms));
24
+ }
25
+ function cleanupDOM() {
26
+ document.querySelectorAll('.sh3-sh3-host').forEach((h) => h.remove());
27
+ }
28
+ describe('handheld viewport flip e2e', () => {
29
+ beforeEach(() => {
30
+ cleanupDOM();
31
+ resetFramework();
32
+ sh3.viewport.override(null);
33
+ });
34
+ it('viewport override flips chrome and body branches', async () => {
35
+ // Stable per-mount stamp so we can recognise the same DOM element.
36
+ let stampedEl = null;
37
+ registerView('test:body', {
38
+ mount: (el) => {
39
+ el.dataset.testStamp = 'body-' + Math.random().toString(36).slice(2);
40
+ stampedEl = el;
41
+ return { unmount: () => { } };
42
+ },
43
+ });
44
+ registerView('test:sb', {
45
+ mount: (el) => {
46
+ el.dataset.testStamp = 'sb-' + Math.random().toString(36).slice(2);
47
+ return { unmount: () => { } };
48
+ },
49
+ });
50
+ registerApp(makeApp({
51
+ manifest: makeAppManifest({ id: 'flip-app', label: 'Flip' }),
52
+ initialLayout: {
53
+ type: 'split', direction: 'horizontal', sizes: [0.3, 0.7],
54
+ children: [
55
+ { type: 'slot', slotId: 'sb', viewId: 'test:sb', role: 'sidebar' },
56
+ { type: 'slot', slotId: 'body', viewId: 'test:body', role: 'body' },
57
+ ],
58
+ },
59
+ }));
60
+ // Pin desktop before mounting so the gate's first render is desktop.
61
+ sh3.viewport.override('desktop');
62
+ renderWithShell(Sh3, {});
63
+ await launchApp('flip-app');
64
+ await tick();
65
+ await settle();
66
+ expect(document.querySelector('[data-sh3-region="tabbar"]')).not.toBeNull();
67
+ expect(document.querySelector('[data-sh3-region="compact-chrome"]')).toBeNull();
68
+ expect(stampedEl).not.toBeNull();
69
+ const initialStamp = stampedEl.dataset.testStamp;
70
+ // Flip to compact.
71
+ sh3.viewport.override('compact');
72
+ await tick();
73
+ await settle();
74
+ expect(document.querySelector('[data-sh3-region="compact-chrome"]')).not.toBeNull();
75
+ expect(document.querySelector('[data-sh3-region="tabbar"]')).toBeNull();
76
+ expect(document.querySelector('[data-sh3-region="compact-body"]')).not.toBeNull();
77
+ // Body slot host survived the swap — query the test stamp.
78
+ const survivors = document.querySelectorAll('[data-test-stamp]');
79
+ const compactStamps = Array.from(survivors).map((el) => el.dataset.testStamp);
80
+ expect(compactStamps).toContain(initialStamp);
81
+ // Flip back to desktop.
82
+ sh3.viewport.override('desktop');
83
+ await tick();
84
+ await settle();
85
+ expect(document.querySelector('[data-sh3-region="tabbar"]')).not.toBeNull();
86
+ const finalStamps = Array.from(document.querySelectorAll('[data-test-stamp]')).map((el) => el.dataset.testStamp);
87
+ // Slot was not remounted across two flips — original stamp survives.
88
+ expect(finalStamps).toContain(initialStamp);
89
+ });
90
+ });
package/dist/host.js CHANGED
@@ -39,6 +39,7 @@ import { returnToHome } from './apps/lifecycle';
39
39
  export { __setBackend };
40
40
  export { setLocalOwner };
41
41
  export { __setActiveScope, __setTenantId, __setDocumentBackend } from './documents/config';
42
+ import { getActiveScopeId } from './documents/config';
42
43
  export function registerShard(shard) {
43
44
  registerShardInternal(shard);
44
45
  }
@@ -66,7 +67,6 @@ export async function bootstrap(config) {
66
67
  // Per ADR-002 amendment, app workspace state is keyed by (scopeId, appId).
67
68
  // Rewrite legacy unkeyed entries to the personal scope namespace.
68
69
  const { migrateLegacyWorkspaceKeys } = await import('./apps/workspace-rekey');
69
- const { getActiveScopeId } = await import('./documents/config');
70
70
  migrateLegacyWorkspaceKeys(getActiveScopeId());
71
71
  }
72
72
  const exShards = new Set(config === null || config === void 0 ? void 0 : config.excludeShards);
@@ -143,8 +143,33 @@ export async function bootstrapSatellite(config) {
143
143
  }
144
144
  // 3. Load any packages installed in a previous session from IndexedDB
145
145
  await loadInstalledPackages();
146
- // 4. Activate exactly the requested shards.
146
+ // 4. Autostart sweep mirror host bootstrap. Every shard with an
147
+ // `autostart` hook runs in satellites too: services (LLM providers),
148
+ // ambient action hosts (`__sh3core__` provides the command palette
149
+ // via `sh3.palette.open`), and any user-installed self-starter.
150
+ // `addAutostartShard` keeps action scoping consistent with the host
151
+ // so palette/global actions remain ambient inside `'app'` scope.
152
+ const autostartActivated = new Set();
153
+ for (const [id, shard] of registeredShards) {
154
+ if (shard.autostart) {
155
+ addAutostartShard(id);
156
+ try {
157
+ await activateShard(id, { phase: 'autostart' });
158
+ autostartActivated.add(id);
159
+ }
160
+ catch (_a) {
161
+ // Already logged + recorded in erroredShards by activateShard.
162
+ // One bad self-starting shard must not prevent the satellite from booting.
163
+ }
164
+ }
165
+ }
166
+ // 5. Activate explicit satellite shards (those carried by activateShards
167
+ // in the payload — typically view-providing shards walked from the
168
+ // layout). Skip ids already activated by the autostart sweep so the
169
+ // diagnostic phase stays correct and we do no redundant work.
147
170
  for (const id of config.activateShardIds) {
171
+ if (autostartActivated.has(id))
172
+ continue;
148
173
  if (registeredShards.has(id)) {
149
174
  try {
150
175
  await activateShard(id, { phase: 'satellite' });
@@ -0,0 +1 @@
1
+ export {};