sh3-core 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/Sh3.svelte +107 -39
  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/CommandPalette.svelte +1 -2
  4. package/dist/actions/listActionsFromEntries.test.js +29 -0
  5. package/dist/actions/listActive.js +2 -0
  6. package/dist/actions/listeners.js +16 -1
  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 +8 -1
  10. package/dist/app/store/storeShard.svelte.js +1 -21
  11. package/dist/app/store/version.d.ts +11 -0
  12. package/dist/app/store/version.js +39 -0
  13. package/dist/app/store/version.test.d.ts +1 -0
  14. package/dist/app/store/version.test.js +44 -0
  15. package/dist/apps/lifecycle.d.ts +6 -0
  16. package/dist/apps/lifecycle.js +5 -2
  17. package/dist/apps/lifecycle.test.js +30 -0
  18. package/dist/apps/types.d.ts +12 -0
  19. package/dist/assets/iconIds.generated.d.ts +1 -1
  20. package/dist/assets/iconIds.generated.js +5 -0
  21. package/dist/assets/icons.svg +31 -0
  22. package/dist/auth/auth.svelte.js +18 -8
  23. package/dist/auth/types.d.ts +6 -0
  24. package/dist/chrome/CompactChrome.svelte +130 -0
  25. package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
  26. package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
  27. package/dist/chrome/CompactChrome.svelte.test.js +174 -0
  28. package/dist/chrome/MenuSheet.svelte +224 -0
  29. package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
  30. package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
  31. package/dist/chrome/MenuSheet.svelte.test.js +46 -0
  32. package/dist/createShell.d.ts +9 -0
  33. package/dist/createShell.js +20 -7
  34. package/dist/createShell.remoteAuth.test.d.ts +1 -0
  35. package/dist/createShell.remoteAuth.test.js +71 -0
  36. package/dist/documents/http-backend.js +12 -11
  37. package/dist/env/client.js +11 -5
  38. package/dist/files/types.d.ts +106 -0
  39. package/dist/files/types.js +1 -0
  40. package/dist/gestures/gestureRegistry.d.ts +6 -0
  41. package/dist/gestures/gestureRegistry.js +190 -0
  42. package/dist/gestures/gestureRegistry.test.d.ts +1 -0
  43. package/dist/gestures/gestureRegistry.test.js +119 -0
  44. package/dist/gestures/index.d.ts +6 -0
  45. package/dist/gestures/index.js +12 -0
  46. package/dist/gestures/pointerClaim.d.ts +7 -0
  47. package/dist/gestures/pointerClaim.js +36 -0
  48. package/dist/gestures/pointerClaim.test.d.ts +1 -0
  49. package/dist/gestures/pointerClaim.test.js +64 -0
  50. package/dist/gestures/types.d.ts +83 -0
  51. package/dist/gestures/types.js +1 -0
  52. package/dist/handheld.browser.test.d.ts +1 -0
  53. package/dist/handheld.browser.test.js +90 -0
  54. package/dist/host-entry.d.ts +1 -0
  55. package/dist/host-entry.js +1 -0
  56. package/dist/layout/LayoutRenderer.browser.test.js +15 -3
  57. package/dist/layout/LayoutRenderer.svelte +27 -3
  58. package/dist/layout/LayoutRenderer.svelte.d.ts +4 -1
  59. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-3-splitter-drag-updates-split-sizes-when-the-splitter-handle-is-dragged-1.png +0 -0
  60. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-5-splitter-collapse-toggle-toggles-collapsed-i--on-double-click-1.png +0 -0
  61. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-hides-the-collapse-widget-on-a-fixed-pane-but-keeps-it-on-panes-with-a-non-fixed-neighbor-1.png +0 -0
  62. package/dist/layout/compact/CarouselTabs.svelte +361 -0
  63. package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
  64. package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
  65. package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
  66. package/dist/layout/compact/CompactRenderer.svelte +53 -0
  67. package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
  68. package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
  69. package/dist/layout/compact/CompactRenderer.svelte.test.js +125 -0
  70. package/dist/layout/compact/derive.d.ts +3 -0
  71. package/dist/layout/compact/derive.js +157 -0
  72. package/dist/layout/compact/derive.test.d.ts +1 -0
  73. package/dist/layout/compact/derive.test.js +197 -0
  74. package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
  75. package/dist/layout/compact/drawerStore.svelte.js +75 -0
  76. package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
  77. package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
  78. package/dist/layout/compact/enrichCarousels.d.ts +8 -0
  79. package/dist/layout/compact/enrichCarousels.js +44 -0
  80. package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
  81. package/dist/layout/compact/enrichCarousels.test.js +88 -0
  82. package/dist/layout/compact/resolveRole.d.ts +6 -0
  83. package/dist/layout/compact/resolveRole.js +13 -0
  84. package/dist/layout/compact/resolveRole.test.d.ts +1 -0
  85. package/dist/layout/compact/resolveRole.test.js +18 -0
  86. package/dist/layout/compact/types.d.ts +30 -0
  87. package/dist/layout/compact/types.js +15 -0
  88. package/dist/layout/drag.svelte.js +13 -0
  89. package/dist/layout/presets.compactVariant.test.d.ts +1 -0
  90. package/dist/layout/presets.compactVariant.test.js +27 -0
  91. package/dist/layout/presets.d.ts +12 -0
  92. package/dist/layout/presets.js +16 -0
  93. package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
  94. package/dist/layout/store.drawers.svelte.test.js +49 -0
  95. package/dist/layout/store.schemaVersion.test.d.ts +1 -0
  96. package/dist/layout/store.schemaVersion.test.js +35 -0
  97. package/dist/layout/store.svelte.js +52 -2
  98. package/dist/layout/types.d.ts +51 -1
  99. package/dist/layout/types.js +1 -1
  100. package/dist/layout/types.test.d.ts +1 -0
  101. package/dist/layout/types.test.js +26 -0
  102. package/dist/overlays/DrawerSurface.svelte +141 -0
  103. package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
  104. package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
  105. package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
  106. package/dist/overlays/ModalFrame.svelte +3 -1
  107. package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
  108. package/dist/overlays/OverlayRoots.svelte +12 -9
  109. package/dist/overlays/floatDismiss.js +5 -0
  110. package/dist/overlays/focusTrap.d.ts +11 -1
  111. package/dist/overlays/focusTrap.js +11 -9
  112. package/dist/overlays/modal.js +1 -0
  113. package/dist/overlays/popup.js +4 -0
  114. package/dist/overlays/types.d.ts +10 -1
  115. package/dist/primitives/Button.svelte +18 -0
  116. package/dist/primitives/Button.svelte.d.ts +6 -0
  117. package/dist/primitives/ResizableSplitter.svelte +71 -11
  118. package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
  119. package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
  120. package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
  121. package/dist/server-shard/types.d.ts +2 -1
  122. package/dist/sh3Api/headless.js +9 -1
  123. package/dist/sh3Api/headless.svelte.test.js +45 -1
  124. package/dist/sh3Runtime.svelte.d.ts +36 -0
  125. package/dist/sh3Runtime.svelte.js +33 -0
  126. package/dist/shards/activate.svelte.js +10 -0
  127. package/dist/shards/ctx-fetch.test.d.ts +1 -0
  128. package/dist/shards/ctx-fetch.test.js +66 -0
  129. package/dist/shards/types.d.ts +22 -1
  130. package/dist/tokens.css +3 -2
  131. package/dist/transport/apiFetch.d.ts +1 -0
  132. package/dist/transport/apiFetch.js +65 -0
  133. package/dist/transport/apiFetch.test.d.ts +1 -0
  134. package/dist/transport/apiFetch.test.js +37 -0
  135. package/dist/transport/authToken.d.ts +2 -0
  136. package/dist/transport/authToken.js +53 -0
  137. package/dist/transport/authToken.test.d.ts +1 -0
  138. package/dist/transport/authToken.test.js +33 -0
  139. package/dist/verbs/types.d.ts +5 -2
  140. package/dist/version.d.ts +1 -1
  141. package/dist/version.js +1 -1
  142. package/dist/viewport/classify.d.ts +8 -0
  143. package/dist/viewport/classify.js +20 -0
  144. package/dist/viewport/classify.test.d.ts +1 -0
  145. package/dist/viewport/classify.test.js +32 -0
  146. package/dist/viewport/store.browser.test.d.ts +1 -0
  147. package/dist/viewport/store.browser.test.js +33 -0
  148. package/dist/viewport/store.svelte.d.ts +9 -0
  149. package/dist/viewport/store.svelte.js +71 -0
  150. package/dist/viewport/store.svelte.test.d.ts +1 -0
  151. package/dist/viewport/store.svelte.test.js +54 -0
  152. package/dist/viewport/types.d.ts +9 -0
  153. package/dist/viewport/types.js +6 -0
  154. package/package.json +1 -1
