sh3-core 0.3.0 → 0.4.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/Shell.svelte CHANGED
@@ -101,7 +101,7 @@
101
101
  height: 100%;
102
102
  width: 100%;
103
103
  position: relative;
104
- background: var(--shell-bg);
104
+ background: var(--shell-grad-bg, var(--shell-bg));
105
105
  color: var(--shell-fg);
106
106
  }
107
107
 
@@ -110,7 +110,7 @@
110
110
  align-items: center;
111
111
  gap: var(--shell-pad-md);
112
112
  padding: 0 var(--shell-pad-md);
113
- background: var(--shell-bg-elevated);
113
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
114
114
  border-bottom: 1px solid var(--shell-border);
115
115
  user-select: none;
116
116
  }
@@ -123,7 +123,7 @@
123
123
  .shell-content {
124
124
  position: relative;
125
125
  overflow: hidden;
126
- background: var(--shell-bg);
126
+ background: var(--shell-grad-bg, var(--shell-bg));
127
127
  min-width: 0;
128
128
  min-height: 0;
129
129
  }
@@ -132,7 +132,7 @@
132
132
  align-items: center;
133
133
  justify-content: space-between;
134
134
  padding: 0 var(--shell-pad-md);
135
- background: var(--shell-bg-sunken);
135
+ background: var(--shell-grad-bg-sunken, var(--shell-bg-sunken));
136
136
  border-top: 1px solid var(--shell-border);
137
137
  color: var(--shell-fg-muted);
138
138
  font-size: 11px;
package/dist/api.d.ts CHANGED
@@ -21,3 +21,4 @@ export declare const capabilities: {
21
21
  readonly hotInstall: boolean;
22
22
  };
23
23
  export type { ServerShard, ServerShardContext } from './server-shard/types';
24
+ export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
package/dist/api.js CHANGED
@@ -43,3 +43,5 @@ export const capabilities = {
43
43
  /** Whether this target supports hot-installing packages via dynamic import from blob URL. */
44
44
  hotInstall: typeof Blob !== 'undefined' && typeof URL.createObjectURL === 'function',
45
45
  };
46
+ // Theme token override API (shell-level theming support).
47
+ export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
@@ -10,6 +10,7 @@ import { mount } from 'svelte';
10
10
  import { Shell } from './index';
11
11
  import { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner, } from './host';
12
12
  import { resolvePlatform } from './platform/index';
