sh3-core 0.1.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 +185 -0
- package/dist/Shell.svelte.d.ts +4 -0
- package/dist/api.d.ts +22 -0
- package/dist/api.js +45 -0
- package/dist/apps/lifecycle.d.ts +37 -0
- package/dist/apps/lifecycle.js +153 -0
- package/dist/apps/registry.svelte.d.ts +37 -0
- package/dist/apps/registry.svelte.js +60 -0
- package/dist/apps/types.d.ts +61 -0
- package/dist/apps/types.js +10 -0
- package/dist/assets/icons.svg +1119 -0
- package/dist/auth/auth.svelte.d.ts +44 -0
- package/dist/auth/auth.svelte.js +119 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/build.d.ts +29 -0
- package/dist/build.js +85 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +28 -0
- package/dist/documents/backends.d.ts +17 -0
- package/dist/documents/backends.js +156 -0
- package/dist/documents/config.d.ts +7 -0
- package/dist/documents/config.js +27 -0
- package/dist/documents/handle.d.ts +6 -0
- package/dist/documents/handle.js +154 -0
- package/dist/documents/http-backend.d.ts +22 -0
- package/dist/documents/http-backend.js +78 -0
- package/dist/documents/index.d.ts +6 -0
- package/dist/documents/index.js +8 -0
- package/dist/documents/notifications.d.ts +9 -0
- package/dist/documents/notifications.js +39 -0
- package/dist/documents/types.d.ts +97 -0
- package/dist/documents/types.js +12 -0
- package/dist/host-entry.d.ts +9 -0
- package/dist/host-entry.js +15 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/layout/DragPreview.svelte +63 -0
- package/dist/layout/DragPreview.svelte.d.ts +3 -0
- package/dist/layout/LayoutRenderer.svelte +260 -0
- package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
- package/dist/layout/SlotContainer.svelte +140 -0
- package/dist/layout/SlotContainer.svelte.d.ts +8 -0
- package/dist/layout/SlotDropZone.svelte +122 -0
- package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
- package/dist/layout/drag.svelte.d.ts +45 -0
- package/dist/layout/drag.svelte.js +191 -0
- package/dist/layout/inspection.d.ts +52 -0
- package/dist/layout/inspection.js +157 -0
- package/dist/layout/ops.d.ts +78 -0
- package/dist/layout/ops.js +281 -0
- package/dist/layout/slotHostPool.svelte.d.ts +36 -0
- package/dist/layout/slotHostPool.svelte.js +229 -0
- package/dist/layout/store.svelte.d.ts +39 -0
- package/dist/layout/store.svelte.js +150 -0
- package/dist/layout/tree-walk.d.ts +15 -0
- package/dist/layout/tree-walk.js +33 -0
- package/dist/layout/types.d.ts +108 -0
- package/dist/layout/types.js +25 -0
- package/dist/overlays/ModalFrame.svelte +87 -0
- package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
- package/dist/overlays/PopupFrame.svelte +85 -0
- package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
- package/dist/overlays/ToastItem.svelte +77 -0
- package/dist/overlays/ToastItem.svelte.d.ts +9 -0
- package/dist/overlays/focusTrap.d.ts +1 -0
- package/dist/overlays/focusTrap.js +64 -0
- package/dist/overlays/modal.d.ts +9 -0
- package/dist/overlays/modal.js +141 -0
- package/dist/overlays/popup.d.ts +9 -0
- package/dist/overlays/popup.js +108 -0
- package/dist/overlays/roots.d.ts +4 -0
- package/dist/overlays/roots.js +31 -0
- package/dist/overlays/toast.d.ts +6 -0
- package/dist/overlays/toast.js +93 -0
- package/dist/overlays/types.d.ts +31 -0
- package/dist/overlays/types.js +15 -0
- package/dist/primitives/.gitkeep +0 -0
- package/dist/primitives/ResizableSplitter.svelte +333 -0
- package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
- package/dist/primitives/TabbedPanel.svelte +305 -0
- package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
- package/dist/registry/client.d.ts +74 -0
- package/dist/registry/client.js +118 -0
- package/dist/registry/index.d.ts +13 -0
- package/dist/registry/index.js +14 -0
- package/dist/registry/installer.d.ts +53 -0
- package/dist/registry/installer.js +170 -0
- package/dist/registry/integrity.d.ts +32 -0
- package/dist/registry/integrity.js +92 -0
- package/dist/registry/loader.d.ts +50 -0
- package/dist/registry/loader.js +145 -0
- package/dist/registry/schema.d.ts +47 -0
- package/dist/registry/schema.js +180 -0
- package/dist/registry/storage.d.ts +37 -0
- package/dist/registry/storage.js +101 -0
- package/dist/registry/types.d.ts +245 -0
- package/dist/registry/types.js +14 -0
- package/dist/registry-shard/RegistryView.svelte +561 -0
- package/dist/registry-shard/RegistryView.svelte.d.ts +3 -0
- package/dist/registry-shard/registryApp.d.ts +10 -0
- package/dist/registry-shard/registryApp.js +24 -0
- package/dist/registry-shard/registryShard.svelte.d.ts +45 -0
- package/dist/registry-shard/registryShard.svelte.js +125 -0
- package/dist/shards/activate.svelte.d.ts +45 -0
- package/dist/shards/activate.svelte.js +124 -0
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +28 -0
- package/dist/shards/types.d.ts +155 -0
- package/dist/shards/types.js +20 -0
- package/dist/shell-shard/ShellHome.svelte +285 -0
- package/dist/shell-shard/ShellHome.svelte.d.ts +3 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
- package/dist/shell-shard/shellShard.svelte.js +47 -0
- package/dist/shellRuntime.svelte.d.ts +27 -0
- package/dist/shellRuntime.svelte.js +27 -0
- package/dist/state/backends.d.ts +26 -0
- package/dist/state/backends.js +99 -0
- package/dist/state/types.d.ts +38 -0
- package/dist/state/types.js +15 -0
- package/dist/state/zones.svelte.d.ts +52 -0
- package/dist/state/zones.svelte.js +141 -0
- package/dist/store/InstalledView.svelte +201 -0
- package/dist/store/InstalledView.svelte.d.ts +3 -0
- package/dist/store/StoreView.svelte +470 -0
- package/dist/store/StoreView.svelte.d.ts +3 -0
- package/dist/store/storeApp.d.ts +11 -0
- package/dist/store/storeApp.js +26 -0
- package/dist/store/storeShard.svelte.d.ts +29 -0
- package/dist/store/storeShard.svelte.js +99 -0
- package/dist/tokens.css +79 -0
- package/package.json +50 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* Shell home — the view shown when no app is active. Sections:
|
|
4
|
+
* 1. User apps — always visible
|
|
5
|
+
* 2. Admin apps — visible when elevated (admin mode)
|
|
6
|
+
* 3. Elevate prompt — shown when not elevated
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { listRegisteredApps, launchApp, isAdmin } from '../api';
|
|
10
|
+
import { elevate, deescalate } from '../auth/index';
|
|
11
|
+
import { adminAppIds } from '../host';
|
|
12
|
+
|
|
13
|
+
const apps = $derived(listRegisteredApps());
|
|
14
|
+
const userApps = $derived(apps.filter(m => !adminAppIds.has(m.id)));
|
|
15
|
+
const adminApps = $derived(apps.filter(m => adminAppIds.has(m.id)));
|
|
16
|
+
const elevated = $derived(isAdmin());
|
|
17
|
+
|
|
18
|
+
let keyInput = $state('');
|
|
19
|
+
let elevateError = $state<string | null>(null);
|
|
20
|
+
let elevating = $state(false);
|
|
21
|
+
|
|
22
|
+
async function handleElevate() {
|
|
23
|
+
if (!keyInput.trim() || elevating) return;
|
|
24
|
+
elevating = true;
|
|
25
|
+
elevateError = null;
|
|
26
|
+
const ok = await elevate(keyInput.trim());
|
|
27
|
+
if (!ok) {
|
|
28
|
+
elevateError = 'Invalid API key';
|
|
29
|
+
}
|
|
30
|
+
keyInput = '';
|
|
31
|
+
elevating = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleDeescalate() {
|
|
35
|
+
deescalate();
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div class="shell-home">
|
|
40
|
+
<header class="shell-home-header">
|
|
41
|
+
<h1>SH3</h1>
|
|
42
|
+
<span class="shell-home-alpha">alpha</span>
|
|
43
|
+
{#if elevated}
|
|
44
|
+
<button type="button" class="shell-home-deescalate" onclick={handleDeescalate}>
|
|
45
|
+
Exit admin mode
|
|
46
|
+
</button>
|
|
47
|
+
{/if}
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
{#if userApps.length > 0}
|
|
51
|
+
<section class="shell-home-section">
|
|
52
|
+
<h2 class="shell-home-section-title">Apps</h2>
|
|
53
|
+
<ul class="shell-home-list">
|
|
54
|
+
{#each userApps as manifest (manifest.id)}
|
|
55
|
+
<li class="shell-home-entry">
|
|
56
|
+
<div class="shell-home-entry-label">{manifest.label}</div>
|
|
57
|
+
<div class="shell-home-entry-meta">
|
|
58
|
+
{manifest.id} · v{manifest.version}
|
|
59
|
+
</div>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="shell-home-launch"
|
|
63
|
+
onclick={() => launchApp(manifest.id)}
|
|
64
|
+
>
|
|
65
|
+
Launch
|
|
66
|
+
</button>
|
|
67
|
+
</li>
|
|
68
|
+
{/each}
|
|
69
|
+
</ul>
|
|
70
|
+
</section>
|
|
71
|
+
{/if}
|
|
72
|
+
|
|
73
|
+
{#if elevated}
|
|
74
|
+
{#if adminApps.length > 0}
|
|
75
|
+
<section class="shell-home-section">
|
|
76
|
+
<h2 class="shell-home-section-title">Admin</h2>
|
|
77
|
+
<ul class="shell-home-list">
|
|
78
|
+
{#each adminApps as manifest (manifest.id)}
|
|
79
|
+
<li class="shell-home-entry">
|
|
80
|
+
<div class="shell-home-entry-label">{manifest.label}</div>
|
|
81
|
+
<div class="shell-home-entry-meta">
|
|
82
|
+
{manifest.id} · v{manifest.version}
|
|
83
|
+
</div>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
class="shell-home-launch"
|
|
87
|
+
onclick={() => launchApp(manifest.id)}
|
|
88
|
+
>
|
|
89
|
+
Launch
|
|
90
|
+
</button>
|
|
91
|
+
</li>
|
|
92
|
+
{/each}
|
|
93
|
+
</ul>
|
|
94
|
+
</section>
|
|
95
|
+
{/if}
|
|
96
|
+
{:else}
|
|
97
|
+
<section class="shell-home-section">
|
|
98
|
+
<h2 class="shell-home-section-title">Admin Mode</h2>
|
|
99
|
+
<p class="shell-home-elevate-hint">
|
|
100
|
+
Enter an API key to access admin apps like the Package Store.
|
|
101
|
+
</p>
|
|
102
|
+
<form class="shell-home-elevate-form" onsubmit={(e) => { e.preventDefault(); handleElevate(); }}>
|
|
103
|
+
<input
|
|
104
|
+
class="shell-home-key-input"
|
|
105
|
+
type="password"
|
|
106
|
+
placeholder="API key"
|
|
107
|
+
bind:value={keyInput}
|
|
108
|
+
disabled={elevating}
|
|
109
|
+
/>
|
|
110
|
+
<button
|
|
111
|
+
type="submit"
|
|
112
|
+
class="shell-home-elevate-btn"
|
|
113
|
+
disabled={elevating || !keyInput.trim()}
|
|
114
|
+
>
|
|
115
|
+
{elevating ? 'Verifying...' : 'Elevate'}
|
|
116
|
+
</button>
|
|
117
|
+
</form>
|
|
118
|
+
{#if elevateError}
|
|
119
|
+
<div class="shell-home-elevate-error">{elevateError}</div>
|
|
120
|
+
{/if}
|
|
121
|
+
</section>
|
|
122
|
+
{/if}
|
|
123
|
+
|
|
124
|
+
{#if userApps.length === 0 && (!elevated || adminApps.length === 0)}
|
|
125
|
+
<p class="shell-home-empty">No apps registered.</p>
|
|
126
|
+
{/if}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<style>
|
|
130
|
+
.shell-home {
|
|
131
|
+
position: absolute;
|
|
132
|
+
inset: 0;
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: flex-start;
|
|
137
|
+
padding: 48px 24px;
|
|
138
|
+
overflow: auto;
|
|
139
|
+
background: var(--shell-bg);
|
|
140
|
+
color: var(--shell-fg);
|
|
141
|
+
font-family: system-ui, sans-serif;
|
|
142
|
+
}
|
|
143
|
+
.shell-home-header {
|
|
144
|
+
text-align: center;
|
|
145
|
+
margin-bottom: 32px;
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: 12px;
|
|
150
|
+
}
|
|
151
|
+
.shell-home-header h1 {
|
|
152
|
+
margin: 0;
|
|
153
|
+
font-size: 42px;
|
|
154
|
+
color: var(--shell-accent);
|
|
155
|
+
letter-spacing: 2px;
|
|
156
|
+
}
|
|
157
|
+
.shell-home-alpha {
|
|
158
|
+
font-size: 11px;
|
|
159
|
+
font-weight: 700;
|
|
160
|
+
text-transform: uppercase;
|
|
161
|
+
letter-spacing: 0.08em;
|
|
162
|
+
color: #fff;
|
|
163
|
+
background: var(--shell-accent);
|
|
164
|
+
padding: 2px 10px;
|
|
165
|
+
border-radius: 10px;
|
|
166
|
+
}
|
|
167
|
+
.shell-home-empty {
|
|
168
|
+
color: var(--shell-fg-muted);
|
|
169
|
+
font-style: italic;
|
|
170
|
+
}
|
|
171
|
+
.shell-home-section {
|
|
172
|
+
width: 100%;
|
|
173
|
+
max-width: 440px;
|
|
174
|
+
margin-bottom: 24px;
|
|
175
|
+
}
|
|
176
|
+
.shell-home-section-title {
|
|
177
|
+
font-size: 13px;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
text-transform: uppercase;
|
|
180
|
+
letter-spacing: 0.06em;
|
|
181
|
+
color: var(--shell-fg-subtle);
|
|
182
|
+
margin: 0 0 12px;
|
|
183
|
+
}
|
|
184
|
+
.shell-home-list {
|
|
185
|
+
list-style: none;
|
|
186
|
+
margin: 0;
|
|
187
|
+
padding: 0;
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
gap: 12px;
|
|
191
|
+
}
|
|
192
|
+
.shell-home-entry {
|
|
193
|
+
display: grid;
|
|
194
|
+
grid-template-columns: 1fr auto;
|
|
195
|
+
grid-template-rows: auto auto;
|
|
196
|
+
gap: 4px 16px;
|
|
197
|
+
align-items: center;
|
|
198
|
+
padding: 14px 18px;
|
|
199
|
+
background: var(--shell-bg-elevated);
|
|
200
|
+
border: 1px solid var(--shell-border);
|
|
201
|
+
border-radius: 6px;
|
|
202
|
+
}
|
|
203
|
+
.shell-home-entry-label {
|
|
204
|
+
grid-column: 1;
|
|
205
|
+
grid-row: 1;
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
}
|
|
208
|
+
.shell-home-entry-meta {
|
|
209
|
+
grid-column: 1;
|
|
210
|
+
grid-row: 2;
|
|
211
|
+
font-size: 11px;
|
|
212
|
+
color: var(--shell-fg-subtle);
|
|
213
|
+
}
|
|
214
|
+
.shell-home-launch {
|
|
215
|
+
grid-column: 2;
|
|
216
|
+
grid-row: 1 / span 2;
|
|
217
|
+
padding: 8px 16px;
|
|
218
|
+
background: var(--shell-accent);
|
|
219
|
+
color: var(--shell-bg);
|
|
220
|
+
border: none;
|
|
221
|
+
border-radius: 4px;
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
}
|
|
225
|
+
.shell-home-launch:hover {
|
|
226
|
+
filter: brightness(1.1);
|
|
227
|
+
}
|
|
228
|
+
.shell-home-deescalate {
|
|
229
|
+
padding: 6px 12px;
|
|
230
|
+
background: transparent;
|
|
231
|
+
color: var(--shell-fg-subtle);
|
|
232
|
+
border: 1px solid var(--shell-border);
|
|
233
|
+
border-radius: 4px;
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
}
|
|
237
|
+
.shell-home-deescalate:hover {
|
|
238
|
+
color: var(--shell-fg);
|
|
239
|
+
border-color: var(--shell-fg-subtle);
|
|
240
|
+
}
|
|
241
|
+
.shell-home-elevate-hint {
|
|
242
|
+
margin: 0 0 12px;
|
|
243
|
+
font-size: 13px;
|
|
244
|
+
color: var(--shell-fg-muted);
|
|
245
|
+
}
|
|
246
|
+
.shell-home-elevate-form {
|
|
247
|
+
display: flex;
|
|
248
|
+
gap: 8px;
|
|
249
|
+
}
|
|
250
|
+
.shell-home-key-input {
|
|
251
|
+
flex: 1;
|
|
252
|
+
padding: 8px 12px;
|
|
253
|
+
background: var(--shell-bg-elevated);
|
|
254
|
+
color: var(--shell-fg);
|
|
255
|
+
border: 1px solid var(--shell-border);
|
|
256
|
+
border-radius: 4px;
|
|
257
|
+
font-family: monospace;
|
|
258
|
+
font-size: 13px;
|
|
259
|
+
}
|
|
260
|
+
.shell-home-key-input::placeholder {
|
|
261
|
+
color: var(--shell-fg-muted);
|
|
262
|
+
}
|
|
263
|
+
.shell-home-elevate-btn {
|
|
264
|
+
padding: 8px 16px;
|
|
265
|
+
background: var(--shell-accent);
|
|
266
|
+
color: var(--shell-bg);
|
|
267
|
+
border: none;
|
|
268
|
+
border-radius: 4px;
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
white-space: nowrap;
|
|
272
|
+
}
|
|
273
|
+
.shell-home-elevate-btn:disabled {
|
|
274
|
+
opacity: 0.6;
|
|
275
|
+
cursor: not-allowed;
|
|
276
|
+
}
|
|
277
|
+
.shell-home-elevate-error {
|
|
278
|
+
margin-top: 8px;
|
|
279
|
+
padding: 6px 10px;
|
|
280
|
+
font-size: 12px;
|
|
281
|
+
color: var(--shell-error, #d32f2f);
|
|
282
|
+
background: color-mix(in srgb, var(--shell-error, #d32f2f) 10%, transparent);
|
|
283
|
+
border-radius: 4px;
|
|
284
|
+
}
|
|
285
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shell pseudo-shard — the framework-owned shard with reserved id
|
|
3
|
+
* `__shell__`. Contributes exactly one view, `shell:home`, which the
|
|
4
|
+
* layout manager's home tree references.
|
|
5
|
+
*
|
|
6
|
+
* This shard uses the public contract same as any other shard (see
|
|
7
|
+
* ADR-008 / the phase-8 shell-as-shard feasibility check). Its only
|
|
8
|
+
* concession to being framework-owned is:
|
|
9
|
+
* - The reserved id `__shell__`
|
|
10
|
+
* - It is registered by `bootstrap()` itself, not by the glob loop
|
|
11
|
+
*
|
|
12
|
+
* Activation goes through the same `registerShard` / `activateShard`
|
|
13
|
+
* pipeline as mock and diagnostic. `autostart` is a no-op, defined only
|
|
14
|
+
* so activation is driven by the self-starting pass in bootstrap rather
|
|
15
|
+
* than by requiring a special code path.
|
|
16
|
+
*
|
|
17
|
+
* `.svelte.ts` because mounting Svelte components requires rune access.
|
|
18
|
+
*/
|
|
19
|
+
import { mount, unmount } from 'svelte';
|
|
20
|
+
import ShellHome from './ShellHome.svelte';
|
|
21
|
+
export const shellShard = {
|
|
22
|
+
manifest: {
|
|
23
|
+
id: '__shell__',
|
|
24
|
+
label: 'SH3 Shell',
|
|
25
|
+
version: '0.1.0',
|
|
26
|
+
views: [{ id: 'shell:home', label: 'Home' }],
|
|
27
|
+
},
|
|
28
|
+
activate(ctx) {
|
|
29
|
+
const factory = {
|
|
30
|
+
mount(container, _context) {
|
|
31
|
+
const instance = mount(ShellHome, { target: container });
|
|
32
|
+
return {
|
|
33
|
+
unmount() {
|
|
34
|
+
unmount(instance);
|
|
35
|
+
},
|
|
36
|
+
// Home does not need onResize — it uses absolute inset:0 layout.
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
ctx.registerView('shell:home', factory);
|
|
41
|
+
},
|
|
42
|
+
autostart() {
|
|
43
|
+
// Intentionally empty. Defining this field is what puts the shell
|
|
44
|
+
// pseudo-shard on the self-starting path at boot (see bootstrap),
|
|
45
|
+
// so `shell:home` is available before any app launches.
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StateZones } from './state/zones.svelte';
|
|
2
|
+
import type { ZoneSchema } from './state/types';
|
|
3
|
+
import { type ModalManager } from './overlays/modal';
|
|
4
|
+
import { type PopupManager } from './overlays/popup';
|
|
5
|
+
import { type ToastManager } from './overlays/toast';
|
|
6
|
+
/**
|
|
7
|
+
* The process-wide shell singleton exposed to shards and the shell's own
|
|
8
|
+
* internal code. Provides state zone creation and overlay managers.
|
|
9
|
+
* Shards receive a pre-bound version via `ShardContext` rather than
|
|
10
|
+
* accessing `shell` directly.
|
|
11
|
+
*/
|
|
12
|
+
export interface Shell {
|
|
13
|
+
/**
|
|
14
|
+
* Declare the state zones a shard (or the shell itself) wants to use.
|
|
15
|
+
* Returns a live reactive object per declared zone. Persistent zones
|
|
16
|
+
* are hydrated from the backend before the call returns.
|
|
17
|
+
*/
|
|
18
|
+
state<T extends ZoneSchema>(shardId: string, schema: T): StateZones<T>;
|
|
19
|
+
/** Stackable, focus-trapped dialogs. See overlays/modal.ts. */
|
|
20
|
+
modal: ModalManager;
|
|
21
|
+
/** Anchored, single-item, outside-click-dismissable popups. */
|
|
22
|
+
popup: PopupManager;
|
|
23
|
+
/** Auto-dismissing notification toasts. */
|
|
24
|
+
toast: ToastManager;
|
|
25
|
+
}
|
|
26
|
+
/** The process-wide shell instance. Framework-internal code uses this directly; shards receive a scoped view via `ShardContext`. */
|
|
27
|
+
export declare const shell: Shell;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* shell — the framework-facing entry point passed to shards.
|
|
3
|
+
*
|
|
4
|
+
* Conceptually this is the object a shard receives from its activation
|
|
5
|
+
* callback; it exposes the APIs the shard is allowed to touch.
|
|
6
|
+
*
|
|
7
|
+
* Phase 3: state zones.
|
|
8
|
+
* Phase 4: registerView (lives on ShardContext, not here).
|
|
9
|
+
* Phase 5: modal / popup / toast overlay managers.
|
|
10
|
+
*
|
|
11
|
+
* For now `shell` is a module-level singleton. Once the shard lifecycle
|
|
12
|
+
* exists in a fuller form, per-shard shells will be constructed per
|
|
13
|
+
* activation with the shardId baked in, so shards no longer need to pass
|
|
14
|
+
* their own id to `state`. Overlay managers stay process-wide because
|
|
15
|
+
* the layer stack is a shell-global resource.
|
|
16
|
+
*/
|
|
17
|
+
import { createStateZones } from './state/zones.svelte';
|
|
18
|
+
import { modalManager } from './overlays/modal';
|
|
19
|
+
import { popupManager } from './overlays/popup';
|
|
20
|
+
import { toastManager } from './overlays/toast';
|
|
21
|
+
/** The process-wide shell instance. Framework-internal code uses this directly; shards receive a scoped view via `ShardContext`. */
|
|
22
|
+
export const shell = {
|
|
23
|
+
state: createStateZones,
|
|
24
|
+
modal: modalManager,
|
|
25
|
+
popup: popupManager,
|
|
26
|
+
toast: toastManager,
|
|
27
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Backend } from './types';
|
|
2
|
+
export declare class MemoryBackend implements Backend {
|
|
3
|
+
#private;
|
|
4
|
+
read(shardId: string): unknown | undefined;
|
|
5
|
+
write(shardId: string, value: unknown): void;
|
|
6
|
+
delete(shardId: string): void;
|
|
7
|
+
list(): string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Persists one JSON blob per shardId under a prefixed localStorage key.
|
|
11
|
+
*
|
|
12
|
+
* Keys are of the form `${prefix}${shardId}`. Each zone that uses this
|
|
13
|
+
* backend picks its own prefix so keys don't collide across zones (e.g.
|
|
14
|
+
* `sh3:workspace:` for the workspace zone, `sh3:user:` for the user zone).
|
|
15
|
+
*
|
|
16
|
+
* Values are JSON-serialized; non-serializable values are silently dropped
|
|
17
|
+
* by `JSON.stringify`. Callers are expected to keep zone contents plain.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LocalStorageBackend implements Backend {
|
|
20
|
+
#private;
|
|
21
|
+
constructor(prefix: string);
|
|
22
|
+
read(shardId: string): unknown | undefined;
|
|
23
|
+
write(shardId: string, value: unknown): void;
|
|
24
|
+
delete(shardId: string): void;
|
|
25
|
+
list(): string[];
|
|
26
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Backend implementations for phase 3.
|
|
3
|
+
*
|
|
4
|
+
* MemoryBackend — ephemeral/session zones (and tests)
|
|
5
|
+
* LocalStorageBackend — workspace/user zones on web
|
|
6
|
+
*
|
|
7
|
+
* Tauri FS backends, IndexedDB, and remote sync are deferred per the
|
|
8
|
+
* roadmap. Any future backend need only conform to the `Backend` interface
|
|
9
|
+
* in ./types.ts for shard code to work against it unchanged.
|
|
10
|
+
*/
|
|
11
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
12
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
13
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
14
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
15
|
+
};
|
|
16
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
17
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
18
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
19
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
20
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
21
|
+
};
|
|
22
|
+
var _MemoryBackend_map, _LocalStorageBackend_prefix;
|
|
23
|
+
export class MemoryBackend {
|
|
24
|
+
constructor() {
|
|
25
|
+
_MemoryBackend_map.set(this, new Map());
|
|
26
|
+
}
|
|
27
|
+
read(shardId) {
|
|
28
|
+
return __classPrivateFieldGet(this, _MemoryBackend_map, "f").get(shardId);
|
|
29
|
+
}
|
|
30
|
+
write(shardId, value) {
|
|
31
|
+
__classPrivateFieldGet(this, _MemoryBackend_map, "f").set(shardId, value);
|
|
32
|
+
}
|
|
33
|
+
delete(shardId) {
|
|
34
|
+
__classPrivateFieldGet(this, _MemoryBackend_map, "f").delete(shardId);
|
|
35
|
+
}
|
|
36
|
+
list() {
|
|
37
|
+
return [...__classPrivateFieldGet(this, _MemoryBackend_map, "f").keys()];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
_MemoryBackend_map = new WeakMap();
|
|
41
|
+
/**
|
|
42
|
+
* Persists one JSON blob per shardId under a prefixed localStorage key.
|
|
43
|
+
*
|
|
44
|
+
* Keys are of the form `${prefix}${shardId}`. Each zone that uses this
|
|
45
|
+
* backend picks its own prefix so keys don't collide across zones (e.g.
|
|
46
|
+
* `sh3:workspace:` for the workspace zone, `sh3:user:` for the user zone).
|
|
47
|
+
*
|
|
48
|
+
* Values are JSON-serialized; non-serializable values are silently dropped
|
|
49
|
+
* by `JSON.stringify`. Callers are expected to keep zone contents plain.
|
|
50
|
+
*/
|
|
51
|
+
export class LocalStorageBackend {
|
|
52
|
+
constructor(prefix) {
|
|
53
|
+
_LocalStorageBackend_prefix.set(this, void 0);
|
|
54
|
+
__classPrivateFieldSet(this, _LocalStorageBackend_prefix, prefix, "f");
|
|
55
|
+
}
|
|
56
|
+
read(shardId) {
|
|
57
|
+
if (typeof localStorage === 'undefined')
|
|
58
|
+
return undefined;
|
|
59
|
+
const raw = localStorage.getItem(__classPrivateFieldGet(this, _LocalStorageBackend_prefix, "f") + shardId);
|
|
60
|
+
if (raw == null)
|
|
61
|
+
return undefined;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(raw);
|
|
64
|
+
}
|
|
65
|
+
catch (_a) {
|
|
66
|
+
// Corrupt entry — treat as missing rather than crashing the shard.
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
write(shardId, value) {
|
|
71
|
+
if (typeof localStorage === 'undefined')
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
localStorage.setItem(__classPrivateFieldGet(this, _LocalStorageBackend_prefix, "f") + shardId, JSON.stringify(value));
|
|
75
|
+
}
|
|
76
|
+
catch (_a) {
|
|
77
|
+
// Quota exceeded or serialization failure — swallow for phase 3;
|
|
78
|
+
// phase "post-prototype" will surface a proper error channel.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
delete(shardId) {
|
|
82
|
+
if (typeof localStorage === 'undefined')
|
|
83
|
+
return;
|
|
84
|
+
localStorage.removeItem(__classPrivateFieldGet(this, _LocalStorageBackend_prefix, "f") + shardId);
|
|
85
|
+
}
|
|
86
|
+
list() {
|
|
87
|
+
if (typeof localStorage === 'undefined')
|
|
88
|
+
return [];
|
|
89
|
+
const out = [];
|
|
90
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
91
|
+
const key = localStorage.key(i);
|
|
92
|
+
if (key && key.startsWith(__classPrivateFieldGet(this, _LocalStorageBackend_prefix, "f"))) {
|
|
93
|
+
out.push(key.slice(__classPrivateFieldGet(this, _LocalStorageBackend_prefix, "f").length));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
_LocalStorageBackend_prefix = new WeakMap();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four state zones available to shards. Each zone has different lifetime
|
|
3
|
+
* and persistence semantics:
|
|
4
|
+
* - `ephemeral`: in-memory only; cleared when the shard is unloaded.
|
|
5
|
+
* - `session`: in-memory only; cleared when the app is closed.
|
|
6
|
+
* - `workspace`: persisted to localStorage; survives page reload.
|
|
7
|
+
* - `user`: persisted to localStorage; shared across all workspaces for this user.
|
|
8
|
+
*/
|
|
9
|
+
export type ZoneName = 'ephemeral' | 'session' | 'workspace' | 'user';
|
|
10
|
+
/** Zones whose contents are flushed to a persistent backend on change. */
|
|
11
|
+
export declare const PERSISTENT_ZONES: readonly ZoneName[];
|
|
12
|
+
/**
|
|
13
|
+
* A backend is a tiny KV store keyed by shardId. Each zone owns one backend.
|
|
14
|
+
* Values are arbitrary JSON-serializable objects — the backend is responsible
|
|
15
|
+
* for any (de)serialization needed for its medium.
|
|
16
|
+
*
|
|
17
|
+
* Future backends: IndexedDB, Tauri FS, remote sync — all conform to this
|
|
18
|
+
* interface so shard code is unaffected by deployment target.
|
|
19
|
+
*/
|
|
20
|
+
export interface Backend {
|
|
21
|
+
/** Read the stored value for a shard. Returns undefined if not present. */
|
|
22
|
+
read(shardId: string): unknown | undefined;
|
|
23
|
+
/** Write (create or overwrite) the stored value for a shard. */
|
|
24
|
+
write(shardId: string, value: unknown): void;
|
|
25
|
+
/** Delete the stored value for a shard. No-op if not present. */
|
|
26
|
+
delete(shardId: string): void;
|
|
27
|
+
/** Return all shard ids that have stored entries in this backend. */
|
|
28
|
+
list(): string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A zone schema is a record of zone name → initial state object. Any subset
|
|
32
|
+
* of zones may be declared; unused zones are simply omitted. Values inside
|
|
33
|
+
* each zone object become the defaults used when the backend has no prior
|
|
34
|
+
* entry for this shard.
|
|
35
|
+
*/
|
|
36
|
+
export type ZoneSchema = {
|
|
37
|
+
[K in ZoneName]?: Record<string, unknown>;
|
|
38
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* State zone types — framework plumbing for docs/design/state-zones.md.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 implements four of the six zones:
|
|
5
|
+
* ephemeral — in-memory, cleared on shard unload
|
|
6
|
+
* session — in-memory, cleared on app close
|
|
7
|
+
* workspace — persisted (localStorage KV for web)
|
|
8
|
+
* user — persisted (localStorage KV for web)
|
|
9
|
+
*
|
|
10
|
+
* Deferred until later phases:
|
|
11
|
+
* document — file-on-disk (phase "post-prototype")
|
|
12
|
+
* shared — lives on the bus, not storage (not a zone in the same sense)
|
|
13
|
+
*/
|
|
14
|
+
/** Zones whose contents are flushed to a persistent backend on change. */
|
|
15
|
+
export const PERSISTENT_ZONES = ['workspace', 'user'];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Backend, ZoneName, ZoneSchema } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Live reactive state object returned by `createStateZones`. Each key
|
|
4
|
+
* mirrors a zone declared in the schema; the value is a deeply-reactive
|
|
5
|
+
* proxy of the same shape. Undeclared zones are absent from the result.
|
|
6
|
+
*
|
|
7
|
+
* @typeParam T - The `ZoneSchema` used to declare zones and their defaults.
|
|
8
|
+
*/
|
|
9
|
+
export type StateZones<T extends ZoneSchema> = {
|
|
10
|
+
[K in keyof T]: T[K];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Create live reactive state zones for a shard (or the shell itself).
|
|
14
|
+
*
|
|
15
|
+
* Each zone declared in `schema` is backed by the appropriate store:
|
|
16
|
+
* `ephemeral` and `session` are in-memory; `workspace` and `user` are
|
|
17
|
+
* persisted to localStorage and hydrated before this call returns.
|
|
18
|
+
*
|
|
19
|
+
* Writes to persistent zones are debounced and coalesced per microtask.
|
|
20
|
+
* Zones not declared in `schema` are not created; the result only
|
|
21
|
+
* contains the keys the caller asked for.
|
|
22
|
+
*
|
|
23
|
+
* @param shardId - Unique shard identifier used as the storage namespace.
|
|
24
|
+
* @param schema - Record of zone names to their default values.
|
|
25
|
+
* @returns A reactive `StateZones<T>` object keyed to the declared zones.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createStateZones<T extends ZoneSchema>(shardId: string, schema: T): StateZones<T>;
|
|
28
|
+
/**
|
|
29
|
+
* Swap a backend implementation. Intended for tests and for phase 4+ when
|
|
30
|
+
* Tauri or IndexedDB backends replace the defaults. Not part of the shard-
|
|
31
|
+
* facing API.
|
|
32
|
+
*/
|
|
33
|
+
export declare function __setBackend(zone: ZoneName, backend: Backend): void;
|
|
34
|
+
/**
|
|
35
|
+
* Read a raw persisted zone entry without creating a reactive proxy.
|
|
36
|
+
*
|
|
37
|
+
* Intended for framework-internal consumers (e.g. the shell's layout
|
|
38
|
+
* persistence) that need to inspect a stored value before deciding
|
|
39
|
+
* whether to pass it to `createStateZones`. Not part of the shard-facing
|
|
40
|
+
* API — shards always go through `createStateZones` so hydration is
|
|
41
|
+
* reactive and debounced-flush applies.
|
|
42
|
+
*
|
|
43
|
+
* Returns `undefined` for missing, corrupt, or memory-only-zone entries
|
|
44
|
+
* that have no stored value.
|
|
45
|
+
*/
|
|
46
|
+
export declare function peekZone(zone: ZoneName, shardId: string): unknown | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Delete a persisted zone entry. Intended for framework-internal use
|
|
49
|
+
* when a stored value is known to be incompatible (version mismatch,
|
|
50
|
+
* corruption) and should be removed before `createStateZones` hydrates.
|
|
51
|
+
*/
|
|
52
|
+
export declare function clearZone(zone: ZoneName, shardId: string): void;
|