@allegrocdp/preview 0.1.0-dev.13 → 0.1.0-dev.15
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 +218 -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 +221 -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,57 @@ window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
|
|
|
27
27
|
}
|
|
28
28
|
};`;
|
|
29
29
|
|
|
30
|
+
// src/scenarios.ts
|
|
31
|
+
var SCENARIO_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
32
|
+
function isValidScenarioId(id) {
|
|
33
|
+
return SCENARIO_ID_PATTERN.test(id);
|
|
34
|
+
}
|
|
35
|
+
function emptyPlacement() {
|
|
36
|
+
return {
|
|
37
|
+
target_selector: null,
|
|
38
|
+
placement_method: null,
|
|
39
|
+
truncation_unit: null,
|
|
40
|
+
truncation_count: null,
|
|
41
|
+
truncation_style: null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
var ARTICLE_TARGET_SELECTOR = "#allegro-preview-article";
|
|
45
|
+
var BUILT_IN_SCENARIOS = [
|
|
46
|
+
{
|
|
47
|
+
id: "standalone",
|
|
48
|
+
name: "Standalone",
|
|
49
|
+
builtIn: true,
|
|
50
|
+
hostKind: "none",
|
|
51
|
+
placement: emptyPlacement()
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "append",
|
|
55
|
+
name: "Article \xB7 Append",
|
|
56
|
+
builtIn: true,
|
|
57
|
+
hostKind: "article",
|
|
58
|
+
placement: {
|
|
59
|
+
target_selector: ARTICLE_TARGET_SELECTOR,
|
|
60
|
+
placement_method: "append",
|
|
61
|
+
truncation_unit: null,
|
|
62
|
+
truncation_count: null,
|
|
63
|
+
truncation_style: null
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "cut",
|
|
68
|
+
name: "Article \xB7 Cut",
|
|
69
|
+
builtIn: true,
|
|
70
|
+
hostKind: "article",
|
|
71
|
+
placement: {
|
|
72
|
+
target_selector: ARTICLE_TARGET_SELECTOR,
|
|
73
|
+
placement_method: "truncate",
|
|
74
|
+
truncation_unit: "paragraphs",
|
|
75
|
+
truncation_count: 3,
|
|
76
|
+
truncation_style: "cut"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
|
|
30
81
|
// src/slugify.ts
|
|
31
82
|
function slugify(name) {
|
|
32
83
|
return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -227,7 +278,8 @@ function buildPreviewData(options) {
|
|
|
227
278
|
css: options.css,
|
|
228
279
|
js: options.js,
|
|
229
280
|
fieldValues: options.fieldValues,
|
|
230
|
-
tab: options.
|
|
281
|
+
tab: options.scenario.id,
|
|
282
|
+
placement: options.scenario.placement
|
|
231
283
|
});
|
|
232
284
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
233
285
|
}
|
|
@@ -293,6 +345,33 @@ function articleShell(options) {
|
|
|
293
345
|
</body>
|
|
294
346
|
</html>`;
|
|
295
347
|
}
|
|
348
|
+
function customHostShell(options) {
|
|
349
|
+
const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
|
|
350
|
+
const hostCss = options.scenario.hostCss ? `<style>${options.scenario.hostCss}</style>` : "";
|
|
351
|
+
const hostHtml = options.scenario.hostHtml ?? "";
|
|
352
|
+
return `<!DOCTYPE html>
|
|
353
|
+
<html>
|
|
354
|
+
<head>
|
|
355
|
+
<meta charset="UTF-8">
|
|
356
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
357
|
+
<script>${PREVIEW_BOOTSTRAP_SCRIPT}</script>
|
|
358
|
+
${cookieScript}
|
|
359
|
+
<style>
|
|
360
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
361
|
+
body { margin: 0; }
|
|
362
|
+
[data-allegro-interaction] { display: block; }
|
|
363
|
+
</style>
|
|
364
|
+
${previewCssTag(options.previewCss)}
|
|
365
|
+
${hostCss}
|
|
366
|
+
</head>
|
|
367
|
+
<body>
|
|
368
|
+
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
369
|
+
${hostHtml}
|
|
370
|
+
${sdkScriptTag(options.sdkUrl)}
|
|
371
|
+
<script src="/preview-client.js"></script>
|
|
372
|
+
</body>
|
|
373
|
+
</html>`;
|
|
374
|
+
}
|
|
296
375
|
var sseClients = /* @__PURE__ */ new Set();
|
|
297
376
|
function broadcast(event) {
|
|
298
377
|
for (const client of sseClients) {
|
|
@@ -384,6 +463,115 @@ function readTemplateFiles(dir) {
|
|
|
384
463
|
function readTemplateFilesCached(dir) {
|
|
385
464
|
return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
|
|
386
465
|
}
|
|
466
|
+
var SCENARIOS_DIRNAME = ".allegro-preview";
|
|
467
|
+
function parsePlacement(raw) {
|
|
468
|
+
const placement = emptyPlacement();
|
|
469
|
+
if (typeof raw.target_selector === "string") {
|
|
470
|
+
placement.target_selector = raw.target_selector;
|
|
471
|
+
}
|
|
472
|
+
if (raw.placement_method === "truncate" || raw.placement_method === "append") {
|
|
473
|
+
placement.placement_method = raw.placement_method;
|
|
474
|
+
}
|
|
475
|
+
if (raw.truncation_unit === "paragraphs") {
|
|
476
|
+
placement.truncation_unit = raw.truncation_unit;
|
|
477
|
+
}
|
|
478
|
+
if (typeof raw.truncation_count === "number" && Number.isInteger(raw.truncation_count) && raw.truncation_count > 0) {
|
|
479
|
+
placement.truncation_count = raw.truncation_count;
|
|
480
|
+
}
|
|
481
|
+
if (raw.truncation_style === "cut") {
|
|
482
|
+
placement.truncation_style = raw.truncation_style;
|
|
483
|
+
}
|
|
484
|
+
return placement;
|
|
485
|
+
}
|
|
486
|
+
function readCustomScenario(scenariosDir, id) {
|
|
487
|
+
const dir = join(scenariosDir, id);
|
|
488
|
+
const jsonPath = join(dir, "scenario.json");
|
|
489
|
+
const base = {
|
|
490
|
+
id,
|
|
491
|
+
name: id,
|
|
492
|
+
builtIn: false,
|
|
493
|
+
hostKind: "none",
|
|
494
|
+
placement: emptyPlacement(),
|
|
495
|
+
parseError: null
|
|
496
|
+
};
|
|
497
|
+
let raw;
|
|
498
|
+
try {
|
|
499
|
+
raw = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
500
|
+
} catch (err) {
|
|
501
|
+
base.parseError = err instanceof Error ? err.message : "Invalid JSON in scenario.json";
|
|
502
|
+
return base;
|
|
503
|
+
}
|
|
504
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
505
|
+
base.parseError = "scenario.json must be a JSON object";
|
|
506
|
+
return base;
|
|
507
|
+
}
|
|
508
|
+
if (typeof raw.name === "string" && raw.name.length > 0) {
|
|
509
|
+
base.name = raw.name;
|
|
510
|
+
}
|
|
511
|
+
const hostHtmlPath = join(dir, "host.html");
|
|
512
|
+
if (existsSync(hostHtmlPath)) {
|
|
513
|
+
try {
|
|
514
|
+
base.hostKind = "custom";
|
|
515
|
+
base.hostHtml = readFileSync(hostHtmlPath, "utf-8");
|
|
516
|
+
const hostCssPath = join(dir, "host.css");
|
|
517
|
+
base.hostCss = existsSync(hostCssPath) ? readFileSync(hostCssPath, "utf-8") : "";
|
|
518
|
+
base.placement = parsePlacement(raw);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
base.parseError = err instanceof Error ? err.message : "Failed to read scenario host files";
|
|
521
|
+
return base;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return base;
|
|
525
|
+
}
|
|
526
|
+
function scanCustomScenarios(baseDir) {
|
|
527
|
+
const scenariosDir = join(baseDir, SCENARIOS_DIRNAME);
|
|
528
|
+
if (!existsSync(scenariosDir)) {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
const results = [];
|
|
532
|
+
try {
|
|
533
|
+
for (const entry of readdirSync(scenariosDir, { withFileTypes: true })) {
|
|
534
|
+
if (!entry.isDirectory()) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (!existsSync(join(scenariosDir, entry.name, "scenario.json"))) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (!isValidScenarioId(entry.name)) {
|
|
541
|
+
console.warn(
|
|
542
|
+
`[allegro-preview] Skipping scenario folder "${entry.name}": name must be a kebab-case slug (lowercase letters, digits, hyphens).`
|
|
543
|
+
);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const scenario = readCustomScenario(scenariosDir, entry.name);
|
|
547
|
+
if (scenario.parseError) {
|
|
548
|
+
console.warn(`[allegro-preview] Skipping scenario "${entry.name}": ${scenario.parseError}`);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
results.push(scenario);
|
|
552
|
+
}
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
return results;
|
|
556
|
+
}
|
|
557
|
+
function listScenarioSummaries(customScenarios) {
|
|
558
|
+
return [
|
|
559
|
+
...BUILT_IN_SCENARIOS.map((s) => ({ id: s.id, name: s.name })),
|
|
560
|
+
...customScenarios.map((s) => ({ id: s.id, name: s.name }))
|
|
561
|
+
];
|
|
562
|
+
}
|
|
563
|
+
function resolveScenario(baseDir, id) {
|
|
564
|
+
const builtIn = BUILT_IN_SCENARIOS.find((s) => s.id === id);
|
|
565
|
+
if (builtIn) {
|
|
566
|
+
return builtIn;
|
|
567
|
+
}
|
|
568
|
+
const scenariosDir = join(baseDir, SCENARIOS_DIRNAME);
|
|
569
|
+
if (existsSync(join(scenariosDir, id, "scenario.json"))) {
|
|
570
|
+
const scenario = readCustomScenario(scenariosDir, id);
|
|
571
|
+
return scenario.parseError ? null : scenario;
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
387
575
|
async function startServer(templatePath, port = 0, tenant = "") {
|
|
388
576
|
const baseDir = resolve(process.cwd(), templatePath);
|
|
389
577
|
if (!existsSync(baseDir)) {
|
|
@@ -391,11 +579,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
391
579
|
process.exit(1);
|
|
392
580
|
}
|
|
393
581
|
let templates = scanTemplates(baseDir);
|
|
582
|
+
let customScenarios = scanCustomScenarios(baseDir);
|
|
394
583
|
const app = express();
|
|
395
584
|
const clientDistDir = join(__dirname, "client");
|
|
396
585
|
app.use(express.static(clientDistDir));
|
|
397
586
|
app.get("/api/config", (_req, res) => {
|
|
398
|
-
res.json({
|
|
587
|
+
res.json({
|
|
588
|
+
defaultSlug: templates[0]?.slug ?? null,
|
|
589
|
+
templates,
|
|
590
|
+
tenant,
|
|
591
|
+
scenarios: listScenarioSummaries(customScenarios)
|
|
592
|
+
});
|
|
399
593
|
});
|
|
400
594
|
app.get("/api/template", (req, res) => {
|
|
401
595
|
const slug = req.query.slug ?? ".";
|
|
@@ -481,6 +675,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
481
675
|
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
482
676
|
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
483
677
|
const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
|
|
678
|
+
if (!isValidScenarioId(tab)) {
|
|
679
|
+
res.status(404).send("Scenario not found");
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const scenario = resolveScenario(baseDir, tab);
|
|
683
|
+
if (!scenario) {
|
|
684
|
+
res.status(404).send("Scenario not found");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
484
687
|
if (!slug) {
|
|
485
688
|
res.status(400).send("Missing slug");
|
|
486
689
|
return;
|
|
@@ -525,12 +728,12 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
525
728
|
css: templateFiles.css,
|
|
526
729
|
js: templateFiles.js,
|
|
527
730
|
fieldValues,
|
|
528
|
-
|
|
731
|
+
scenario,
|
|
529
732
|
sdkUrl: effectiveSdkUrl,
|
|
530
733
|
blockCookies,
|
|
531
734
|
previewCss
|
|
532
735
|
};
|
|
533
|
-
const rendered =
|
|
736
|
+
const rendered = scenario.hostKind === "custom" ? customHostShell(shellOptions) : scenario.hostKind === "article" ? articleShell(shellOptions) : standaloneShell(shellOptions);
|
|
534
737
|
res.setHeader("Cache-Control", "no-store");
|
|
535
738
|
res.type("html").send(rendered);
|
|
536
739
|
});
|
|
@@ -569,6 +772,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
569
772
|
broadcastReload();
|
|
570
773
|
});
|
|
571
774
|
const dirWatcher = chokidar.watch(baseDir, { depth: 1, ignoreInitial: true });
|
|
775
|
+
const scenariosWatcher = chokidar.watch(join(baseDir, SCENARIOS_DIRNAME), {
|
|
776
|
+
ignoreInitial: true
|
|
777
|
+
});
|
|
778
|
+
function rescanScenarios() {
|
|
779
|
+
customScenarios = scanCustomScenarios(baseDir);
|
|
780
|
+
console.log(`Scenarios updated: ${customScenarios.map((s) => s.id).join(", ") || "(none)"}`);
|
|
781
|
+
broadcastConfig();
|
|
782
|
+
}
|
|
783
|
+
scenariosWatcher.on("all", () => {
|
|
784
|
+
rescanScenarios();
|
|
785
|
+
});
|
|
572
786
|
function rescan() {
|
|
573
787
|
templates = scanTemplates(baseDir);
|
|
574
788
|
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,57 @@ window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
|
|
|
24
24
|
}
|
|
25
25
|
};`;
|
|
26
26
|
|
|
27
|
+
// src/scenarios.ts
|
|
28
|
+
var SCENARIO_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
29
|
+
function isValidScenarioId(id) {
|
|
30
|
+
return SCENARIO_ID_PATTERN.test(id);
|
|
31
|
+
}
|
|
32
|
+
function emptyPlacement() {
|
|
33
|
+
return {
|
|
34
|
+
target_selector: null,
|
|
35
|
+
placement_method: null,
|
|
36
|
+
truncation_unit: null,
|
|
37
|
+
truncation_count: null,
|
|
38
|
+
truncation_style: null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
var ARTICLE_TARGET_SELECTOR = "#allegro-preview-article";
|
|
42
|
+
var BUILT_IN_SCENARIOS = [
|
|
43
|
+
{
|
|
44
|
+
id: "standalone",
|
|
45
|
+
name: "Standalone",
|
|
46
|
+
builtIn: true,
|
|
47
|
+
hostKind: "none",
|
|
48
|
+
placement: emptyPlacement()
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "append",
|
|
52
|
+
name: "Article \xB7 Append",
|
|
53
|
+
builtIn: true,
|
|
54
|
+
hostKind: "article",
|
|
55
|
+
placement: {
|
|
56
|
+
target_selector: ARTICLE_TARGET_SELECTOR,
|
|
57
|
+
placement_method: "append",
|
|
58
|
+
truncation_unit: null,
|
|
59
|
+
truncation_count: null,
|
|
60
|
+
truncation_style: null
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "cut",
|
|
65
|
+
name: "Article \xB7 Cut",
|
|
66
|
+
builtIn: true,
|
|
67
|
+
hostKind: "article",
|
|
68
|
+
placement: {
|
|
69
|
+
target_selector: ARTICLE_TARGET_SELECTOR,
|
|
70
|
+
placement_method: "truncate",
|
|
71
|
+
truncation_unit: "paragraphs",
|
|
72
|
+
truncation_count: 3,
|
|
73
|
+
truncation_style: "cut"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
|
|
27
78
|
// src/slugify.ts
|
|
28
79
|
function slugify(name) {
|
|
29
80
|
return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -224,7 +275,8 @@ function buildPreviewData(options) {
|
|
|
224
275
|
css: options.css,
|
|
225
276
|
js: options.js,
|
|
226
277
|
fieldValues: options.fieldValues,
|
|
227
|
-
tab: options.
|
|
278
|
+
tab: options.scenario.id,
|
|
279
|
+
placement: options.scenario.placement
|
|
228
280
|
});
|
|
229
281
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
230
282
|
}
|
|
@@ -290,6 +342,33 @@ function articleShell(options) {
|
|
|
290
342
|
</body>
|
|
291
343
|
</html>`;
|
|
292
344
|
}
|
|
345
|
+
function customHostShell(options) {
|
|
346
|
+
const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
|
|
347
|
+
const hostCss = options.scenario.hostCss ? `<style>${options.scenario.hostCss}</style>` : "";
|
|
348
|
+
const hostHtml = options.scenario.hostHtml ?? "";
|
|
349
|
+
return `<!DOCTYPE html>
|
|
350
|
+
<html>
|
|
351
|
+
<head>
|
|
352
|
+
<meta charset="UTF-8">
|
|
353
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
354
|
+
<script>${PREVIEW_BOOTSTRAP_SCRIPT}</script>
|
|
355
|
+
${cookieScript}
|
|
356
|
+
<style>
|
|
357
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
358
|
+
body { margin: 0; }
|
|
359
|
+
[data-allegro-interaction] { display: block; }
|
|
360
|
+
</style>
|
|
361
|
+
${previewCssTag(options.previewCss)}
|
|
362
|
+
${hostCss}
|
|
363
|
+
</head>
|
|
364
|
+
<body>
|
|
365
|
+
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
366
|
+
${hostHtml}
|
|
367
|
+
${sdkScriptTag(options.sdkUrl)}
|
|
368
|
+
<script src="/preview-client.js"></script>
|
|
369
|
+
</body>
|
|
370
|
+
</html>`;
|
|
371
|
+
}
|
|
293
372
|
var sseClients = /* @__PURE__ */ new Set();
|
|
294
373
|
function broadcast(event) {
|
|
295
374
|
for (const client of sseClients) {
|
|
@@ -381,6 +460,115 @@ function readTemplateFiles(dir) {
|
|
|
381
460
|
function readTemplateFilesCached(dir) {
|
|
382
461
|
return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
|
|
383
462
|
}
|
|
463
|
+
var SCENARIOS_DIRNAME = ".allegro-preview";
|
|
464
|
+
function parsePlacement(raw) {
|
|
465
|
+
const placement = emptyPlacement();
|
|
466
|
+
if (typeof raw.target_selector === "string") {
|
|
467
|
+
placement.target_selector = raw.target_selector;
|
|
468
|
+
}
|
|
469
|
+
if (raw.placement_method === "truncate" || raw.placement_method === "append") {
|
|
470
|
+
placement.placement_method = raw.placement_method;
|
|
471
|
+
}
|
|
472
|
+
if (raw.truncation_unit === "paragraphs") {
|
|
473
|
+
placement.truncation_unit = raw.truncation_unit;
|
|
474
|
+
}
|
|
475
|
+
if (typeof raw.truncation_count === "number" && Number.isInteger(raw.truncation_count) && raw.truncation_count > 0) {
|
|
476
|
+
placement.truncation_count = raw.truncation_count;
|
|
477
|
+
}
|
|
478
|
+
if (raw.truncation_style === "cut") {
|
|
479
|
+
placement.truncation_style = raw.truncation_style;
|
|
480
|
+
}
|
|
481
|
+
return placement;
|
|
482
|
+
}
|
|
483
|
+
function readCustomScenario(scenariosDir, id) {
|
|
484
|
+
const dir = join(scenariosDir, id);
|
|
485
|
+
const jsonPath = join(dir, "scenario.json");
|
|
486
|
+
const base = {
|
|
487
|
+
id,
|
|
488
|
+
name: id,
|
|
489
|
+
builtIn: false,
|
|
490
|
+
hostKind: "none",
|
|
491
|
+
placement: emptyPlacement(),
|
|
492
|
+
parseError: null
|
|
493
|
+
};
|
|
494
|
+
let raw;
|
|
495
|
+
try {
|
|
496
|
+
raw = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
497
|
+
} catch (err) {
|
|
498
|
+
base.parseError = err instanceof Error ? err.message : "Invalid JSON in scenario.json";
|
|
499
|
+
return base;
|
|
500
|
+
}
|
|
501
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
502
|
+
base.parseError = "scenario.json must be a JSON object";
|
|
503
|
+
return base;
|
|
504
|
+
}
|
|
505
|
+
if (typeof raw.name === "string" && raw.name.length > 0) {
|
|
506
|
+
base.name = raw.name;
|
|
507
|
+
}
|
|
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
|
+
base.placement = parsePlacement(raw);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
base.parseError = err instanceof Error ? err.message : "Failed to read scenario host files";
|
|
518
|
+
return base;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return base;
|
|
522
|
+
}
|
|
523
|
+
function scanCustomScenarios(baseDir) {
|
|
524
|
+
const scenariosDir = join(baseDir, SCENARIOS_DIRNAME);
|
|
525
|
+
if (!existsSync(scenariosDir)) {
|
|
526
|
+
return [];
|
|
527
|
+
}
|
|
528
|
+
const results = [];
|
|
529
|
+
try {
|
|
530
|
+
for (const entry of readdirSync(scenariosDir, { withFileTypes: true })) {
|
|
531
|
+
if (!entry.isDirectory()) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (!existsSync(join(scenariosDir, entry.name, "scenario.json"))) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (!isValidScenarioId(entry.name)) {
|
|
538
|
+
console.warn(
|
|
539
|
+
`[allegro-preview] Skipping scenario folder "${entry.name}": name must be a kebab-case slug (lowercase letters, digits, hyphens).`
|
|
540
|
+
);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const scenario = readCustomScenario(scenariosDir, entry.name);
|
|
544
|
+
if (scenario.parseError) {
|
|
545
|
+
console.warn(`[allegro-preview] Skipping scenario "${entry.name}": ${scenario.parseError}`);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
results.push(scenario);
|
|
549
|
+
}
|
|
550
|
+
} catch {
|
|
551
|
+
}
|
|
552
|
+
return results;
|
|
553
|
+
}
|
|
554
|
+
function listScenarioSummaries(customScenarios) {
|
|
555
|
+
return [
|
|
556
|
+
...BUILT_IN_SCENARIOS.map((s) => ({ id: s.id, name: s.name })),
|
|
557
|
+
...customScenarios.map((s) => ({ id: s.id, name: s.name }))
|
|
558
|
+
];
|
|
559
|
+
}
|
|
560
|
+
function resolveScenario(baseDir, id) {
|
|
561
|
+
const builtIn = BUILT_IN_SCENARIOS.find((s) => s.id === id);
|
|
562
|
+
if (builtIn) {
|
|
563
|
+
return builtIn;
|
|
564
|
+
}
|
|
565
|
+
const scenariosDir = join(baseDir, SCENARIOS_DIRNAME);
|
|
566
|
+
if (existsSync(join(scenariosDir, id, "scenario.json"))) {
|
|
567
|
+
const scenario = readCustomScenario(scenariosDir, id);
|
|
568
|
+
return scenario.parseError ? null : scenario;
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
384
572
|
async function startServer(templatePath, port = 0, tenant = "") {
|
|
385
573
|
const baseDir = resolve(process.cwd(), templatePath);
|
|
386
574
|
if (!existsSync(baseDir)) {
|
|
@@ -388,11 +576,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
388
576
|
process.exit(1);
|
|
389
577
|
}
|
|
390
578
|
let templates = scanTemplates(baseDir);
|
|
579
|
+
let customScenarios = scanCustomScenarios(baseDir);
|
|
391
580
|
const app = express();
|
|
392
581
|
const clientDistDir = join(__dirname, "client");
|
|
393
582
|
app.use(express.static(clientDistDir));
|
|
394
583
|
app.get("/api/config", (_req, res) => {
|
|
395
|
-
res.json({
|
|
584
|
+
res.json({
|
|
585
|
+
defaultSlug: templates[0]?.slug ?? null,
|
|
586
|
+
templates,
|
|
587
|
+
tenant,
|
|
588
|
+
scenarios: listScenarioSummaries(customScenarios)
|
|
589
|
+
});
|
|
396
590
|
});
|
|
397
591
|
app.get("/api/template", (req, res) => {
|
|
398
592
|
const slug = req.query.slug ?? ".";
|
|
@@ -478,6 +672,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
478
672
|
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
479
673
|
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
480
674
|
const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
|
|
675
|
+
if (!isValidScenarioId(tab)) {
|
|
676
|
+
res.status(404).send("Scenario not found");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const scenario = resolveScenario(baseDir, tab);
|
|
680
|
+
if (!scenario) {
|
|
681
|
+
res.status(404).send("Scenario not found");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
481
684
|
if (!slug) {
|
|
482
685
|
res.status(400).send("Missing slug");
|
|
483
686
|
return;
|
|
@@ -522,12 +725,12 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
522
725
|
css: templateFiles.css,
|
|
523
726
|
js: templateFiles.js,
|
|
524
727
|
fieldValues,
|
|
525
|
-
|
|
728
|
+
scenario,
|
|
526
729
|
sdkUrl: effectiveSdkUrl,
|
|
527
730
|
blockCookies,
|
|
528
731
|
previewCss
|
|
529
732
|
};
|
|
530
|
-
const rendered =
|
|
733
|
+
const rendered = scenario.hostKind === "custom" ? customHostShell(shellOptions) : scenario.hostKind === "article" ? articleShell(shellOptions) : standaloneShell(shellOptions);
|
|
531
734
|
res.setHeader("Cache-Control", "no-store");
|
|
532
735
|
res.type("html").send(rendered);
|
|
533
736
|
});
|
|
@@ -566,6 +769,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
566
769
|
broadcastReload();
|
|
567
770
|
});
|
|
568
771
|
const dirWatcher = chokidar.watch(baseDir, { depth: 1, ignoreInitial: true });
|
|
772
|
+
const scenariosWatcher = chokidar.watch(join(baseDir, SCENARIOS_DIRNAME), {
|
|
773
|
+
ignoreInitial: true
|
|
774
|
+
});
|
|
775
|
+
function rescanScenarios() {
|
|
776
|
+
customScenarios = scanCustomScenarios(baseDir);
|
|
777
|
+
console.log(`Scenarios updated: ${customScenarios.map((s) => s.id).join(", ") || "(none)"}`);
|
|
778
|
+
broadcastConfig();
|
|
779
|
+
}
|
|
780
|
+
scenariosWatcher.on("all", () => {
|
|
781
|
+
rescanScenarios();
|
|
782
|
+
});
|
|
569
783
|
function rescan() {
|
|
570
784
|
templates = scanTemplates(baseDir);
|
|
571
785
|
console.log(`Templates updated: ${templates.map((t) => t.slug).join(", ") || "(none)"}`);
|
|
@@ -621,5 +835,8 @@ allegro-preview \u2192 ${url}
|
|
|
621
835
|
}
|
|
622
836
|
export {
|
|
623
837
|
escapeStyle,
|
|
838
|
+
parsePlacement,
|
|
839
|
+
readCustomScenario,
|
|
840
|
+
scanCustomScenarios,
|
|
624
841
|
startServer
|
|
625
842
|
};
|
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();
|