@inkly-org/cli 0.5.1

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.
@@ -0,0 +1,567 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>inkly dev — demo</title>
7
+ <!-- Default to the Inkly platform favicon; replaced below if the hub
8
+ sets its own `brand.favicon`. -->
9
+ <link rel="icon" href="https://www.inklyai.dev/favicon.ico" />
10
+ <link rel="stylesheet" href="/__inkly/runtime.css" />
11
+ <script type="importmap">{"imports":{"react":"/__inkly/vendor/react.mjs","react-dom":"/__inkly/vendor/react-dom.mjs","react-dom/client":"/__inkly/vendor/react-dom-client.mjs","react/jsx-runtime":"/__inkly/vendor/react-jsx-runtime.mjs"}}</script>
12
+ <link rel="modulepreload" href="/__inkly/vendor/react.mjs" crossorigin="anonymous" />
13
+ <link rel="modulepreload" href="/__inkly/vendor/react-dom.mjs" crossorigin="anonymous" />
14
+ <link rel="modulepreload" href="/__inkly/vendor/react-dom-client.mjs" crossorigin="anonymous" />
15
+ <link rel="modulepreload" href="/__inkly/vendor/react-jsx-runtime.mjs" crossorigin="anonymous" />
16
+ <!-- Curated theme preset from @inkly/demo-react/themes.
17
+ Loaded after runtime.css so the preset's scoped CSS wins cascade.
18
+ The dev server serves this from the preset registry; the published
19
+ bundle ships the same string baked into its stylesheet. -->
20
+ <link id="inkly-hub-theme-css" rel="stylesheet" href="/__inkly/theme/inkly.css" />
21
+ <script id="__inkly_hub_meta" type="application/json">{"hub":{"name":"","brand":null,"tokens":null,"theme":null,"layout":null},"demoSlug":"","demoTitle":""}</script>
22
+ <style>
23
+ /*
24
+ * Player chrome matches the canonical /d/[slug] viewer:
25
+ * - light surface header (56px) with brand pill + demo name + CTA
26
+ * - dotted-grid canvas below
27
+ * - Demo runtime sits transparently on top of the canvas
28
+ * Hub branding (logo, CTA) layers as overrides.
29
+ */
30
+ :root {
31
+ --surface-2: #fcfcfc;
32
+ --line-soft: #dddddd;
33
+ --ink-strong: #1f1f1f;
34
+ --ink-muted: #6b6b6b;
35
+ --canvas: #f5f5f5;
36
+ }
37
+ * { box-sizing: border-box; }
38
+ html, body { margin: 0; height: 100%; }
39
+ body {
40
+ display: flex;
41
+ flex-direction: column;
42
+ min-height: 100vh;
43
+ background: var(--canvas);
44
+ color: var(--ink-strong);
45
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
46
+ font-size: 14px;
47
+ }
48
+
49
+ header.inkly-bar {
50
+ flex: 0 0 auto;
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 12px;
54
+ height: 56px;
55
+ padding: 0 20px;
56
+ background: var(--surface-2);
57
+ border-bottom: 1px solid var(--line-soft);
58
+ }
59
+ .inkly-btn {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ height: 32px;
64
+ border-radius: 8px;
65
+ border: 1px solid transparent;
66
+ background: transparent;
67
+ color: var(--ink-strong);
68
+ font-size: 13px;
69
+ text-decoration: none;
70
+ }
71
+ .inkly-btn:hover { background: rgba(0, 0, 0, 0.04); }
72
+
73
+ .brand {
74
+ min-width: 32px;
75
+ padding: 0 8px;
76
+ font-weight: 700;
77
+ }
78
+ .brand img { display: block; max-height: 24px; max-width: 96px; }
79
+ .brand-mark { line-height: 1; }
80
+
81
+ .divider {
82
+ width: 1px;
83
+ height: 20px;
84
+ background: var(--line-soft);
85
+ flex: 0 0 auto;
86
+ }
87
+ .demo-name {
88
+ flex: 1 1 auto;
89
+ min-width: 0;
90
+ font-weight: 600;
91
+ color: var(--ink-strong);
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ white-space: nowrap;
95
+ }
96
+ .hub-name {
97
+ min-width: 0;
98
+ max-width: 34vw;
99
+ font-weight: 600;
100
+ color: var(--ink-strong);
101
+ overflow: hidden;
102
+ text-overflow: ellipsis;
103
+ white-space: nowrap;
104
+ text-decoration: none;
105
+ }
106
+ .hub-name:hover { text-decoration: underline; }
107
+ .slash {
108
+ color: var(--ink-muted);
109
+ flex: 0 0 auto;
110
+ }
111
+ .cta {
112
+ padding: 0 12px;
113
+ font-weight: 500;
114
+ }
115
+ .inkly-button-scope {
116
+ margin-left: auto;
117
+ display: inline-flex;
118
+ align-items: center;
119
+ gap: 8px;
120
+ background: transparent !important;
121
+ background-image: none !important;
122
+ color: inherit;
123
+ font-family: inherit;
124
+ }
125
+ .inkly-button-scope .inkly-hub-pa-cta,
126
+ .inkly-button-scope .inkly-hub-editorial-cta,
127
+ .inkly-button-scope .inkly-hub-substack-cta,
128
+ .inkly-button-scope .inkly-hub-mono-cta {
129
+ margin: 0;
130
+ white-space: nowrap;
131
+ }
132
+ .inkly-button-scope [hidden] {
133
+ display: none !important;
134
+ }
135
+ /* Canvas fills remaining viewport vertically AND has an explicit
136
+ * definite width — this is what gives the Demo runtime's
137
+ * `width: min(100%, 960px, …)` something concrete to resolve
138
+ * against. Without it, Demo collapses to its min-content. */
139
+ #root {
140
+ flex: 1 1 auto;
141
+ min-height: 0;
142
+ width: 100%;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ background: var(--canvas);
147
+ background-image: radial-gradient(circle at 1px 1px, rgba(0, 0, 0, 0.10) 1px, transparent 1.6px);
148
+ background-size: 14px 14px;
149
+ container-type: size;
150
+ }
151
+ /* Plain block container — NOT a flex parent — that gives .demo-root
152
+ * a definite 100% width to resolve against. Matches the canonical
153
+ * bundle App.tsx exactly:
154
+ * <main className="min-h-screen w-full flex items-center justify-center">
155
+ * <div className="w-full"> <Demo /> </div>
156
+ * </main>
157
+ * If you make this flex/grid, Demo becomes a flex/grid child of
158
+ * a centering container and shrinks to min-content — the original
159
+ * collapse bug. */
160
+ .demo-wrap {
161
+ width: 100%;
162
+ }
163
+
164
+ .inkly-loading, .inkly-error {
165
+ padding: 24px;
166
+ max-width: 720px;
167
+ margin: 48px auto;
168
+ background: #fff;
169
+ border: 1px solid var(--line-soft);
170
+ border-radius: 8px;
171
+ line-height: 1.5;
172
+ color: var(--ink-strong);
173
+ }
174
+ .inkly-error { border-color: #ef4444; background: #fff5f5; }
175
+ .inkly-error pre {
176
+ background: #fafafa;
177
+ padding: 12px;
178
+ border-radius: 6px;
179
+ overflow: auto;
180
+ white-space: pre-wrap;
181
+ word-break: break-word;
182
+ font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
183
+ font-size: 12px;
184
+ }
185
+ </style>
186
+ <!-- Demo config is injected by the dev server right into this script tag
187
+ (parsed at render time, no fetch round-trip). Empty default so the
188
+ page parses standalone if served raw without the middleware. -->
189
+ <script id="__inkly_demo_config" type="application/json">null</script>
190
+ <!-- assets.json's `assets[]` — paired with the resolver below to turn
191
+ `asset:<id>` URIs inside demo.config into fetchable asset URLs.
192
+ Empty default for pre-capture sample demos. -->
193
+ <script id="__inkly_demo_assets" type="application/json">[]</script>
194
+ </head>
195
+ <body>
196
+ <header class="inkly-bar">
197
+ <a href="/" class="inkly-btn brand" id="brand-link" aria-label="Inkly" hidden>
198
+ </a>
199
+ <span class="divider" id="brand-divider" aria-hidden="true" hidden></span>
200
+ <a href="/" class="hub-name" id="hub-link" hidden></a>
201
+ <span class="slash" id="hub-slash" aria-hidden="true" hidden>/</span>
202
+ <span class="demo-name" id="demo-name"></span>
203
+ <span class="inkly-button-scope hub-index" id="inkly-button-scope" data-theme="inkly">
204
+ <a href="/" class="inkly-hub-pa-cta is-secondary" id="secondary-cta-link" hidden></a>
205
+ <a href="/" class="inkly-hub-pa-cta is-primary" id="cta-link">Build your own</a>
206
+ </span>
207
+ </header>
208
+ <div id="root">
209
+ <div class="inkly-loading">Loading demo runtime…</div>
210
+ </div>
211
+ <script>
212
+ (function () {
213
+ var node = document.getElementById('__inkly_hub_meta');
214
+ if (!node) return;
215
+ var meta;
216
+ try { meta = JSON.parse(node.textContent || '{}'); } catch (e) { return; }
217
+ var hub = meta.hub || {};
218
+ var brand = hub.brand || {};
219
+ var tokens = hub.tokens || {};
220
+ var demoConfig = null;
221
+ var configNode = document.getElementById('__inkly_demo_config');
222
+ try { demoConfig = JSON.parse(configNode && configNode.textContent || 'null'); } catch (e) {}
223
+
224
+ var headerThemeId = hub.theme || 'inkly';
225
+ var themeLink = document.getElementById('inkly-hub-theme-css');
226
+ if (themeLink) {
227
+ themeLink.href = '/__inkly/theme/' + encodeURIComponent(headerThemeId) + '.css';
228
+ }
229
+
230
+ var buttonScope = document.getElementById('inkly-button-scope');
231
+ if (buttonScope) {
232
+ buttonScope.dataset.theme = headerThemeId;
233
+ if (tokens.secondary) buttonScope.style.setProperty('--demo-secondary', tokens.secondary);
234
+ if (tokens.font) buttonScope.style.setProperty('--demo-font', tokens.font);
235
+ if (tokens.radius) buttonScope.style.setProperty('--demo-radius', tokens.radius);
236
+ }
237
+
238
+ document.title = (meta.demoTitle || meta.demoSlug || 'demo') + ' — ' + (hub.name || 'inkly dev');
239
+
240
+ // Hub favicon replaces the browser tab icon if set.
241
+ if (brand.favicon) {
242
+ var iconLink = document.querySelector('link[rel="icon"]');
243
+ if (!iconLink) {
244
+ iconLink = document.createElement('link');
245
+ iconLink.rel = 'icon';
246
+ document.head.appendChild(iconLink);
247
+ }
248
+ iconLink.href = brand.favicon;
249
+ }
250
+
251
+ // Header center slot. `chrome.showHubBreadcrumb` opts into a
252
+ // clickable hub crumb; otherwise the local shell shows only the
253
+ // demo name.
254
+ var showHubBreadcrumb = !!(
255
+ demoConfig &&
256
+ demoConfig.chrome &&
257
+ demoConfig.chrome.showHubBreadcrumb
258
+ );
259
+ var hubLink = document.getElementById('hub-link');
260
+ var hubSlash = document.getElementById('hub-slash');
261
+ if (showHubBreadcrumb && hubLink && hubSlash) {
262
+ hubLink.textContent = hub.name || 'Demo hub';
263
+ hubLink.hidden = false;
264
+ hubSlash.hidden = false;
265
+ }
266
+ var nameEl = document.getElementById('demo-name');
267
+ if (nameEl) nameEl.textContent = meta.demoTitle || meta.demoSlug || '';
268
+ function resolveHubPublicUrl(src) {
269
+ if (!src || typeof src !== 'string') return '';
270
+ var value = src.trim();
271
+ if (!value) return '';
272
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(value)) return value;
273
+ if (value.charAt(0) === '/') return value;
274
+ if (value.indexOf('public/') === 0) return '/' + value.slice('public/'.length);
275
+ return '/' + value.replace(/^\.\/+/, '');
276
+ }
277
+
278
+ // Hub logo replaces the default Inkly brand mark if set.
279
+ var brandLink = document.getElementById('brand-link');
280
+ var brandDivider = document.getElementById('brand-divider');
281
+ var defaultBrandLogoSrc = 'https://www.inklyai.dev/assets/logo.png';
282
+ var brandLogoSrc = resolveHubPublicUrl(brand.logo) || defaultBrandLogoSrc;
283
+ if (brandLink && brandLogoSrc) {
284
+ var brandMark = document.createElement('img');
285
+ brandMark.className = 'brand-mark';
286
+ brandMark.style.cssText = 'height:22px;width:22px;border-radius:0;object-fit:contain;display:block';
287
+ brandMark.src = brandLogoSrc;
288
+ brandMark.alt = hub.name || '';
289
+ brandMark.onerror = function () {
290
+ brandLink.hidden = true;
291
+ if (brandDivider) brandDivider.hidden = true;
292
+ };
293
+ brandLink.appendChild(brandMark);
294
+ brandLink.hidden = false;
295
+ if (brandDivider) brandDivider.hidden = false;
296
+ if (hub.name) brandLink.setAttribute('aria-label', hub.name);
297
+ }
298
+
299
+ function ctaClassForTheme(themeId, kind) {
300
+ var baseByTheme = {
301
+ editorial: 'inkly-hub-editorial-cta',
302
+ mono: 'inkly-hub-mono-cta',
303
+ 'inkly': 'inkly-hub-pa-cta',
304
+ substack: 'inkly-hub-substack-cta'
305
+ };
306
+ var base = baseByTheme[themeId] || baseByTheme['inkly'];
307
+ return base + ' is-' + kind;
308
+ }
309
+
310
+ function configureExternalLink(el, link) {
311
+ el.href = link.href;
312
+ el.target = '_blank';
313
+ el.rel = 'noopener noreferrer';
314
+ el.textContent = link.label;
315
+ }
316
+
317
+ // Hub CTA buttons mirror the index-page theme classes.
318
+ var ctaLink = document.getElementById('cta-link');
319
+ if (ctaLink) {
320
+ ctaLink.className = ctaClassForTheme(headerThemeId, 'primary');
321
+ if (brand.cta && brand.cta.href && brand.cta.label) {
322
+ configureExternalLink(ctaLink, brand.cta);
323
+ }
324
+ }
325
+ var secondaryCtaLink = document.getElementById('secondary-cta-link');
326
+ if (secondaryCtaLink) {
327
+ secondaryCtaLink.className = ctaClassForTheme(headerThemeId, 'secondary');
328
+ if (brand.secondaryCta && brand.secondaryCta.href && brand.secondaryCta.label) {
329
+ configureExternalLink(secondaryCtaLink, brand.secondaryCta);
330
+ secondaryCtaLink.hidden = false;
331
+ }
332
+ }
333
+
334
+ // Expose hub-level theme cascade pieces so the runtime mount
335
+ // below can resolve the effective theme id without a refetch.
336
+ // `demo.theme.preset` (from the inlined config) wins; the hub's
337
+ // `theme` is the next-best default.
338
+ window.__INKLY_HUB_THEME__ = hub.theme || null;
339
+ window.__INKLY_HUB_TOKENS__ = hub.tokens || null;
340
+ window.__INKLY_DEMO_SLUG__ = meta.demoSlug || '';
341
+ })();
342
+ </script>
343
+ <script type="module">
344
+ // runtime.js externalizes React. The import map above resolves
345
+ // those bare specifiers to the vendored ESM files served by
346
+ // inkly dev.
347
+ let createElement;
348
+ let createRoot;
349
+ let Demo;
350
+ try {
351
+ ({ createElement, createRoot, Demo } = await import('/__inkly/runtime.js'));
352
+ } catch (err) {
353
+ const errorEl = document.createElement('div');
354
+ errorEl.className = 'inkly-error';
355
+ const title = document.createElement('h2');
356
+ title.textContent = 'Runtime bundle missing';
357
+ const body = document.createElement('pre');
358
+ body.textContent =
359
+ 'The demo loaded, but the Inkly runtime did not.\n\n' +
360
+ 'Check /__inkly/runtime.js and /__inkly/vendor/react*.mjs, then restart inkly dev.\n\n' +
361
+ String(err && err.stack ? err.stack : err);
362
+ errorEl.append(title, body);
363
+ const rootEl = document.getElementById('root');
364
+ if (rootEl) rootEl.replaceChildren(errorEl);
365
+ throw err;
366
+ }
367
+
368
+ const rootEl = document.getElementById('root');
369
+ const root = createRoot(rootEl);
370
+
371
+ function readInlineConfig() {
372
+ const node = document.getElementById('__inkly_demo_config');
373
+ if (!node) return null;
374
+ try { return JSON.parse(node.textContent || 'null'); } catch (e) { return null; }
375
+ }
376
+
377
+ function readInlineAssets() {
378
+ const node = document.getElementById('__inkly_demo_assets');
379
+ if (!node) return [];
380
+ try {
381
+ const parsed = JSON.parse(node.textContent || '[]');
382
+ return Array.isArray(parsed) ? parsed : [];
383
+ } catch (e) { return []; }
384
+ }
385
+
386
+ // Managed assets must carry the URL they should render from.
387
+ // Local ZIPs use `/<slug>/<file>`; synced assets use a CDN URL.
388
+ function resolveAssetUrlForCli(entry) {
389
+ if (entry && typeof entry.publicUrl === 'string' && entry.publicUrl) {
390
+ return entry.publicUrl;
391
+ }
392
+ return '';
393
+ }
394
+ function resolveHtmlUrlForCli(path) {
395
+ return '/__inkly/snapshot/' +
396
+ String(window.__INKLY_DEMO_SLUG__ || '').split('/').map(encodeURIComponent).join('/') +
397
+ '/' +
398
+ String(path || '').split('/').map(encodeURIComponent).join('/');
399
+ }
400
+
401
+ function renderError(title, body) {
402
+ root.render(
403
+ createElement(
404
+ 'div',
405
+ { className: 'inkly-error' },
406
+ createElement('h2', null, title),
407
+ createElement('pre', null, body),
408
+ ),
409
+ );
410
+ }
411
+
412
+ function renderConfig(config, assets) {
413
+ if (!config) {
414
+ renderError(
415
+ 'No demo config',
416
+ 'The page was served without an inlined config. This usually means the dev server middleware is not running.',
417
+ );
418
+ return;
419
+ }
420
+ // Resolve the per-demo theme cascade. Per the spec:
421
+ // effective.theme = demo.theme.preset ?? hub.theme ?? "inkly"
422
+ // The hub fields are stashed onto `window` by the inline meta
423
+ // script above; the demo's own preset (if any) wins.
424
+ var demoTheme = (config && config.theme) || {};
425
+ var themeId =
426
+ (demoTheme && demoTheme.preset) ||
427
+ window.__INKLY_HUB_THEME__ ||
428
+ 'inkly';
429
+
430
+ // Make sure the preset's CSS is loaded — the CLI serves it at
431
+ // /__inkly/theme/<id>.css. Inject once per theme id.
432
+ if (!document.querySelector('link[data-inkly-theme="' + themeId + '"]')) {
433
+ var link = document.createElement('link');
434
+ link.rel = 'stylesheet';
435
+ link.href = '/__inkly/theme/' + encodeURIComponent(themeId) + '.css';
436
+ link.setAttribute('data-inkly-theme', themeId);
437
+ document.head.appendChild(link);
438
+ }
439
+
440
+ function applyThemeCanvas(themeId) {
441
+ rootEl.style.background = '';
442
+ rootEl.style.backgroundColor = '';
443
+ rootEl.style.backgroundImage = '';
444
+ rootEl.style.backgroundPosition = '';
445
+ rootEl.style.backgroundRepeat = '';
446
+ rootEl.style.backgroundSize = '';
447
+
448
+ if (themeId === 'editorial') {
449
+ rootEl.style.background = '#faf9f5';
450
+ return;
451
+ }
452
+ if (themeId === 'mono') {
453
+ rootEl.style.background = '#f7f7f7';
454
+ return;
455
+ }
456
+ if (themeId === 'substack') {
457
+ rootEl.style.background = '#fff7f2';
458
+ return;
459
+ }
460
+ if (themeId === 'inkly') {
461
+ rootEl.style.background = '#f5f5f5';
462
+ rootEl.style.backgroundImage = 'radial-gradient(circle at 1px 1px, rgba(0, 0, 0, 0.10) 1px, transparent 1.6px)';
463
+ rootEl.style.backgroundSize = '14px 14px';
464
+ return;
465
+ }
466
+ rootEl.style.background = '#f5f5f7';
467
+ }
468
+
469
+ function resolveCanvasAssetUri(uri) {
470
+ if (!uri) return uri;
471
+ if (/^(https?:|data:|blob:)/i.test(uri)) return uri;
472
+ var list = Array.isArray(assets) ? assets : [];
473
+ if (String(uri).startsWith('asset:')) {
474
+ var id = String(uri).slice('asset:'.length);
475
+ var found = list.find(function (entry) { return entry && entry.id === id; });
476
+ return found ? resolveAssetUrlForCli(found) : uri;
477
+ }
478
+ return uri;
479
+ }
480
+
481
+ var demoBackground = config && config.background;
482
+ var canvasColor =
483
+ demoBackground && demoBackground.type === 'color' && demoBackground.color
484
+ ? demoBackground.color
485
+ : demoBackground && demoBackground.type === 'solid'
486
+ ? demoBackground.color
487
+ : config && typeof config.backgroundColor === 'string'
488
+ ? config.backgroundColor
489
+ : '';
490
+ applyThemeCanvas(themeId);
491
+ if (
492
+ demoBackground &&
493
+ (
494
+ (demoBackground.type === 'color' && demoBackground.from && demoBackground.to) ||
495
+ demoBackground.type === 'gradient'
496
+ )
497
+ ) {
498
+ rootEl.style.background =
499
+ 'linear-gradient(135deg, ' + demoBackground.from + ', ' + demoBackground.to + ')';
500
+ } else if (
501
+ demoBackground &&
502
+ (demoBackground.type === 'image' || demoBackground.type === 'wallpaper') &&
503
+ demoBackground.src
504
+ ) {
505
+ rootEl.style.backgroundColor = '#f5f5f5';
506
+ rootEl.style.backgroundImage = 'url(\"' + resolveCanvasAssetUri(demoBackground.src) + '\")';
507
+ rootEl.style.backgroundPosition = 'center';
508
+ rootEl.style.backgroundRepeat = 'no-repeat';
509
+ rootEl.style.backgroundSize = 'cover';
510
+ } else if (demoBackground && demoBackground.type === 'none') {
511
+ // Keep the active theme's default player canvas.
512
+ } else if (canvasColor) {
513
+ rootEl.style.background = canvasColor;
514
+ }
515
+
516
+ // The .demo-wrap div gives <Demo> a definite-width containing block
517
+ // so its internal `width: min(100%, 960px, ...)` resolves correctly.
518
+ // Mirrors the canonical bundle App.tsx pattern.
519
+ root.render(
520
+ createElement(
521
+ 'div',
522
+ { className: 'demo-wrap' },
523
+ createElement(Demo, {
524
+ config: config,
525
+ themeId: themeId,
526
+ themeTokens: window.__INKLY_HUB_TOKENS__ || undefined,
527
+ assets: assets || [],
528
+ resolveAssetUrl: resolveAssetUrlForCli,
529
+ resolveHtmlUrl: resolveHtmlUrlForCli,
530
+ allowHtmlSnapshotScripts: false,
531
+ }),
532
+ ),
533
+ );
534
+ }
535
+
536
+ // First paint uses the config + assets inlined into the HTML — no fetch.
537
+ renderConfig(readInlineConfig(), readInlineAssets());
538
+
539
+ // On HMR change events, refetch the config (the inlined copy is
540
+ // now stale). The endpoint now returns `{ demo, assets }` so the
541
+ // resolver sees the latest assets.json without a separate fetch.
542
+ async function refetchAndRender() {
543
+ try {
544
+ const res = await fetch('/__inkly/demo/' + encodeURIComponent(window.__INKLY_DEMO_SLUG__ || ''), { cache: 'no-store' });
545
+ if (!res.ok) {
546
+ renderError('demo.config.json is invalid', await res.text());
547
+ return;
548
+ }
549
+ const payload = await res.json();
550
+ // Endpoint returns `{ demo, assets }`; tolerate older response
551
+ // shapes (raw config) for resilience during the transition.
552
+ if (payload && typeof payload === 'object' && 'demo' in payload) {
553
+ renderConfig(payload.demo, Array.isArray(payload.assets) ? payload.assets : []);
554
+ } else {
555
+ renderConfig(payload, []);
556
+ }
557
+ } catch (err) {
558
+ renderError('Failed to load config', String(err && err.stack ? err.stack : err));
559
+ }
560
+ }
561
+ if (import.meta.hot) {
562
+ import.meta.hot.on('inkly:hub-changed', refetchAndRender);
563
+ import.meta.hot.on('inkly:config-changed', refetchAndRender);
564
+ }
565
+ </script>
566
+ </body>
567
+ </html>