@@ -1128,4 +1128,35 @@
1128
1128
  <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
1129
1129
  </symbol>
1130
1130
 
1131
+ <!-- lucide/menu -->
1132
+ <symbol id="menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1133
+ <path d="M4 5h16" />
1134
+ <path d="M4 12h16" />
1135
+ <path d="M4 19h16" />
1136
+ </symbol>
1137
+
1138
+ <!-- lucide/panel-right -->
1139
+ <symbol id="panel-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1140
+ <rect width="18" height="18" x="3" y="3" rx="2" />
1141
+ <path d="M15 3v18" />
1142
+ </symbol>
1143
+
1144
+ <!-- lucide/panel-top -->
1145
+ <symbol id="panel-top" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1146
+ <rect width="18" height="18" x="3" y="3" rx="2" />
1147
+ <path d="M3 9h18" />
1148
+ </symbol>
1149
+
1150
+ <!-- lucide/command -->
1151
+ <symbol id="command" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1152
+ <path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
1153
+ </symbol>
1154
+
1155
+ <!-- lucide/ellipsis-vertical -->
1156
+ <symbol id="ellipsis-vertical" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1157
+ <circle cx="12" cy="12" r="1" />
1158
+ <circle cx="12" cy="5" r="1" />
1159
+ <circle cx="12" cy="19" r="1" />
1160
+ </symbol>
1161
+
1131
1162
  </svg>
