@rool-dev/app 0.3.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/README.md +306 -0
- package/dist/cli/dev.d.ts +10 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +241 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +108 -0
- package/dist/cli/publish.d.ts +9 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/publish.js +213 -0
- package/dist/cli/vite-utils.d.ts +22 -0
- package/dist/cli/vite-utils.d.ts.map +1 -0
- package/dist/cli/vite-utils.js +96 -0
- package/dist/client.d.ts +79 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +235 -0
- package/dist/dev/AppGrid.svelte +246 -0
- package/dist/dev/AppGrid.svelte.d.ts +14 -0
- package/dist/dev/AppGrid.svelte.d.ts.map +1 -0
- package/dist/dev/DevHostController.d.ts +86 -0
- package/dist/dev/DevHostController.d.ts.map +1 -0
- package/dist/dev/DevHostController.js +395 -0
- package/dist/dev/HostShell.svelte +110 -0
- package/dist/dev/HostShell.svelte.d.ts +11 -0
- package/dist/dev/HostShell.svelte.d.ts.map +1 -0
- package/dist/dev/Sidebar.svelte +223 -0
- package/dist/dev/Sidebar.svelte.d.ts +19 -0
- package/dist/dev/Sidebar.svelte.d.ts.map +1 -0
- package/dist/dev/TabBar.svelte +83 -0
- package/dist/dev/TabBar.svelte.d.ts +14 -0
- package/dist/dev/TabBar.svelte.d.ts.map +1 -0
- package/dist/dev/app.css +1 -0
- package/dist/dev/host-shell.d.ts +8 -0
- package/dist/dev/host-shell.d.ts.map +1 -0
- package/dist/dev/host-shell.js +14807 -0
- package/dist/dev/host-shell.js.map +1 -0
- package/dist/dev/vite-env.d.ts +4 -0
- package/dist/host.d.ts +54 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +171 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +10 -0
- package/dist/protocol.d.ts +46 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +14 -0
- package/dist/reactive.svelte.d.ts +100 -0
- package/dist/reactive.svelte.d.ts.map +1 -0
- package/dist/reactive.svelte.js +267 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/package.json +78 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevHostController — business logic for the dev host shell.
|
|
3
|
+
*
|
|
4
|
+
* Owns the RoolClient lifecycle, space management, channel-per-app management,
|
|
5
|
+
* bridge hosting, and published app management. The Svelte component is a thin
|
|
6
|
+
* view layer that reads this controller's state and calls its methods.
|
|
7
|
+
*
|
|
8
|
+
* The controller is self-sufficient: it manages the full lifecycle including
|
|
9
|
+
* DOM flush (via injected tick) and bridge binding. Svelte components can call
|
|
10
|
+
* controller methods directly without needing wrappers.
|
|
11
|
+
*/
|
|
12
|
+
import { RoolClient } from '@rool-dev/sdk';
|
|
13
|
+
import { createBridgeHost } from '../host.js';
|
|
14
|
+
import { ENV_URLS } from '../manifest.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// localStorage helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function storageGet(key) {
|
|
19
|
+
try {
|
|
20
|
+
return localStorage.getItem(key);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function storageSet(key, value) {
|
|
27
|
+
try {
|
|
28
|
+
if (value === null)
|
|
29
|
+
localStorage.removeItem(key);
|
|
30
|
+
else
|
|
31
|
+
localStorage.setItem(key, value);
|
|
32
|
+
}
|
|
33
|
+
catch { /* ignore */ }
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// DevHostController
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
export class DevHostController {
|
|
39
|
+
// --- Config (immutable after construction) ---
|
|
40
|
+
channelId;
|
|
41
|
+
appUrl;
|
|
42
|
+
manifest;
|
|
43
|
+
manifestError;
|
|
44
|
+
// --- SDK client ---
|
|
45
|
+
client;
|
|
46
|
+
// --- Observable state (Svelte component mirrors these via $state) ---
|
|
47
|
+
spaces = [];
|
|
48
|
+
currentSpaceId = null;
|
|
49
|
+
statusText = 'Initializing...';
|
|
50
|
+
statusState = 'off';
|
|
51
|
+
placeholderText = 'Authenticating...';
|
|
52
|
+
env;
|
|
53
|
+
publishedApps = [];
|
|
54
|
+
installedAppIds = [];
|
|
55
|
+
sidebarCollapsed = false;
|
|
56
|
+
// --- Per-tab state (imperative, not rendered directly) ---
|
|
57
|
+
channels = {};
|
|
58
|
+
iframeEls = {};
|
|
59
|
+
bridgeHosts = {};
|
|
60
|
+
// --- Dependencies ---
|
|
61
|
+
_onChange;
|
|
62
|
+
_tick;
|
|
63
|
+
// --- Storage keys ---
|
|
64
|
+
_spaceKey;
|
|
65
|
+
constructor(options, onChange, tick) {
|
|
66
|
+
this.channelId = options.channelId;
|
|
67
|
+
this.appUrl = options.appUrl;
|
|
68
|
+
this.manifest = options.manifest;
|
|
69
|
+
this.manifestError = options.manifestError;
|
|
70
|
+
this._onChange = onChange;
|
|
71
|
+
this._tick = tick;
|
|
72
|
+
this._spaceKey = `rool-devhost:${options.channelId}:space`;
|
|
73
|
+
// Restore persisted state
|
|
74
|
+
this.env = this._getSavedEnv();
|
|
75
|
+
this.sidebarCollapsed = storageGet('rool-devhost:collapsed') === 'true';
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Derived
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
get tabs() {
|
|
81
|
+
const localTab = {
|
|
82
|
+
id: 'local',
|
|
83
|
+
name: this.manifest?.name ?? 'Local',
|
|
84
|
+
url: this.appUrl,
|
|
85
|
+
isLocal: true,
|
|
86
|
+
};
|
|
87
|
+
const appTabs = this.installedAppIds
|
|
88
|
+
.map((id) => {
|
|
89
|
+
const app = this.publishedApps.find((a) => a.appId === id);
|
|
90
|
+
if (!app)
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
id: app.appId,
|
|
94
|
+
name: app.name,
|
|
95
|
+
url: `https://${app.appId}.${ENV_URLS[this.env].appsDomain}`,
|
|
96
|
+
isLocal: false,
|
|
97
|
+
};
|
|
98
|
+
})
|
|
99
|
+
.filter((t) => t !== null);
|
|
100
|
+
return [localTab, ...appTabs];
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Bootstrap
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
async boot() {
|
|
106
|
+
const urls = ENV_URLS[this.env];
|
|
107
|
+
this.client = new RoolClient({ baseUrl: urls.baseUrl, authUrl: urls.authUrl });
|
|
108
|
+
const authenticated = await this.client.initialize();
|
|
109
|
+
if (!authenticated) {
|
|
110
|
+
this.placeholderText = 'Redirecting to login...';
|
|
111
|
+
this.statusText = 'Authenticating...';
|
|
112
|
+
this.statusState = 'loading';
|
|
113
|
+
this._onChange();
|
|
114
|
+
this.client.login('App Dev Host');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.placeholderText = 'Loading spaces...';
|
|
118
|
+
this.statusText = 'Loading spaces...';
|
|
119
|
+
this.statusState = 'loading';
|
|
120
|
+
this._onChange();
|
|
121
|
+
const [spaceList, appList] = await Promise.all([
|
|
122
|
+
this.client.listSpaces(),
|
|
123
|
+
this.client.listApps().catch(() => []),
|
|
124
|
+
]);
|
|
125
|
+
this.spaces = spaceList;
|
|
126
|
+
this.publishedApps = appList;
|
|
127
|
+
this.client.on('spaceAdded', (space) => {
|
|
128
|
+
if (!this.spaces.some((s) => s.id === space.id)) {
|
|
129
|
+
this.spaces = [...this.spaces, space];
|
|
130
|
+
this._onChange();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
this.client.on('spaceRemoved', (id) => {
|
|
134
|
+
this.spaces = this.spaces.filter((s) => s.id !== id);
|
|
135
|
+
if (this.currentSpaceId === id) {
|
|
136
|
+
this.currentSpaceId = null;
|
|
137
|
+
this.statusText = 'Disconnected';
|
|
138
|
+
this.statusState = 'off';
|
|
139
|
+
}
|
|
140
|
+
this._onChange();
|
|
141
|
+
});
|
|
142
|
+
this.client.on('spaceRenamed', (id, name) => {
|
|
143
|
+
this.spaces = this.spaces.map((s) => (s.id === id ? { ...s, name } : s));
|
|
144
|
+
this._onChange();
|
|
145
|
+
});
|
|
146
|
+
this.statusText = 'Ready';
|
|
147
|
+
this.statusState = 'off';
|
|
148
|
+
const savedSpace = storageGet(this._spaceKey);
|
|
149
|
+
if (savedSpace && this.spaces.some((s) => s.id === savedSpace)) {
|
|
150
|
+
await this.selectSpace(savedSpace);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.placeholderText = 'Select a space to load the app';
|
|
154
|
+
this._onChange();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Space selection
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
async selectSpace(spaceId) {
|
|
161
|
+
this._destroyAllBridgesAndChannels();
|
|
162
|
+
this.currentSpaceId = spaceId;
|
|
163
|
+
storageSet(this._spaceKey, spaceId);
|
|
164
|
+
this.statusText = 'Opening channels...';
|
|
165
|
+
this.statusState = 'loading';
|
|
166
|
+
this.placeholderText = 'Opening channels...';
|
|
167
|
+
this._onChange();
|
|
168
|
+
try {
|
|
169
|
+
// Open the local app's channel
|
|
170
|
+
const localChannel = await this.client.openChannel(spaceId, this.channelId);
|
|
171
|
+
this.channels['local'] = localChannel;
|
|
172
|
+
// Apply manifest settings to the local channel
|
|
173
|
+
await this._syncManifest(localChannel, this.manifest);
|
|
174
|
+
// Discover installed apps: space channels whose ID matches a published app
|
|
175
|
+
const space = await this.client.openSpace(spaceId);
|
|
176
|
+
const spaceChannels = space.getChannels();
|
|
177
|
+
const publishedAppIds = new Set(this.publishedApps.map((a) => a.appId));
|
|
178
|
+
this.installedAppIds = spaceChannels
|
|
179
|
+
.map((ch) => ch.id)
|
|
180
|
+
.filter((id) => publishedAppIds.has(id) && id !== this.channelId);
|
|
181
|
+
// Open channels for each installed app and sync their manifests
|
|
182
|
+
for (const appId of this.installedAppIds) {
|
|
183
|
+
try {
|
|
184
|
+
const ch = await this.client.openChannel(spaceId, appId);
|
|
185
|
+
this.channels[appId] = ch;
|
|
186
|
+
const remoteManifest = await this._fetchRemoteManifest(appId);
|
|
187
|
+
if (remoteManifest) {
|
|
188
|
+
await this._syncManifest(ch, remoteManifest);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
console.error(`Failed to open channel for app ${appId}:`, e);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Show iframes, wait for DOM to mount them, then bind bridges
|
|
196
|
+
this.placeholderText = null;
|
|
197
|
+
this._onChange();
|
|
198
|
+
await this._tick();
|
|
199
|
+
this._bindAllBridges();
|
|
200
|
+
const spaceName = this.spaces.find((s) => s.id === this.currentSpaceId)?.name ?? spaceId;
|
|
201
|
+
this.statusText = `Connected \u2014 ${spaceName}`;
|
|
202
|
+
this.statusState = 'ok';
|
|
203
|
+
this._onChange();
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
console.error('Failed to open channel:', e);
|
|
207
|
+
this.placeholderText = `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
208
|
+
this.statusText = 'Error';
|
|
209
|
+
this.statusState = 'off';
|
|
210
|
+
this._onChange();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// App installation / removal
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
/**
|
|
217
|
+
* Install an app in the current space.
|
|
218
|
+
*
|
|
219
|
+
* Opens the channel first, THEN adds the tab. This ensures the channel
|
|
220
|
+
* exists when the iframe mounts so registerIframe → _bindBridge can
|
|
221
|
+
* connect the bridge before the app sends its init message.
|
|
222
|
+
*/
|
|
223
|
+
async installApp(appId) {
|
|
224
|
+
if (!this.currentSpaceId)
|
|
225
|
+
return;
|
|
226
|
+
if (this.installedAppIds.includes(appId))
|
|
227
|
+
return;
|
|
228
|
+
try {
|
|
229
|
+
// Step 1: open channel (creates it on server == install)
|
|
230
|
+
const ch = await this.client.openChannel(this.currentSpaceId, appId);
|
|
231
|
+
this.channels[appId] = ch;
|
|
232
|
+
// Step 2: add the card, flush DOM, bind bridge
|
|
233
|
+
this.installedAppIds = [...this.installedAppIds, appId];
|
|
234
|
+
this._onChange();
|
|
235
|
+
await this._tick();
|
|
236
|
+
this._bindBridge(appId);
|
|
237
|
+
// Step 3: fetch and apply the published app's manifest
|
|
238
|
+
const remoteManifest = await this._fetchRemoteManifest(appId);
|
|
239
|
+
if (remoteManifest) {
|
|
240
|
+
await this._syncManifest(ch, remoteManifest);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
console.error(`Failed to install app ${appId}:`, e);
|
|
245
|
+
this.installedAppIds = this.installedAppIds.filter((id) => id !== appId);
|
|
246
|
+
this._onChange();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Uninstall an app from the current space.
|
|
251
|
+
* Deletes the channel and removes the card.
|
|
252
|
+
*/
|
|
253
|
+
removeApp(appId) {
|
|
254
|
+
this._destroyTab(appId);
|
|
255
|
+
this.installedAppIds = this.installedAppIds.filter((id) => id !== appId);
|
|
256
|
+
this._onChange();
|
|
257
|
+
// Delete the channel in the background (fire-and-forget)
|
|
258
|
+
if (this.currentSpaceId) {
|
|
259
|
+
this.client.deleteChannel(this.currentSpaceId, appId).catch((e) => {
|
|
260
|
+
console.error(`Failed to delete channel for app ${appId}:`, e);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Environment switching
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
async switchEnv(newEnv) {
|
|
268
|
+
if (newEnv === this.env)
|
|
269
|
+
return;
|
|
270
|
+
this.env = newEnv;
|
|
271
|
+
storageSet('rool-devhost:env', newEnv);
|
|
272
|
+
this._destroyAllBridgesAndChannels();
|
|
273
|
+
this.currentSpaceId = null;
|
|
274
|
+
this.spaces = [];
|
|
275
|
+
this.publishedApps = [];
|
|
276
|
+
this.installedAppIds = [];
|
|
277
|
+
this._onChange();
|
|
278
|
+
await this.boot();
|
|
279
|
+
}
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Sidebar
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
toggleSidebar() {
|
|
284
|
+
this.sidebarCollapsed = !this.sidebarCollapsed;
|
|
285
|
+
storageSet('rool-devhost:collapsed', String(this.sidebarCollapsed));
|
|
286
|
+
this._onChange();
|
|
287
|
+
}
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Iframe registration (called by Svelte action)
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
registerIframe(tabId, el) {
|
|
292
|
+
this.iframeEls[tabId] = el;
|
|
293
|
+
this._bindBridge(tabId);
|
|
294
|
+
}
|
|
295
|
+
unregisterIframe(tabId) {
|
|
296
|
+
delete this.iframeEls[tabId];
|
|
297
|
+
}
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Cleanup
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
logout() {
|
|
302
|
+
this.client.logout();
|
|
303
|
+
window.location.reload();
|
|
304
|
+
}
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Private helpers
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
_bindBridge(tabId) {
|
|
309
|
+
const el = this.iframeEls[tabId];
|
|
310
|
+
const ch = this.channels[tabId];
|
|
311
|
+
if (el && ch && !this.bridgeHosts[tabId]) {
|
|
312
|
+
this.bridgeHosts[tabId] = createBridgeHost({ channel: ch, iframe: el });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
_bindAllBridges() {
|
|
316
|
+
for (const tab of this.tabs) {
|
|
317
|
+
this._bindBridge(tab.id);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
_destroyTab(tabId) {
|
|
321
|
+
this.bridgeHosts[tabId]?.destroy();
|
|
322
|
+
delete this.bridgeHosts[tabId];
|
|
323
|
+
this.channels[tabId]?.close();
|
|
324
|
+
delete this.channels[tabId];
|
|
325
|
+
delete this.iframeEls[tabId];
|
|
326
|
+
}
|
|
327
|
+
_destroyAllBridgesAndChannels() {
|
|
328
|
+
for (const host of Object.values(this.bridgeHosts)) {
|
|
329
|
+
host.destroy();
|
|
330
|
+
}
|
|
331
|
+
for (const ch of Object.values(this.channels)) {
|
|
332
|
+
ch.close();
|
|
333
|
+
}
|
|
334
|
+
this.bridgeHosts = {};
|
|
335
|
+
this.channels = {};
|
|
336
|
+
this.iframeEls = {};
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Fetch `rool-app.json` from a published app's URL.
|
|
340
|
+
* Returns null if the fetch fails (app might not have a manifest).
|
|
341
|
+
*/
|
|
342
|
+
async _fetchRemoteManifest(appId) {
|
|
343
|
+
const domain = ENV_URLS[this.env].appsDomain;
|
|
344
|
+
const url = `https://${appId}.${domain}/rool-app.json`;
|
|
345
|
+
try {
|
|
346
|
+
const res = await fetch(url);
|
|
347
|
+
if (!res.ok)
|
|
348
|
+
return null;
|
|
349
|
+
return await res.json();
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Idempotently sync a manifest's settings (name, system instruction, collections)
|
|
357
|
+
* onto a channel. Safe to call every time the app is opened.
|
|
358
|
+
*/
|
|
359
|
+
async _syncManifest(channel, manifest) {
|
|
360
|
+
if (!manifest)
|
|
361
|
+
return;
|
|
362
|
+
if (channel.channelName !== manifest.name) {
|
|
363
|
+
await channel.rename(manifest.name);
|
|
364
|
+
}
|
|
365
|
+
const targetInstruction = manifest.systemInstruction ?? null;
|
|
366
|
+
const currentInstruction = channel.getSystemInstruction() ?? null;
|
|
367
|
+
if (currentInstruction !== targetInstruction) {
|
|
368
|
+
await channel.setSystemInstruction(targetInstruction);
|
|
369
|
+
}
|
|
370
|
+
if (manifest.collections) {
|
|
371
|
+
const currentSchema = channel.getSchema();
|
|
372
|
+
const syncCollections = async (colls) => {
|
|
373
|
+
for (const [name, fields] of Object.entries(colls)) {
|
|
374
|
+
if (name in currentSchema) {
|
|
375
|
+
await channel.alterCollection(name, fields);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
await channel.createCollection(name, fields);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
const { write: w, read: r } = manifest.collections;
|
|
383
|
+
if (w && w !== '*')
|
|
384
|
+
await syncCollections(w);
|
|
385
|
+
if (r && r !== '*')
|
|
386
|
+
await syncCollections(r);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
_getSavedEnv() {
|
|
390
|
+
const saved = storageGet('rool-devhost:env');
|
|
391
|
+
if (saved === 'dev' || saved === 'prod')
|
|
392
|
+
return saved;
|
|
393
|
+
return 'prod';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, tick } from 'svelte';
|
|
3
|
+
import { DevHostController } from './DevHostController.js';
|
|
4
|
+
import type { AppTab } from './DevHostController.js';
|
|
5
|
+
import type { AppManifest } from '../manifest.js';
|
|
6
|
+
import type { RoolSpaceInfo, PublishedAppInfo } from '@rool-dev/sdk';
|
|
7
|
+
import type { Environment } from '../manifest.js';
|
|
8
|
+
import Sidebar from './Sidebar.svelte';
|
|
9
|
+
import AppGrid from './AppGrid.svelte';
|
|
10
|
+
|
|
11
|
+
// Props injected from the mount entry
|
|
12
|
+
interface Props {
|
|
13
|
+
channelId: string;
|
|
14
|
+
appUrl: string;
|
|
15
|
+
manifest: AppManifest | null;
|
|
16
|
+
manifestError: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props: Props = $props();
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Controller + reactive state mirror
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
let spaces: RoolSpaceInfo[] = $state([]);
|
|
26
|
+
let currentSpaceId: string | null = $state(null);
|
|
27
|
+
let statusText: string = $state('Initializing...');
|
|
28
|
+
let statusState: 'ok' | 'loading' | 'off' = $state('off');
|
|
29
|
+
let placeholderText: string | null = $state('Authenticating...');
|
|
30
|
+
let sidebarCollapsed: boolean = $state(false);
|
|
31
|
+
let env: Environment = $state('prod');
|
|
32
|
+
let publishedApps: PublishedAppInfo[] = $state([]);
|
|
33
|
+
let installedAppIds: string[] = $state([]);
|
|
34
|
+
let tabs: AppTab[] = $state([]);
|
|
35
|
+
|
|
36
|
+
// UI-only state (not in controller)
|
|
37
|
+
let dropdownOpen: boolean = $state(false);
|
|
38
|
+
|
|
39
|
+
const controller = new DevHostController(
|
|
40
|
+
props,
|
|
41
|
+
syncState,
|
|
42
|
+
tick,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
function syncState() {
|
|
46
|
+
spaces = controller.spaces;
|
|
47
|
+
currentSpaceId = controller.currentSpaceId;
|
|
48
|
+
statusText = controller.statusText;
|
|
49
|
+
statusState = controller.statusState;
|
|
50
|
+
placeholderText = controller.placeholderText;
|
|
51
|
+
sidebarCollapsed = controller.sidebarCollapsed;
|
|
52
|
+
env = controller.env;
|
|
53
|
+
publishedApps = controller.publishedApps;
|
|
54
|
+
installedAppIds = controller.installedAppIds;
|
|
55
|
+
tabs = controller.tabs;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Derived: published apps not yet installed (excluding the local dev app)
|
|
59
|
+
let uninstalledApps = $derived(
|
|
60
|
+
publishedApps.filter((app) => app.appId !== props.channelId && !installedAppIds.includes(app.appId)),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Initial sync
|
|
64
|
+
syncState();
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Bootstrap
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
onMount(() => {
|
|
71
|
+
controller.boot();
|
|
72
|
+
|
|
73
|
+
function handleClickOutside(e: MouseEvent) {
|
|
74
|
+
if (dropdownOpen && !(e.target as Element)?.closest('[data-dropdown]')) {
|
|
75
|
+
dropdownOpen = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
document.addEventListener('click', handleClickOutside);
|
|
79
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
80
|
+
});
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<!-- Sidebar -->
|
|
84
|
+
<Sidebar
|
|
85
|
+
{controller}
|
|
86
|
+
manifest={props.manifest}
|
|
87
|
+
manifestError={props.manifestError}
|
|
88
|
+
{spaces}
|
|
89
|
+
{currentSpaceId}
|
|
90
|
+
{env}
|
|
91
|
+
{statusText}
|
|
92
|
+
{statusState}
|
|
93
|
+
{sidebarCollapsed}
|
|
94
|
+
bind:dropdownOpen
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<!-- Main area -->
|
|
98
|
+
<div class="flex-1 min-w-0 flex flex-col">
|
|
99
|
+
{#if placeholderText}
|
|
100
|
+
<div class="flex items-center justify-center h-full text-slate-400 text-sm">{placeholderText}</div>
|
|
101
|
+
{:else}
|
|
102
|
+
<AppGrid
|
|
103
|
+
{controller}
|
|
104
|
+
{tabs}
|
|
105
|
+
{uninstalledApps}
|
|
106
|
+
onInstallApp={(id) => controller.installApp(id)}
|
|
107
|
+
onRemoveApp={(id) => controller.removeApp(id)}
|
|
108
|
+
/>
|
|
109
|
+
{/if}
|
|
110
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AppManifest } from '../manifest.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
channelId: string;
|
|
4
|
+
appUrl: string;
|
|
5
|
+
manifest: AppManifest | null;
|
|
6
|
+
manifestError: string | null;
|
|
7
|
+
}
|
|
8
|
+
declare const HostShell: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type HostShell = ReturnType<typeof HostShell>;
|
|
10
|
+
export default HostShell;
|
|
11
|
+
//# sourceMappingURL=HostShell.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostShell.svelte.d.ts","sourceRoot":"","sources":["../../src/dev/HostShell.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAQhD,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AA0FH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|