13
+ import { hydrateTokenOverrides } from './theme';
13
14
  export async function createShell(config) {
14
15
  var _a, _b;
15
16
  // 1. Platform detection — must run before bootstrap so state zones
@@ -23,6 +24,9 @@ export async function createShell(config) {
23
24
  if (platform.localOwner) {
24
25
  setLocalOwner();
25
26
  }
27
+ // 1c. Apply persisted theme token overrides before any component mounts,
28
+ // so the first frame renders with the user's chosen theme.
29
+ hydrateTokenOverrides();
26
30
  // 1b. Load server-discovered packages (fetched by frontend from /api/packages).
27
31
  if ((_a = config === null || config === void 0 ? void 0 : config.discoveredPackages) === null || _a === void 0 ? void 0 : _a.length) {
28
32
  const { loadBundleModule } = await import('./registry/loader');
@@ -24,6 +24,7 @@
24
24
  const layout = $derived(inspectActiveLayout());
25
25
  const regShards = $derived(Array.from(registeredShards.values()));
26
26
  const actShards = $derived(Array.from(activeShards.keys()));
27
+
27
28
  </script>
28
29
 
29
30
  <div class="diagnostic">
@@ -68,6 +69,7 @@
68
69
  {/each}
69
70
  </ul>
70
71
  </section>
72
+
71
73
  </div>
72
74
 
73
75
  <style>
@@ -76,7 +78,7 @@
76
78
  inset: 0;
77
79
  padding: 12px 16px;
78
80
  overflow: auto;
79
- background: var(--shell-bg);
81
+ background: var(--shell-grad-bg, var(--shell-bg));
80
82
  color: var(--shell-fg);
81
83
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
82
84
  font-size: 12px;
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Diagnostic routes view — lists all server API routes.
4
+ *
5
+ * Fetches the route table from GET /api/routes. GET routes render
6
+ * as clickable links for quick testing of parameterless endpoints.
7
+ */
8
+
9
+ interface ApiRoute { method: string; path: string; }
10
+ let routes: ApiRoute[] = $state([]);
11
+ let error: string | null = $state(null);
12
+
13
+ async function fetchRoutes() {
14
+ try {
15
+ const res = await fetch('/api/routes');
16
+ if (!res.ok) { error = `${res.status}`; return; }
17
+ routes = await res.json();
18
+ error = null;
19
+ } catch {
20
+ error = 'unavailable';
21
+ }
22
+ }
23
+
24
+ fetchRoutes();
25
+ </script>
26
+
27
+ <div class="diagnostic">
28
+ <h2>API Routes</h2>
29
+
30
+ {#if error}
31
+ <p class="muted">Server not reachable or route introspection unavailable ({error}).</p>
32
+ {:else}
33
+ <p class="muted">{routes.length} unique routes</p>
34
+ <ul>
35
+ {#each routes as route}
36
+ <li>
37
+ <span class="method" class:get={route.method === 'GET'}>{route.method}</span>
38
+ {#if route.method === 'GET'}
39
+ <a href={route.path} target="_blank" rel="noopener">{route.path}</a>
40
+ {:else}
41
+ <span class="path">{route.path}</span>
42
+ {/if}
43
+ </li>
44
+ {/each}
45
+ </ul>
46
+ {/if}
47
+ </div>
48
+
49
+ <style>
50
+ .diagnostic {
51
+ position: absolute;
52
+ inset: 0;
53
+ padding: 12px 16px;
54
+ overflow: auto;
55
+ background: var(--shell-grad-bg, var(--shell-bg));
56
+ color: var(--shell-fg);
57
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
58
+ font-size: 12px;
59
+ }
60
+ h2 {
61
+ margin: 0 0 12px;
62
+ color: var(--shell-accent);
63
+ font-size: 14px;
64
+ }
65
+ .muted {
66
+ color: var(--shell-fg-muted);
67
+ margin: 0 0 8px;
68
+ }
69
+ ul {
70
+ margin: 0;
71
+ padding: 0;
72
+ list-style: none;
73
+ }
74
+ li {
75
+ margin: 0;
76
+ padding: 2px 0;
77
+ display: flex;
78
+ align-items: baseline;
79
+ gap: 8px;
80
+ }
81
+ .method {
82
+ min-width: 6ch;
83
+ color: var(--shell-fg-muted);
84
+ flex-shrink: 0;
85
+ }
86
+ .method.get {
87
+ color: var(--shell-accent);
88
+ }
89
+ a {
90
+ color: var(--shell-accent);
91
+ text-decoration: none;
92
+ }
93
+ a:hover {
94
+ text-decoration: underline;
95
+ }
96
+ .path {
97
+ color: var(--shell-fg);
98
+ }
99
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const DiagnosticRoutes: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type DiagnosticRoutes = ReturnType<typeof DiagnosticRoutes>;
3
+ export default DiagnosticRoutes;
@@ -19,6 +19,7 @@ export const diagnosticApp = {
19
19
  activeTab: 0,
20
20
  tabs: [
21
21
  { slotId: 'diagnostic.main', viewId: 'diagnostic:panel', label: 'Diagnostic' },
22
+ { slotId: 'diagnostic.routes', viewId: 'diagnostic:routes', label: 'API Routes' },
22
23
  ],
23
24
  },
24
25
  };
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { mount, unmount } from 'svelte';
27
27
  import DiagnosticPanel from './DiagnosticPanel.svelte';
28
+ import DiagnosticRoutes from './DiagnosticRoutes.svelte';
28
29
  import DiagnosticPromptModal from './DiagnosticPromptModal.svelte';
29
30
  import { shell, getActiveApp, spliceIntoActiveLayout, inspectActiveLayout, } from '../api';
30
31
  export const diagnosticShard = {
@@ -32,7 +33,10 @@ export const diagnosticShard = {
32
33
  id: 'diagnostic',
33
34
  label: 'Diagnostic',
34
35
  version: '0.1.0',
35
- views: [{ id: 'diagnostic:panel', label: 'Diagnostic' }],
36
+ views: [
37
+ { id: 'diagnostic:panel', label: 'Diagnostic' },
38
+ { id: 'diagnostic:routes', label: 'API Routes' },
39
+ ],
36
40
  },
37
41
  activate(ctx) {
38
42
  const factory = {
@@ -46,6 +50,17 @@ export const diagnosticShard = {
46
50
  },
47
51
  };
48
52
  ctx.registerView('diagnostic:panel', factory);
53
+ const routesFactory = {
54
+ mount(container, _context) {
55
+ const instance = mount(DiagnosticRoutes, { target: container });
56
+ return {
57
+ unmount() {
58
+ unmount(instance);
59
+ },
60
+ };
61
+ },
62
+ };
63
+ ctx.registerView('diagnostic:routes', routesFactory);
49
64
  },
50
65
  autostart(ctx) {
51
66
  const state = ctx.state({
@@ -46,7 +46,7 @@
46
46
  align-items: center;
47
47
  gap: var(--shell-pad-sm);
48
48
  padding: var(--shell-pad-sm) var(--shell-pad-md);
49
- background: var(--shell-bg-elevated);
49
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
50
50
  color: var(--shell-fg);
51
51
  border: 1px solid var(--shell-accent);
52
52
  border-radius: 3px;
@@ -186,10 +186,12 @@
186
186
  />
187
187
  {#snippet tabBody(i: number)}
188
188
  {@const entry = tabs.tabs[i]}
189
- <div class="tab-slot-wrapper">
190
- <SlotContainer node={{ type: 'slot', slotId: entry.slotId, viewId: entry.viewId }} label={entry.label} />
191
- <SlotDropZone path={path} />
192
- </div>
189
+ {#if entry}
190
+ <div class="tab-slot-wrapper">
191
+ <SlotContainer node={{ type: 'slot', slotId: entry.slotId, viewId: entry.viewId }} label={entry.label} />
192
+ <SlotDropZone path={path} />
193
+ </div>
194
+ {/if}
193
195
  {/snippet}
194
196
  {:else if tabs?.persistent}
195
197
  <div class="empty-tabs-placeholder">
@@ -73,7 +73,7 @@
73
73
  pointer-events: auto;
74
74
  }
75
75
  .modal-box {
76
- background: var(--shell-bg-elevated);
76
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
77
77
  color: var(--shell-fg);
78
78
  border: 1px solid var(--shell-border-strong);
79
79
  border-radius: 4px;
@@ -73,7 +73,7 @@
73
73
  <style>
74
74
  .popup-frame {
75
75
  position: absolute;
76
- background: var(--shell-bg-elevated);
76
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
77
77
  color: var(--shell-fg);
78
78
  border: 1px solid var(--shell-border-strong);
79
79
  border-radius: 3px;
@@ -44,7 +44,7 @@
44
44
  align-items: center;
45
45
  gap: var(--shell-pad-md);
46
46
  padding: var(--shell-pad-sm) var(--shell-pad-md);
47
- background: var(--shell-bg-elevated);
47
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
48
48
  color: var(--shell-fg);
49
49
  border: 1px solid var(--shell-border-strong);
50
50
  border-left-width: 3px;
@@ -269,7 +269,7 @@
269
269
  display: flex;
270
270
  align-items: center;
271
271
  justify-content: center;
272
- background: var(--shell-bg-elevated);
272
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
273
273
  border: none;
274
274
  color: var(--shell-fg-muted);
275
275
  cursor: pointer;
@@ -206,7 +206,7 @@
206
206
  height: 100%;
207
207
  min-width: 0;
208
208
  min-height: 0;
209
- background: var(--shell-bg);
209
+ background: var(--shell-grad-bg, var(--shell-bg));
210
210
  }
211
211
 
212
212
  .tab-strip {
@@ -214,7 +214,7 @@
214
214
  flex: 0 0 auto;
215
215
  display: flex;
216
216
  gap: 1px;
217
- background: var(--shell-bg-sunken);
217
+ background: var(--shell-grad-bg-sunken, var(--shell-bg-sunken));
218
218
  border-bottom: 1px solid var(--shell-border);
219
219
  padding: 0 var(--shell-pad-sm);
220
220
  user-select: none;
@@ -243,11 +243,11 @@
243
243
  }
244
244
  .tab:hover {
245
245
  color: var(--shell-fg);
246
- background: var(--shell-bg-elevated);
246
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
247
247
  }
248
248
  .tab.active {
249
249
  color: var(--shell-fg);
250
- background: var(--shell-bg);
250
+ background: var(--shell-grad-bg, var(--shell-bg));
251
251
  border-top-color: var(--shell-accent);
252
252
  }
253
253
  .tab-icon { font-size: 11px; }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A server shard is the optional backend counterpart to a client shard.
5
5
  * It runs in Node inside sh3-server and declares routes that are mounted
6
- * under `/s/<shard-id>/`. Server shards have full Node access — filesystem,
6
+ * under `/api/<shard-id>/`. Server shards have full Node access — filesystem,
7
7
  * child_process, network, etc. — and are trusted by the admin who installed
8
8
  * them.
9
9
  *
@@ -39,8 +39,8 @@ export interface ServerShard {
39
39
  id: string;
40
40
  /**
41
41
  * Called once at mount time. Register Hono routes on the provided router.
42
- * Routes are relative to `/s/<shard-id>/` — e.g. `router.get('/data', ...)`
43
- * becomes `GET /s/<shard-id>/data`.
42
+ * Routes are relative to `/api/<shard-id>/` — e.g. `router.get('/data', ...)`
43
+ * becomes `GET /api/<shard-id>/data`.
44
44
  *
45
45
  * May be async if the shard needs to initialise resources before serving.
46
46
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A server shard is the optional backend counterpart to a client shard.
5
5
  * It runs in Node inside sh3-server and declares routes that are mounted
6
- * under `/s/<shard-id>/`. Server shards have full Node access — filesystem,
6
+ * under `/api/<shard-id>/`. Server shards have full Node access — filesystem,
7
7
  * child_process, network, etc. — and are trusted by the admin who installed
8
8
  * them.
9
9
  *
@@ -98,7 +98,7 @@ export interface ShardManifest {
98
98
  /**
99
99
  * Optional filename of a server-side bundle for this shard. When present,
100
100
  * sh3-server loads the bundle at boot and mounts its routes at
101
- * `/s/<shard-id>/`. The server bundle runs in Node with full access.
101
+ * `/api/<shard-id>/`. The server bundle runs in Node with full access.
102
102
  * Only relevant for shards installed via the package store; framework-
103
103
  * shipped shards do not use this field.
104
104
  */
@@ -97,7 +97,7 @@
97
97
  <section class="shell-home-section">
98
98
  <h2 class="shell-home-section-title">Admin Mode</h2>
99
99
  <p class="shell-home-elevate-hint">
100
- Enter an API key to access admin apps like the Package Store.
100
+ Elevate Permissions
101
101
  </p>
102
102
  <form class="shell-home-elevate-form" onsubmit={(e) => { e.preventDefault(); handleElevate(); }}>
103
103
  <input
@@ -136,7 +136,7 @@
136
136
  justify-content: flex-start;
137
137
  padding: 48px 24px;
138
138
  overflow: auto;
139
- background: var(--shell-bg);
139
+ background: var(--shell-grad-bg, var(--shell-bg));
140
140
  color: var(--shell-fg);
141
141
  font-family: system-ui, sans-serif;
142
142
  }
@@ -196,7 +196,7 @@
196
196
  gap: 4px 16px;
197
197
  align-items: center;
198
198
  padding: 14px 18px;
199
- background: var(--shell-bg-elevated);
199
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
200
200
  border: 1px solid var(--shell-border);
201
201
  border-radius: 6px;
202
202
  }
@@ -250,7 +250,7 @@
250
250
  .shell-home-key-input {
251
251
  flex: 1;
252
252
  padding: 8px 12px;
253
- background: var(--shell-bg-elevated);
253
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
254
254
  color: var(--shell-fg);
255
255
  border: 1px solid var(--shell-border);
256
256
  border-radius: 4px;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Apply CSS token overrides to :root and persist to User zone.
3
+ *
4
+ * Keys are token names without the `--` prefix (e.g. `'shell-accent'`).
5
+ * Values are any valid CSS value string.
6
+ *
7
+ * Calling this replaces ALL previous overrides — tokens not present in
8
+ * the new map are removed from :root.
9
+ */
10
+ export declare function setTokenOverrides(overrides: Record<string, string>): void;
11
+ /**
12
+ * Remove all token overrides from :root and clear persisted state.
13
+ * The shell reverts to the default tokens defined in tokens.css.
14
+ */
15
+ export declare function clearTokenOverrides(): void;
16
+ /**
17
+ * Read the currently persisted token overrides.
18
+ * Returns an empty object if no overrides are stored.
19
+ */
20
+ export declare function getTokenOverrides(): Record<string, string>;
21
+ /**
22
+ * Apply persisted token overrides to :root. Called once during shell boot
23
+ * before any component mounts, so the themed colors are visible from the
24
+ * first frame.
25
+ *
26
+ * This is framework-internal — not part of the shard-facing API.
27
+ */
28
+ export declare function hydrateTokenOverrides(): void;
package/dist/theme.js ADDED
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Token override API — allows external packages (like sh3-style) to
3
+ * dynamically change shell CSS tokens and persist the selection to
4
+ * the User zone.
5
+ *
6
+ * The shell reads persisted overrides at boot (see createShell.ts) so
7
+ * themes survive page reloads. This module provides the write path.
8
+ */
9
+ const STORAGE_KEY = 'sh3:user:__shell__:theme';
10
+ /** Keys currently set on :root by setTokenOverrides, tracked for clearTokenOverrides. */
11
+ let appliedKeys = [];
12
+ /**
13
+ * Apply CSS token overrides to :root and persist to User zone.
14
+ *
15
+ * Keys are token names without the `--` prefix (e.g. `'shell-accent'`).
16
+ * Values are any valid CSS value string.
17
+ *
18
+ * Calling this replaces ALL previous overrides — tokens not present in
19
+ * the new map are removed from :root.
20
+ */
21
+ export function setTokenOverrides(overrides) {
22
+ // Remove previously applied keys that are not in the new set
23
+ const newKeys = Object.keys(overrides);
24
+ for (const key of appliedKeys) {
25
+ if (!newKeys.includes(key)) {
26
+ document.documentElement.style.removeProperty(`--${key}`);
27
+ }
28
+ }
29
+ // Apply new overrides
30
+ for (const [key, value] of Object.entries(overrides)) {
31
+ document.documentElement.style.setProperty(`--${key}`, value);
32
+ }
33
+ appliedKeys = newKeys;
34
+ // Persist
35
+ try {
36
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides));
37
+ }
38
+ catch (_a) {
39
+ // Storage full or unavailable — theme applies for this session only
40
+ }
41
+ }
42
+ /**
43
+ * Remove all token overrides from :root and clear persisted state.
44
+ * The shell reverts to the default tokens defined in tokens.css.
45
+ */
46
+ export function clearTokenOverrides() {
47
+ for (const key of appliedKeys) {
48
+ document.documentElement.style.removeProperty(`--${key}`);
49
+ }
50
+ appliedKeys = [];
51
+ try {
52
+ localStorage.removeItem(STORAGE_KEY);
53
+ }
54
+ catch (_a) {
55
+ // Ignore
56
+ }
57
+ }
58
+ /**
59
+ * Read the currently persisted token overrides.
60
+ * Returns an empty object if no overrides are stored.
61
+ */
62
+ export function getTokenOverrides() {
63
+ try {
64
+ const raw = localStorage.getItem(STORAGE_KEY);
65
+ if (!raw)
66
+ return {};
67
+ const parsed = JSON.parse(raw);
68
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
69
+ return parsed;
70
+ }
71
+ return {};
72
+ }
73
+ catch (_a) {
74
+ return {};
75
+ }
76
+ }
77
+ /**
78
+ * Apply persisted token overrides to :root. Called once during shell boot
79
+ * before any component mounts, so the themed colors are visible from the
80
+ * first frame.
81
+ *
82
+ * This is framework-internal — not part of the shard-facing API.
83
+ */
84
+ export function hydrateTokenOverrides() {
85
+ const overrides = getTokenOverrides();
86
+ if (Object.keys(overrides).length === 0)
87
+ return;
88
+ for (const [key, value] of Object.entries(overrides)) {
89
+ document.documentElement.style.setProperty(`--${key}`, value);
90
+ }
91
+ appliedKeys = Object.keys(overrides);
92
+ }
package/dist/tokens.css CHANGED
@@ -11,6 +11,12 @@
11
11
  --shell-bg: #1a1b1e;
12
12
  --shell-bg-elevated: #22232a;
13
13
  --shell-bg-sunken: #141518;
14
+
15
+ /* Gradient layer — empty by default, overrides surface when set */
16
+ --shell-grad-bg: ;
17
+ --shell-grad-bg-elevated: ;
18
+ --shell-grad-bg-sunken: ;
19
+
14
20
  --shell-border: #2e3038;
15
21
  --shell-border-strong: #3c3f4a;
16
22
 
@@ -66,7 +72,7 @@ body {
66
72
  padding: 0;
67
73
  height: 100%;
68
74
  overflow: hidden; /* SH3 is a shell, not a scrollable document */
69
- background: var(--shell-bg);
75
+ background: var(--shell-grad-bg, var(--shell-bg));
70
76
  color: var(--shell-fg);
71
77
  font-family: var(--shell-font-ui);
72
78
  font-size: var(--shell-font-size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"