@@ -8,6 +8,8 @@
8
8
  *
9
9
  * .svelte.ts because it uses $state for reactive auth status.
10
10
  */
11
+ import { apiFetch } from '../transport/apiFetch';
12
+ import { setAuthToken } from '../transport/authToken';
11
13
  /** Reactive auth state. */
12
14
  let currentUser = $state(null);
13
15
  let currentSession = $state(null);
@@ -23,22 +25,28 @@ let authConfig = null;
23
25
  * after fetching /api/boot.
24
26
  */
25
27
  export function initFromBoot(url, config) {
28
+ var _a, _b;
26
29
  serverUrl = url;
27
30
  authConfig = config.auth;
28
31
  currentUser = config.user;
29
32
  currentSession = config.session;
30
33
  guest = !config.session && !config.user;
34
+ // Sync the cross-origin auth-token store. If the boot returned no
35
+ // session but localStorage still holds a stale token, drop it — the
36
+ // server already disowned it (otherwise it would have echoed the
37
+ // session back).
38
+ setAuthToken((_b = (_a = config.session) === null || _a === void 0 ? void 0 : _a.token) !== null && _b !== void 0 ? _b : null);
31
39
  }
32
40
  /**
33
41
  * Log in with username + password. On success, updates reactive state.
34
42
  * Returns { ok: true } or { ok: false, error: string }.
35
43
  */
