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
package/dist/Sh3.svelte CHANGED
@@ -15,11 +15,7 @@
15
15
  import './tokens.css';
16
16
  import './primitives/base.css';
17
17
  import LayoutRenderer from './layout/LayoutRenderer.svelte';
18
- import DragPreview from './layout/DragPreview.svelte';
19
- import FloatLayer from './overlays/FloatLayer.svelte';
20
- import type { OverlayLayer } from './overlays/types';
21
- import { registerLayerRoot, unregisterLayerRoot } from './overlays/roots';
22
- import { bindFloatStore, unbindFloatStore } from './overlays/float';
18
+ import OverlayRoots from './overlays/OverlayRoots.svelte';
23
19
  import { returnToHome, isAdmin } from './api';
24
20
  import { getActiveRoot, layoutStore } from './layout/store.svelte';
25
21
  import { syncMountedViewIdsFromLayout } from './actions/state.svelte';
@@ -30,6 +26,9 @@
30
26
  import { startServerSideStream } from './keys/revocation-bus.svelte';
31
27
  import BrandSlot from './BrandSlot.svelte';
32
28
  import MenuBar from './actions/MenuBar.svelte';
29
+ import CompactChrome from './chrome/CompactChrome.svelte';
30
+ import CompactRenderer from './layout/compact/CompactRenderer.svelte';
31
+ import { sh3 } from './sh3Runtime.svelte';
33
32
 
34
33
  const authenticated = $derived(isAuthenticated());
35
34
  const user = $derived(getUser());
@@ -38,41 +37,8 @@
38
37
  // getActiveApp() here: returnToHome() keeps the app warm (activeApp.id
39
38
  // stays set) and only flips the layout store's activeRoot back to 'home'.
40
39
  const onHome = $derived(getActiveRoot() === 'home');
41
-
42
- // Layer metadata — order matches the stack in docs/design/layout.md.
43
- // Index 0 here is layer 1 (floating panels); layer 0 is the content area.
44
- const overlayLayers: { layer: number; name: OverlayLayer }[] = [
45
- { layer: 1, name: 'floating' },
46
- { layer: 2, name: 'drag-preview' },
47
- { layer: 3, name: 'popup' },
48
- { layer: 4, name: 'modal' },
49
- { layer: 5, name: 'toast' },
50
- { layer: 6, name: 'command' },
51
- ];
52
-
53
- // Populated by bind:this during render; registered with the overlay
54
- // module via $effect after mount so layer managers (sh3.modal,
55
- // sh3.popup, sh3.toast) can find their target DOM roots.
56
- const overlayRoots: Partial<Record<OverlayLayer, HTMLDivElement>> = $state({});
57
-
58
- $effect(() => {
59
- for (const { name } of overlayLayers) {
60
- const el = overlayRoots[name];
61
- if (el) registerLayerRoot(name, el);
62
- }
63
- return () => {
64
- for (const { name } of overlayLayers) unregisterLayerRoot(name);
65
- };
66
- });
67
-
68
- $effect(() => {
69
- const tree = layoutStore.tree;
70
- bindFloatStore(tree.floats, () => ({
71
- w: window.innerWidth,
72
- h: window.innerHeight,
73
- }));
74
- return () => unbindFloatStore();
75
- });
40
+ // Reactive viewport class — drives the compact-vs-desktop chrome/body fork.
41
+ const viewportClass = $derived(sh3.viewport.current.class);
76
42
 
77
43
  // Keep the actions dispatcher's `mountedViewIds` set in sync with the
78
44
  // live layout tree, so `view:<viewId>` scope checks (context menu,
@@ -99,73 +65,60 @@
99
65
  });
100
66
  </script>
101
67
 
