sh3-core 0.19.5 → 0.20.1
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/api.d.ts +1 -0
- package/dist/app/admin/AuthSettingsView.svelte +3 -9
- package/dist/app/admin/MountsView.svelte +276 -0
- package/dist/app/admin/MountsView.svelte.d.ts +3 -0
- package/dist/app/admin/SystemView.svelte +6 -6
- package/dist/app/admin/UsersView.svelte +103 -7
- package/dist/app/admin/adminApp.js +1 -0
- package/dist/app/admin/adminShard.svelte.js +10 -0
- package/dist/apps/lifecycle.js +1 -0
- package/dist/apps/types.d.ts +7 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +1 -0
- package/dist/assets/icons.svg +5 -0
- package/dist/auth/admin-users.svelte.js +2 -1
- package/dist/auth/auth.svelte.d.ts +4 -5
- package/dist/auth/auth.svelte.js +5 -6
- package/dist/auth/types.d.ts +0 -2
- package/dist/chrome/CompactChrome.svelte +25 -6
- package/dist/chrome/FloatsSheet.svelte +7 -32
- package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
- package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
- package/dist/chrome/MenuSheet.svelte +154 -148
- package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
- package/dist/chrome/MenuSheet.svelte.test.js +24 -12
- package/dist/createShell.js +32 -21
- package/dist/createShell.remoteAuth.test.js +9 -3
- package/dist/documents/browse.d.ts +18 -1
- package/dist/documents/browse.js +40 -7
- package/dist/documents/browse.test.js +35 -35
- package/dist/documents/config.d.ts +4 -0
- package/dist/documents/config.js +15 -2
- package/dist/documents/handle.js +25 -17
- package/dist/documents/http-backend.js +10 -2
- package/dist/documents/index.d.ts +2 -2
- package/dist/documents/index.js +1 -1
- package/dist/documents/picker-api.d.ts +33 -0
- package/dist/documents/picker-api.js +1 -0
- package/dist/documents/picker-api.test.d.ts +1 -0
- package/dist/documents/picker-api.test.js +162 -0
- package/dist/documents/picker-primitive.d.ts +11 -0
- package/dist/documents/picker-primitive.js +56 -0
- package/dist/documents/types.d.ts +17 -5
- package/dist/documents/types.js +2 -0
- package/dist/layout/presets.test.js +4 -4
- package/dist/layout/types.d.ts +1 -1
- package/dist/layouts-shard/LayoutsSection.svelte +3 -16
- package/dist/primitives/widgets/DocumentFilePicker.svelte +4 -4
- package/dist/primitives/widgets/PickerList.svelte +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +7 -8
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +1 -0
- package/dist/projects-shard/DeleteProjectDialog.svelte +32 -1
- package/dist/projects-shard/ProjectManage.svelte +197 -28
- package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
- package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
- package/dist/projects-shard/ProjectsSection.svelte +3 -16
- package/dist/projects-shard/projectsApi.js +2 -1
- package/dist/registry/permission-descriptions.js +4 -0
- package/dist/server-shard/types.d.ts +21 -0
- package/dist/sh3core-shard/HomeSection.svelte +107 -0
- package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
- package/dist/sh3core-shard/Sh3Home.svelte +9 -23
- package/dist/shards/activate.svelte.d.ts +4 -0
- package/dist/shards/activate.svelte.js +31 -14
- package/dist/shards/types.d.ts +15 -0
- package/dist/shell-shard/tenant-fs-client.js +2 -1
- package/dist/transport/apiFetch.js +12 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/assets/icons.svg
CHANGED
|
@@ -1159,4 +1159,9 @@
|
|
|
1159
1159
|
<circle cx="12" cy="19" r="1" />
|
|
1160
1160
|
</symbol>
|
|
1161
1161
|
|
|
1162
|
+
<!-- lucide/chevron-left -->
|
|
1163
|
+
<symbol id="chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1164
|
+
<path d="m15 18-6-6 6-6" />
|
|
1165
|
+
</symbol>
|
|
1166
|
+
|
|
1162
1167
|
</svg>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* the user list. Concurrent calls reuse the in-flight promise; subsequent
|
|
8
8
|
* calls after completion fetch fresh data.
|
|
9
9
|
*/
|
|
10
|
+
import { apiFetch } from '../transport/apiFetch';
|
|
10
11
|
export const usersAdminState = $state({ users: [], loading: false, error: null });
|
|
11
12
|
let inflight = null;
|
|
12
13
|
export function refreshAdminUsers() {
|
|
@@ -16,7 +17,7 @@ export function refreshAdminUsers() {
|
|
|
16
17
|
usersAdminState.error = null;
|
|
17
18
|
inflight = (async () => {
|
|
18
19
|
try {
|
|
19
|
-
const res = await
|
|
20
|
+
const res = await apiFetch('/api/admin/users');
|
|
20
21
|
if (!res.ok) {
|
|
21
22
|
usersAdminState.error = `GET /api/admin/users failed: ${res.status}`;
|
|
22
23
|
return;
|
|
@@ -37,11 +37,10 @@ export declare function register(username: string, password: string, displayName
|
|
|
37
37
|
/**
|
|
38
38
|
* Log out — clear session on server and client.
|
|
39
39
|
*
|
|
40
|
-
* If the boot policy forbids guest browsing (auth.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* duplicating it here.
|
|
40
|
+
* If the boot policy forbids guest browsing (!auth.guestAllowed), trigger
|
|
41
|
+
* a full page reload so the boot-time hard gate in createShell.ts re-runs
|
|
42
|
+
* and shows the sign-in wall. This keeps the policy authoritative in a
|
|
43
|
+
* single place rather than duplicating it here.
|
|
45
44
|
*/
|
|
46
45
|
export declare function logout(): Promise<void>;
|
|
47
46
|
/**
|
package/dist/auth/auth.svelte.js
CHANGED
|
@@ -94,11 +94,10 @@ export async function register(username, password, displayName) {
|
|
|
94
94
|
/**
|
|
95
95
|
* Log out — clear session on server and client.
|
|
96
96
|
*
|
|
97
|
-
* If the boot policy forbids guest browsing (auth.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* duplicating it here.
|
|
97
|
+
* If the boot policy forbids guest browsing (!auth.guestAllowed), trigger
|
|
98
|
+
* a full page reload so the boot-time hard gate in createShell.ts re-runs
|
|
99
|
+
* and shows the sign-in wall. This keeps the policy authoritative in a
|
|
100
|
+
* single place rather than duplicating it here.
|
|
102
101
|
*/
|
|
103
102
|
export async function logout() {
|
|
104
103
|
try {
|
|
@@ -110,7 +109,7 @@ export async function logout() {
|
|
|
110
109
|
// Best effort
|
|
111
110
|
}
|
|
112
111
|
setAuthToken(null);
|
|
113
|
-
if (
|
|
112
|
+
if (authConfig && !authConfig.guestAllowed) {
|
|
114
113
|
// Policy forbids guest browsing — re-run the boot-time hard gate.
|
|
115
114
|
// Do not touch reactive state: the page is leaving.
|
|
116
115
|
window.location.reload();
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -22,7 +22,6 @@ export interface AuthSession {
|
|
|
22
22
|
/** Response from GET /api/boot. */
|
|
23
23
|
export interface BootConfig {
|
|
24
24
|
auth: {
|
|
25
|
-
required: boolean;
|
|
26
25
|
guestAllowed: boolean;
|
|
27
26
|
selfRegistration: boolean;
|
|
28
27
|
};
|
|
@@ -39,7 +38,6 @@ export interface BootConfig {
|
|
|
39
38
|
/** Global settings shape. */
|
|
40
39
|
export interface GlobalSettings {
|
|
41
40
|
auth: {
|
|
42
|
-
required: boolean;
|
|
43
41
|
guestAllowed: boolean;
|
|
44
42
|
sessionTTL: number;
|
|
45
43
|
selfRegistration: boolean;
|
|
@@ -76,9 +76,31 @@
|
|
|
76
76
|
return appLabel;
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
let menuOpen = $state(false);
|
|
80
79
|
let floatsOpen = $state(false);
|
|
81
80
|
|
|
81
|
+
function openMenuSheet() {
|
|
82
|
+
sh3.modal.open(
|
|
83
|
+
MenuSheet,
|
|
84
|
+
{},
|
|
85
|
+
{ dismissOnBackdrop: true, boxStyle: 'max-width: 320px;' },
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toggleFloatsSheet() {
|
|
90
|
+
if (floatsOpen) return;
|
|
91
|
+
floatsOpen = true;
|
|
92
|
+
const handle = sh3.modal.open(
|
|
93
|
+
FloatsSheet,
|
|
94
|
+
{},
|
|
95
|
+
{ dismissOnBackdrop: true, boxStyle: 'max-width: 320px;' },
|
|
96
|
+
);
|
|
97
|
+
const origClose = handle.close;
|
|
98
|
+
handle.close = () => {
|
|
99
|
+
origClose();
|
|
100
|
+
floatsOpen = false;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
function toggleDrawer(anchor: DrawerAnchor) {
|
|
83
105
|
sh3.drawers.toggle(anchor);
|
|
84
106
|
}
|
|
@@ -122,15 +144,12 @@
|
|
|
122
144
|
ariaLabel="Floats"
|
|
123
145
|
title="Floats"
|
|
124
146
|
pressed={floatsOpen}
|
|
125
|
-
onclick={
|
|
147
|
+
onclick={toggleFloatsSheet}
|
|
126
148
|
/>
|
|
127
|
-
<Button variant="icon" icon="ellipsis-vertical" ariaLabel="Open menu" title="Open menu" onclick={
|
|
149
|
+
<Button variant="icon" icon="ellipsis-vertical" ariaLabel="Open menu" title="Open menu" onclick={openMenuSheet} />
|
|
128
150
|
</div>
|
|
129
151
|
</header>
|
|
130
152
|
|
|
131
|
-
<MenuSheet open={menuOpen} onClose={() => (menuOpen = false)} />
|
|
132
|
-
<FloatsSheet open={floatsOpen} onClose={() => (floatsOpen = false)} />
|
|
133
|
-
|
|
134
153
|
<style>
|
|
135
154
|
.sh3-compact-chrome {
|
|
136
155
|
display: grid;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { getRegisteredApp } from '../apps/registry.svelte';
|
|
19
19
|
import type { FloatEntry } from '../layout/types';
|
|
20
20
|
|
|
21
|
-
let {
|
|
21
|
+
let { close }: { close: () => void } = $props();
|
|
22
22
|
|
|
23
23
|
const dispatcher = $derived(getLiveDispatcherState());
|
|
24
24
|
const dockedLabel = $derived.by(() => {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
} else {
|
|
61
61
|
compactRootStore.setRoot({ kind: 'float', floatId: rowId });
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
close();
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// ----- swipe-to-close --------------------------------------------------
|
|
@@ -135,14 +135,7 @@
|
|
|
135
135
|
}
|
|
136
136
|
</script>
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
<div
|
|
140
|
-
class="backdrop"
|
|
141
|
-
onclick={onClose}
|
|
142
|
-
onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
|
|
143
|
-
role="presentation"
|
|
144
|
-
></div>
|
|
145
|
-
<div class="sheet" role="dialog" aria-label="Floats" data-sh3-region="floats-sheet">
|
|
138
|
+
<div class="sh3-floats-sheet" role="dialog" aria-label="Floats" data-sh3-region="floats-sheet">
|
|
146
139
|
<div class="scroll">
|
|
147
140
|
{#each rows as row (row.id)}
|
|
148
141
|
<button
|
|
@@ -163,32 +156,16 @@
|
|
|
163
156
|
</button>
|
|
164
157
|
{/each}
|
|
165
158
|
</div>
|
|
166
|
-
<button class="cancel" onclick={
|
|
159
|
+
<button class="cancel" onclick={() => close()}>Cancel</button>
|
|
167
160
|
</div>
|
|
168
|
-
{/if}
|
|
169
161
|
|
|
170
162
|
<style>
|
|
171
|
-
.
|
|
172
|
-
position: absolute;
|
|
173
|
-
inset: 0;
|
|
174
|
-
background: var(--sh3-overlay-backdrop, rgba(0, 0, 0, 0.35));
|
|
175
|
-
pointer-events: auto;
|
|
176
|
-
z-index: var(--sh3-z-layer-4);
|
|
177
|
-
}
|
|
178
|
-
.sheet {
|
|
179
|
-
position: absolute;
|
|
180
|
-
left: 0;
|
|
181
|
-
right: 0;
|
|
182
|
-
bottom: 0;
|
|
183
|
-
max-height: 70vh;
|
|
163
|
+
.sh3-floats-sheet {
|
|
184
164
|
display: flex;
|
|
185
165
|
flex-direction: column;
|
|
186
|
-
|
|
166
|
+
max-height: 70vh;
|
|
167
|
+
overflow: hidden;
|
|
187
168
|
color: var(--sh3-fg);
|
|
188
|
-
border-top: 1px solid var(--sh3-border);
|
|
189
|
-
box-shadow: var(--sh3-shadow-md, 0 -4px 16px rgba(0, 0, 0, 0.2));
|
|
190
|
-
pointer-events: auto;
|
|
191
|
-
z-index: var(--sh3-z-layer-4);
|
|
192
169
|
}
|
|
193
170
|
.scroll {
|
|
194
171
|
flex: 1;
|
|
@@ -207,8 +184,6 @@
|
|
|
207
184
|
color: var(--sh3-fg);
|
|
208
185
|
text-align: left;
|
|
209
186
|
cursor: pointer;
|
|
210
|
-
/* Suppress browser-claimed horizontal pan so swipe-to-close survives
|
|
211
|
-
past the system scroll-claim threshold on Android/iOS. */
|
|
212
187
|
touch-action: pan-y;
|
|
213
188
|
user-select: none;
|
|
214
189
|
}
|
|
@@ -29,8 +29,7 @@ describe('FloatsSheet', () => {
|
|
|
29
29
|
});
|
|
30
30
|
it('renders the active-layout row even when no floats exist', async () => {
|
|
31
31
|
const { container } = renderWithShell(FloatsSheet, {
|
|
32
|
-
|
|
33
|
-
onClose: () => { },
|
|
32
|
+
close: () => { },
|
|
34
33
|
});
|
|
35
34
|
await tick();
|
|
36
35
|
const rows = container.querySelectorAll('[data-sh3-floats-row]');
|
|
@@ -42,8 +41,7 @@ describe('FloatsSheet', () => {
|
|
|
42
41
|
layoutStore.tree.floats.push(makeFloat('f-2', 'Editor'));
|
|
43
42
|
layoutStore.tree.floats.push(Object.assign(Object.assign({}, makeFloat('f-3', 'Picker')), { dismissable: true }));
|
|
44
43
|
const { container } = renderWithShell(FloatsSheet, {
|
|
45
|
-
|
|
46
|
-
onClose: () => { },
|
|
44
|
+
close: () => { },
|
|
47
45
|
});
|
|
48
46
|
await tick();
|
|
49
47
|
const rows = container.querySelectorAll('[data-sh3-floats-row]');
|
|
@@ -56,7 +54,7 @@ describe('FloatsSheet', () => {
|
|
|
56
54
|
let closed = false;
|
|
57
55
|
const { container } = renderWithShell(FloatsSheet, {
|
|
58
56
|
open: true,
|
|
59
|
-
|
|
57
|
+
close: () => { closed = true; },
|
|
60
58
|
});
|
|
61
59
|
await tick();
|
|
62
60
|
const row = container.querySelector('[data-sh3-floats-row="f-9"]');
|
|
@@ -70,7 +68,7 @@ describe('FloatsSheet', () => {
|
|
|
70
68
|
let closed = false;
|
|
71
69
|
const { container } = renderWithShell(FloatsSheet, {
|
|
72
70
|
open: true,
|
|
73
|
-
|
|
71
|
+
close: () => { closed = true; },
|
|
74
72
|
});
|
|
75
73
|
await tick();
|
|
76
74
|
const row = container.querySelector('[data-sh3-floats-row="docked"]');
|
|
@@ -82,8 +80,7 @@ describe('FloatsSheet', () => {
|
|
|
82
80
|
layoutStore.tree.floats.push(makeFloat('f-11', 'Notes'));
|
|
83
81
|
compactRootStore.setRoot({ kind: 'float', floatId: 'f-11' });
|
|
84
82
|
const { container } = renderWithShell(FloatsSheet, {
|
|
85
|
-
|
|
86
|
-
onClose: () => { },
|
|
83
|
+
close: () => { },
|
|
87
84
|
});
|
|
88
85
|
await tick();
|
|
89
86
|
const cur = container.querySelector('[data-current="true"]');
|
|
@@ -111,8 +108,7 @@ describe('FloatsSheet — swipe to close', () => {
|
|
|
111
108
|
const id = floatManager.open('test:view', { title: 'Notes' });
|
|
112
109
|
expect(layoutStore.floats.find((f) => f.id === id)).toBeTruthy();
|
|
113
110
|
const { container } = renderWithShell(FloatsSheet, {
|
|
114
|
-
|
|
115
|
-
onClose: () => { },
|
|
111
|
+
close: () => { },
|
|
116
112
|
});
|
|
117
113
|
await tick();
|
|
118
114
|
const row = container.querySelector(`[data-sh3-floats-row="${id}"]`);
|
|
@@ -125,8 +121,7 @@ describe('FloatsSheet — swipe to close', () => {
|
|
|
125
121
|
});
|
|
126
122
|
it('does not let the docked row be swiped (no throw, no state change)', async () => {
|
|
127
123
|
const { container } = renderWithShell(FloatsSheet, {
|
|
128
|
-
|
|
129
|
-
onClose: () => { },
|
|
124
|
+
close: () => { },
|
|
130
125
|
});
|
|
131
126
|
await tick();
|
|
132
127
|
const row = container.querySelector('[data-sh3-floats-row="docked"]');
|
|
@@ -140,8 +135,7 @@ describe('FloatsSheet — swipe to close', () => {
|
|
|
140
135
|
it('swiping less than 40% width does not close', async () => {
|
|
141
136
|
const id = floatManager.open('test:view', { title: 'Notes' });
|
|
142
137
|
const { container } = renderWithShell(FloatsSheet, {
|
|
143
|
-
|
|
144
|
-
onClose: () => { },
|
|
138
|
+
close: () => { },
|
|
145
139
|
});
|
|
146
140
|
await tick();
|
|
147
141
|
const row = container.querySelector(`[data-sh3-floats-row="${id}"]`);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* MenuSheet — modal-hosted menu card for compact mode.
|
|
4
|
+
*
|
|
5
|
+
* Push-navigation: tapping a container (File, Edit, …) or a submenu
|
|
6
|
+
* parent replaces the entire list with the sub-items. A back button
|
|
7
|
+
* returns to the parent level. Leaf items invoke the action + close.
|
|
7
8
|
*
|
|
8
9
|
* Reads the same dispatcher state and registry as MenuBar:
|
|
9
10
|
* resolveMenuContainers(activeAppId, declared)
|
|
@@ -20,8 +21,9 @@
|
|
|
20
21
|
import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
21
22
|
import { getRegisteredApp } from '../apps/registry.svelte';
|
|
22
23
|
import { resolveLabel } from '../actions/types';
|
|
24
|
+
import Button from '../primitives/Button.svelte';
|
|
23
25
|
|
|
24
|
-
let {
|
|
26
|
+
let { close }: { close: () => void } = $props();
|
|
25
27
|
|
|
26
28
|
const dispatcher = $derived(getLiveDispatcherState());
|
|
27
29
|
const activeAppId = $derived(dispatcher.activeAppId);
|
|
@@ -30,39 +32,84 @@
|
|
|
30
32
|
return getRegisteredApp(activeAppId)?.manifest.menus;
|
|
31
33
|
});
|
|
32
34
|
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
35
|
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
// --- navigation stack ------------------------------------------------
|
|
37
|
+
// Push-navigation replaces inline expand. Tapping a container pushes
|
|
38
|
+
// onto the stack; tapping a submenu parent pushes again. Back pops.
|
|
39
|
+
type NavEntry =
|
|
40
|
+
| { kind: 'root' }
|
|
41
|
+
| { kind: 'container'; containerId: string; label: string }
|
|
42
|
+
| { kind: 'submenu'; parentId: string; label: string };
|
|
43
|
+
|
|
44
|
+
let navStack = $state<NavEntry[]>([{ kind: 'root' }]);
|
|
45
|
+
const currentNav = $derived(navStack[navStack.length - 1]);
|
|
45
46
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
if (next.has(id)) next.delete(id);
|
|
49
|
-
else next.add(id);
|
|
50
|
-
expanded = next;
|
|
47
|
+
function push(e: NavEntry) {
|
|
48
|
+
navStack = [...navStack, e];
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
else next.add(id);
|
|
57
|
-
expandedSubmenu = next;
|
|
51
|
+
function pop() {
|
|
52
|
+
if (navStack.length <= 1) return;
|
|
53
|
+
navStack = navStack.slice(0, -1);
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
// --- derived items for current nav level ---------------------------
|
|
57
|
+
const currentItems = $derived.by(() => {
|
|
58
|
+
const entries = listActions();
|
|
59
|
+
const nav = currentNav;
|
|
60
|
+
|
|
61
|
+
if (nav.kind === 'root') {
|
|
62
|
+
// Show all containers with items
|
|
63
|
+
return containers
|
|
64
|
+
.filter((c) => resolveMenuItems(entries, dispatcher, c.id).length > 0)
|
|
65
|
+
.map((c) => ({
|
|
66
|
+
id: c.id,
|
|
67
|
+
label: c.label,
|
|
68
|
+
isContainer: true as const,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (nav.kind === 'container') {
|
|
73
|
+
const items = resolveMenuItems(entries, dispatcher, nav.containerId);
|
|
74
|
+
return items.map((item) => ({
|
|
75
|
+
id: item.id,
|
|
76
|
+
label: item.label,
|
|
77
|
+
shortcut: item.shortcut,
|
|
78
|
+
isSubmenu: item.submenu === true,
|
|
79
|
+
disabled: item.disabled,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// submenu
|
|
84
|
+
const items = resolveSubmenuItems(entries, dispatcher, nav.parentId);
|
|
85
|
+
return items.map((item) => ({
|
|
86
|
+
id: item.id,
|
|
87
|
+
label: item.label,
|
|
88
|
+
shortcut: item.shortcut,
|
|
89
|
+
disabled: item.disabled,
|
|
90
|
+
isSubmenu: item.submenu === true,
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// --- actions --------------------------------------------------------
|
|
95
|
+
function handleTap(entry: { id: string; isContainer?: boolean; isSubmenu?: boolean }) {
|
|
96
|
+
if (entry.isContainer) {
|
|
97
|
+
const c = containers.find((x) => x.id === entry.id);
|
|
98
|
+
if (c) push({ kind: 'container', containerId: c.id, label: c.label });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (entry.isSubmenu) {
|
|
103
|
+
push({ kind: 'submenu', parentId: entry.id, label: entry.label });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// leaf item — invoke action
|
|
108
|
+
const actionEntry = listActions().find((e) => e.action.id === entry.id);
|
|
109
|
+
if (!actionEntry || typeof actionEntry.action.run !== 'function') return;
|
|
63
110
|
try {
|
|
64
|
-
void
|
|
65
|
-
action: { id:
|
|
111
|
+
void actionEntry.action.run({
|
|
112
|
+
action: { id: entry.id, label: resolveLabel(actionEntry.action) },
|
|
66
113
|
appId: dispatcher.activeAppId,
|
|
67
114
|
viewId: dispatcher.focusedViewId ?? undefined,
|
|
68
115
|
selection: dispatcher.selection ?? undefined,
|
|
@@ -70,155 +117,114 @@
|
|
|
70
117
|
dispatch: () => {},
|
|
71
118
|
});
|
|
72
119
|
} catch (err) {
|
|
73
|
-
console.error(`[sh3] menu-sheet action "${
|
|
120
|
+
console.error(`[sh3] menu-sheet action "${entry.id}" threw:`, err);
|
|
121
|
+
}
|
|
122
|
+
close();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function back() {
|
|
126
|
+
pop();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function onKeydown(e: KeyboardEvent) {
|
|
130
|
+
if (e.key === 'Escape') {
|
|
131
|
+
if (navStack.length > 1) {
|
|
132
|
+
pop();
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
} else {
|
|
135
|
+
close();
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
74
138
|
}
|
|
75
|
-
onClose();
|
|
76
139
|
}
|
|
77
140
|
</script>
|
|
78
141
|
|
|
79
|
-
{
|
|
80
|
-
<div
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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>
|
|
142
|
+
<div class="sh3-menu-sheet" role="dialog" aria-label="Menu" tabindex="-1" data-sh3-region="menu-sheet" onkeydown={onKeydown}>
|
|
143
|
+
<div class="head">
|
|
144
|
+
{#if navStack.length > 1}
|
|
145
|
+
<Button variant="icon" icon="chevron-left" ariaLabel="Back" title="Back" onclick={back} />
|
|
146
|
+
{/if}
|
|
147
|
+
<span class="title">
|
|
148
|
+
{currentNav.kind === 'root' ? 'Menu' : currentNav.label}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="scroll">
|
|
152
|
+
{#each currentItems as entry (entry.id)}
|
|
153
|
+
<button
|
|
154
|
+
class="item"
|
|
155
|
+
disabled={entry.disabled}
|
|
156
|
+
onclick={() => handleTap(entry)}
|
|
157
|
+
>
|
|
158
|
+
<span class="label">{entry.label}</span>
|
|
159
|
+
{#if entry.isContainer || entry.isSubmenu}
|
|
160
|
+
<span class="chevron">›</span>
|
|
161
|
+
{:else if entry.shortcut}
|
|
162
|
+
<span class="shortcut">{entry.shortcut}</span>
|
|
140
163
|
{/if}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<button class="cancel" onclick={onClose}>Cancel</button>
|
|
164
|
+
</button>
|
|
165
|
+
{/each}
|
|
144
166
|
</div>
|
|
167
|
+
{#if currentItems.length === 0}
|
|
168
|
+
<div class="empty">No menu items available.</div>
|
|
145
169
|
{/if}
|
|
170
|
+
</div>
|
|
146
171
|
|
|
147
172
|
<style>
|
|
148
|
-
.
|
|
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;
|
|
173
|
+
.sh3-menu-sheet {
|
|
161
174
|
display: flex;
|
|
162
175
|
flex-direction: column;
|
|
163
|
-
|
|
176
|
+
max-height: 70vh;
|
|
177
|
+
overflow: hidden;
|
|
164
178
|
color: var(--sh3-fg);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
}
|
|
180
|
+
.head {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: var(--sh3-pad-xs);
|
|
184
|
+
padding: 6px 8px;
|
|
185
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
186
|
+
}
|
|
187
|
+
.title {
|
|
188
|
+
font-size: 10px;
|
|
189
|
+
text-transform: uppercase;
|
|
190
|
+
letter-spacing: 0.5px;
|
|
191
|
+
color: var(--sh3-fg-muted);
|
|
169
192
|
}
|
|
170
193
|
.scroll {
|
|
171
194
|
flex: 1;
|
|
172
195
|
min-height: 0;
|
|
173
196
|
overflow: auto;
|
|
174
|
-
padding:
|
|
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;
|
|
197
|
+
padding: 4px 0;
|
|
188
198
|
}
|
|
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
199
|
.item {
|
|
193
200
|
display: flex;
|
|
194
201
|
align-items: center;
|
|
195
202
|
gap: var(--sh3-pad-sm);
|
|
196
203
|
width: 100%;
|
|
197
|
-
padding:
|
|
204
|
+
padding: 9px var(--sh3-pad-md);
|
|
198
205
|
border: none;
|
|
199
206
|
background: none;
|
|
200
207
|
color: var(--sh3-fg);
|
|
201
208
|
text-align: left;
|
|
202
209
|
cursor: pointer;
|
|
210
|
+
font: inherit;
|
|
203
211
|
}
|
|
204
212
|
.item:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
205
213
|
.item:active:not(:disabled) { background: var(--sh3-bg-sunken); }
|
|
206
|
-
.item.child { padding-left: calc(var(--sh3-pad-md) * 2); }
|
|
207
214
|
.label { flex: 1; }
|
|
208
|
-
.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
width: 1em;
|
|
212
|
-
transition: transform 120ms;
|
|
215
|
+
.chevron {
|
|
216
|
+
opacity: 0.3;
|
|
217
|
+
font-size: 14px;
|
|
213
218
|
}
|
|
214
|
-
.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
.shortcut {
|
|
220
|
+
color: var(--sh3-fg-muted);
|
|
221
|
+
font-family: var(--sh3-font-mono);
|
|
222
|
+
font-size: 0.9em;
|
|
223
|
+
}
|
|
224
|
+
.empty {
|
|
225
|
+
padding: 20px 12px;
|
|
226
|
+
text-align: center;
|
|
227
|
+
color: var(--sh3-fg-muted);
|
|
228
|
+
font-style: italic;
|
|
223
229
|
}
|
|
224
230
|
</style>
|