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.
- package/dist/Sh3.svelte +107 -39
- package/dist/__screenshots__/handheld.browser.test.ts/handheld-viewport-flip-e2e-viewport-override-flips-chrome-and-body-branches-1.png +0 -0
- package/dist/actions/CommandPalette.svelte +1 -2
- package/dist/actions/listActionsFromEntries.test.js +29 -0
- package/dist/actions/listActive.js +2 -0
- package/dist/actions/listeners.js +16 -1
- package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
- package/dist/actions/types.d.ts +8 -0
- package/dist/api.d.ts +8 -1
- package/dist/app/store/storeShard.svelte.js +1 -21
- package/dist/app/store/version.d.ts +11 -0
- package/dist/app/store/version.js +39 -0
- package/dist/app/store/version.test.d.ts +1 -0
- package/dist/app/store/version.test.js +44 -0
- package/dist/apps/lifecycle.d.ts +6 -0
- package/dist/apps/lifecycle.js +5 -2
- package/dist/apps/lifecycle.test.js +30 -0
- package/dist/apps/types.d.ts +12 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +5 -0
- package/dist/assets/icons.svg +31 -0
- package/dist/auth/auth.svelte.js +18 -8
- package/dist/auth/types.d.ts +6 -0
- package/dist/chrome/CompactChrome.svelte +130 -0
- package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
- package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
- package/dist/chrome/CompactChrome.svelte.test.js +174 -0
- package/dist/chrome/MenuSheet.svelte +224 -0
- package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
- package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
- package/dist/chrome/MenuSheet.svelte.test.js +46 -0
- package/dist/createShell.d.ts +9 -0
- package/dist/createShell.js +20 -7
- package/dist/createShell.remoteAuth.test.d.ts +1 -0
- package/dist/createShell.remoteAuth.test.js +71 -0
- package/dist/documents/http-backend.js +12 -11
- package/dist/env/client.js +11 -5
- package/dist/files/types.d.ts +106 -0
- package/dist/files/types.js +1 -0
- package/dist/gestures/gestureRegistry.d.ts +6 -0
- package/dist/gestures/gestureRegistry.js +190 -0
- package/dist/gestures/gestureRegistry.test.d.ts +1 -0
- package/dist/gestures/gestureRegistry.test.js +119 -0
- package/dist/gestures/index.d.ts +6 -0
- package/dist/gestures/index.js +12 -0
- package/dist/gestures/pointerClaim.d.ts +7 -0
- package/dist/gestures/pointerClaim.js +36 -0
- package/dist/gestures/pointerClaim.test.d.ts +1 -0
- package/dist/gestures/pointerClaim.test.js +64 -0
- package/dist/gestures/types.d.ts +83 -0
- package/dist/gestures/types.js +1 -0
- package/dist/handheld.browser.test.d.ts +1 -0
- package/dist/handheld.browser.test.js +90 -0
- package/dist/host-entry.d.ts +1 -0
- package/dist/host-entry.js +1 -0
- package/dist/layout/LayoutRenderer.browser.test.js +15 -3
- package/dist/layout/LayoutRenderer.svelte +27 -3
- package/dist/layout/LayoutRenderer.svelte.d.ts +4 -1
- 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
- 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
- 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
- package/dist/layout/compact/CarouselTabs.svelte +361 -0
- package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
- package/dist/layout/compact/CompactRenderer.svelte +53 -0
- package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.js +125 -0
- package/dist/layout/compact/derive.d.ts +3 -0
- package/dist/layout/compact/derive.js +157 -0
- package/dist/layout/compact/derive.test.d.ts +1 -0
- package/dist/layout/compact/derive.test.js +197 -0
- package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
- package/dist/layout/compact/drawerStore.svelte.js +75 -0
- package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
- package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
- package/dist/layout/compact/enrichCarousels.d.ts +8 -0
- package/dist/layout/compact/enrichCarousels.js +44 -0
- package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
- package/dist/layout/compact/enrichCarousels.test.js +88 -0
- package/dist/layout/compact/resolveRole.d.ts +6 -0
- package/dist/layout/compact/resolveRole.js +13 -0
- package/dist/layout/compact/resolveRole.test.d.ts +1 -0
- package/dist/layout/compact/resolveRole.test.js +18 -0
- package/dist/layout/compact/types.d.ts +30 -0
- package/dist/layout/compact/types.js +15 -0
- package/dist/layout/drag.svelte.js +13 -0
- package/dist/layout/presets.compactVariant.test.d.ts +1 -0
- package/dist/layout/presets.compactVariant.test.js +27 -0
- package/dist/layout/presets.d.ts +12 -0
- package/dist/layout/presets.js +16 -0
- package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
- package/dist/layout/store.drawers.svelte.test.js +49 -0
- package/dist/layout/store.schemaVersion.test.d.ts +1 -0
- package/dist/layout/store.schemaVersion.test.js +35 -0
- package/dist/layout/store.svelte.js +52 -2
- package/dist/layout/types.d.ts +51 -1
- package/dist/layout/types.js +1 -1
- package/dist/layout/types.test.d.ts +1 -0
- package/dist/layout/types.test.js +26 -0
- package/dist/overlays/DrawerSurface.svelte +141 -0
- package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
- package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
- package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
- package/dist/overlays/ModalFrame.svelte +3 -1
- package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
- package/dist/overlays/OverlayRoots.svelte +12 -9
- package/dist/overlays/floatDismiss.js +5 -0
- package/dist/overlays/focusTrap.d.ts +11 -1
- package/dist/overlays/focusTrap.js +11 -9
- package/dist/overlays/modal.js +1 -0
- package/dist/overlays/popup.js +4 -0
- package/dist/overlays/types.d.ts +10 -1
- package/dist/primitives/Button.svelte +18 -0
- package/dist/primitives/Button.svelte.d.ts +6 -0
- package/dist/primitives/ResizableSplitter.svelte +71 -11
- package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
- package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
- package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
- package/dist/server-shard/types.d.ts +2 -1
- package/dist/sh3Api/headless.js +9 -1
- package/dist/sh3Api/headless.svelte.test.js +45 -1
- package/dist/sh3Runtime.svelte.d.ts +36 -0
- package/dist/sh3Runtime.svelte.js +33 -0
- package/dist/shards/activate.svelte.js +10 -0
- package/dist/shards/ctx-fetch.test.d.ts +1 -0
- package/dist/shards/ctx-fetch.test.js +66 -0
- package/dist/shards/types.d.ts +22 -1
- package/dist/tokens.css +3 -2
- package/dist/transport/apiFetch.d.ts +1 -0
- package/dist/transport/apiFetch.js +65 -0
- package/dist/transport/apiFetch.test.d.ts +1 -0
- package/dist/transport/apiFetch.test.js +37 -0
- package/dist/transport/authToken.d.ts +2 -0
- package/dist/transport/authToken.js +53 -0
- package/dist/transport/authToken.test.d.ts +1 -0
- package/dist/transport/authToken.test.js +33 -0
- package/dist/verbs/types.d.ts +5 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewport/classify.d.ts +8 -0
- package/dist/viewport/classify.js +20 -0
- package/dist/viewport/classify.test.d.ts +1 -0
- package/dist/viewport/classify.test.js +32 -0
- package/dist/viewport/store.browser.test.d.ts +1 -0
- package/dist/viewport/store.browser.test.js +33 -0
- package/dist/viewport/store.svelte.d.ts +9 -0
- package/dist/viewport/store.svelte.js +71 -0
- package/dist/viewport/store.svelte.test.d.ts +1 -0
- package/dist/viewport/store.svelte.test.js +54 -0
- package/dist/viewport/types.d.ts +9 -0
- package/dist/viewport/types.js +6 -0
- package/package.json +1 -1
package/dist/assets/icons.svg
CHANGED
|
@@ -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>
|
package/dist/auth/auth.svelte.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -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 @@
|
|
|
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>
|