102
- <div class="sh3">
103
- <header class="sh3-tabbar" data-sh3-region="tabbar">
104
- <button
105
- type="button"
106
- class="sh3-tabbar-home-button"
107
- onclick={() => returnToHome()}
108
- disabled={onHome}
109
- title="Home"
110
- >
111
- <svg class="sh3-tabbar-home-icon" aria-hidden="true">
112
- <use href="{iconsUrl}#house" />
113
- </svg>
114
- </button>
115
- <BrandSlot />
116
- <MenuBar />
117
- {#if authenticated && user}
118
- <div class="sh3-tabbar-user">
119
- <span class="sh3-tabbar-user-name">{user.displayName}</span>
120
- <span class="sh3-tabbar-tag">{elevated ? 'admin' : 'user'}</span>
121
- {#if !isLocalOwner()}
122
- <button
123
- type="button"
124
- class="sh3-tabbar-signout"
125
- onclick={() => logout()}
126
- title="Sign out"
127
- >
128
- <svg class="sh3-tabbar-signout-icon" aria-hidden="true">
129
- <use href="{iconsUrl}#log-out" />
130
- </svg>
131
- </button>
132
- {/if}
133
- </div>
134
- {/if}
135
- </header>
68
+ <div class="sh3" data-sh3-viewport={viewportClass}>
69
+ {#if viewportClass === 'desktop'}
70
+ <header class="sh3-tabbar" data-sh3-region="tabbar">
71
+ <button
72
+ type="button"
73
+ class="sh3-tabbar-home-button"
74
+ onclick={() => returnToHome()}
75
+ disabled={onHome}
76
+ title="Home"
77
+ >
78
+ <svg class="sh3-tabbar-home-icon" aria-hidden="true">
79
+ <use href="{iconsUrl}#house" />
80
+ </svg>
81
+ </button>
82
+ <BrandSlot />
83
+ <MenuBar />
84
+ {#if authenticated && user}
85
+ <div class="sh3-tabbar-user">
86
+ <span class="sh3-tabbar-user-name">{user.displayName}</span>
87
+ <span class="sh3-tabbar-tag">{elevated ? 'admin' : 'user'}</span>
88
+ {#if !isLocalOwner()}
89
+ <button
90
+ type="button"
91
+ class="sh3-tabbar-signout"
92
+ onclick={() => logout()}
93
+ title="Sign out"
94
+ >
95
+ <svg class="sh3-tabbar-signout-icon" aria-hidden="true">
96
+ <use href="{iconsUrl}#log-out" />
97
+ </svg>
98
+ </button>
99
+ {/if}
100
+ </div>
101
+ {/if}
102
+ </header>
103
+ {:else}
104
+ <CompactChrome />
105
+ {/if}
136
106
 
137
107
  <GuestBanner />
138
108
 
139
109
  <main class="sh3-content" data-sh3-region="content" data-sh3-layer="0">
140
- <LayoutRenderer />
110
+ {#if viewportClass === 'desktop'}
111
+ <LayoutRenderer />
112
+ {:else}
113
+ <CompactRenderer />
114
+ {/if}
141
115
  </main>
142
116
 
143
117
  <footer class="sh3-statusbar" data-sh3-region="statusbar">
144
118
  <!-- alpha tag moved to Sh3Home title row -->
145
119
  </footer>
146
120
 
147
- <!--
148
- Overlay roots. Each is absolutely positioned over the entire sh3 with
149
- pointer-events: none by default; layer managers enable pointer events on
150
- the specific surfaces they portal in.
151
- -->
152
- <div class="sh3-overlays" aria-hidden="true">
153
- {#each overlayLayers as { layer, name } (layer)}
154
- <div
155
- class="sh3-overlay-root"
156
- data-sh3-overlay={name}
157
- data-sh3-layer={layer}
158
- style="z-index: var(--sh3-z-layer-{layer});"
159
- bind:this={overlayRoots[name]}
160
- >
161
- {#if name === 'floating'}
162
- <FloatLayer />
163
- {:else if name === 'drag-preview'}
164
- <DragPreview />
165
- {/if}
166
- </div>
167
- {/each}
168
- </div>
121
+ <OverlayRoots />
169
122
 
170
123
  <!--
171
124
  Sh3-owned consent dialog for ctx.keys.mint().
@@ -217,17 +170,6 @@
217
170
  user-select: none;
218
171
  }
219
172
 
220
- .sh3-overlays {
221
- position: absolute;
222
- inset: 0;
223
- pointer-events: none;
224
- }
225
- .sh3-overlay-root {
226
- position: absolute;
227
- inset: 0;
228
- pointer-events: none;
229
- }
230
-
231
173
  .sh3-tabbar-home-button {
232
174
  display: flex;
233
175
  align-items: center;
@@ -35,7 +35,7 @@ describe('ShardContext.listActions / runAction (integration)', () => {
35
35
  });
36
36
  await activateShard('producer');
37
37
  await activateShard('consumer');
38
- const list = consumerCtx.listActions();
38
+ const list = consumerCtx.sh3.listActions();
39
39
  const ids = list.map((d) => d.id);
40
40
  expect(ids).toContain('producer.do');
41
41
  const desc = list.find((d) => d.id === 'producer.do');
@@ -62,7 +62,7 @@ describe('ShardContext.listActions / runAction (integration)', () => {
62
62
  });
63
63
  await activateShard('producer');
64
64
  await activateShard('consumer');
65
- const snapshot = consumerCtx.listActions({ activeOnly: true });
65
+ const snapshot = consumerCtx.sh3.listActions({ activeOnly: true });
66
66
  const ids = snapshot.map((d) => d.id);
67
67
  expect(ids).toContain('home.go');
68
68
  expect(ids).not.toContain('app.go');
@@ -87,7 +87,7 @@ describe('ShardContext.listActions / runAction (integration)', () => {
87
87
  });
88
88
  await activateShard('producer');
89
89
  await activateShard('consumer');
90
- await consumerCtx.runAction('producer.go');
90
+ await consumerCtx.sh3.runAction('producer.go');
91
91
  expect(invokedVia).toBe('programmatic');
92
92
  });
93
93
  it('runAction rejects when the target action is inactive', async () => {
@@ -106,6 +106,6 @@ describe('ShardContext.listActions / runAction (integration)', () => {
106
106
  });
107
107
  await activateShard('producer');
108
108
  await activateShard('consumer');
109
- await expect(consumerCtx.runAction('gated.go')).rejects.toThrow(/not active/);
109
+ await expect(consumerCtx.sh3.runAction('gated.go')).rejects.toThrow(/not active/);
110
110
  });
111
111
  });
@@ -75,4 +75,33 @@ describe('listActionsFromEntries', () => {
75
75
  expect(out).toHaveLength(1);
76
76
  expect(out[0].ownerShardId).toBe('shard.a');
77
77
  });
78
+ it('passes through submenu=true on a parent descriptor', () => {
79
+ const entries = [{
80
+ ownerShardId: 'shard.x',
81
+ action: { id: 'p', label: 'P', scope: 'home', submenu: true },
82
+ }];
83
+ const out = listActionsFromEntries(entries, mkState());
84
+ expect(out[0].submenu).toBe(true);
85
+ expect(out[0].submenuOf).toBeUndefined();
86
+ });
87
+ it('passes through submenuOf on a child descriptor', () => {
88
+ const entries = [
89
+ {
90
+ ownerShardId: 'shard.x',
91
+ action: {
92
+ id: 'p:dark', label: 'Dark', scope: 'home',
93
+ submenuOf: 'p', run: () => { },
94
+ },
95
+ },
96
+ ];
97
+ const out = listActionsFromEntries(entries, mkState());
98
+ expect(out[0].submenuOf).toBe('p');
99
+ expect(out[0].submenu).toBeUndefined();
100
+ });
101
+ it('omits submenu and submenuOf on plain actions', () => {
102
+ const entries = [mkEntry({ id: 'plain', scope: 'home' })];
103
+ const out = listActionsFromEntries(entries, mkState());
104
+ expect(out[0].submenu).toBeUndefined();
105
+ expect(out[0].submenuOf).toBeUndefined();
106
+ });
78
107
  });
@@ -50,6 +50,8 @@ export function listActionsFromEntries(entries, state) {
50
50
  ownerShardId: entry.ownerShardId,
51
51
  paletteItem: entry.action.paletteItem !== false,
52
52
  contextItem: entry.action.contextItem !== false,
53
+ submenu: entry.action.submenu,
54
+ submenuOf: entry.action.submenuOf,
53
55
  active,
54
56
  };
55
57
  if (active) {
@@ -94,6 +94,10 @@ export function dispatchActionProgrammatic(actionId, _opts) {
94
94
  const state = getLiveDispatcherState();
95
95
  const desc = listActionsFromEntries(entries, state).find((d) => d.id === actionId);
96
96
  if (!desc || !desc.active) {
97
+ if (entry.action.submenu === true &&
98
+ typeof entry.action.run !== 'function') {
99
+ return Promise.reject(new Error(`action "${actionId}" is a submenu parent — list children with listActions({ submenuOf: "${actionId}" })`));
100
+ }
97
101
  return Promise.reject(new Error(`action "${actionId}" is not active in current scope`));
98
102
  }
99
103
  // run is guaranteed non-null by `desc.active === true`.
@@ -34,11 +34,18 @@ describe('dispatchActionProgrammatic', () => {
34
34
  await expect(dispatchActionProgrammatic('d')).rejects.toThrow(/action "d" is not active/);
35
35
  expect(run).not.toHaveBeenCalled();
36
36
  });
37
- it('rejects on a submenu parent without run', async () => {
37
+ it('rejects on a submenu parent with the parent-specific message', async () => {
38
38
  registerAction({
39
39
  id: 's', label: 'S', scope: 'home', submenu: true,
40
40
  }, 'shard.x');
41
- await expect(dispatchActionProgrammatic('s')).rejects.toThrow(/action "s" is not active/);
41
+ await expect(dispatchActionProgrammatic('s')).rejects.toThrow(/action "s" is a submenu parent — list children with listActions\(\{ submenuOf: "s" \}\)/);
42
+ });
43
+ it('still uses the generic inactive message for non-submenu inactive actions', async () => {
44
+ registerAction({
45
+ id: 'gated2', label: 'G', scope: 'app', run: () => { },
46
+ }, 'shard.x');
47
+ // No active app -> 'app' scope is inactive.
48
+ await expect(dispatchActionProgrammatic('gated2')).rejects.toThrow(/action "gated2" is not active/);
42
49
  });
43
50
  it('invokes run with invokedVia="programmatic" on happy path', async () => {
44
51
  let captured = null;
@@ -148,6 +148,10 @@ export interface ActiveActionDescriptor {
148
148
  ownerShardId: string;
149
149
  paletteItem: boolean;
150
150
  contextItem: boolean;
151
+ /** True when this action is a submenu parent (children opened by drill). */
152
+ submenu?: true;
153
+ /** Parent action id when this action is a submenu child. */
154
+ submenuOf?: string;
151
155
  }
152
156
  /**
153
157
  * Read-only snapshot describing one action in the registry. Produced by
@@ -183,6 +187,10 @@ export interface ActionDescriptor {
183
187
  ownerShardId: string;
184
188
  paletteItem: boolean;
185
189
  contextItem: boolean;
190
+ /** True when this action is a submenu parent (children opened by drill). */
191
+ submenu?: true;
192
+ /** Parent action id when this action is a submenu child. */
193
+ submenuOf?: string;
186
194
  /** True when `runAction(id)` would dispatch right now. */
187
195
  active: boolean;
188
196
  }
package/dist/api.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { sh3 } from './sh3Runtime.svelte';
2
- export type { Sh3 } from './sh3Runtime.svelte';
2
+ export type { Sh3, Sh3Drawers, Sh3Viewport } from './sh3Runtime.svelte';
3
+ export type { ViewportClass, ViewportInfo } from './viewport/types';
4
+ export type { SlotRole } from './layout/types';
5
+ export type { DrawerAnchor, DrawerSpec, DrawerState, DrawerStateMap, CompactRendering, } from './layout/compact/types';
3
6
  export type { Shard, ShardManifest, SourceShard, SourceShardManifest, ShardContext, ViewDeclaration, ViewFactory, ViewHandle, MountContext, } from './shards/types';
4
7
  export type { LayoutNode, SplitNode, TabsNode, SlotNode, TabEntry, SplitDirection, SizeMode, LayoutTree, FloatEntry, LayoutPreset, CanonicalPreset, TreeRootRef as SlotLocation, } from './layout/types';
5
8
  export type { ActionDescriptor, ActiveActionDescriptor, BindingSource, AtomicScope, ActionScope, } from './actions/types';
@@ -80,3 +83,5 @@ export { default as Select } from './primitives/widgets/Select.svelte';
80
83
  export type { SelectOption } from './primitives/widgets/Select';
81
84
  export { default as AppPicker } from './primitives/widgets/AppPicker.svelte';
82
85
  export { default as UserPicker } from './primitives/widgets/UserPicker.svelte';
86
+ export type { FieldKind, FieldAddress, FieldView, ControllableFieldDescriptor, ImperativeFieldDescriptor, ElementRefFieldDescriptor, ReadonlyFieldDescriptor, FieldsApi, DecorationHandle, } from './fields/types';
87
+ export { fieldAddressToString, fieldAddressFromString } from './fields/address';
package/dist/api.js CHANGED
@@ -97,3 +97,4 @@ export { default as FilePicker } from './primitives/widgets/FilePicker.svelte';
97
97
  export { default as Select } from './primitives/widgets/Select.svelte';
98
98
  export { default as AppPicker } from './primitives/widgets/AppPicker.svelte';
99
99
  export { default as UserPicker } from './primitives/widgets/UserPicker.svelte';
100
+ export { fieldAddressToString, fieldAddressFromString } from './fields/address';
@@ -0,0 +1,96 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Top app bar for compact mode. Three-column grid:
4
+ * leading — one button per non-null drawer anchor (read from
5
+ * the active CompactRendering)
6
+ * title — active app name
7
+ * trailing — palette button + overflow (menu sheet) button
8
+ *
9
+ * MenuSheet handles the overflow menu; this component owns the open
10
+ * state and renders MenuSheet conditionally.
11
+ */
12
+ import { sh3 } from '../sh3Runtime.svelte';
13
+ import { layoutStore } from '../layout/store.svelte';
14
+ import { derive } from '../layout/compact/derive';
15
+ import { getLiveDispatcherState } from '../actions/state.svelte';
16
+ import { getRegisteredApp } from '../apps/registry.svelte';
17
+ import MenuSheet from './MenuSheet.svelte';
18
+ import type { DrawerAnchor } from '../layout/compact/types';
19
+
20
+ const rendering = $derived(derive(layoutStore.root));
21
+ const dispatcher = $derived(getLiveDispatcherState());
22
+ const title = $derived.by(() => {
23
+ const id = dispatcher.activeAppId;
24
+ if (!id) return 'SH3';
25
+ return getRegisteredApp(id)?.manifest.label ?? id;
26
+ });
27
+
28
+ let menuOpen = $state(false);
29
+
30
+ function toggleDrawer(anchor: DrawerAnchor) {
31
+ sh3.drawers.toggle(anchor);
32
+ }
33
+ function openPalette() {
34
+ sh3.actions.openPalette();
35
+ }
36
+ </script>
37
+
38
+ <header class="sh3-compact-chrome" data-sh3-region="compact-chrome">
39
+ <div class="leading">
40
+ {#if rendering.drawers.left}
41
+ <button onclick={() => toggleDrawer('left')} aria-label="Toggle left drawer" data-sh3-anchor="left">≡</button>
42
+ {/if}
43
+ {#if rendering.drawers.right}
44
+ <button onclick={() => toggleDrawer('right')} aria-label="Toggle right drawer" data-sh3-anchor="right">▣</button>
45
+ {/if}
46
+ {#if rendering.drawers.top}
47
+ <button onclick={() => toggleDrawer('top')} aria-label="Toggle top drawer" data-sh3-anchor="top">⫶</button>
48
+ {/if}
49
+ </div>
50
+ <div class="title">{title}</div>
51
+ <div class="trailing">
52
+ <button onclick={openPalette} aria-label="Open command palette">⌘</button>
53
+ <button onclick={() => (menuOpen = true)} aria-label="Open menu">⋯</button>
54
+ </div>
55
+ </header>
56
+
57
+ <MenuSheet open={menuOpen} onClose={() => (menuOpen = false)} />
58
+
59
+ <style>
60
+ .sh3-compact-chrome {
61
+ display: grid;
62
+ grid-template-columns: auto 1fr auto;
63
+ align-items: center;
64
+ height: var(--sh3-tabbar-height);
65
+ padding: 0 var(--sh3-pad-sm);
66
+ gap: var(--sh3-pad-sm);
67
+ background: var(--sh3-grad-bg-elevated, var(--sh3-bg-elevated));
68
+ border-bottom: 1px solid var(--sh3-border);
69
+ color: var(--sh3-fg);
70
+ }
71
+ .leading,
72
+ .trailing {
73
+ display: inline-flex;
74
+ gap: var(--sh3-pad-xs);
75
+ }
76
+ button {
77
+ width: 40px;
78
+ height: 40px;
79
+ font-size: var(--sh3-font-lg);
80
+ border: none;
81
+ background: none;
82
+ cursor: pointer;
83
+ border-radius: var(--sh3-radius-sm);
84
+ color: var(--sh3-fg);
85
+ }
86
+ button:active {
87
+ background: var(--sh3-bg-sunken);
88
+ }
89
+ .title {
90
+ font-weight: 600;
91
+ text-align: center;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ white-space: nowrap;
95
+ }
96
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const CompactChrome: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type CompactChrome = ReturnType<typeof CompactChrome>;
3
+ export default CompactChrome;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ /*
2
+ * DOM smoke for CompactChrome — verifies the toolbar renders the
3
+ * expected leading drawer toggles based on the active layout's
4
+ * derived rendering, plus the trailing palette + overflow buttons.
5
+ */
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { mount, unmount, flushSync } from 'svelte';
8
+ import CompactChrome from './CompactChrome.svelte';
9
+ import { __resetLayoutStoreForTest, attachApp, detachApp, switchToApp, } from '../layout/store.svelte';
10
+ import { drawerStore } from '../layout/compact/drawerStore.svelte';
11
+ const CompactChromeAny = CompactChrome;
12
+ function fakeApp() {
13
+ return {
14
+ manifest: { id: 'cc-app', label: 'CC App', layoutVersion: 5 },
15
+ initialLayout: {
16
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.6, 0.2],
17
+ children: [
18
+ { type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
19
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
20
+ { type: 'slot', slotId: 'ins', viewId: 'v:ins', role: 'inspector' },
21
+ ],
22
+ },
23
+ };
24
+ }
25
+ let mounted = null;
26
+ let host = null;
27
+ afterEach(() => {
28
+ if (mounted) {
29
+ unmount(mounted);
30
+ mounted = null;
31
+ }
32
+ if (host) {
33
+ host.remove();
34
+ host = null;
35
+ }
36
+ detachApp();
37
+ });
38
+ beforeEach(() => {
39
+ __resetLayoutStoreForTest();
40
+ drawerStore.__reset();
41
+ });
42
+ describe('CompactChrome (dom)', () => {
43
+ it('renders a leading toggle for each present drawer anchor', () => {
44
+ attachApp(fakeApp());
45
+ switchToApp();
46
+ flushSync();
47
+ host = document.createElement('div');
48
+ document.body.appendChild(host);
49
+ mounted = mount(CompactChromeAny, { target: host });
50
+ flushSync();
51
+ const leading = host.querySelectorAll('.leading button');
52
+ expect(leading.length).toBe(2);
53
+ expect(host.querySelector('.leading button[data-sh3-anchor="left"]')).not.toBeNull();
54
+ expect(host.querySelector('.leading button[data-sh3-anchor="right"]')).not.toBeNull();
55
+ });
56
+ it('renders palette + overflow buttons in the trailing section', () => {
57
+ attachApp(fakeApp());
58
+ switchToApp();
59
+ flushSync();
60
+ host = document.createElement('div');
61
+ document.body.appendChild(host);
62
+ mounted = mount(CompactChromeAny, { target: host });
63
+ flushSync();
64
+ const trailing = host.querySelectorAll('.trailing button');
65
+ expect(trailing.length).toBe(2);
66
+ });
67
+ });