36
44
  export async function login(username, password) {
45
+ var _a, _b;
37
46
  try {
38
- const res = await fetch(`${serverUrl}/api/auth/login`, {
47
+ const res = await apiFetch(`${serverUrl}/api/auth/login`, {
39
48
  method: 'POST',
40
49
  headers: { 'Content-Type': 'application/json' },
41
- credentials: 'include',
42
50
  body: JSON.stringify({ username, password }),
43
51
  });
44
52
  if (!res.ok) {
@@ -49,9 +57,10 @@ export async function login(username, password) {
49
57
  currentUser = body.user;
50
58
  currentSession = body.session;
51
59
  guest = false;
60
+ setAuthToken((_b = (_a = body.session) === null || _a === void 0 ? void 0 : _a.token) !== null && _b !== void 0 ? _b : null);
52
61
  return { ok: true };
53
62
  }
54
- catch (_a) {
63
+ catch (_c) {
55
64
  return { ok: false, error: 'Network error' };
56
65
  }
57
66
  }
@@ -60,11 +69,11 @@ export async function login(username, password) {
60
69
  * On success, auto-logs in and updates reactive state.
61
70
  */
62
71
  export async function register(username, password, displayName) {
72
+ var _a, _b;
63
73
  try {
64
- const res = await fetch(`${serverUrl}/api/auth/register`, {
74
+ const res = await apiFetch(`${serverUrl}/api/auth/register`, {
65
75
  method: 'POST',
66
76
  headers: { 'Content-Type': 'application/json' },
67
- credentials: 'include',
68
77
  body: JSON.stringify({ username, password, displayName }),
69
78
  });
70
79
  if (!res.ok) {
@@ -75,9 +84,10 @@ export async function register(username, password, displayName) {
75
84
  currentUser = body.user;
76
85
  currentSession = body.session;
77
86
  guest = false;
87
+ setAuthToken((_b = (_a = body.session) === null || _a === void 0 ? void 0 : _a.token) !== null && _b !== void 0 ? _b : null);
78
88
  return { ok: true };
79
89
  }
80
- catch (_a) {
90
+ catch (_c) {
81
91
  return { ok: false, error: 'Network error' };
82
92
  }
83
93
  }
@@ -92,14 +102,14 @@ export async function register(username, password, displayName) {
92
102
  */
93
103
  export async function logout() {
94
104
  try {
95
- await fetch(`${serverUrl}/api/auth/logout`, {
105
+ await apiFetch(`${serverUrl}/api/auth/logout`, {
96
106
  method: 'POST',
97
- credentials: 'include',
98
107
  });
99
108
  }
100
109
  catch (_a) {
101
110
  // Best effort
102
111
  }
112
+ setAuthToken(null);
103
113
  if ((authConfig === null || authConfig === void 0 ? void 0 : authConfig.required) && !authConfig.guestAllowed) {
104
114
  // Policy forbids guest browsing — re-run the boot-time hard gate.
105
115
  // Do not touch reactive state: the page is leaving.
@@ -29,6 +29,12 @@ export interface BootConfig {
29
29
  user: AuthUser | null;
30
30
  session: AuthSession | null;
31
31
  tenantId: string;
32
+ /**
33
+ * Server's `sh3-server` semver. Optional for back-compat with
34
+ * pre-0.18.1 servers that didn't emit it; the cross-origin probe
35
+ * in `Onboarding.svelte` falls back to `'unknown'` when absent.
36
+ */
37
+ version?: string;
32
38
  }
33
39
  /** Global settings shape. */
34
40
  export interface GlobalSettings {
@@ -0,0 +1,130 @@
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, getActiveRoot } 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 { returnToHome } from '../apps/lifecycle';
18
+ import Button from '../primitives/Button.svelte';
19
+ import MenuSheet from './MenuSheet.svelte';
20
+ import type { DrawerAnchor } from '../layout/compact/types';
21
+
22
+ const rendering = $derived(derive(layoutStore.root));
23
+ const dispatcher = $derived(getLiveDispatcherState());
24
+ const onHome = $derived(getActiveRoot() === 'home');
25
+ const appLabel = $derived.by(() => {
26
+ const id = dispatcher.activeAppId;
27
+ if (!id) return 'SH3';
28
+ return getRegisteredApp(id)?.manifest.label ?? id;
29
+ });
30
+
31
+ /**
32
+ * Pick the topmost carousel — the one with the lexicographically smallest
33
+ * path key. `''` (root) sorts before `'0'` etc.; among siblings, smaller
34
+ * indices sort first. This deterministically selects the spatially-topmost
35
+ * full-width tab group when multiple carousels are stacked vertically.
36
+ */
37
+ const topmostCarouselLabel = $derived.by(() => {
38
+ if (rendering.carousels.size === 0) return null;
39
+ let bestKey: string | null = null;
40
+ let bestLabel = '';
41
+ for (const [key, info] of rendering.carousels) {
42
+ if (bestKey === null || key < bestKey) {
43
+ bestKey = key;
44
+ bestLabel = info.activeLabel;
45
+ }
46
+ }
47
+ return bestLabel;
48
+ });
49
+
50
+ const title = $derived.by(() => {
51
+ const carouselLabel = topmostCarouselLabel;
52
+ if (carouselLabel) return `${appLabel} › ${carouselLabel}`;
53
+ return appLabel;
54
+ });
55
+
56
+ let menuOpen = $state(false);
57
+
58
+ function toggleDrawer(anchor: DrawerAnchor) {
59
+ sh3.drawers.toggle(anchor);
60
+ }
61
+ function openPalette() {
62
+ sh3.actions.openPalette();
63
+ }
64
+ </script>
65
+
66
+ <header class="sh3-compact-chrome" data-sh3-region="compact-chrome">
67
+ <div class="leading">
68
+ <Button
69
+ variant="icon"
70
+ icon="house"
71
+ ariaLabel="Home"
72
+ title="Home"
73
+ disabled={onHome}
74
+ onclick={() => returnToHome()}
75
+ />
76
+ {#if rendering.drawers.left}
77
+ <span data-sh3-anchor="left">
78
+ <Button variant="icon" icon="menu" ariaLabel="Toggle left drawer" title="Toggle left drawer" onclick={() => toggleDrawer('left')} />
79
+ </span>
80
+ {/if}
81
+ {#if rendering.drawers.right}
82
+ <span data-sh3-anchor="right">
83
+ <Button variant="icon" icon="panel-right" ariaLabel="Toggle right drawer" title="Toggle right drawer" onclick={() => toggleDrawer('right')} />
84
+ </span>
85
+ {/if}
86
+ {#if rendering.drawers.top}
87
+ <span data-sh3-anchor="top">
88
+ <Button variant="icon" icon="panel-top" ariaLabel="Toggle top drawer" title="Toggle top drawer" onclick={() => toggleDrawer('top')} />
89
+ </span>
90
+ {/if}
91
+ </div>
92
+ <div class="title">{title}</div>
93
+ <div class="trailing">
94
+ <Button variant="icon" icon="command" ariaLabel="Open command palette" title="Open command palette" onclick={openPalette} />
95
+ <Button variant="icon" icon="ellipsis-vertical" ariaLabel="Open menu" title="Open menu" onclick={() => { menuOpen = true; }} />
96
+ </div>
97
+ </header>
98
+
99
+ <MenuSheet open={menuOpen} onClose={() => (menuOpen = false)} />
100
+
101
+ <style>
102
+ .sh3-compact-chrome {
103
+ display: grid;
104
+ grid-template-columns: auto 1fr auto;
105
+ align-items: center;
106
+ height: var(--sh3-tabbar-height);
107
+ padding: 0 var(--sh3-pad-sm);
108
+ gap: var(--sh3-pad-sm);
109
+ background: var(--sh3-grad-bg-elevated, var(--sh3-bg-elevated));
110
+ border-bottom: 1px solid var(--sh3-border);
111
+ color: var(--sh3-fg);
112
+ }
113
+ .leading,
114
+ .trailing {
115
+ display: inline-flex;
116
+ align-items: center;
117
+ gap: var(--sh3-pad-xs);
118
+ }
119
+ .leading > span {
120
+ display: inline-flex;
121
+ align-items: center;
122
+ }
123
+ .title {
124
+ font-weight: 600;
125
+ text-align: center;
126
+ overflow: hidden;
127
+ text-overflow: ellipsis;
128
+ white-space: nowrap;
129
+ }
130
+ </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,174 @@
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, switchToHome, } from '../layout/store.svelte';
10
+ import { drawerStore } from '../layout/compact/drawerStore.svelte';
11
+ import { setActiveApp, __resetDispatcherStateForTest } from '../actions/state.svelte';
12
+ import { registerApp, __resetAppRegistryForTest } from '../apps/registry.svelte';
13
+ const CompactChromeAny = CompactChrome;
14
+ function fakeApp() {
15
+ return {
16
+ manifest: { id: 'cc-app', label: 'CC App', layoutVersion: 5 },
17
+ initialLayout: {
18
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.6, 0.2],
19
+ children: [
20
+ { type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
21
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
22
+ { type: 'slot', slotId: 'ins', viewId: 'v:ins', role: 'inspector' },
23
+ ],
24
+ },
25
+ };
26
+ }
27
+ let mounted = null;
28
+ let host = null;
29
+ afterEach(() => {
30
+ if (mounted) {
31
+ unmount(mounted);
32
+ mounted = null;
33
+ }
34
+ if (host) {
35
+ host.remove();
36
+ host = null;
37
+ }
38
+ detachApp();
39
+ __resetAppRegistryForTest();
40
+ __resetDispatcherStateForTest();
41
+ });
42
+ beforeEach(() => {
43
+ __resetLayoutStoreForTest();
44
+ drawerStore.__reset();
45
+ __resetAppRegistryForTest();
46
+ __resetDispatcherStateForTest();
47
+ });
48
+ function attachAndActivate(app) {
49
+ var _a;
50
+ registerApp(app);
51
+ attachApp(app);
52
+ switchToApp();
53
+ setActiveApp(app.manifest.id, new Set((_a = app.manifest.requiredShards) !== null && _a !== void 0 ? _a : []));
54
+ }
55
+ describe('CompactChrome (dom)', () => {
56
+ it('renders a leading toggle for each present drawer anchor', () => {
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
+ expect(host.querySelector('.leading [data-sh3-anchor="left"] button')).not.toBeNull();
65
+ expect(host.querySelector('.leading [data-sh3-anchor="right"] button')).not.toBeNull();
66
+ expect(host.querySelector('.leading [data-sh3-anchor="top"] button')).toBeNull();
67
+ });
68
+ it('renders palette + overflow buttons in the trailing section', () => {
69
+ attachApp(fakeApp());
70
+ switchToApp();
71
+ flushSync();
72
+ host = document.createElement('div');
73
+ document.body.appendChild(host);
74
+ mounted = mount(CompactChromeAny, { target: host });
75
+ flushSync();
76
+ const trailing = host.querySelectorAll('.trailing button');
77
+ expect(trailing.length).toBe(2);
78
+ });
79
+ });
80
+ describe('CompactChrome — home button', () => {
81
+ it('renders a home button as the first leading item', () => {
82
+ attachApp(fakeApp());
83
+ switchToApp();
84
+ flushSync();
85
+ host = document.createElement('div');
86
+ document.body.appendChild(host);
87
+ mounted = mount(CompactChromeAny, { target: host });
88
+ flushSync();
89
+ const homeBtn = host.querySelector('.leading button[aria-label="Home"]');
90
+ expect(homeBtn).not.toBeNull();
91
+ expect(homeBtn === null || homeBtn === void 0 ? void 0 : homeBtn.hasAttribute('disabled')).toBe(false);
92
+ });
93
+ it('disables the home button when on home', () => {
94
+ switchToHome();
95
+ flushSync();
96
+ host = document.createElement('div');
97
+ document.body.appendChild(host);
98
+ mounted = mount(CompactChromeAny, { target: host });
99
+ flushSync();
100
+ const homeBtn = host.querySelector('.leading button[aria-label="Home"]');
101
+ expect(homeBtn).not.toBeNull();
102
+ expect(homeBtn === null || homeBtn === void 0 ? void 0 : homeBtn.hasAttribute('disabled')).toBe(true);
103
+ });
104
+ });
105
+ describe('CompactChrome — breadcrumb', () => {
106
+ it('renders only AppName when no carousel is active', () => {
107
+ var _a;
108
+ const app = {
109
+ manifest: { id: 'plain-app', label: 'Plain App', layoutVersion: 6 },
110
+ initialLayout: { type: 'slot', slotId: 'b', viewId: null, role: 'body' },
111
+ };
112
+ attachAndActivate(app);
113
+ flushSync();
114
+ host = document.createElement('div');
115
+ document.body.appendChild(host);
116
+ mounted = mount(CompactChromeAny, { target: host });
117
+ flushSync();
118
+ const title = host.querySelector('.title');
119
+ expect((_a = title === null || title === void 0 ? void 0 : title.textContent) === null || _a === void 0 ? void 0 : _a.trim()).toBe('Plain App');
120
+ });
121
+ it('renders AppName › ActiveLabel when a carousel is active', () => {
122
+ var _a;
123
+ const app = {
124
+ manifest: { id: 'carousel-app', label: 'Carousel App', layoutVersion: 6 },
125
+ initialLayout: {
126
+ type: 'tabs',
127
+ activeTab: 1,
128
+ tabs: [
129
+ { slotId: 's0', viewId: null, label: 'First', role: 'body' },
130
+ { slotId: 's1', viewId: null, label: 'Second', role: 'body' },
131
+ ],
132
+ },
133
+ };
134
+ attachAndActivate(app);
135
+ flushSync();
136
+ host = document.createElement('div');
137
+ document.body.appendChild(host);
138
+ mounted = mount(CompactChromeAny, { target: host });
139
+ flushSync();
140
+ const title = host.querySelector('.title');
141
+ expect((_a = title === null || title === void 0 ? void 0 : title.textContent) === null || _a === void 0 ? void 0 : _a.trim()).toBe('Carousel App › Second');
142
+ });
143
+ it('with multiple stacked carousels, breadcrumb uses the topmost (lowest path-key sort order)', () => {
144
+ var _a;
145
+ const app = {
146
+ manifest: { id: 'stacked-app', label: 'Stacked', layoutVersion: 6 },
147
+ initialLayout: {
148
+ type: 'split',
149
+ direction: 'vertical',
150
+ sizes: [0.5, 0.5],
151
+ children: [
152
+ {
153
+ type: 'tabs',
154
+ activeTab: 0,
155
+ tabs: [{ slotId: 'top0', viewId: null, label: 'TopActive', role: 'body' }],
156
+ },
157
+ {
158
+ type: 'tabs',
159
+ activeTab: 0,
160
+ tabs: [{ slotId: 'bot0', viewId: null, label: 'BottomActive', role: 'body' }],
161
+ },
162
+ ],
163
+ },
164
+ };
165
+ attachAndActivate(app);
166
+ flushSync();
167
+ host = document.createElement('div');
168
+ document.body.appendChild(host);
169
+ mounted = mount(CompactChromeAny, { target: host });
170
+ flushSync();
171
+ const title = host.querySelector('.title');
172
+ expect((_a = title === null || title === void 0 ? void 0 : title.textContent) === null || _a === void 0 ? void 0 : _a.trim()).toBe('Stacked › TopActive');
173
+ });
174
+ });
@@ -0,0 +1,224 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Touch-friendly replacement for MenuBar — bottom-anchored sheet with
4
+ * collapsible sections per menu container. Tapping a submenu parent
5
+ * expands its children inline (no nested popover stack — see
6
+ * docs/superpowers/specs/2026-05-09-action-submenu-discoverability-design.md).
7
+ *
8
+ * Reads the same dispatcher state and registry as MenuBar:
9
+ * resolveMenuContainers(activeAppId, declared)
10
+ * resolveMenuItems(entries, dispatcherState, containerId)
11
+ * resolveSubmenuItems(entries, dispatcherState, parentId)
12
+ */
13
+ import {
14
+ resolveMenuContainers,
15
+ resolveMenuItems,
16
+ resolveSubmenuItems,
17
+ type MenuBarItem,
18
+ } from '../actions/menuBarModel';
19
+ import { listActions } from '../actions/registry';
20
+ import { getLiveDispatcherState } from '../actions/state.svelte';
21
+ import { getRegisteredApp } from '../apps/registry.svelte';
22
+ import { resolveLabel } from '../actions/types';
23
+
24
+ let { open, onClose }: { open: boolean; onClose: () => void } = $props();
25
+
26
+ const dispatcher = $derived(getLiveDispatcherState());
27
+ const activeAppId = $derived(dispatcher.activeAppId);
28
+ const declaredMenus = $derived.by(() => {
29
+ if (!activeAppId) return undefined;
30
+ return getRegisteredApp(activeAppId)?.manifest.menus;
31
+ });
32
+ const containers = $derived(resolveMenuContainers(activeAppId, declaredMenus));
33
+ const containerItems = $derived.by(() => {
34
+ const out: { containerId: string; label: string; items: MenuBarItem[] }[] = [];
35
+ const entries = listActions();
36
+ for (const c of containers) {
37
+ const items = resolveMenuItems(entries, dispatcher, c.id);
38
+ if (items.length > 0) out.push({ containerId: c.id, label: c.label, items });
39
+ }
40
+ return out;
41
+ });
42
+
43
+ let expanded = $state(new Set<string>());
44
+ let expandedSubmenu = $state(new Set<string>());
45
+
46
+ function toggleContainer(id: string) {
47
+ const next = new Set(expanded);
48
+ if (next.has(id)) next.delete(id);
49
+ else next.add(id);
50
+ expanded = next;
51
+ }
52
+
53
+ function toggleSubmenu(id: string) {
54
+ const next = new Set(expandedSubmenu);
55
+ if (next.has(id)) next.delete(id);
56
+ else next.add(id);
57
+ expandedSubmenu = next;
58
+ }
59
+
60
+ function invoke(itemId: string) {
61
+ const entry = listActions().find((e) => e.action.id === itemId);
62
+ if (!entry || typeof entry.action.run !== 'function') return;
63
+ try {
64
+ void entry.action.run({
65
+ action: { id: itemId, label: resolveLabel(entry.action) },
66
+ appId: dispatcher.activeAppId,
67
+ viewId: dispatcher.focusedViewId ?? undefined,
68
+ selection: dispatcher.selection ?? undefined,
69
+ invokedVia: 'palette',
70
+ dispatch: () => {},
71
+ });
72
+ } catch (err) {
73
+ console.error(`[sh3] menu-sheet action "${itemId}" threw:`, err);
74
+ }
75
+ onClose();
76
+ }
77
+ </script>
78
+
79
+ {#if open}
80
+ <div
81
+ class="backdrop"
82
+ onclick={onClose}
83
+ onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
84
+ role="presentation"
85
+ ></div>
86
+ <div class="sheet" role="dialog" aria-label="Menu" data-sh3-region="menu-sheet">
87
+ <div class="scroll">
88
+ {#each containerItems as { containerId, label, items } (containerId)}
89
+ <button
90
+ class="container"
91
+ aria-expanded={expanded.has(containerId)}
92
+ onclick={() => toggleContainer(containerId)}
93
+ >
94
+ <span class="caret" class:open={expanded.has(containerId)}>▸</span>
95
+ <span class="label">{label}</span>
96
+ </button>
97
+ {#if expanded.has(containerId)}
98
+ <div class="items">
99
+ {#each items as item (item.id)}
100
+ {#if item.submenu}
101
+ <button
102
+ class="item submenu"
103
+ aria-expanded={expandedSubmenu.has(item.id)}
104
+ disabled={item.disabled}
105
+ onclick={() => toggleSubmenu(item.id)}
106
+ >
107
+ <span class="caret" class:open={expandedSubmenu.has(item.id)}>▸</span>
108
+ <span class="label">{item.label}</span>
109
+ </button>
110
+ {#if expandedSubmenu.has(item.id)}
111
+ <div class="subitems">
112
+ {#each resolveSubmenuItems(listActions(), dispatcher, item.id) as sub (sub.id)}
113
+ <button
114
+ class="item child"
115
+ disabled={sub.disabled}
116
+ onclick={() => invoke(sub.id)}
117
+ >
118
+ <span class="label">{sub.label}</span>
119
+ {#if sub.shortcut}
120
+ <span class="shortcut">{sub.shortcut}</span>
121
+ {/if}
122
+ </button>
123
+ {/each}
124
+ </div>
125
+ {/if}
126
+ {:else}
127
+ <button
128
+ class="item"
129
+ disabled={item.disabled}
130
+ onclick={() => invoke(item.id)}
131
+ >
132
+ <span class="label">{item.label}</span>
133
+ {#if item.shortcut}
134
+ <span class="shortcut">{item.shortcut}</span>
135
+ {/if}
136
+ </button>
137
+ {/if}
138
+ {/each}
139
+ </div>
140
+ {/if}
141
+ {/each}
142
+ </div>
143
+ <button class="cancel" onclick={onClose}>Cancel</button>
144
+ </div>
145
+ {/if}
146
+
147
+ <style>
148
+ .backdrop {
149
+ position: absolute;
150
+ inset: 0;
151
+ background: var(--sh3-overlay-backdrop, rgba(0, 0, 0, 0.35));
152
+ pointer-events: auto;
153
+ z-index: var(--sh3-z-layer-4);
154
+ }
155
+ .sheet {
156
+ position: absolute;
157
+ left: 0;
158
+ right: 0;
159
+ bottom: 0;
160
+ max-height: 70vh;
161
+ display: flex;
162
+ flex-direction: column;
163
+ background: var(--sh3-bg);
164
+ color: var(--sh3-fg);
165
+ border-top: 1px solid var(--sh3-border);
166
+ box-shadow: var(--sh3-shadow-md, 0 -4px 16px rgba(0, 0, 0, 0.2));
167
+ pointer-events: auto;
168
+ z-index: var(--sh3-z-layer-4);
169
+ }
170
+ .scroll {
171
+ flex: 1;
172
+ min-height: 0;
173
+ overflow: auto;
174
+ padding: var(--sh3-pad-sm) 0;
175
+ }
176
+ .container {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: var(--sh3-pad-sm);
180
+ width: 100%;
181
+ padding: var(--sh3-pad-sm) var(--sh3-pad-md);
182
+ border: none;
183
+ background: none;
184
+ color: var(--sh3-fg);
185
+ font-weight: 600;
186
+ text-align: left;
187
+ cursor: pointer;
188
+ }
189
+ .container:active { background: var(--sh3-bg-sunken); }
190
+ .items { padding-left: var(--sh3-pad-md); }
191
+ .subitems { padding-left: var(--sh3-pad-md); }
192
+ .item {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: var(--sh3-pad-sm);
196
+ width: 100%;
197
+ padding: var(--sh3-pad-sm) var(--sh3-pad-md);
198
+ border: none;
199
+ background: none;
200
+ color: var(--sh3-fg);
201
+ text-align: left;
202
+ cursor: pointer;
203
+ }
204
+ .item:disabled { opacity: 0.5; cursor: not-allowed; }
205
+ .item:active:not(:disabled) { background: var(--sh3-bg-sunken); }
206
+ .item.child { padding-left: calc(var(--sh3-pad-md) * 2); }
207
+ .label { flex: 1; }
208
+ .shortcut { color: var(--sh3-fg-muted); font-family: var(--sh3-font-mono); }
209
+ .caret {
210
+ display: inline-block;
211
+ width: 1em;
212
+ transition: transform 120ms;
213
+ }
214
+ .caret.open { transform: rotate(90deg); }
215
+ .cancel {
216
+ padding: var(--sh3-pad-md);
217
+ border: none;
218
+ border-top: 1px solid var(--sh3-border);
219
+ background: var(--sh3-bg-elevated);
220
+ color: var(--sh3-fg);
221
+ font-weight: 600;
222
+ cursor: pointer;
223
+ }
224
+ </style>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ };
5
+ declare const MenuSheet: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type MenuSheet = ReturnType<typeof MenuSheet>;
7
+ export default MenuSheet;