sh3-core 0.5.2 → 0.5.5

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 (54) hide show
  1. package/dist/Shell.svelte +6 -3
  2. package/dist/admin/AuthSettingsView.svelte +105 -0
  3. package/dist/admin/AuthSettingsView.svelte.d.ts +3 -0
  4. package/dist/admin/SystemView.svelte +73 -0
  5. package/dist/admin/SystemView.svelte.d.ts +3 -0
  6. package/dist/admin/UsersView.svelte +189 -0
  7. package/dist/admin/UsersView.svelte.d.ts +3 -0
  8. package/dist/admin/adminApp.d.ts +7 -0
  9. package/dist/admin/adminApp.js +24 -0
  10. package/dist/admin/adminShard.svelte.d.ts +4 -0
  11. package/dist/admin/adminShard.svelte.js +52 -0
  12. package/dist/api.d.ts +2 -1
  13. package/dist/api.js +1 -1
  14. package/dist/apps/lifecycle.d.ts +6 -1
  15. package/dist/apps/lifecycle.js +28 -4
  16. package/dist/apps/registry.svelte.d.ts +5 -2
  17. package/dist/apps/registry.svelte.js +6 -7
  18. package/dist/apps/types.d.ts +13 -0
  19. package/dist/auth/GuestBanner.svelte +144 -0
  20. package/dist/auth/GuestBanner.svelte.d.ts +3 -0
  21. package/dist/auth/SignInWall.svelte +213 -0
  22. package/dist/auth/SignInWall.svelte.d.ts +8 -0
  23. package/dist/auth/auth.svelte.d.ts +42 -31
  24. package/dist/auth/auth.svelte.js +106 -89
  25. package/dist/auth/index.d.ts +2 -1
  26. package/dist/auth/index.js +1 -1
  27. package/dist/auth/types.d.ts +41 -0
  28. package/dist/auth/types.js +6 -0
  29. package/dist/build.js +70 -16
  30. package/dist/createShell.d.ts +2 -2
  31. package/dist/createShell.js +78 -33
  32. package/dist/diagnostic/DiagnosticPromptModal.svelte +1 -1
  33. package/dist/host-entry.d.ts +2 -1
  34. package/dist/host-entry.js +2 -2
  35. package/dist/host.d.ts +0 -2
  36. package/dist/host.js +11 -25
  37. package/dist/layout/DragPreview.svelte +1 -1
  38. package/dist/overlays/ModalFrame.svelte +1 -1
  39. package/dist/overlays/PopupFrame.svelte +1 -1
  40. package/dist/overlays/ToastItem.svelte +1 -1
  41. package/dist/primitives/TabbedPanel.svelte +1 -1
  42. package/dist/registry/installer.js +0 -2
  43. package/dist/shards/activate.svelte.d.ts +13 -6
  44. package/dist/shards/activate.svelte.js +19 -8
  45. package/dist/shards/types.d.ts +11 -0
  46. package/dist/shell-shard/ShellHome.svelte +32 -118
  47. package/dist/store/InstalledView.svelte +7 -7
  48. package/dist/store/StoreView.svelte +16 -16
  49. package/dist/store/storeApp.js +1 -1
  50. package/dist/store/storeShard.svelte.js +5 -4
  51. package/dist/tokens.css +14 -0
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/package.json +1 -1
package/dist/Shell.svelte CHANGED
@@ -19,6 +19,7 @@
19
19
  import { registerLayerRoot, unregisterLayerRoot } from './overlays/roots';
20
20
  import { returnToHome, getActiveApp } from './api';
21
21
  import iconsUrl from './assets/icons.svg';
22
+ import GuestBanner from './auth/GuestBanner.svelte';
22
23
 
23
24
  // Layer metadata — order matches the stack in docs/design/layout.md.
24
25
  // Index 0 here is layer 1 (floating panels); layer 0 is the content area.
@@ -64,6 +65,8 @@
64
65
  {/if}
65
66
  </header>
66
67
 
68
+ <GuestBanner />
69
+
67
70
  <main class="shell-content" data-shell-region="content" data-shell-layer="0">
