@allegrocdp/preview 0.1.0-dev.13 → 0.1.0-dev.14
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 +37 -3
- package/dist/cli.js +208 -4
- package/dist/client/assets/app-C--PVKMq.js +14 -0
- package/dist/client/index.html +2 -6
- package/dist/client/preview-client.js +1 -1
- package/dist/server.js +208 -4
- package/package.json +1 -1
- package/dist/client/assets/app-B0ybFOKs.js +0 -14
package/README.md
CHANGED
|
@@ -58,13 +58,47 @@ my-template/
|
|
|
58
58
|
|
|
59
59
|
Supported field types: `text`, `number`, `checkbox`, `textarea`.
|
|
60
60
|
|
|
61
|
-
##
|
|
61
|
+
## Scenarios
|
|
62
|
+
|
|
63
|
+
`allegro-preview` ships three built-in scenarios:
|
|
62
64
|
|
|
63
65
|
| Tab | Description |
|
|
64
66
|
|-----|-------------|
|
|
65
|
-
| **Standalone** | Template rendered on its own in an iframe |
|
|
67
|
+
| **Standalone** | Template rendered on its own in an isolated iframe |
|
|
68
|
+
| **Article · Append** | Template injected after the full content of a mock article |
|
|
66
69
|
| **Article · Cut** | Template injected after paragraph 3 of a mock article using the `truncate` placement method |
|
|
67
|
-
|
|
70
|
+
|
|
71
|
+
### Custom scenarios
|
|
72
|
+
|
|
73
|
+
Define your own scenarios by creating an `.allegro-preview/` folder at the root of your templates directory, with one subfolder per scenario:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
.allegro-preview/
|
|
77
|
+
├── paywalled-article/
|
|
78
|
+
│ ├── scenario.json # required: name + placement config
|
|
79
|
+
│ ├── host.html # optional: your mock host page markup
|
|
80
|
+
│ └── host.css # optional: host page styles
|
|
81
|
+
└── homepage-rail/
|
|
82
|
+
├── scenario.json
|
|
83
|
+
└── host.html
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each `scenario.json` specifies the scenario name and placement parameters:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"name": "Paywalled Article",
|
|
91
|
+
"target_selector": "#article-body",
|
|
92
|
+
"placement_method": "truncate",
|
|
93
|
+
"truncation_unit": "paragraphs",
|
|
94
|
+
"truncation_count": 4,
|
|
95
|
+
"truncation_style": "cut"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
When `host.html` is present, the template is injected into `target_selector` using `placement_method`. Without `host.html`, the template renders standalone. Custom scenarios appear as additional tabs alongside the built-in ones and live-reload when edited.
|
|
100
|
+
|
|
101
|
+
> **Note:** The `.allegro-preview/` folder is ignored by the GitHub template sync — it never becomes a template and never produces a sync warning.
|
|
68
102
|
|
|
69
103
|
## Device breakpoints
|
|
70
104
|
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,53 @@ window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
|
|
|
27
27
|
}
|
|
28
28
|
};`;
|
|
29
29
|
|
|
30
|
+
// src/scenarios.ts
|
|
31
|
+
function emptyPlacement() {
|
|
32
|
+
return {
|
|
33
|
+
target_selector: null,
|
|
34
|
+
placement_method: null,
|
|
35
|
+
truncation_unit: null,
|
|
36
|
+
truncation_count: null,
|
|
37
|
+
truncation_style: null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var ARTICLE_TARGET_SELECTOR = "#allegro-preview-article";
|
|
41
|
+
var BUILT_IN_SCENARIOS = [
|
|
42
|
+
{
|
|
43
|
+
id: "standalone",
|
|
44
|
+
name: "Standalone",
|
|
45
|
+
builtIn: true,
|
|
46
|
+
hostKind: "none",
|
|
47
|
+
placement: emptyPlacement()
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "append",
|
|
51
|
+
name: "Article \xB7 Append",
|
|
52
|
+
builtIn: true,
|
|
53
|
+
hostKind: "article",
|
|
54
|
+
placement: {
|
|
55
|
+
target_selector: ARTICLE_TARGET_SELECTOR,
|
|
56
|
+
placement_method: "append",
|
|
57
|
+
truncation_unit: null,
|
|
58
|
+
truncation_count: null,
|
|
59
|
+
truncation_style: null
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "cut",
|
|
64
|
+
name: "Article \xB7 Cut",
|
|
65
|
+
builtIn: true,
|
|
66
|
+
hostKind: "article",
|
|
67
|
+
placement: {
|
|
68
|
+
target_selector: ARTICLE_TARGET_SELECTOR,
|
|
69
|
+
placement_method: "truncate",
|
|
70
|
+
truncation_unit: "paragraphs",
|
|
71
|
+
truncation_count: 3,
|
|
72
|
+
truncation_style: "cut"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
|
|
30
77
|
// src/slugify.ts
|
|
31
78
|
function slugify(name) {
|
|
32
79
|
return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -227,7 +274,8 @@ function buildPreviewData(options) {
|
|
|
227
274
|
css: options.css,
|
|
228
275
|
js: options.js,
|
|
229
276
|
fieldValues: options.fieldValues,
|
|
230
|
-
tab: options.
|
|
277
|
+
tab: options.scenario.id,
|
|
278
|
+
placement: options.scenario.placement
|
|
231
279
|
});
|
|
232
280
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
233
281
|
}
|
|
@@ -293,6 +341,33 @@ function articleShell(options) {
|
|
|
293
341
|
</body>
|
|
294
342
|
</html>`;
|
|
295
343
|
}
|
|
344
|
+
function customHostShell(options) {
|
|
345
|
+
const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
|
|
346
|
+
const hostCss = options.scenario.hostCss ? `<style>${options.scenario.hostCss}</style>` : "";
|
|
347
|
+
const hostHtml = options.scenario.hostHtml ?? "";
|
|
348
|
+
return `<!DOCTYPE html>
|
|
349
|
+
<html>
|
|
350
|
+
<head>
|
|
351
|
+
<meta charset="UTF-8">
|
|
352
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
353
|
+
<script>${PREVIEW_BOOTSTRAP_SCRIPT}</script>
|
|
354
|
+
${cookieScript}
|
|
355
|
+
<style>
|
|
356
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
357
|
+
body { margin: 0; }
|
|
358
|
+
[data-allegro-interaction] { display: block; }
|
|
359
|
+
</style>
|
|
360
|
+
${previewCssTag(options.previewCss)}
|
|
361
|
+
${hostCss}
|
|
362
|
+
</head>
|
|
363
|
+
<body>
|
|
364
|
+
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
365
|
+
${hostHtml}
|
|
366
|
+
${sdkScriptTag(options.sdkUrl)}
|
|
367
|
+
<script src="/preview-client.js"></script>
|
|
368
|
+
</body>
|
|
369
|
+
</html>`;
|
|
370
|
+
}
|
|
296
371
|
var sseClients = /* @__PURE__ */ new Set();
|
|
297
372
|
function broadcast(event) {
|
|
298
373
|
for (const client of sseClients) {
|
|
@@ -384,6 +459,109 @@ function readTemplateFiles(dir) {
|
|
|
384
459
|
function readTemplateFilesCached(dir) {
|
|
385
460
|
return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
|
|
386
461
|
}
|
|
462
|
+
var SCENARIOS_DIRNAME = ".allegro-preview";
|
|
463
|
+
function parsePlacement(raw) {
|
|
464
|
+
const placement = emptyPlacement();
|
|
465
|
+
if (typeof raw.target_selector === "string") {
|
|
466
|
+
placement.target_selector = raw.target_selector;
|
|
467
|
+
}
|
|
468
|
+
if (raw.placement_method === "truncate" || raw.placement_method === "append") {
|
|
469
|
+
placement.placement_method = raw.placement_method;
|
|
470
|
+
}
|
|
471
|
+
if (raw.truncation_unit === "paragraphs") {
|
|
472
|
+
placement.truncation_unit = raw.truncation_unit;
|
|
473
|
+
}
|
|
474
|
+
if (typeof raw.truncation_count === "number" && Number.isFinite(raw.truncation_count)) {
|
|
475
|
+
placement.truncation_count = raw.truncation_count;
|
|
476
|
+
}
|
|
477
|
+
if (raw.truncation_style === "cut") {
|
|
478
|
+
placement.truncation_style = raw.truncation_style;
|
|
479
|
+
}
|
|
480
|
+
return placement;
|
|
481
|
+
}
|
|
482
|
+
function readCustomScenario(scenariosDir, id) {
|
|
483
|
+
const dir = join(scenariosDir, id);
|
|
484
|
+
const jsonPath = join(dir, "scenario.json");
|
|
485
|
+
const base = {
|
|
486
|
+
id,
|
|
487
|
+
name: id,
|
|
488
|
+
builtIn: false,
|
|
489
|
+
hostKind: "none",
|
|
490
|
+
placement: emptyPlacement(),
|
|
491
|
+
parseError: null
|
|
492
|
+
};
|
|
493
|
+
let raw;
|
|
494
|
+
try {
|
|
495
|
+
raw = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
496
|
+
} catch (err) {
|
|
497
|
+
base.parseError = err instanceof Error ? err.message : "Invalid JSON in scenario.json";
|
|
498
|
+
return base;
|
|
499
|
+
}
|
|
500
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
501
|
+
base.parseError = "scenario.json must be a JSON object";
|
|
502
|
+
return base;
|
|
503
|
+
}
|
|
504
|
+
if (typeof raw.name === "string" && raw.name.length > 0) {
|
|
505
|
+
base.name = raw.name;
|
|
506
|
+
}
|
|
507
|
+
base.placement = parsePlacement(raw);
|
|
508
|
+
const hostHtmlPath = join(dir, "host.html");
|
|
509
|
+
if (existsSync(hostHtmlPath)) {
|
|
510
|
+
try {
|
|
511
|
+
base.hostKind = "custom";
|
|
512
|
+
base.hostHtml = readFileSync(hostHtmlPath, "utf-8");
|
|
513
|
+
const hostCssPath = join(dir, "host.css");
|
|
514
|
+
base.hostCss = existsSync(hostCssPath) ? readFileSync(hostCssPath, "utf-8") : "";
|
|
515
|
+
} catch (err) {
|
|
516
|
+
base.parseError = err instanceof Error ? err.message : "Failed to read scenario host files";
|
|
517
|
+
return base;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return base;
|
|
521
|
+
}
|
|
522
|
+
function scanCustomScenarios(baseDir) {
|
|
523
|
+
const scenariosDir = join(baseDir, SCENARIOS_DIRNAME);
|
|
524
|
+
if (!existsSync(scenariosDir)) {
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
const results = [];
|
|
528
|
+
try {
|
|
529
|
+
for (const entry of readdirSync(scenariosDir, { withFileTypes: true })) {
|
|
530
|
+
if (!entry.isDirectory()) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (!existsSync(join(scenariosDir, entry.name, "scenario.json"))) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const scenario = readCustomScenario(scenariosDir, entry.name);
|
|
537
|
+
if (scenario.parseError) {
|
|
538
|
+
console.warn(`[allegro-preview] Skipping scenario "${entry.name}": ${scenario.parseError}`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
results.push(scenario);
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
return results;
|
|
546
|
+
}
|
|
547
|
+
function listScenarioSummaries(customScenarios) {
|
|
548
|
+
return [
|
|
549
|
+
...BUILT_IN_SCENARIOS.map((s) => ({ id: s.id, name: s.name })),
|
|
550
|
+
...customScenarios.map((s) => ({ id: s.id, name: s.name }))
|
|
551
|
+
];
|
|
552
|
+
}
|
|
553
|
+
function resolveScenario(baseDir, id) {
|
|
554
|
+
const builtIn = BUILT_IN_SCENARIOS.find((s) => s.id === id);
|
|
555
|
+
if (builtIn) {
|
|
556
|
+
return builtIn;
|
|
557
|
+
}
|
|
558
|
+
const scenariosDir = join(baseDir, SCENARIOS_DIRNAME);
|
|
559
|
+
if (existsSync(join(scenariosDir, id, "scenario.json"))) {
|
|
560
|
+
const scenario = readCustomScenario(scenariosDir, id);
|
|
561
|
+
return scenario.parseError ? null : scenario;
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
387
565
|
async function startServer(templatePath, port = 0, tenant = "") {
|
|
388
566
|
const baseDir = resolve(process.cwd(), templatePath);
|
|
389
567
|
if (!existsSync(baseDir)) {
|
|
@@ -391,11 +569,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
391
569
|
process.exit(1);
|
|
392
570
|
}
|
|
393
571
|
let templates = scanTemplates(baseDir);
|
|
572
|
+
let customScenarios = scanCustomScenarios(baseDir);
|
|
394
573
|
const app = express();
|
|
395
574
|
const clientDistDir = join(__dirname, "client");
|
|
396
575
|
app.use(express.static(clientDistDir));
|
|
397
576
|
app.get("/api/config", (_req, res) => {
|
|
398
|
-
res.json({
|
|
577
|
+
res.json({
|
|
578
|
+
defaultSlug: templates[0]?.slug ?? null,
|
|
579
|
+
templates,
|
|
580
|
+
tenant,
|
|
581
|
+
scenarios: listScenarioSummaries(customScenarios)
|
|
582
|
+
});
|
|
399
583
|
});
|
|
400
584
|
app.get("/api/template", (req, res) => {
|
|
401
585
|
const slug = req.query.slug ?? ".";
|
|
@@ -481,6 +665,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
481
665
|
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
482
666
|
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
483
667
|
const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
|
|
668
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(tab)) {
|
|
669
|
+
res.status(404).send("Scenario not found");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const scenario = resolveScenario(baseDir, tab);
|
|
673
|
+
if (!scenario) {
|
|
674
|
+
res.status(404).send("Scenario not found");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
484
677
|
if (!slug) {
|
|
485
678
|
res.status(400).send("Missing slug");
|
|
486
679
|
return;
|
|
@@ -525,12 +718,12 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
525
718
|
css: templateFiles.css,
|
|
526
719
|
js: templateFiles.js,
|
|
527
720
|
fieldValues,
|
|
528
|
-
|
|
721
|
+
scenario,
|
|
529
722
|
sdkUrl: effectiveSdkUrl,
|
|
530
723
|
blockCookies,
|
|
531
724
|
previewCss
|
|
532
725
|
};
|
|
533
|
-
const rendered =
|
|
726
|
+
const rendered = scenario.hostKind === "custom" ? customHostShell(shellOptions) : scenario.hostKind === "article" ? articleShell(shellOptions) : standaloneShell(shellOptions);
|
|
534
727
|
res.setHeader("Cache-Control", "no-store");
|
|
535
728
|
res.type("html").send(rendered);
|
|
536
729
|
});
|
|
@@ -569,6 +762,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
569
762
|
broadcastReload();
|
|
570
763
|
});
|
|
571
764
|
const dirWatcher = chokidar.watch(baseDir, { depth: 1, ignoreInitial: true });
|
|
765
|
+
const scenariosWatcher = chokidar.watch(join(baseDir, SCENARIOS_DIRNAME), {
|
|
766
|
+
ignoreInitial: true
|
|
767
|
+
});
|
|
768
|
+
function rescanScenarios() {
|
|
769
|
+
customScenarios = scanCustomScenarios(baseDir);
|
|
770
|
+
console.log(`Scenarios updated: ${customScenarios.map((s) => s.id).join(", ") || "(none)"}`);
|
|
771
|
+
broadcastConfig();
|
|
772
|
+
}
|
|
773
|
+
scenariosWatcher.on("all", () => {
|
|
774
|
+
rescanScenarios();
|
|
775
|
+
});
|
|
572
776
|
function rescan() {
|
|
573
777
|
templates = scanTemplates(baseDir);
|
|
574
778
|
console.log(`Templates updated: ${templates.map((t) => t.slug).join(", ") || "(none)"}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))l(a);new MutationObserver(a=>{for(const s of a)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&l(o)}).observe(document,{childList:!0,subtree:!0});function n(a){const s={};return a.integrity&&(s.integrity=a.integrity),a.referrerPolicy&&(s.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?s.credentials="include":a.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function l(a){if(a.ep)return;a.ep=!0;const s=n(a);fetch(a.href,s)}})();function V(e){return e.normalize("NFKD").replace(/[\u0300-\u036f]/g,"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}const ae={full:0,desktop:1280,tablet:768,mobile:390};function se(e){return ae[e]}let d=[],L=[],m=null,r="standalone",w="full",y=null,b={},P,D=!0,M=null,g=null,E=null;const J=document.getElementById("template-nav"),v=document.getElementById("field-list"),f=document.getElementById("preview-area"),I=document.getElementById("tab-group"),j=document.getElementById("device-group"),H=document.getElementById("toolbar-badge"),ie=document.getElementById("theme-toggle"),ce=document.getElementById("theme-icon-sun"),re=document.getElementById("theme-icon-moon"),$=document.getElementById("tenant-input"),B=document.getElementById("tenant-status"),de=document.getElementById("new-template-btn"),W=document.getElementById("new-window-btn"),x=document.getElementById("create-modal-backdrop"),c=document.getElementById("modal-name-input"),G=document.getElementById("modal-slug-hint"),h=document.getElementById("modal-error"),ue=document.getElementById("modal-cancel-btn"),k=document.getElementById("modal-create-btn"),O=document.getElementById("block-cookies-checkbox"),z=document.getElementById("cookie-auth-area"),T=document.getElementById("cookie-auth-user"),R=document.getElementById("cookie-auth-hint"),me=document.getElementById("cookie-clear-btn"),C=document.getElementById("jwt-modal-backdrop"),pe=document.getElementById("jwt-payload-content"),fe=document.getElementById("jwt-modal-close-btn");function K(e){document.documentElement.setAttribute("data-theme",e?"dark":"light"),ce.style.display=e?"none":"",re.style.display=e?"":"none"}function ge(){const e=localStorage.getItem("allegro-preview-theme"),t=window.matchMedia("(prefers-color-scheme: dark)").matches;K(e?e==="dark":t)}ie.addEventListener("click",()=>{const e=document.documentElement.getAttribute("data-theme")!=="dark";K(e),localStorage.setItem("allegro-preview-theme",e?"dark":"light")});function ve(e){const t=e.trim();return t?(/^https?:\/\//.test(t)?t:`https://${t}`).replace(/\/+$/,"")+"/client.js":""}function Y(e){const t=ve(e);P=t||void 0,t?(B.className="tenant-active-badge",B.textContent=t):(B.className="tenant-hint",B.textContent="Loads /client.js on the preview page"),y&&p()}$.addEventListener("input",()=>{localStorage.setItem("allegro-preview-tenant",$.value),Y($.value)});function he(e=""){const t=localStorage.getItem("allegro-preview-tenant"),n=e||t||"";n&&($.value=n,e&&localStorage.setItem("allegro-preview-tenant",n),Y(n))}const ye=[{name:"_ALLEGROT",label:"Session JWT",isJwt:!0},{name:"_ALLEGROD",label:"Device ID"},{name:"_ALLEGROS",label:"Session ID"}];function Ee(e){const t=document.cookie.split("; ").find(n=>n.startsWith(e+"="));if(!t)return null;try{return decodeURIComponent(t.split("=").slice(1).join("="))}catch{return t.split("=").slice(1).join("=")}}function we(e){try{const t=e.split(".");if(t.length!==3)return null;const n=t[1].replace(/-/g,"+").replace(/_/g,"/"),l=atob(n);return JSON.parse(l)}catch{return null}}function S(){const e=ye.map(t=>({...t,value:Ee(t.name)})).filter(t=>t.value!==null);if(T.innerHTML="",e.length===0){T.style.display="none",R.style.display="";return}R.style.display="none",T.style.display="";for(const t of e){const n=document.createElement("div");n.className="cookie-row";const l=document.createElement("div");l.className="cookie-auth-dot";const a=document.createElement("div");a.className="cookie-row-info";const s=document.createElement("div");s.className="cookie-row-label",s.textContent=t.label;const o=document.createElement("div");if(o.className="cookie-row-value",o.textContent=t.value.length>24?t.value.slice(0,24)+"…":t.value,a.appendChild(s),a.appendChild(o),n.appendChild(l),n.appendChild(a),t.isJwt){const i=document.createElement("button");i.className="cookie-view-btn",i.textContent="Decode",i.onclick=()=>{const u=we(t.value);pe.textContent=u?JSON.stringify(u,null,2):t.value,C.classList.remove("hidden")},n.appendChild(i)}T.appendChild(n)}}function be(e){D=e,localStorage.setItem("allegro-preview-block-cookies",e?"true":"false"),z.style.display=e?"none":"",e?E&&(clearInterval(E),E=null):(S(),E||(E=setInterval(S,2e3))),y&&p()}function ke(){const t=localStorage.getItem("allegro-preview-block-cookies")!=="false";O.checked=t,D=t,z.style.display=t?"none":"",t||(S(),E=setInterval(S,2e3))}O.addEventListener("change",()=>{be(O.checked)});me.addEventListener("click",()=>{const e=document.cookie.split("; ");for(const t of e){const n=t.split("=")[0];document.cookie=`${n}=; max-age=0; path=/`}S()});fe.addEventListener("click",()=>{C.classList.add("hidden")});C.addEventListener("click",e=>{e.target===C&&C.classList.add("hidden")});function Ce(){const e=window.location.hash.slice(1);return e?decodeURIComponent(e):null}function Le(e){history.replaceState(null,"",`#${encodeURIComponent(e)}`)}function Ie(){history.replaceState(null,"",window.location.pathname)}function Q(e){I.querySelectorAll(".tab-btn").forEach(t=>{t.disabled=!e}),j.querySelectorAll(".device-btn").forEach(t=>{t.disabled=!e}),W.disabled=!e}function X(){I.innerHTML="";for(const e of L){const t=document.createElement("button");t.className="tab-btn"+(e.id===r?" active":""),t.dataset.tab=e.id,t.textContent=e.name,t.disabled=y===null,I.appendChild(t)}}I.addEventListener("click",e=>{const t=e.target.closest(".tab-btn");!t||t.disabled||(r=t.dataset.tab,I.querySelectorAll(".tab-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-tab",r),p())});j.addEventListener("click",e=>{const t=e.target.closest(".device-btn");!t||t.disabled||(w=t.dataset.device,j.querySelectorAll(".device-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-device",w),p())});function N(){J.innerHTML="";for(const e of d){const t=document.createElement("button");t.className="template-nav-item"+(e.slug===m?" active":""),t.innerHTML=`<span class="template-nav-dot"></span><span class="template-nav-name">${_(e.name)}</span>`,t.addEventListener("click",()=>void q(e.slug)),J.appendChild(t)}}async function q(e){m=e,Le(e),N(),await U(e)}function Z(){y=null,m=null,M=null,g=null,Ie(),N(),Q(!1),H.textContent="No template selected",H.style.opacity="0.4",v.innerHTML='<p class="no-fields">Select a template to edit fields.</p>';const e=document.createElement("div");if(e.className="selector-screen",d.length===0){e.innerHTML=`
|
|
2
|
+
<div class="selector-empty">
|
|
3
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
|
4
|
+
<span>No templates found in this directory.</span>
|
|
5
|
+
</div>`,f.innerHTML="",f.appendChild(e);return}e.innerHTML=`
|
|
6
|
+
<div class="selector-eyebrow">Allegro Preview</div>
|
|
7
|
+
<h2 class="selector-heading">Choose a template</h2>
|
|
8
|
+
<p class="selector-sub">Select a template to start previewing.</p>
|
|
9
|
+
<div class="selector-grid" id="selector-grid"></div>`,f.innerHTML="",f.appendChild(e);const t=e.querySelector("#selector-grid");for(const n of d){const l=document.createElement("button");l.className="selector-card",l.innerHTML=`
|
|
10
|
+
<div class="selector-card-icon">
|
|
11
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="selector-card-name">${_(n.name)}</div>
|
|
14
|
+
<div class="selector-card-slug">${_(n.slug)}</div>`,l.addEventListener("click",()=>void q(n.slug)),t.appendChild(l)}}const F=["text","number","checkbox","textarea"];function ee(e){if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.slug=="string"&&t.slug.length>0&&F.includes(t.type)}function xe(e){if(typeof e!="object"||e===null)return`Invalid field: expected an object, got ${JSON.stringify(e)}.`;const t=e,n=[];if((typeof t.slug!="string"||t.slug.length===0)&&n.push('missing required "slug" property'),!F.includes(t.type)){const a=t.type!==void 0?`"${String(t.type)}"`:"none";n.push(`invalid type ${a} — valid types: ${F.join(", ")}`)}return`${typeof t.slug=="string"&&t.slug.length>0?`Field "${t.slug}"`:"Field"}: ${n.join("; ")}.`}function Se(e){b={};for(const t of e)ee(t)&&(b[t.slug]=t.default_value??"")}function Ne(e,t){if(v.innerHTML="",t){const n=document.createElement("div");n.className="field-error",n.textContent=`template.json parse error: ${t}`,v.appendChild(n);return}if(e.length===0){v.innerHTML='<p class="no-fields">No fields in template.json.</p>';return}for(const n of e){if(!ee(n)){const o=document.createElement("div");o.className="field-error",o.textContent=xe(n),v.appendChild(o);continue}const l=n;if(l.type==="checkbox"){const o=document.createElement("div");o.className="field-checkbox-row";const i=document.createElement("input");i.type="checkbox",i.id=`field-${l.slug}`,i.checked=l.default_value==="true"||l.default_value==="1",i.addEventListener("change",()=>{b[l.slug]=i.checked?"true":"false",p()});const u=document.createElement("label");u.className="field-label",u.htmlFor=`field-${l.slug}`,u.textContent=l.label||l.slug,o.appendChild(i),o.appendChild(u),v.appendChild(o);continue}const a=document.createElement("div");a.className="field-group";const s=document.createElement("label");if(s.className="field-label",s.htmlFor=`field-${l.slug}`,s.textContent=l.label||l.slug,a.appendChild(s),l.type==="textarea"){const o=document.createElement("textarea");o.className="field-input",o.id=`field-${l.slug}`,o.rows=3,o.value=l.default_value??"",o.addEventListener("input",()=>{b[l.slug]=o.value,p()}),a.appendChild(o)}else{const o=document.createElement("input");o.type=l.type==="number"?"number":"text",o.className="field-input",o.id=`field-${l.slug}`,o.value=l.default_value??"",o.addEventListener("input",()=>{b[l.slug]=o.value,p()}),a.appendChild(o)}v.appendChild(a)}}function Be(){const e=new URLSearchParams;e.set("slug",m),e.set("tab",r),P&&e.set("sdk",P),D||e.set("blockCookies","false");for(const[t,n]of Object.entries(b))e.set(`f_${t}`,n);return`/preview?${e.toString()}`}function p(){if(!y||!m)return;const e=Be();M=e;const t=se(w),n=t===0;if(g){const i=g.parentElement,u=i.parentElement,oe=u.parentElement;oe.className=n?"preview-stage":"preview-stage preview-stage--breakpoint",u.style.width=n?"100%":"",i.style.width=n?"100%":`${t}px`,g.style.width=n?"100%":`${t}px`,g.style.maxHeight=w==="mobile"?"812px":"",g.src=e;return}f.innerHTML="";const l=document.createElement("div");l.className=n?"preview-stage":"preview-stage preview-stage--breakpoint";const a=document.createElement("div");a.className="preview-scaler";const s=document.createElement("div");s.className="preview-frame-wrapper";const o=document.createElement("iframe");o.style.height="calc(100vh - 48px)",n?(a.style.width="100%",s.style.width="100%",o.style.width="100%"):(s.style.width=`${t}px`,o.style.width=`${t}px`,w==="mobile"&&(o.style.maxHeight="812px")),o.src=e,s.appendChild(o),a.appendChild(s),l.appendChild(a),f.appendChild(l),g=o}async function U(e){try{const t=await fetch(`/api/template?slug=${encodeURIComponent(e)}`);if(!t.ok)throw new Error(`HTTP ${t.status}`);const n=await t.json();y=n,H.textContent=`Template: ${n.name}`,H.style.opacity="1",Q(!0),Se(n.fields),Ne(n.fields,n.fieldsParseError),p()}catch(t){console.error("[allegro-preview] Failed to load template:",t),f.innerHTML="";const n=document.createElement("div");n.style.padding="40px",n.style.color="var(--text-muted)",n.style.fontSize="13px",n.textContent=`Failed to load template "${e}".`,f.appendChild(n)}}async function Te(){const t=await(await fetch("/api/config")).json();d=t.templates,L=t.scenarios??[],L.some(a=>a.id===r)||(r="standalone",localStorage.setItem("allegro-preview-tab",r)),X(),he(t.tenant??"");const n=Ce(),l=n&&d.find(a=>a.slug===n)?n:null;l?(m=l,N(),await U(l)):Z()}function $e(){const e=localStorage.getItem("allegro-preview-tab"),t=localStorage.getItem("allegro-preview-device");e&&(r=e),t&&["full","desktop","tablet","mobile"].includes(t)&&(w=t,j.querySelectorAll(".device-btn").forEach(n=>{n.classList.toggle("active",n.dataset.device===t)}))}function te(){const e=new EventSource("/api/events");e.addEventListener("reload",()=>{m&&U(m)}),e.addEventListener("config",()=>{Me()}),e.onerror=()=>{e.close(),setTimeout(te,2e3)}}async function Me(){const t=await(await fetch("/api/config")).json();d=t.templates,L=t.scenarios??[],L.some(n=>n.id===r)||(r="standalone",localStorage.setItem("allegro-preview-tab",r),y&&p()),X(),N(),m&&!d.find(n=>n.slug===m)&&Z()}function _(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function je(){c.value="",h.textContent="",G.textContent="",c.classList.remove("invalid"),k.disabled=!0,x.classList.remove("hidden"),c.focus()}function A(){x.classList.add("hidden")}function ne(e){if(!e)return"Template name is required.";const t=V(e);return t?d.some(n=>n.slug===t)?`A template with the slug "${t}" already exists.`:d.some(n=>n.name.toLowerCase()===e.toLowerCase())?`A template named "${e}" already exists.`:null:"Template name must contain at least one letter or number."}W.addEventListener("click",()=>{M&&window.open(M,"_blank")});de.addEventListener("click",je);ue.addEventListener("click",A);x.addEventListener("click",e=>{e.target===x&&A()});document.addEventListener("keydown",e=>{e.key==="Escape"&&!x.classList.contains("hidden")&&A()});c.addEventListener("input",()=>{const e=c.value.trim(),t=V(e);G.textContent=t?`Slug: ${t}`:"";const n=ne(e);k.disabled=n!==null,n&&e?(c.classList.add("invalid"),h.textContent=n):(c.classList.remove("invalid"),h.textContent="")});c.addEventListener("keydown",e=>{e.key==="Enter"&&le()});k.addEventListener("click",()=>void le());async function le(){const e=c.value.trim(),t=ne(e);if(t){c.classList.add("invalid"),h.textContent=t,c.focus();return}k.disabled=!0,h.textContent="";try{const n=await fetch("/api/template/create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e})}),l=await n.json();if(!n.ok||!l.slug){h.textContent=l.error??"Failed to create template.",k.disabled=!1;return}A(),d=[...d,{slug:l.slug,name:l.name??e}],N(),await q(l.slug)}catch{h.textContent="Network error. Please try again.",k.disabled=!1}}ge();$e();ke();Te();te();
|
package/dist/client/index.html
CHANGED
|
@@ -1014,7 +1014,7 @@
|
|
|
1014
1014
|
display: block;
|
|
1015
1015
|
}
|
|
1016
1016
|
</style>
|
|
1017
|
-
<script type="module" crossorigin src="/assets/app-
|
|
1017
|
+
<script type="module" crossorigin src="/assets/app-C--PVKMq.js"></script>
|
|
1018
1018
|
</head>
|
|
1019
1019
|
<body>
|
|
1020
1020
|
<aside class="sidebar">
|
|
@@ -1144,11 +1144,7 @@
|
|
|
1144
1144
|
|
|
1145
1145
|
<div class="main">
|
|
1146
1146
|
<div class="toolbar">
|
|
1147
|
-
<div class="tab-group" id="tab-group">
|
|
1148
|
-
<button class="tab-btn active" data-tab="standalone" disabled>Standalone</button>
|
|
1149
|
-
<button class="tab-btn" data-tab="append" disabled>Article · Append</button>
|
|
1150
|
-
<button class="tab-btn" data-tab="cut" disabled>Article · Cut</button>
|
|
1151
|
-
</div>
|
|
1147
|
+
<div class="tab-group" id="tab-group"></div>
|
|
1152
1148
|
<div class="toolbar-sep"></div>
|
|
1153
1149
|
<span class="template-badge" id="toolbar-badge" style="opacity: 0.4">No template selected</span>
|
|
1154
1150
|
<div class="device-group" id="device-group">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function r(){const e=document.getElementById("allegro-preview-data");if(!e||!e.textContent)throw new Error("[allegro-preview] Missing #allegro-preview-data element");return JSON.parse(e.textContent)}function o(e){const t=e.
|
|
1
|
+
function r(){const e=document.getElementById("allegro-preview-data");if(!e||!e.textContent)throw new Error("[allegro-preview] Missing #allegro-preview-data element");return JSON.parse(e.textContent)}function o(e){const t=e.placement;return{type:"template",template_id:e.slug,target_selector:t.target_selector,field_values:e.fieldValues,placement_method:t.placement_method,truncation_unit:t.truncation_unit,truncation_count:t.truncation_count,truncation_style:t.truncation_style,template:{id:e.slug,title:e.slug,slug:e.slug,html:e.html,css:e.css,js:e.js,external_css_urls:[]}}}window.allegro=window.allegro||[];window.allegro.push(e=>{const t=r(),n=o(t);e.interaction.renderActions([n],{skipDelays:!0}).catch(l=>{console.error("[allegro-preview] renderActions failed:",l)})});
|
package/dist/server.js
CHANGED
|
@@ -24,6 +24,53 @@ window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
|
|
|
24
24
|
}
|
|
25
25
|
};`;
|
|
26
26
|
|
|
27
|
+
// src/scenarios.ts
|
|
28
|
+
function emptyPlacement() {
|
|
29
|
+
return {
|
|
30
|
+
target_selector: null,
|
|
31
|
+
placement_method: null,
|
|
32
|
+
truncation_unit: null,
|
|
33
|
+
truncation_count: null,
|
|
34
|
+
truncation_style: null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var ARTICLE_TARGET_SELECTOR = "#allegro-preview-article";
|
|
38
|
+
var BUILT_IN_SCENARIOS = [
|
|
39
|
+
{
|
|
40
|
+
id: "standalone",
|
|
41
|
+
name: "Standalone",
|
|
42
|
+
builtIn: true,
|
|
43
|
+
hostKind: "none",
|
|
44
|
+
placement: emptyPlacement()
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "append",
|
|
48
|
+
name: "Article \xB7 Append",
|
|
49
|
+
builtIn: true,
|
|
50
|
+
hostKind: "article",
|
|
51
|
+
placement: {
|
|
52
|
+
target_selector: ARTICLE_TARGET_SELECTOR,
|
|
53
|
+
placement_method: "append",
|
|
54
|
+
truncation_unit: null,
|
|
55
|
+
truncation_count: null,
|
|
56
|
+
truncation_style: null
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "cut",
|
|
61
|
+
name: "Article \xB7 Cut",
|
|
62
|
+
builtIn: true,
|
|
63
|
+
hostKind: "article",
|
|
64
|
+
placement: {
|
|
65
|
+
target_selector: ARTICLE_TARGET_SELECTOR,
|
|
66
|
+
placement_method: "truncate",
|
|
67
|
+
truncation_unit: "paragraphs",
|
|
68
|
+
truncation_count: 3,
|
|
69
|
+
truncation_style: "cut"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
|
|
27
74
|
// src/slugify.ts
|
|
28
75
|
function slugify(name) {
|
|
29
76
|
return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -224,7 +271,8 @@ function buildPreviewData(options) {
|
|
|
224
271
|
css: options.css,
|
|
225
272
|
js: options.js,
|
|
226
273
|
fieldValues: options.fieldValues,
|
|
227
|
-
tab: options.
|
|
274
|
+
tab: options.scenario.id,
|
|
275
|
+
placement: options.scenario.placement
|
|
228
276
|
});
|
|
229
277
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
230
278
|
}
|
|
@@ -290,6 +338,33 @@ function articleShell(options) {
|
|
|
290
338
|
</body>
|
|
291
339
|
</html>`;
|
|
292
340
|
}
|
|
341
|
+
function customHostShell(options) {
|
|
342
|
+
const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
|
|
343
|
+
const hostCss = options.scenario.hostCss ? `<style>${options.scenario.hostCss}</style>` : "";
|
|
344
|
+
const hostHtml = options.scenario.hostHtml ?? "";
|
|
345
|
+
return `<!DOCTYPE html>
|
|
346
|
+
<html>
|
|
347
|
+
<head>
|
|
348
|
+
<meta charset="UTF-8">
|
|
349
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
350
|
+
<script>${PREVIEW_BOOTSTRAP_SCRIPT}</script>
|
|
351
|
+
${cookieScript}
|
|
352
|
+
<style>
|
|
353
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
354
|
+
body { margin: 0; }
|
|
355
|
+
[data-allegro-interaction] { display: block; }
|
|
356
|
+
</style>
|
|
357
|
+
${previewCssTag(options.previewCss)}
|
|
358
|
+
${hostCss}
|
|
359
|
+
</head>
|
|
360
|
+
<body>
|
|
361
|
+
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
362
|
+
${hostHtml}
|
|
363
|
+
${sdkScriptTag(options.sdkUrl)}
|
|
364
|
+
<script src="/preview-client.js"></script>
|
|
365
|
+
</body>
|
|
366
|
+
</html>`;
|
|
367
|
+
}
|
|
293
368
|
var sseClients = /* @__PURE__ */ new Set();
|
|
294
369
|
function broadcast(event) {
|
|
295
370
|
for (const client of sseClients) {
|
|
@@ -381,6 +456,109 @@ function readTemplateFiles(dir) {
|
|
|
381
456
|
function readTemplateFilesCached(dir) {
|
|
382
457
|
return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
|
|
383
458
|
}
|
|
459
|
+
var SCENARIOS_DIRNAME = ".allegro-preview";
|
|
460
|
+
function parsePlacement(raw) {
|
|
461
|
+
const placement = emptyPlacement();
|
|
462
|
+
if (typeof raw.target_selector === "string") {
|
|
463
|
+
placement.target_selector = raw.target_selector;
|
|
464
|
+
}
|
|
465
|
+
if (raw.placement_method === "truncate" || raw.placement_method === "append") {
|
|
466
|
+
placement.placement_method = raw.placement_method;
|
|
467
|
+
}
|
|
468
|
+
if (raw.truncation_unit === "paragraphs") {
|
|
469
|
+
placement.truncation_unit = raw.truncation_unit;
|
|
470
|
+
}
|
|
471
|
+
if (typeof raw.truncation_count === "number" && Number.isFinite(raw.truncation_count)) {
|
|
472
|
+
placement.truncation_count = raw.truncation_count;
|
|
473
|
+
}
|
|
474
|
+
if (raw.truncation_style === "cut") {
|
|
475
|
+
placement.truncation_style = raw.truncation_style;
|
|
476
|
+
}
|
|
477
|
+
return placement;
|
|
478
|
+
}
|
|
479
|
+
function readCustomScenario(scenariosDir, id) {
|
|
480
|
+
const dir = join(scenariosDir, id);
|
|
481
|
+
const jsonPath = join(dir, "scenario.json");
|
|
482
|
+
const base = {
|
|
483
|
+
id,
|
|
484
|
+
name: id,
|
|
485
|
+
builtIn: false,
|
|
486
|
+
hostKind: "none",
|
|
487
|
+
placement: emptyPlacement(),
|
|
488
|
+
parseError: null
|
|
489
|
+
};
|
|
490
|
+
let raw;
|
|
491
|
+
try {
|
|
492
|
+
raw = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
493
|
+
} catch (err) {
|
|
494
|
+
base.parseError = err instanceof Error ? err.message : "Invalid JSON in scenario.json";
|
|
495
|
+
return base;
|
|
496
|
+
}
|
|
497
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
498
|
+
base.parseError = "scenario.json must be a JSON object";
|
|
499
|
+
return base;
|
|
500
|
+
}
|
|
501
|
+
if (typeof raw.name === "string" && raw.name.length > 0) {
|
|
502
|
+
base.name = raw.name;
|
|
503
|
+
}
|
|
504
|
+
base.placement = parsePlacement(raw);
|
|
505
|
+
const hostHtmlPath = join(dir, "host.html");
|
|
506
|
+
if (existsSync(hostHtmlPath)) {
|
|
507
|
+
try {
|
|
508
|
+
base.hostKind = "custom";
|
|
509
|
+
base.hostHtml = readFileSync(hostHtmlPath, "utf-8");
|
|
510
|
+
const hostCssPath = join(dir, "host.css");
|
|
511
|
+
base.hostCss = existsSync(hostCssPath) ? readFileSync(hostCssPath, "utf-8") : "";
|
|
512
|
+
} catch (err) {
|
|
513
|
+
base.parseError = err instanceof Error ? err.message : "Failed to read scenario host files";
|
|
514
|
+
return base;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return base;
|
|
518
|
+
}
|
|
519
|
+
function scanCustomScenarios(baseDir) {
|
|
520
|
+
const scenariosDir = join(baseDir, SCENARIOS_DIRNAME);
|
|
521
|
+
if (!existsSync(scenariosDir)) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
const results = [];
|
|
525
|
+
try {
|
|
526
|
+
for (const entry of readdirSync(scenariosDir, { withFileTypes: true })) {
|
|
527
|
+
if (!entry.isDirectory()) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (!existsSync(join(scenariosDir, entry.name, "scenario.json"))) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const scenario = readCustomScenario(scenariosDir, entry.name);
|
|
534
|
+
if (scenario.parseError) {
|
|
535
|
+
console.warn(`[allegro-preview] Skipping scenario "${entry.name}": ${scenario.parseError}`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
results.push(scenario);
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
return results;
|
|
543
|
+
}
|
|
544
|
+
function listScenarioSummaries(customScenarios) {
|
|
545
|
+
return [
|
|
546
|
+
...BUILT_IN_SCENARIOS.map((s) => ({ id: s.id, name: s.name })),
|
|
547
|
+
...customScenarios.map((s) => ({ id: s.id, name: s.name }))
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
function resolveScenario(baseDir, id) {
|
|
551
|
+
const builtIn = BUILT_IN_SCENARIOS.find((s) => s.id === id);
|
|
552
|
+
if (builtIn) {
|
|
553
|
+
return builtIn;
|
|
554
|
+
}
|
|
555
|
+
const scenariosDir = join(baseDir, SCENARIOS_DIRNAME);
|
|
556
|
+
if (existsSync(join(scenariosDir, id, "scenario.json"))) {
|
|
557
|
+
const scenario = readCustomScenario(scenariosDir, id);
|
|
558
|
+
return scenario.parseError ? null : scenario;
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
384
562
|
async function startServer(templatePath, port = 0, tenant = "") {
|
|
385
563
|
const baseDir = resolve(process.cwd(), templatePath);
|
|
386
564
|
if (!existsSync(baseDir)) {
|
|
@@ -388,11 +566,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
388
566
|
process.exit(1);
|
|
389
567
|
}
|
|
390
568
|
let templates = scanTemplates(baseDir);
|
|
569
|
+
let customScenarios = scanCustomScenarios(baseDir);
|
|
391
570
|
const app = express();
|
|
392
571
|
const clientDistDir = join(__dirname, "client");
|
|
393
572
|
app.use(express.static(clientDistDir));
|
|
394
573
|
app.get("/api/config", (_req, res) => {
|
|
395
|
-
res.json({
|
|
574
|
+
res.json({
|
|
575
|
+
defaultSlug: templates[0]?.slug ?? null,
|
|
576
|
+
templates,
|
|
577
|
+
tenant,
|
|
578
|
+
scenarios: listScenarioSummaries(customScenarios)
|
|
579
|
+
});
|
|
396
580
|
});
|
|
397
581
|
app.get("/api/template", (req, res) => {
|
|
398
582
|
const slug = req.query.slug ?? ".";
|
|
@@ -478,6 +662,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
478
662
|
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
479
663
|
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
480
664
|
const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
|
|
665
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(tab)) {
|
|
666
|
+
res.status(404).send("Scenario not found");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const scenario = resolveScenario(baseDir, tab);
|
|
670
|
+
if (!scenario) {
|
|
671
|
+
res.status(404).send("Scenario not found");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
481
674
|
if (!slug) {
|
|
482
675
|
res.status(400).send("Missing slug");
|
|
483
676
|
return;
|
|
@@ -522,12 +715,12 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
522
715
|
css: templateFiles.css,
|
|
523
716
|
js: templateFiles.js,
|
|
524
717
|
fieldValues,
|
|
525
|
-
|
|
718
|
+
scenario,
|
|
526
719
|
sdkUrl: effectiveSdkUrl,
|
|
527
720
|
blockCookies,
|
|
528
721
|
previewCss
|
|
529
722
|
};
|
|
530
|
-
const rendered =
|
|
723
|
+
const rendered = scenario.hostKind === "custom" ? customHostShell(shellOptions) : scenario.hostKind === "article" ? articleShell(shellOptions) : standaloneShell(shellOptions);
|
|
531
724
|
res.setHeader("Cache-Control", "no-store");
|
|
532
725
|
res.type("html").send(rendered);
|
|
533
726
|
});
|
|
@@ -566,6 +759,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
566
759
|
broadcastReload();
|
|
567
760
|
});
|
|
568
761
|
const dirWatcher = chokidar.watch(baseDir, { depth: 1, ignoreInitial: true });
|
|
762
|
+
const scenariosWatcher = chokidar.watch(join(baseDir, SCENARIOS_DIRNAME), {
|
|
763
|
+
ignoreInitial: true
|
|
764
|
+
});
|
|
765
|
+
function rescanScenarios() {
|
|
766
|
+
customScenarios = scanCustomScenarios(baseDir);
|
|
767
|
+
console.log(`Scenarios updated: ${customScenarios.map((s) => s.id).join(", ") || "(none)"}`);
|
|
768
|
+
broadcastConfig();
|
|
769
|
+
}
|
|
770
|
+
scenariosWatcher.on("all", () => {
|
|
771
|
+
rescanScenarios();
|
|
772
|
+
});
|
|
569
773
|
function rescan() {
|
|
570
774
|
templates = scanTemplates(baseDir);
|
|
571
775
|
console.log(`Templates updated: ${templates.map((t) => t.slug).join(", ") || "(none)"}`);
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))l(a);new MutationObserver(a=>{for(const s of a)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&l(o)}).observe(document,{childList:!0,subtree:!0});function n(a){const s={};return a.integrity&&(s.integrity=a.integrity),a.referrerPolicy&&(s.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?s.credentials="include":a.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function l(a){if(a.ep)return;a.ep=!0;const s=n(a);fetch(a.href,s)}})();function R(e){return e.normalize("NFKD").replace(/[\u0300-\u036f]/g,"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}const le={full:0,desktop:1280,tablet:768,mobile:390};function oe(e){return le[e]}let r=[],u=null,N="standalone",y="full",L=null,E={},H,_=!0,T=null,f=null,h=null;const U=document.getElementById("template-nav"),g=document.getElementById("field-list"),m=document.getElementById("preview-area"),$=document.getElementById("tab-group"),M=document.getElementById("device-group"),j=document.getElementById("toolbar-badge"),ae=document.getElementById("theme-toggle"),se=document.getElementById("theme-icon-sun"),ie=document.getElementById("theme-icon-moon"),B=document.getElementById("tenant-input"),x=document.getElementById("tenant-status"),ce=document.getElementById("new-template-btn"),V=document.getElementById("new-window-btn"),k=document.getElementById("create-modal-backdrop"),c=document.getElementById("modal-name-input"),W=document.getElementById("modal-slug-hint"),v=document.getElementById("modal-error"),re=document.getElementById("modal-cancel-btn"),w=document.getElementById("modal-create-btn"),P=document.getElementById("block-cookies-checkbox"),G=document.getElementById("cookie-auth-area"),S=document.getElementById("cookie-auth-user"),J=document.getElementById("cookie-auth-hint"),de=document.getElementById("cookie-clear-btn"),b=document.getElementById("jwt-modal-backdrop"),ue=document.getElementById("jwt-payload-content"),me=document.getElementById("jwt-modal-close-btn");function z(e){document.documentElement.setAttribute("data-theme",e?"dark":"light"),se.style.display=e?"none":"",ie.style.display=e?"":"none"}function pe(){const e=localStorage.getItem("allegro-preview-theme"),t=window.matchMedia("(prefers-color-scheme: dark)").matches;z(e?e==="dark":t)}ae.addEventListener("click",()=>{const e=document.documentElement.getAttribute("data-theme")!=="dark";z(e),localStorage.setItem("allegro-preview-theme",e?"dark":"light")});function fe(e){const t=e.trim();return t?(/^https?:\/\//.test(t)?t:`https://${t}`).replace(/\/+$/,"")+"/client.js":""}function K(e){const t=fe(e);H=t||void 0,t?(x.className="tenant-active-badge",x.textContent=t):(x.className="tenant-hint",x.textContent="Loads /client.js on the preview page"),L&&p()}B.addEventListener("input",()=>{localStorage.setItem("allegro-preview-tenant",B.value),K(B.value)});function ge(e=""){const t=localStorage.getItem("allegro-preview-tenant"),n=e||t||"";n&&(B.value=n,e&&localStorage.setItem("allegro-preview-tenant",n),K(n))}const ve=[{name:"_ALLEGROT",label:"Session JWT",isJwt:!0},{name:"_ALLEGROD",label:"Device ID"},{name:"_ALLEGROS",label:"Session ID"}];function he(e){const t=document.cookie.split("; ").find(n=>n.startsWith(e+"="));if(!t)return null;try{return decodeURIComponent(t.split("=").slice(1).join("="))}catch{return t.split("=").slice(1).join("=")}}function ye(e){try{const t=e.split(".");if(t.length!==3)return null;const n=t[1].replace(/-/g,"+").replace(/_/g,"/"),l=atob(n);return JSON.parse(l)}catch{return null}}function C(){const e=ve.map(t=>({...t,value:he(t.name)})).filter(t=>t.value!==null);if(S.innerHTML="",e.length===0){S.style.display="none",J.style.display="";return}J.style.display="none",S.style.display="";for(const t of e){const n=document.createElement("div");n.className="cookie-row";const l=document.createElement("div");l.className="cookie-auth-dot";const a=document.createElement("div");a.className="cookie-row-info";const s=document.createElement("div");s.className="cookie-row-label",s.textContent=t.label;const o=document.createElement("div");if(o.className="cookie-row-value",o.textContent=t.value.length>24?t.value.slice(0,24)+"…":t.value,a.appendChild(s),a.appendChild(o),n.appendChild(l),n.appendChild(a),t.isJwt){const i=document.createElement("button");i.className="cookie-view-btn",i.textContent="Decode",i.onclick=()=>{const d=ye(t.value);ue.textContent=d?JSON.stringify(d,null,2):t.value,b.classList.remove("hidden")},n.appendChild(i)}S.appendChild(n)}}function Ee(e){_=e,localStorage.setItem("allegro-preview-block-cookies",e?"true":"false"),G.style.display=e?"none":"",e?h&&(clearInterval(h),h=null):(C(),h||(h=setInterval(C,2e3))),L&&p()}function we(){const t=localStorage.getItem("allegro-preview-block-cookies")!=="false";P.checked=t,_=t,G.style.display=t?"none":"",t||(C(),h=setInterval(C,2e3))}P.addEventListener("change",()=>{Ee(P.checked)});de.addEventListener("click",()=>{const e=document.cookie.split("; ");for(const t of e){const n=t.split("=")[0];document.cookie=`${n}=; max-age=0; path=/`}C()});me.addEventListener("click",()=>{b.classList.add("hidden")});b.addEventListener("click",e=>{e.target===b&&b.classList.add("hidden")});function be(){const e=window.location.hash.slice(1);return e?decodeURIComponent(e):null}function ke(e){history.replaceState(null,"",`#${encodeURIComponent(e)}`)}function Ce(){history.replaceState(null,"",window.location.pathname)}function Y(e){$.querySelectorAll(".tab-btn").forEach(t=>{t.disabled=!e}),M.querySelectorAll(".device-btn").forEach(t=>{t.disabled=!e}),V.disabled=!e}$.addEventListener("click",e=>{const t=e.target.closest(".tab-btn");!t||t.disabled||(N=t.dataset.tab,$.querySelectorAll(".tab-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-tab",N),p())});M.addEventListener("click",e=>{const t=e.target.closest(".device-btn");!t||t.disabled||(y=t.dataset.device,M.querySelectorAll(".device-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-device",y),p())});function I(){U.innerHTML="";for(const e of r){const t=document.createElement("button");t.className="template-nav-item"+(e.slug===u?" active":""),t.innerHTML=`<span class="template-nav-dot"></span><span class="template-nav-name">${F(e.name)}</span>`,t.addEventListener("click",()=>void D(e.slug)),U.appendChild(t)}}async function D(e){u=e,ke(e),I(),await q(e)}function Q(){L=null,u=null,T=null,f=null,Ce(),I(),Y(!1),j.textContent="No template selected",j.style.opacity="0.4",g.innerHTML='<p class="no-fields">Select a template to edit fields.</p>';const e=document.createElement("div");if(e.className="selector-screen",r.length===0){e.innerHTML=`
|
|
2
|
-
<div class="selector-empty">
|
|
3
|
-
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
|
4
|
-
<span>No templates found in this directory.</span>
|
|
5
|
-
</div>`,m.innerHTML="",m.appendChild(e);return}e.innerHTML=`
|
|
6
|
-
<div class="selector-eyebrow">Allegro Preview</div>
|
|
7
|
-
<h2 class="selector-heading">Choose a template</h2>
|
|
8
|
-
<p class="selector-sub">Select a template to start previewing.</p>
|
|
9
|
-
<div class="selector-grid" id="selector-grid"></div>`,m.innerHTML="",m.appendChild(e);const t=e.querySelector("#selector-grid");for(const n of r){const l=document.createElement("button");l.className="selector-card",l.innerHTML=`
|
|
10
|
-
<div class="selector-card-icon">
|
|
11
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
|
12
|
-
</div>
|
|
13
|
-
<div class="selector-card-name">${F(n.name)}</div>
|
|
14
|
-
<div class="selector-card-slug">${F(n.slug)}</div>`,l.addEventListener("click",()=>void D(n.slug)),t.appendChild(l)}}const O=["text","number","checkbox","textarea"];function X(e){if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.slug=="string"&&t.slug.length>0&&O.includes(t.type)}function Le(e){if(typeof e!="object"||e===null)return`Invalid field: expected an object, got ${JSON.stringify(e)}.`;const t=e,n=[];if((typeof t.slug!="string"||t.slug.length===0)&&n.push('missing required "slug" property'),!O.includes(t.type)){const a=t.type!==void 0?`"${String(t.type)}"`:"none";n.push(`invalid type ${a} — valid types: ${O.join(", ")}`)}return`${typeof t.slug=="string"&&t.slug.length>0?`Field "${t.slug}"`:"Field"}: ${n.join("; ")}.`}function Ie(e){E={};for(const t of e)X(t)&&(E[t.slug]=t.default_value??"")}function xe(e,t){if(g.innerHTML="",t){const n=document.createElement("div");n.className="field-error",n.textContent=`template.json parse error: ${t}`,g.appendChild(n);return}if(e.length===0){g.innerHTML='<p class="no-fields">No fields in template.json.</p>';return}for(const n of e){if(!X(n)){const o=document.createElement("div");o.className="field-error",o.textContent=Le(n),g.appendChild(o);continue}const l=n;if(l.type==="checkbox"){const o=document.createElement("div");o.className="field-checkbox-row";const i=document.createElement("input");i.type="checkbox",i.id=`field-${l.slug}`,i.checked=l.default_value==="true"||l.default_value==="1",i.addEventListener("change",()=>{E[l.slug]=i.checked?"true":"false",p()});const d=document.createElement("label");d.className="field-label",d.htmlFor=`field-${l.slug}`,d.textContent=l.label||l.slug,o.appendChild(i),o.appendChild(d),g.appendChild(o);continue}const a=document.createElement("div");a.className="field-group";const s=document.createElement("label");if(s.className="field-label",s.htmlFor=`field-${l.slug}`,s.textContent=l.label||l.slug,a.appendChild(s),l.type==="textarea"){const o=document.createElement("textarea");o.className="field-input",o.id=`field-${l.slug}`,o.rows=3,o.value=l.default_value??"",o.addEventListener("input",()=>{E[l.slug]=o.value,p()}),a.appendChild(o)}else{const o=document.createElement("input");o.type=l.type==="number"?"number":"text",o.className="field-input",o.id=`field-${l.slug}`,o.value=l.default_value??"",o.addEventListener("input",()=>{E[l.slug]=o.value,p()}),a.appendChild(o)}g.appendChild(a)}}function Se(){const e=new URLSearchParams;e.set("slug",u),e.set("tab",N),H&&e.set("sdk",H),_||e.set("blockCookies","false");for(const[t,n]of Object.entries(E))e.set(`f_${t}`,n);return`/preview?${e.toString()}`}function p(){if(!L||!u)return;const e=Se();T=e;const t=oe(y),n=t===0;if(f){const i=f.parentElement,d=i.parentElement,ne=d.parentElement;ne.className=n?"preview-stage":"preview-stage preview-stage--breakpoint",d.style.width=n?"100%":"",i.style.width=n?"100%":`${t}px`,f.style.width=n?"100%":`${t}px`,f.style.maxHeight=y==="mobile"?"812px":"",f.src=e;return}m.innerHTML="";const l=document.createElement("div");l.className=n?"preview-stage":"preview-stage preview-stage--breakpoint";const a=document.createElement("div");a.className="preview-scaler";const s=document.createElement("div");s.className="preview-frame-wrapper";const o=document.createElement("iframe");o.style.height="calc(100vh - 48px)",n?(a.style.width="100%",s.style.width="100%",o.style.width="100%"):(s.style.width=`${t}px`,o.style.width=`${t}px`,y==="mobile"&&(o.style.maxHeight="812px")),o.src=e,s.appendChild(o),a.appendChild(s),l.appendChild(a),m.appendChild(l),f=o}async function q(e){try{const t=await fetch(`/api/template?slug=${encodeURIComponent(e)}`);if(!t.ok)throw new Error(`HTTP ${t.status}`);const n=await t.json();L=n,j.textContent=`Template: ${n.name}`,j.style.opacity="1",Y(!0),Ie(n.fields),xe(n.fields,n.fieldsParseError),p()}catch(t){console.error("[allegro-preview] Failed to load template:",t),m.innerHTML="";const n=document.createElement("div");n.style.padding="40px",n.style.color="var(--text-muted)",n.style.fontSize="13px",n.textContent=`Failed to load template "${e}".`,m.appendChild(n)}}async function Be(){const t=await(await fetch("/api/config")).json();r=t.templates,ge(t.tenant??"");const n=be(),l=n&&r.find(a=>a.slug===n)?n:null;l?(u=l,I(),await q(l)):Q()}function Ne(){const e=localStorage.getItem("allegro-preview-tab"),t=localStorage.getItem("allegro-preview-device");e&&["standalone","append","cut"].includes(e)&&(N=e,$.querySelectorAll(".tab-btn").forEach(n=>{n.classList.toggle("active",n.dataset.tab===e)})),t&&["full","desktop","tablet","mobile"].includes(t)&&(y=t,M.querySelectorAll(".device-btn").forEach(n=>{n.classList.toggle("active",n.dataset.device===t)}))}function Z(){const e=new EventSource("/api/events");e.addEventListener("reload",()=>{u&&q(u)}),e.addEventListener("config",()=>{Te()}),e.onerror=()=>{e.close(),setTimeout(Z,2e3)}}async function Te(){r=(await(await fetch("/api/config")).json()).templates,I(),u&&!r.find(n=>n.slug===u)&&Q()}function F(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function $e(){c.value="",v.textContent="",W.textContent="",c.classList.remove("invalid"),w.disabled=!0,k.classList.remove("hidden"),c.focus()}function A(){k.classList.add("hidden")}function ee(e){if(!e)return"Template name is required.";const t=R(e);return t?r.some(n=>n.slug===t)?`A template with the slug "${t}" already exists.`:r.some(n=>n.name.toLowerCase()===e.toLowerCase())?`A template named "${e}" already exists.`:null:"Template name must contain at least one letter or number."}V.addEventListener("click",()=>{T&&window.open(T,"_blank")});ce.addEventListener("click",$e);re.addEventListener("click",A);k.addEventListener("click",e=>{e.target===k&&A()});document.addEventListener("keydown",e=>{e.key==="Escape"&&!k.classList.contains("hidden")&&A()});c.addEventListener("input",()=>{const e=c.value.trim(),t=R(e);W.textContent=t?`Slug: ${t}`:"";const n=ee(e);w.disabled=n!==null,n&&e?(c.classList.add("invalid"),v.textContent=n):(c.classList.remove("invalid"),v.textContent="")});c.addEventListener("keydown",e=>{e.key==="Enter"&&te()});w.addEventListener("click",()=>void te());async function te(){const e=c.value.trim(),t=ee(e);if(t){c.classList.add("invalid"),v.textContent=t,c.focus();return}w.disabled=!0,v.textContent="";try{const n=await fetch("/api/template/create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e})}),l=await n.json();if(!n.ok||!l.slug){v.textContent=l.error??"Failed to create template.",w.disabled=!1;return}A(),r=[...r,{slug:l.slug,name:l.name??e}],I(),await D(l.slug)}catch{v.textContent="Network error. Please try again.",w.disabled=!1}}pe();Ne();we();Be();Z();
|