sh3-core 0.5.4 → 0.5.6

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/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;
@@ -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. */
@@ -0,0 +1,144 @@
1
+ <script lang="ts">
2
+ /**
3
+ * GuestBanner — persistent bar shown when browsing as guest.
4
+ * Clicking "Sign in" opens a modal-like overlay with the sign-in form.
5
+ */
6
+
7
+ import { isGuest, login, register } from './index';
8
+
9
+ let showSignIn = $state(false);
10
+ let username = $state('');
11
+ let password = $state('');
12
+ let error = $state<string | null>(null);
13
+ let loading = $state(false);
14
+
15
+ async function handleLogin() {
16
+ if (!username.trim() || !password.trim() || loading) return;
17
+ loading = true;
18
+ error = null;
19
+ const result = await login(username.trim(), password.trim());
20
+ loading = false;
21
+ if (result.ok) {
22
+ showSignIn = false;
23
+ } else {
24
+ error = result.error;
25
+ }
26
+ }
27
+ </script>
28
+
29
+ <div class="guest-banner-slot">
30
+ {#if isGuest()}
31
+ <div class="guest-banner">
32
+ <span class="guest-banner-text">Browsing as guest. Sign in to save your work.</span>
33
+ <button type="button" class="guest-banner-action" onclick={() => { showSignIn = true; }}>
34
+ Sign in
35
+ </button>
36
+ </div>
37
+
38
+ {#if showSignIn}
39
+ <div class="guest-signin-overlay" role="dialog">
40
+ <div class="guest-signin-card">
41
+ <form class="guest-signin-form" onsubmit={(e) => { e.preventDefault(); handleLogin(); }}>
42
+ <input class="guest-signin-input" type="text" placeholder="Username" bind:value={username} disabled={loading} autocomplete="username" />
43
+ <input class="guest-signin-input" type="password" placeholder="Password" bind:value={password} disabled={loading} autocomplete="current-password" />
44
+ <div class="guest-signin-actions">
45
+ <button type="submit" class="guest-signin-btn" disabled={loading || !username.trim() || !password.trim()}>
46
+ {loading ? 'Signing in...' : 'Sign in'}
47
+ </button>
48
+ <button type="button" class="guest-signin-cancel" onclick={() => { showSignIn = false; error = null; }}>
49
+ Cancel
50
+ </button>
51
+ </div>
52
+ </form>
53
+ {#if error}
54
+ <div class="guest-signin-error">{error}</div>
55
+ {/if}
56
+ </div>
57
+ </div>
58
+ {/if}
59
+ {/if}
60
+ </div>
61
+
62
+ <style>
63
+ .guest-banner {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ gap: 12px;
68
+ padding: 6px var(--shell-pad-md, 12px);
69
+ background: color-mix(in srgb, var(--shell-accent, #7c7cf0) 15%, transparent);
70
+ border-bottom: 1px solid var(--shell-border, #3a3a5c);
71
+ font-size: 12px;
72
+ color: var(--shell-fg, #e0e0e0);
73
+ }
74
+ .guest-banner-action {
75
+ padding: 3px 10px;
76
+ background: var(--shell-accent, #7c7cf0);
77
+ color: var(--shell-bg, #1a1a2e);
78
+ border: none;
79
+ border-radius: var(--shell-radius, 6px);
80
+ font-size: 11px;
81
+ font-weight: 600;
82
+ cursor: pointer;
83
+ }
84
+ .guest-signin-overlay {
85
+ position: fixed;
86
+ inset: 0;
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ background: rgba(0, 0, 0, 0.5);
91
+ z-index: 9999;
92
+ }
93
+ .guest-signin-card {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 12px;
97
+ padding: 32px;
98
+ background: var(--shell-bg-elevated, #252540);
99
+ border: 1px solid var(--shell-border, #3a3a5c);
100
+ border-radius: var(--shell-radius-lg, 12px);
101
+ min-width: 300px;
102
+ }
103
+ .guest-signin-form {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 10px;
107
+ }
108
+ .guest-signin-input {
109
+ padding: 8px 12px;
110
+ background: var(--shell-bg, #1a1a2e);
111
+ color: var(--shell-fg, #e0e0e0);
112
+ border: 1px solid var(--shell-border, #3a3a5c);
113
+ border-radius: var(--shell-radius, 6px);
114
+ font-size: 13px;
115
+ }
116
+ .guest-signin-input::placeholder { color: var(--shell-fg-muted, #888); }
117
+ .guest-signin-actions { display: flex; gap: 8px; }
118
+ .guest-signin-btn {
119
+ flex: 1;
120
+ padding: 8px;
121
+ background: var(--shell-accent, #7c7cf0);
122
+ color: var(--shell-bg, #1a1a2e);
123
+ border: none;
124
+ border-radius: var(--shell-radius, 6px);
125
+ font-weight: 600;
126
+ cursor: pointer;
127
+ }
128
+ .guest-signin-btn:disabled { opacity: 0.6; cursor: not-allowed; }
129
+ .guest-signin-cancel {
130
+ padding: 8px 12px;
131
+ background: transparent;
132
+ color: var(--shell-fg-subtle, #aaa);
133
+ border: 1px solid var(--shell-border, #3a3a5c);
134
+ border-radius: var(--shell-radius, 6px);
135
+ cursor: pointer;
136
+ }
137
+ .guest-signin-error {
138
+ padding: 6px 10px;
139
+ font-size: 12px;
140
+ color: var(--shell-error, #d32f2f);
141
+ background: color-mix(in srgb, var(--shell-error, #d32f2f) 10%, transparent);
142
+ border-radius: var(--shell-radius, 6px);
143
+ }
144
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const GuestBanner: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type GuestBanner = ReturnType<typeof GuestBanner>;
3
+ export default GuestBanner;