@allegrocdp/preview 0.1.0-dev.12 → 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 +245 -14
- package/dist/client/assets/app-C--PVKMq.js +14 -0
- package/dist/client/index.html +12 -9
- package/dist/client/preview-client.js +1 -1
- package/dist/server.js +245 -14
- package/package.json +1 -1
- package/dist/client/assets/app-D1XS_DDX.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,58 @@ 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
|
+
|
|
77
|
+
// src/slugify.ts
|
|
78
|
+
function slugify(name) {
|
|
79
|
+
return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
80
|
+
}
|
|
81
|
+
|
|
30
82
|
// src/server.ts
|
|
31
83
|
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
32
84
|
var BOILERPLATE_HTML = `<div class="signup">
|
|
@@ -127,7 +179,7 @@ var BOILERPLATE_FIELDS = [
|
|
|
127
179
|
{
|
|
128
180
|
slug: "subheadline",
|
|
129
181
|
label: "Subheadline",
|
|
130
|
-
type: "
|
|
182
|
+
type: "text",
|
|
131
183
|
default_value: "Get the latest stories delivered to your inbox."
|
|
132
184
|
},
|
|
133
185
|
{ slug: "input_placeholder", label: "Input placeholder", type: "text", default_value: "Enter your email" },
|
|
@@ -222,7 +274,8 @@ function buildPreviewData(options) {
|
|
|
222
274
|
css: options.css,
|
|
223
275
|
js: options.js,
|
|
224
276
|
fieldValues: options.fieldValues,
|
|
225
|
-
tab: options.
|
|
277
|
+
tab: options.scenario.id,
|
|
278
|
+
placement: options.scenario.placement
|
|
226
279
|
});
|
|
227
280
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
228
281
|
}
|
|
@@ -288,6 +341,33 @@ function articleShell(options) {
|
|
|
288
341
|
</body>
|
|
289
342
|
</html>`;
|
|
290
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
|
+
}
|
|
291
371
|
var sseClients = /* @__PURE__ */ new Set();
|
|
292
372
|
function broadcast(event) {
|
|
293
373
|
for (const client of sseClients) {
|
|
@@ -310,7 +390,9 @@ function readTemplateMeta(dir) {
|
|
|
310
390
|
}
|
|
311
391
|
try {
|
|
312
392
|
const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
313
|
-
return {
|
|
393
|
+
return {
|
|
394
|
+
name: typeof parsed.name === "string" && parsed.name.trim() !== "" ? parsed.name : basename(dir)
|
|
395
|
+
};
|
|
314
396
|
} catch {
|
|
315
397
|
return { name: basename(dir) };
|
|
316
398
|
}
|
|
@@ -377,6 +459,109 @@ function readTemplateFiles(dir) {
|
|
|
377
459
|
function readTemplateFilesCached(dir) {
|
|
378
460
|
return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
|
|
379
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
|
+
}
|
|
380
565
|
async function startServer(templatePath, port = 0, tenant = "") {
|
|
381
566
|
const baseDir = resolve(process.cwd(), templatePath);
|
|
382
567
|
if (!existsSync(baseDir)) {
|
|
@@ -384,11 +569,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
384
569
|
process.exit(1);
|
|
385
570
|
}
|
|
386
571
|
let templates = scanTemplates(baseDir);
|
|
572
|
+
let customScenarios = scanCustomScenarios(baseDir);
|
|
387
573
|
const app = express();
|
|
388
574
|
const clientDistDir = join(__dirname, "client");
|
|
389
575
|
app.use(express.static(clientDistDir));
|
|
390
576
|
app.get("/api/config", (_req, res) => {
|
|
391
|
-
res.json({
|
|
577
|
+
res.json({
|
|
578
|
+
defaultSlug: templates[0]?.slug ?? null,
|
|
579
|
+
templates,
|
|
580
|
+
tenant,
|
|
581
|
+
scenarios: listScenarioSummaries(customScenarios)
|
|
582
|
+
});
|
|
392
583
|
});
|
|
393
584
|
app.get("/api/template", (req, res) => {
|
|
394
585
|
const slug = req.query.slug ?? ".";
|
|
@@ -412,16 +603,28 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
412
603
|
});
|
|
413
604
|
app.post("/api/template/create", express.json(), (req, res) => {
|
|
414
605
|
const body = req.body;
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
417
|
-
res.status(400).json({
|
|
418
|
-
|
|
419
|
-
|
|
606
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
607
|
+
if (name === "") {
|
|
608
|
+
res.status(400).json({ error: "Template name is required." });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const slug = slugify(name);
|
|
612
|
+
if (slug === "") {
|
|
613
|
+
res.status(400).json({ error: "Template name must contain at least one letter or number." });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const existing = scanTemplates(baseDir);
|
|
617
|
+
if (existing.some((t) => t.slug === slug)) {
|
|
618
|
+
res.status(400).json({ error: `A template with the slug "${slug}" already exists.` });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (existing.some((t) => t.name.toLowerCase() === name.toLowerCase())) {
|
|
622
|
+
res.status(400).json({ error: `A template named "${name}" already exists.` });
|
|
420
623
|
return;
|
|
421
624
|
}
|
|
422
625
|
const targetDir = join(baseDir, slug);
|
|
423
626
|
if (existsSync(targetDir)) {
|
|
424
|
-
res.status(400).json({ error: `A template
|
|
627
|
+
res.status(400).json({ error: `A template with the slug "${slug}" already exists.` });
|
|
425
628
|
return;
|
|
426
629
|
}
|
|
427
630
|
try {
|
|
@@ -430,7 +633,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
430
633
|
writeFileSync(join(targetDir, "index.css"), BOILERPLATE_CSS);
|
|
431
634
|
writeFileSync(
|
|
432
635
|
join(targetDir, "template.json"),
|
|
433
|
-
JSON.stringify(
|
|
636
|
+
JSON.stringify(
|
|
637
|
+
{
|
|
638
|
+
$schema: "https://docs.allegrocdp.com/template.schema.json",
|
|
639
|
+
name,
|
|
640
|
+
fields: BOILERPLATE_FIELDS
|
|
641
|
+
},
|
|
642
|
+
null,
|
|
643
|
+
2
|
|
644
|
+
) + "\n"
|
|
434
645
|
);
|
|
435
646
|
} catch (err) {
|
|
436
647
|
console.error("[allegro-preview] Failed to create template:", err);
|
|
@@ -438,7 +649,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
438
649
|
return;
|
|
439
650
|
}
|
|
440
651
|
rescan();
|
|
441
|
-
res.status(201).json({ slug });
|
|
652
|
+
res.status(201).json({ slug, name });
|
|
442
653
|
});
|
|
443
654
|
app.get("/api/events", (req, res) => {
|
|
444
655
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -454,6 +665,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
454
665
|
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
455
666
|
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
456
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
|
+
}
|
|
457
677
|
if (!slug) {
|
|
458
678
|
res.status(400).send("Missing slug");
|
|
459
679
|
return;
|
|
@@ -498,12 +718,12 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
498
718
|
css: templateFiles.css,
|
|
499
719
|
js: templateFiles.js,
|
|
500
720
|
fieldValues,
|
|
501
|
-
|
|
721
|
+
scenario,
|
|
502
722
|
sdkUrl: effectiveSdkUrl,
|
|
503
723
|
blockCookies,
|
|
504
724
|
previewCss
|
|
505
725
|
};
|
|
506
|
-
const rendered =
|
|
726
|
+
const rendered = scenario.hostKind === "custom" ? customHostShell(shellOptions) : scenario.hostKind === "article" ? articleShell(shellOptions) : standaloneShell(shellOptions);
|
|
507
727
|
res.setHeader("Cache-Control", "no-store");
|
|
508
728
|
res.type("html").send(rendered);
|
|
509
729
|
});
|
|
@@ -542,6 +762,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
542
762
|
broadcastReload();
|
|
543
763
|
});
|
|
544
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
|
+
});
|
|
545
776
|
function rescan() {
|
|
546
777
|
templates = scanTemplates(baseDir);
|
|
547
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
|
@@ -283,6 +283,12 @@
|
|
|
283
283
|
border-color: #f87171;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
.modal-hint {
|
|
287
|
+
margin-top: 6px;
|
|
288
|
+
font-size: 12px;
|
|
289
|
+
color: #94a3b8;
|
|
290
|
+
}
|
|
291
|
+
|
|
286
292
|
.modal-error {
|
|
287
293
|
font-size: 11px;
|
|
288
294
|
color: #f87171;
|
|
@@ -1008,7 +1014,7 @@
|
|
|
1008
1014
|
display: block;
|
|
1009
1015
|
}
|
|
1010
1016
|
</style>
|
|
1011
|
-
<script type="module" crossorigin src="/assets/app-
|
|
1017
|
+
<script type="module" crossorigin src="/assets/app-C--PVKMq.js"></script>
|
|
1012
1018
|
</head>
|
|
1013
1019
|
<body>
|
|
1014
1020
|
<aside class="sidebar">
|
|
@@ -1138,11 +1144,7 @@
|
|
|
1138
1144
|
|
|
1139
1145
|
<div class="main">
|
|
1140
1146
|
<div class="toolbar">
|
|
1141
|
-
<div class="tab-group" id="tab-group">
|
|
1142
|
-
<button class="tab-btn active" data-tab="standalone" disabled>Standalone</button>
|
|
1143
|
-
<button class="tab-btn" data-tab="append" disabled>Article · Append</button>
|
|
1144
|
-
<button class="tab-btn" data-tab="cut" disabled>Article · Cut</button>
|
|
1145
|
-
</div>
|
|
1147
|
+
<div class="tab-group" id="tab-group"></div>
|
|
1146
1148
|
<div class="toolbar-sep"></div>
|
|
1147
1149
|
<span class="template-badge" id="toolbar-badge" style="opacity: 0.4">No template selected</span>
|
|
1148
1150
|
<div class="device-group" id="device-group">
|
|
@@ -1250,15 +1252,16 @@
|
|
|
1250
1252
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
1251
1253
|
<div class="modal-title" id="modal-title">New Template</div>
|
|
1252
1254
|
<div class="modal-field">
|
|
1253
|
-
<label class="modal-label" for="modal-
|
|
1255
|
+
<label class="modal-label" for="modal-name-input">Template name</label>
|
|
1254
1256
|
<input
|
|
1255
1257
|
class="modal-input"
|
|
1256
|
-
id="modal-
|
|
1258
|
+
id="modal-name-input"
|
|
1257
1259
|
type="text"
|
|
1258
|
-
placeholder="
|
|
1260
|
+
placeholder="My template"
|
|
1259
1261
|
autocomplete="off"
|
|
1260
1262
|
spellcheck="false"
|
|
1261
1263
|
/>
|
|
1264
|
+
<div class="modal-hint" id="modal-slug-hint"></div>
|
|
1262
1265
|
<div class="modal-error" id="modal-error"></div>
|
|
1263
1266
|
</div>
|
|
1264
1267
|
<div class="modal-actions">
|
|
@@ -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,58 @@ 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
|
+
|
|
74
|
+
// src/slugify.ts
|
|
75
|
+
function slugify(name) {
|
|
76
|
+
return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
77
|
+
}
|
|
78
|
+
|
|
27
79
|
// src/server.ts
|
|
28
80
|
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
29
81
|
var BOILERPLATE_HTML = `<div class="signup">
|
|
@@ -124,7 +176,7 @@ var BOILERPLATE_FIELDS = [
|
|
|
124
176
|
{
|
|
125
177
|
slug: "subheadline",
|
|
126
178
|
label: "Subheadline",
|
|
127
|
-
type: "
|
|
179
|
+
type: "text",
|
|
128
180
|
default_value: "Get the latest stories delivered to your inbox."
|
|
129
181
|
},
|
|
130
182
|
{ slug: "input_placeholder", label: "Input placeholder", type: "text", default_value: "Enter your email" },
|
|
@@ -219,7 +271,8 @@ function buildPreviewData(options) {
|
|
|
219
271
|
css: options.css,
|
|
220
272
|
js: options.js,
|
|
221
273
|
fieldValues: options.fieldValues,
|
|
222
|
-
tab: options.
|
|
274
|
+
tab: options.scenario.id,
|
|
275
|
+
placement: options.scenario.placement
|
|
223
276
|
});
|
|
224
277
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
225
278
|
}
|
|
@@ -285,6 +338,33 @@ function articleShell(options) {
|
|
|
285
338
|
</body>
|
|
286
339
|
</html>`;
|
|
287
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
|
+
}
|
|
288
368
|
var sseClients = /* @__PURE__ */ new Set();
|
|
289
369
|
function broadcast(event) {
|
|
290
370
|
for (const client of sseClients) {
|
|
@@ -307,7 +387,9 @@ function readTemplateMeta(dir) {
|
|
|
307
387
|
}
|
|
308
388
|
try {
|
|
309
389
|
const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
310
|
-
return {
|
|
390
|
+
return {
|
|
391
|
+
name: typeof parsed.name === "string" && parsed.name.trim() !== "" ? parsed.name : basename(dir)
|
|
392
|
+
};
|
|
311
393
|
} catch {
|
|
312
394
|
return { name: basename(dir) };
|
|
313
395
|
}
|
|
@@ -374,6 +456,109 @@ function readTemplateFiles(dir) {
|
|
|
374
456
|
function readTemplateFilesCached(dir) {
|
|
375
457
|
return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
|
|
376
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
|
+
}
|
|
377
562
|
async function startServer(templatePath, port = 0, tenant = "") {
|
|
378
563
|
const baseDir = resolve(process.cwd(), templatePath);
|
|
379
564
|
if (!existsSync(baseDir)) {
|
|
@@ -381,11 +566,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
381
566
|
process.exit(1);
|
|
382
567
|
}
|
|
383
568
|
let templates = scanTemplates(baseDir);
|
|
569
|
+
let customScenarios = scanCustomScenarios(baseDir);
|
|
384
570
|
const app = express();
|
|
385
571
|
const clientDistDir = join(__dirname, "client");
|
|
386
572
|
app.use(express.static(clientDistDir));
|
|
387
573
|
app.get("/api/config", (_req, res) => {
|
|
388
|
-
res.json({
|
|
574
|
+
res.json({
|
|
575
|
+
defaultSlug: templates[0]?.slug ?? null,
|
|
576
|
+
templates,
|
|
577
|
+
tenant,
|
|
578
|
+
scenarios: listScenarioSummaries(customScenarios)
|
|
579
|
+
});
|
|
389
580
|
});
|
|
390
581
|
app.get("/api/template", (req, res) => {
|
|
391
582
|
const slug = req.query.slug ?? ".";
|
|
@@ -409,16 +600,28 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
409
600
|
});
|
|
410
601
|
app.post("/api/template/create", express.json(), (req, res) => {
|
|
411
602
|
const body = req.body;
|
|
412
|
-
const
|
|
413
|
-
if (
|
|
414
|
-
res.status(400).json({
|
|
415
|
-
|
|
416
|
-
|
|
603
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
604
|
+
if (name === "") {
|
|
605
|
+
res.status(400).json({ error: "Template name is required." });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const slug = slugify(name);
|
|
609
|
+
if (slug === "") {
|
|
610
|
+
res.status(400).json({ error: "Template name must contain at least one letter or number." });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const existing = scanTemplates(baseDir);
|
|
614
|
+
if (existing.some((t) => t.slug === slug)) {
|
|
615
|
+
res.status(400).json({ error: `A template with the slug "${slug}" already exists.` });
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (existing.some((t) => t.name.toLowerCase() === name.toLowerCase())) {
|
|
619
|
+
res.status(400).json({ error: `A template named "${name}" already exists.` });
|
|
417
620
|
return;
|
|
418
621
|
}
|
|
419
622
|
const targetDir = join(baseDir, slug);
|
|
420
623
|
if (existsSync(targetDir)) {
|
|
421
|
-
res.status(400).json({ error: `A template
|
|
624
|
+
res.status(400).json({ error: `A template with the slug "${slug}" already exists.` });
|
|
422
625
|
return;
|
|
423
626
|
}
|
|
424
627
|
try {
|
|
@@ -427,7 +630,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
427
630
|
writeFileSync(join(targetDir, "index.css"), BOILERPLATE_CSS);
|
|
428
631
|
writeFileSync(
|
|
429
632
|
join(targetDir, "template.json"),
|
|
430
|
-
JSON.stringify(
|
|
633
|
+
JSON.stringify(
|
|
634
|
+
{
|
|
635
|
+
$schema: "https://docs.allegrocdp.com/template.schema.json",
|
|
636
|
+
name,
|
|
637
|
+
fields: BOILERPLATE_FIELDS
|
|
638
|
+
},
|
|
639
|
+
null,
|
|
640
|
+
2
|
|
641
|
+
) + "\n"
|
|
431
642
|
);
|
|
432
643
|
} catch (err) {
|
|
433
644
|
console.error("[allegro-preview] Failed to create template:", err);
|
|
@@ -435,7 +646,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
435
646
|
return;
|
|
436
647
|
}
|
|
437
648
|
rescan();
|
|
438
|
-
res.status(201).json({ slug });
|
|
649
|
+
res.status(201).json({ slug, name });
|
|
439
650
|
});
|
|
440
651
|
app.get("/api/events", (req, res) => {
|
|
441
652
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -451,6 +662,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
451
662
|
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
452
663
|
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
453
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
|
+
}
|
|
454
674
|
if (!slug) {
|
|
455
675
|
res.status(400).send("Missing slug");
|
|
456
676
|
return;
|
|
@@ -495,12 +715,12 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
495
715
|
css: templateFiles.css,
|
|
496
716
|
js: templateFiles.js,
|
|
497
717
|
fieldValues,
|
|
498
|
-
|
|
718
|
+
scenario,
|
|
499
719
|
sdkUrl: effectiveSdkUrl,
|
|
500
720
|
blockCookies,
|
|
501
721
|
previewCss
|
|
502
722
|
};
|
|
503
|
-
const rendered =
|
|
723
|
+
const rendered = scenario.hostKind === "custom" ? customHostShell(shellOptions) : scenario.hostKind === "article" ? articleShell(shellOptions) : standaloneShell(shellOptions);
|
|
504
724
|
res.setHeader("Cache-Control", "no-store");
|
|
505
725
|
res.type("html").send(rendered);
|
|
506
726
|
});
|
|
@@ -539,6 +759,17 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
539
759
|
broadcastReload();
|
|
540
760
|
});
|
|
541
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
|
+
});
|
|
542
773
|
function rescan() {
|
|
543
774
|
templates = scanTemplates(baseDir);
|
|
544
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)}})();const te={full:0,desktop:1280,tablet:768,mobile:390};function ne(e){return te[e]}let u=[],d=null,N="standalone",y="full",L=null,E={},H,F=!0,T=null,f=null,h=null;const q=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"),le=document.getElementById("theme-toggle"),oe=document.getElementById("theme-icon-sun"),ae=document.getElementById("theme-icon-moon"),B=document.getElementById("tenant-input"),x=document.getElementById("tenant-status"),se=document.getElementById("new-template-btn"),J=document.getElementById("new-window-btn"),k=document.getElementById("create-modal-backdrop"),c=document.getElementById("modal-slug-input"),v=document.getElementById("modal-error"),ie=document.getElementById("modal-cancel-btn"),w=document.getElementById("modal-create-btn"),P=document.getElementById("block-cookies-checkbox"),V=document.getElementById("cookie-auth-area"),S=document.getElementById("cookie-auth-user"),R=document.getElementById("cookie-auth-hint"),ce=document.getElementById("cookie-clear-btn"),b=document.getElementById("jwt-modal-backdrop"),re=document.getElementById("jwt-payload-content"),de=document.getElementById("jwt-modal-close-btn");function W(e){document.documentElement.setAttribute("data-theme",e?"dark":"light"),oe.style.display=e?"none":"",ae.style.display=e?"":"none"}function ue(){const e=localStorage.getItem("allegro-preview-theme"),t=window.matchMedia("(prefers-color-scheme: dark)").matches;W(e?e==="dark":t)}le.addEventListener("click",()=>{const e=document.documentElement.getAttribute("data-theme")!=="dark";W(e),localStorage.setItem("allegro-preview-theme",e?"dark":"light")});function me(e){const t=e.trim();return t?(/^https?:\/\//.test(t)?t:`https://${t}`).replace(/\/+$/,"")+"/client.js":""}function G(e){const t=me(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),G(B.value)});function pe(e=""){const t=localStorage.getItem("allegro-preview-tenant"),n=e||t||"";n&&(B.value=n,e&&localStorage.setItem("allegro-preview-tenant",n),G(n))}const fe=[{name:"_ALLEGROT",label:"Session JWT",isJwt:!0},{name:"_ALLEGROD",label:"Device ID"},{name:"_ALLEGROS",label:"Session ID"}];function ge(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 ve(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=fe.map(t=>({...t,value:ge(t.name)})).filter(t=>t.value!==null);if(S.innerHTML="",e.length===0){S.style.display="none",R.style.display="";return}R.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 r=ve(t.value);re.textContent=r?JSON.stringify(r,null,2):t.value,b.classList.remove("hidden")},n.appendChild(i)}S.appendChild(n)}}function he(e){F=e,localStorage.setItem("allegro-preview-block-cookies",e?"true":"false"),V.style.display=e?"none":"",e?h&&(clearInterval(h),h=null):(C(),h||(h=setInterval(C,2e3))),L&&p()}function ye(){const t=localStorage.getItem("allegro-preview-block-cookies")!=="false";P.checked=t,F=t,V.style.display=t?"none":"",t||(C(),h=setInterval(C,2e3))}P.addEventListener("change",()=>{he(P.checked)});ce.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()});de.addEventListener("click",()=>{b.classList.add("hidden")});b.addEventListener("click",e=>{e.target===b&&b.classList.add("hidden")});function Ee(){const e=window.location.hash.slice(1);return e?decodeURIComponent(e):null}function we(e){history.replaceState(null,"",`#${encodeURIComponent(e)}`)}function be(){history.replaceState(null,"",window.location.pathname)}function z(e){$.querySelectorAll(".tab-btn").forEach(t=>{t.disabled=!e}),M.querySelectorAll(".device-btn").forEach(t=>{t.disabled=!e}),J.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(){q.innerHTML="";for(const e of u){const t=document.createElement("button");t.className="template-nav-item"+(e.slug===d?" active":""),t.innerHTML=`<span class="template-nav-dot"></span><span class="template-nav-name">${_(e.name)}</span>`,t.addEventListener("click",()=>void D(e.slug)),q.appendChild(t)}}async function D(e){d=e,we(e),I(),await U(e)}function K(){L=null,d=null,T=null,f=null,be(),I(),z(!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",u.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 u){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 D(n.slug)),t.appendChild(l)}}const O=["text","number","checkbox","textarea"];function Y(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 ke(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 Ce(e){E={};for(const t of e)Y(t)&&(E[t.slug]=t.default_value??"")}function Le(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(!Y(n)){const o=document.createElement("div");o.className="field-error",o.textContent=ke(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 r=document.createElement("label");r.className="field-label",r.htmlFor=`field-${l.slug}`,r.textContent=l.label||l.slug,o.appendChild(i),o.appendChild(r),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 Ie(){const e=new URLSearchParams;e.set("slug",d),e.set("tab",N),H&&e.set("sdk",H),F||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||!d)return;const e=Ie();T=e;const t=ne(y),n=t===0;if(f){const i=f.parentElement,r=i.parentElement,ee=r.parentElement;ee.className=n?"preview-stage":"preview-stage preview-stage--breakpoint",r.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 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();L=n,j.textContent=`Template: ${n.name}`,j.style.opacity="1",z(!0),Ce(n.fields),Le(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 xe(){const t=await(await fetch("/api/config")).json();u=t.templates,pe(t.tenant??"");const n=Ee(),l=n&&u.find(a=>a.slug===n)?n:null;l?(d=l,I(),await U(l)):K()}function Se(){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 Q(){const e=new EventSource("/api/events");e.addEventListener("reload",()=>{d&&U(d)}),e.addEventListener("config",()=>{Be()}),e.onerror=()=>{e.close(),setTimeout(Q,2e3)}}async function Be(){u=(await(await fetch("/api/config")).json()).templates,I(),d&&!u.find(n=>n.slug===d)&&K()}function _(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const Ne=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;function Te(){c.value="",v.textContent="",c.classList.remove("invalid"),w.disabled=!1,k.classList.remove("hidden"),c.focus()}function A(){k.classList.add("hidden")}function X(e){return e?Ne.test(e)?null:"Use lowercase letters, numbers, and hyphens only (e.g. my-template).":"Slug is required."}J.addEventListener("click",()=>{T&&window.open(T,"_blank")});se.addEventListener("click",Te);ie.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=X(c.value);e?(c.classList.add("invalid"),v.textContent=e):(c.classList.remove("invalid"),v.textContent="")});c.addEventListener("keydown",e=>{e.key==="Enter"&&Z()});w.addEventListener("click",()=>void Z());async function Z(){const e=c.value.trim(),t=X(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({slug:e})}),l=await n.json();if(!n.ok){v.textContent=l.error??"Failed to create template.",w.disabled=!1;return}A(),u=[...u,{slug:e,name:e}],I(),await D(e)}catch{v.textContent="Network error. Please try again.",w.disabled=!1}}ue();Se();ye();xe();Q();
|