68
71
  <LayoutRenderer />
69
72
  </main>
@@ -97,7 +100,7 @@
97
100
  <style>
98
101
  .shell {
99
102
  display: grid;
100
- grid-template-rows: var(--shell-tabbar-height) 1fr var(--shell-statusbar-height);
103
+ grid-template-rows: var(--shell-tabbar-height) auto 1fr var(--shell-statusbar-height);
101
104
  height: 100%;
102
105
  width: 100%;
103
106
  position: relative;
@@ -160,7 +163,7 @@
160
163
  background: transparent;
161
164
  color: var(--shell-fg-muted);
162
165
  border: 1px solid var(--shell-border);
163
- border-radius: 4px;
166
+ border-radius: var(--shell-radius);
164
167
  cursor: pointer;
165
168
  }
166
169
  .shell-tabbar-home-button:hover {
@@ -180,6 +183,6 @@
180
183
  color: #fff;
181
184
  background: var(--shell-accent);
182
185
  padding: 1px 6px;
183
- border-radius: 8px;
186
+ border-radius: var(--shell-radius-lg);
184
187
  }
185
188
  </style>
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Admin Auth Settings view — toggle auth-related global settings.
4
+ */
5
+
6
+ import type { GlobalSettings } from '../auth/types';
7
+
8
+ let settings = $state<GlobalSettings | null>(null);
9
+ let loading = $state(true);
10
+ let saving = $state(false);
11
+ let error = $state<string | null>(null);
12
+
13
+ async function fetchSettings() {
14
+ loading = true;
15
+ try {
16
+ const res = await fetch('/api/admin/settings', { credentials: 'include' });
17
+ if (!res.ok) throw new Error('Failed to fetch settings');
18
+ settings = await res.json();
19
+ } catch (err) {
20
+ error = err instanceof Error ? err.message : 'Failed to load settings';
21
+ } finally {
22
+ loading = false;
23
+ }
24
+ }
25
+
26
+ async function save() {
27
+ if (!settings) return;
28
+ saving = true;
29
+ error = null;
30
+ try {
31
+ const res = await fetch('/api/admin/settings', {
32
+ method: 'PUT',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ credentials: 'include',
35
+ body: JSON.stringify(settings),
36
+ });
37
+ if (!res.ok) throw new Error('Failed to save settings');
38
+ settings = await res.json();
39
+ } catch (err) {
40
+ error = err instanceof Error ? err.message : 'Failed to save';
41
+ } finally {
42
+ saving = false;
43
+ }
44
+ }
45
+
46
+ fetchSettings();
47
+ </script>
48
+
49
+ <div class="admin-auth">
50
+ <h2>Auth Settings</h2>
51
+
52
+ {#if loading}
53
+ <p class="admin-muted">Loading...</p>
54
+ {:else if settings}
55
+ <div class="admin-auth-fields">
56
+ <label class="admin-toggle">
57
+ <input type="checkbox" bind:checked={settings.auth.required} />
58
+ <span>Require sign-in</span>
59
+ <span class="admin-hint">When enabled, unauthenticated visitors see a sign-in wall.</span>
60
+ </label>
61
+
62
+ <label class="admin-toggle">
63
+ <input type="checkbox" bind:checked={settings.auth.guestAllowed} />
64
+ <span>Allow guest browsing</span>
65
+ <span class="admin-hint">When sign-in is required, guests can still browse with session-only data.</span>
66
+ </label>
67
+
68
+ <label class="admin-toggle">
69
+ <input type="checkbox" bind:checked={settings.auth.selfRegistration} />
70
+ <span>Self-registration</span>
71
+ <span class="admin-hint">Visitors can create their own accounts from the sign-in screen.</span>
72
+ </label>
73
+
74
+ <label class="admin-field">
75
+ <span>Session lifetime (hours)</span>
76
+ <input type="number" class="admin-input admin-input-sm" min="1" max="8760" bind:value={settings.auth.sessionTTL} />
77
+ </label>
78
+
79
+ <button type="button" class="admin-btn" onclick={save} disabled={saving}>
80
+ {saving ? 'Saving...' : 'Save'}
81
+ </button>
82
+ </div>
83
+
84
+ {#if error}
85
+ <div class="admin-error">{error}</div>
86
+ {/if}
87
+ {/if}
88
+ </div>
89
+
90
+ <style>
91
+ .admin-auth { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
92
+ .admin-auth h2 { margin: 0 0 16px; font-size: 18px; }
93
+ .admin-auth-fields { display: flex; flex-direction: column; gap: 16px; max-width: 480px; }
94
+ .admin-toggle { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; cursor: pointer; }
95
+ .admin-toggle input { accent-color: var(--shell-accent, #7c7cf0); }
96
+ .admin-hint { flex-basis: 100%; font-size: 11px; color: var(--shell-fg-muted); margin-left: 24px; }
97
+ .admin-field { display: flex; flex-direction: column; gap: 4px; }
98
+ .admin-field span { font-size: 13px; }
99
+ .admin-input { padding: 8px 12px; background: var(--shell-bg); color: var(--shell-fg); border: 1px solid var(--shell-border); border-radius: var(--shell-radius, 6px); font-size: 13px; }
100
+ .admin-input-sm { max-width: 120px; }
101
+ .admin-btn { padding: 8px 16px; background: var(--shell-accent, #7c7cf0); color: var(--shell-bg); border: none; border-radius: var(--shell-radius, 6px); font-weight: 600; cursor: pointer; align-self: flex-start; }
102
+ .admin-btn:disabled { opacity: 0.6; cursor: not-allowed; }
103
+ .admin-error { margin-top: 8px; color: var(--shell-error, #d32f2f); font-size: 13px; }
104
+ .admin-muted { color: var(--shell-fg-muted); font-style: italic; }
105
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const AuthSettingsView: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type AuthSettingsView = ReturnType<typeof AuthSettingsView>;
3
+ export default AuthSettingsView;
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Admin System view — server status and restart.
4
+ */
5
+
6
+ let version = $state('...');
7
+ let restarting = $state(false);
8
+ let restartError = $state<string | null>(null);
9
+
10
+ async function fetchVersion() {
11
+ try {
12
+ const res = await fetch('/api/version');
13
+ if (res.ok) {
14
+ const body = await res.json();
15
+ version = body.version;
16
+ }
17
+ } catch { /* ignore */ }
18
+ }
19
+
20
+ async function restart() {
21
+ restarting = true;
22
+ restartError = null;
23
+ try {
24
+ const res = await fetch('/api/admin/restart', {
25
+ method: 'POST',
26
+ credentials: 'include',
27
+ });
28
+ if (!res.ok) {
29
+ const body = await res.json().catch(() => ({}));
30
+ restartError = body.error || 'Restart failed';
31
+ restarting = false;
32
+ }
33
+ // If 202, server will restart — page will reconnect
34
+ } catch {
35
+ restartError = 'Network error';
36
+ restarting = false;
37
+ }
38
+ }
39
+
40
+ fetchVersion();
41
+ </script>
42
+
43
+ <div class="admin-system">
44
+ <h2>System</h2>
45
+
46
+ <div class="admin-system-info">
47
+ <div class="admin-system-row">
48
+ <span class="admin-system-label">Server version</span>
49
+ <span>{version}</span>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="admin-system-actions">
54
+ <button type="button" class="admin-btn-danger" onclick={restart} disabled={restarting}>
55
+ {restarting ? 'Restarting...' : 'Restart server'}
56
+ </button>
57
+ {#if restartError}
58
+ <div class="admin-error">{restartError}</div>
59
+ {/if}
60
+ </div>
61
+ </div>
62
+
63
+ <style>
64
+ .admin-system { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
65
+ .admin-system h2 { margin: 0 0 16px; font-size: 18px; }
66
+ .admin-system-info { margin-bottom: 24px; }
67
+ .admin-system-row { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--shell-border, #3a3a5c); font-size: 13px; }
68
+ .admin-system-label { color: var(--shell-fg-subtle); min-width: 140px; }
69
+ .admin-system-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
70
+ .admin-btn-danger { padding: 8px 16px; background: transparent; color: var(--shell-error, #d32f2f); border: 1px solid var(--shell-error, #d32f2f); border-radius: var(--shell-radius, 6px); font-weight: 600; cursor: pointer; }
71
+ .admin-btn-danger:disabled { opacity: 0.6; cursor: not-allowed; }
72
+ .admin-error { color: var(--shell-error, #d32f2f); font-size: 13px; }
73
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const SystemView: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SystemView = ReturnType<typeof SystemView>;
3
+ export default SystemView;
@@ -0,0 +1,189 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Admin Users view — list, create, edit, delete users.
4
+ */
5
+
6
+ import { getAuthHeader } from '../auth/index';
7
+ import type { AuthUser } from '../auth/types';
8
+
9
+ let users = $state<AuthUser[]>([]);
10
+ let loading = $state(true);
11
+ let error = $state<string | null>(null);
12
+
13
+ // Create form
14
+ let showCreate = $state(false);
15
+ let newUsername = $state('');
16
+ let newDisplayName = $state('');
17
+ let newPassword = $state('');
18
+ let newRole = $state<'admin' | 'user'>('user');
19
+ let createError = $state<string | null>(null);
20
+
21
+ // Edit state
22
+ let editingId = $state<string | null>(null);
23
+ let editDisplayName = $state('');
24
+ let editRole = $state<'admin' | 'user'>('user');
25
+ let editPassword = $state('');
26
+
27
+ async function fetchUsers() {
28
+ loading = true;
29
+ error = null;
30
+ try {
31
+ const res = await fetch('/api/admin/users', { credentials: 'include' });
32
+ if (!res.ok) throw new Error('Failed to fetch users');
33
+ users = await res.json();
34
+ } catch (err) {
35
+ error = err instanceof Error ? err.message : 'Failed to load users';
36
+ } finally {
37
+ loading = false;
38
+ }
39
+ }
40
+
41
+ async function createUser() {
42
+ createError = null;
43
+ try {
44
+ const res = await fetch('/api/admin/users', {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ credentials: 'include',
48
+ body: JSON.stringify({
49
+ username: newUsername,
50
+ displayName: newDisplayName || newUsername,
51
+ password: newPassword,
52
+ role: newRole,
53
+ }),
54
+ });
55
+ if (!res.ok) {
56
+ const body = await res.json().catch(() => ({}));
57
+ createError = body.error || 'Failed to create user';
58
+ return;
59
+ }
60
+ newUsername = '';
61
+ newDisplayName = '';
62
+ newPassword = '';
63
+ newRole = 'user';
64
+ showCreate = false;
65
+ await fetchUsers();
66
+ } catch {
67
+ createError = 'Network error';
68
+ }
69
+ }
70
+
71
+ function startEdit(user: AuthUser) {
72
+ editingId = user.id;
73
+ editDisplayName = user.displayName;
74
+ editRole = user.role;
75
+ editPassword = '';
76
+ }
77
+
78
+ async function saveEdit() {
79
+ if (!editingId) return;
80
+ const patch: Record<string, unknown> = {
81
+ displayName: editDisplayName,
82
+ role: editRole,
83
+ };
84
+ if (editPassword.trim()) patch.password = editPassword;
85
+ try {
86
+ const res = await fetch(`/api/admin/users/${editingId}`, {
87
+ method: 'PUT',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ credentials: 'include',
90
+ body: JSON.stringify(patch),
91
+ });
92
+ if (!res.ok) return;
93
+ editingId = null;
94
+ await fetchUsers();
95
+ } catch { /* ignore */ }
96
+ }
97
+
98
+ async function deleteUser(id: string) {
99
+ try {
100
+ await fetch(`/api/admin/users/${id}`, {
101
+ method: 'DELETE',
102
+ credentials: 'include',
103
+ });
104
+ await fetchUsers();
105
+ } catch { /* ignore */ }
106
+ }
107
+
108
+ fetchUsers();
109
+ </script>
110
+
111
+ <div class="admin-users">
112
+ <div class="admin-users-header">
113
+ <h2>Users</h2>
114
+ <button type="button" class="admin-btn" onclick={() => { showCreate = !showCreate; }}>
115
+ {showCreate ? 'Cancel' : 'New user'}
116
+ </button>
117
+ </div>
118
+
119
+ {#if showCreate}
120
+ <form class="admin-create-form" onsubmit={(e) => { e.preventDefault(); createUser(); }}>
121
+ <input class="admin-input" type="text" placeholder="Username" bind:value={newUsername} />
122
+ <input class="admin-input" type="text" placeholder="Display name" bind:value={newDisplayName} />
123
+ <input class="admin-input" type="password" placeholder="Password" bind:value={newPassword} />
124
+ <select class="admin-input" bind:value={newRole}>
125
+ <option value="user">User</option>
126
+ <option value="admin">Admin</option>
127
+ </select>
128
+ <button type="submit" class="admin-btn" disabled={!newUsername.trim() || !newPassword.trim()}>Create</button>
129
+ {#if createError}<div class="admin-error">{createError}</div>{/if}
130
+ </form>
131
+ {/if}
132
+
133
+ {#if loading}
134
+ <p class="admin-muted">Loading...</p>
135
+ {:else if error}
136
+ <p class="admin-error">{error}</p>
137
+ {:else}
138
+ <ul class="admin-user-list">
139
+ {#each users as user (user.id)}
140
+ <li class="admin-user-item">
141
+ {#if editingId === user.id}
142
+ <form class="admin-edit-form" onsubmit={(e) => { e.preventDefault(); saveEdit(); }}>
143
+ <input class="admin-input" type="text" bind:value={editDisplayName} />
144
+ <input class="admin-input" type="password" placeholder="New password (leave empty to keep)" bind:value={editPassword} />
145
+ <select class="admin-input" bind:value={editRole}>
146
+ <option value="user">User</option>
147
+ <option value="admin">Admin</option>
148
+ </select>
149
+ <div class="admin-edit-actions">
150
+ <button type="submit" class="admin-btn">Save</button>
151
+ <button type="button" class="admin-btn-secondary" onclick={() => { editingId = null; }}>Cancel</button>
152
+ </div>
153
+ </form>
154
+ {:else}
155
+ <div class="admin-user-info">
156
+ <span class="admin-user-name">{user.displayName}</span>
157
+ <span class="admin-user-meta">{user.username} · {user.role}</span>
158
+ </div>
159
+ <div class="admin-user-actions">
160
+ <button type="button" class="admin-btn-secondary" onclick={() => startEdit(user)}>Edit</button>
161
+ <button type="button" class="admin-btn-danger" onclick={() => deleteUser(user.id)}>Delete</button>
162
+ </div>
163
+ {/if}
164
+ </li>
165
+ {/each}
166
+ </ul>
167
+ {/if}
168
+ </div>
169
+
170
+ <style>
171
+ .admin-users { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
172
+ .admin-users-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
173
+ .admin-users-header h2 { margin: 0; font-size: 18px; }
174
+ .admin-create-form, .admin-edit-form { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; max-width: 400px; }
175
+ .admin-input { padding: 8px 12px; background: var(--shell-bg, #1a1a2e); color: var(--shell-fg); border: 1px solid var(--shell-border, #3a3a5c); border-radius: var(--shell-radius, 6px); font-size: 13px; }
176
+ .admin-btn { padding: 6px 14px; background: var(--shell-accent, #7c7cf0); color: var(--shell-bg); border: none; border-radius: var(--shell-radius, 6px); font-weight: 600; cursor: pointer; font-size: 13px; }
177
+ .admin-btn:disabled { opacity: 0.6; cursor: not-allowed; }
178
+ .admin-btn-secondary { padding: 6px 14px; background: transparent; color: var(--shell-fg-subtle); border: 1px solid var(--shell-border); border-radius: var(--shell-radius, 6px); cursor: pointer; font-size: 12px; }
179
+ .admin-btn-danger { padding: 6px 14px; background: transparent; color: var(--shell-error, #d32f2f); border: 1px solid var(--shell-error, #d32f2f); border-radius: var(--shell-radius, 6px); cursor: pointer; font-size: 12px; }
180
+ .admin-user-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
181
+ .admin-user-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--shell-bg-elevated, #252540); border: 1px solid var(--shell-border, #3a3a5c); border-radius: var(--shell-radius, 6px); }
182
+ .admin-user-info { display: flex; flex-direction: column; gap: 2px; }
183
+ .admin-user-name { font-weight: 600; }
184
+ .admin-user-meta { font-size: 11px; color: var(--shell-fg-subtle); }
185
+ .admin-user-actions { display: flex; gap: 6px; }
186
+ .admin-edit-actions { display: flex; gap: 6px; }
187
+ .admin-error { color: var(--shell-error, #d32f2f); font-size: 13px; }
188
+ .admin-muted { color: var(--shell-fg-muted); font-style: italic; }
189
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const UsersView: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type UsersView = ReturnType<typeof UsersView>;
3
+ export default UsersView;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Built-in Admin app — user management, auth settings, and system controls.
3
+ * Framework-shipped: registered in host.ts during bootstrap.
4
+ * Admin-gated via manifest flag (ADR-011).
5
+ */
6
+ import type { App } from '../apps/types';
7
+ export declare const adminApp: App;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Built-in Admin app — user management, auth settings, and system controls.
3
+ * Framework-shipped: registered in host.ts during bootstrap.
4
+ * Admin-gated via manifest flag (ADR-011).
5
+ */
6
+ export const adminApp = {
7
+ manifest: {
8
+ id: 'sh3-admin-app',
9
+ label: 'Admin',
10
+ version: '0.1.0',
11
+ requiredShards: ['sh3-admin'],
12
+ layoutVersion: 1,
13
+ admin: true,
14
+ },
15
+ initialLayout: {
16
+ type: 'tabs',
17
+ activeTab: 0,
18
+ tabs: [
19
+ { slotId: 'admin.users', viewId: 'sh3-admin:users', label: 'Users' },
20
+ { slotId: 'admin.auth', viewId: 'sh3-admin:auth', label: 'Auth' },
21
+ { slotId: 'admin.system', viewId: 'sh3-admin:system', label: 'System' },
22
+ ],
23
+ },
24
+ };
@@ -0,0 +1,4 @@
1
+ import type { Shard } from '../shards/types';
2
+ /** Module-level server URL, set during activate. */
3
+ export declare let adminServerUrl: string;
4
+ export declare const adminShard: Shard;
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Admin shard — framework-shipped shard for user management,
3
+ * auth settings, and system controls.
4
+ *
5
+ * Contributes three views:
6
+ * - `sh3-admin:users` — user CRUD
7
+ * - `sh3-admin:auth` — auth settings toggles
8
+ * - `sh3-admin:system` — restart + status
9
+ *
10
+ * `.svelte.ts` because mounting Svelte components requires rune access.
11
+ */
12
+ import { mount, unmount } from 'svelte';
13
+ import UsersView from './UsersView.svelte';
14
+ import AuthSettingsView from './AuthSettingsView.svelte';
15
+ import SystemView from './SystemView.svelte';
16
+ /** Module-level server URL, set during activate. */
17
+ export let adminServerUrl = '';
18
+ export const adminShard = {
19
+ manifest: {
20
+ id: 'sh3-admin',
21
+ label: 'Admin',
22
+ version: '0.1.0',
23
+ views: [
24
+ { id: 'sh3-admin:users', label: 'Users' },
25
+ { id: 'sh3-admin:auth', label: 'Auth Settings' },
26
+ { id: 'sh3-admin:system', label: 'System' },
27
+ ],
28
+ },
29
+ activate(ctx) {
30
+ const usersFactory = {
31
+ mount(container, _context) {
32
+ const instance = mount(UsersView, { target: container });
33
+ return { unmount() { unmount(instance); } };
34
+ },
35
+ };
36
+ const authFactory = {
37
+ mount(container, _context) {
38
+ const instance = mount(AuthSettingsView, { target: container });
39
+ return { unmount() { unmount(instance); } };
40
+ },
41
+ };
42
+ const systemFactory = {
43
+ mount(container, _context) {
44
+ const instance = mount(SystemView, { target: container });
45
+ return { unmount() { unmount(instance); } };
46
+ },
47
+ };
48
+ ctx.registerView('sh3-admin:users', usersFactory);
49
+ ctx.registerView('sh3-admin:auth', authFactory);
50
+ ctx.registerView('sh3-admin:system', systemFactory);
51
+ },
52
+ };
package/dist/api.d.ts CHANGED
@@ -16,7 +16,8 @@ export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, I
16
16
  export type { ResolvedPackage } from './registry/client';
17
17
  export { fetchRegistries, fetchBundle, buildPackageMeta } from './registry/client';
18
18
  export { validateRegistryIndex } from './registry/schema';
19
- export { isAdmin, getAuthHeader } from './auth/index';
19
+ export { isAdmin, isAuthenticated, isGuest, getUser, getAuthHeader } from './auth/index';
20
+ export type { AuthUser, AuthSession, BootConfig } from './auth/types';
20
21
  /** Runtime feature flags for target-dependent behavior. */
21
22
  export declare const capabilities: {
22
23
  /** Whether this target supports hot-installing packages via dynamic import from blob URL. */
package/dist/api.js CHANGED
@@ -38,7 +38,7 @@ export { registeredShards, activeShards } from './shards/activate.svelte';
38
38
  export { fetchRegistries, fetchBundle, buildPackageMeta } from './registry/client';
39
39
  export { validateRegistryIndex } from './registry/schema';
40
40
  // Admin mode (framework-internal components read admin status).
41
- export { isAdmin, getAuthHeader } from './auth/index';
41
+ export { isAdmin, isAuthenticated, isGuest, getUser, getAuthHeader } from './auth/index';
42
42
  /** Runtime feature flags for target-dependent behavior. */
43
43
  export const capabilities = {
44
44
  /** Whether this target supports hot-installing packages via dynamic import from blob URL. */
@@ -31,7 +31,12 @@ export declare function unloadApp(id: string): void;
31
31
  * refcount hold intact, and its view containers stay alive in the pool.
32
32
  * Launching the same app again is a root swap only.
33
33
  *
34
+ * Fires `suspend` hooks on all required shards (in order), then on the
35
+ * app itself. Any hook returning `false` cancels navigation — the user
36
+ * stays in the app. Returns `true` if navigation succeeded, `false` if
37
+ * cancelled.
38
+ *
34
39
  * Writes `null` to `__shell__:last-app` so reloading the page while on
35
40
  * home lands on home, not on the formerly-active app.
36
41
  */
37
- export declare function returnToHome(): void;
42
+ export declare function returnToHome(): Promise<boolean>;
@@ -12,7 +12,7 @@
12
12
  * return-to-home (null). Boot reads it to decide whether to auto-launch.
13
13
  */
14
14
  import { createStateZones } from '../state/zones.svelte';
15
- import { activateShard, deactivateShard, registeredShards, } from '../shards/activate.svelte';
15
+ import { activateShard, deactivateShard, getShardContext, registeredShards, } from '../shards/activate.svelte';
16
16
  import { attachApp, detachApp, switchToApp, switchToHome, } from '../layout/store.svelte';
17
17
  import { activeApp, getRegisteredApp } from './registry.svelte';
18
18
  import { createZoneManager } from '../state/manage';
@@ -69,7 +69,7 @@ function getOrCreateAppContext(appId) {
69
69
  * @throws If the app is not registered or a required shard is not registered.
70
70
  */
71
71
  export async function launchApp(id) {
72
- var _a;
72
+ var _a, _b, _c;
73
73
  const app = getRegisteredApp(id);
74
74
  if (!app) {
75
75
  throw new Error(`Cannot launch app "${id}": not registered`);
@@ -82,6 +82,14 @@ export async function launchApp(id) {
82
82
  unloadApp(activeApp.id);
83
83
  }
84
84
  else if (activeApp.id === id) {
85
+ // Re-entering the same app from Home — fire resume hooks.
86
+ for (const shardId of app.manifest.requiredShards) {
87
+ const shard = registeredShards.get(shardId);
88
+ const shardCtx = getShardContext(shardId);
89
+ if (shard && shardCtx)
90
+ void ((_a = shard.resume) === null || _a === void 0 ? void 0 : _a.call(shard, shardCtx));
91
+ }
92
+ void ((_b = app.resume) === null || _b === void 0 ? void 0 : _b.call(app, getOrCreateAppContext(id)));
85
93
  switchToApp();
86
94
  writeLastApp(id);
87
95
  return;
@@ -99,7 +107,7 @@ export async function launchApp(id) {
99
107
  // Attach the layout (creates the workspace-zone proxy with version
100
108
  // gate) and run the app's optional activate hook.
101
109
  attachApp(app);
102
- void ((_a = app.activate) === null || _a === void 0 ? void 0 : _a.call(app, getOrCreateAppContext(id)));
110
+ void ((_c = app.activate) === null || _c === void 0 ? void 0 : _c.call(app, getOrCreateAppContext(id)));
103
111
  activeApp.id = id;
104
112
  switchToApp();
105
113
  writeLastApp(id);
@@ -151,10 +159,26 @@ export function unloadApp(id) {
151
159
  * refcount hold intact, and its view containers stay alive in the pool.
152
160
  * Launching the same app again is a root swap only.
153
161
  *
162
+ * Fires `suspend` hooks on all required shards (in order), then on the
163
+ * app itself. Any hook returning `false` cancels navigation — the user
164
+ * stays in the app. Returns `true` if navigation succeeded, `false` if
165
+ * cancelled.
166
+ *
154
167
  * Writes `null` to `__shell__:last-app` so reloading the page while on
155
168
  * home lands on home, not on the formerly-active app.
156
169
  */
157
- export function returnToHome() {
170
+ export async function returnToHome() {
171
+ const app = activeApp.id ? getRegisteredApp(activeApp.id) : null;
172
+ if (app) {
173
+ for (const shardId of app.manifest.requiredShards) {
174
+ const shard = registeredShards.get(shardId);
175
+ if ((shard === null || shard === void 0 ? void 0 : shard.suspend) && (await shard.suspend()) === false)
176
+ return false;
177
+ }
178
+ if (app.suspend && (await app.suspend()) === false)
179
+ return false;
180
+ }
158
181
  switchToHome();
159
182
  writeLastApp(null);
183
+ return true;
160
184
  }
@@ -14,8 +14,11 @@ export declare const activeApp: {
14
14
  id: string | null;
15
15
  };
16
16
  /**
17
- * Register an app with the framework. Must be called before `launchApp`.
18
- * Throws if an app with the same id is already registered.
17
+ * Register (or re-register) an app with the framework.
18
+ *
19
+ * If an app with the same id already exists it is silently replaced,
20
+ * which is the expected path during package updates — the new bundle is
21
+ * loaded and re-registered without requiring a full page reload.
19
22
  *
20
23
  * @param app - The app module to register.
21
24
  */
@@ -21,17 +21,16 @@ export const registeredApps = $state(new Map());
21
21
  */
22
22
  export const activeApp = $state({ id: null });
23
23
  /**
24
- * Register an app with the framework. Must be called before `launchApp`.
25
- * Throws if an app with the same id is already registered.
24
+ * Register (or re-register) an app with the framework.
25
+ *
26
+ * If an app with the same id already exists it is silently replaced,
27
+ * which is the expected path during package updates — the new bundle is
28
+ * loaded and re-registered without requiring a full page reload.
26
29
  *
27
30
  * @param app - The app module to register.
28
31
  */
29
32
  export function registerApp(app) {
30
- const id = app.manifest.id;
31
- if (registeredApps.has(id)) {
32
- throw new Error(`App "${id}" is already registered`);
33
- }
34
- registeredApps.set(id, app);
33
+ registeredApps.set(app.manifest.id, app);
35
34
  }
36
35
  /**
37
36
  * Reactive snapshot of all registered app manifests. Shell home iterates