@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 CHANGED
@@ -58,13 +58,47 @@ my-template/
58
58
 
59
59
  Supported field types: `text`, `number`, `checkbox`, `textarea`.
60
60
 
61
- ## Preview contexts
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
- | **Article · Append** | Template rendered after the full content of a mock article |
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.tab
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({ defaultSlug: templates[0]?.slug ?? null, templates, tenant });
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
- tab,
731
+ scenario,
529
732
  sdkUrl: effectiveSdkUrl,
530
733
  blockCookies,
531
734
  previewCss
532
735
  };
533
- const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}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();
@@ -1014,7 +1014,7 @@
1014
1014
  display: block;
1015
1015
  }
1016
1016
  </style>
1017
- <script type="module" crossorigin src="/assets/app-B0ybFOKs.js"></script>
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 &middot; Append</button>
1150
- <button class="tab-btn" data-tab="cut" disabled>Article &middot; 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.tab==="cut"||e.tab==="append";return{type:"template",template_id:e.slug,target_selector:t?"#allegro-preview-article":null,field_values:e.fieldValues,placement_method:e.tab==="cut"?"truncate":e.tab==="append"?"append":null,truncation_unit:e.tab==="cut"?"paragraphs":null,truncation_count:e.tab==="cut"?3:null,truncation_style:e.tab==="cut"?"cut":null,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(),l=o(t);e.interaction.renderActions([l],{skipDelays:!0}).catch(n=>{console.error("[allegro-preview] renderActions failed:",n)})});
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.tab
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({ defaultSlug: templates[0]?.slug ?? null, templates, tenant });
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
- tab,
728
+ scenario,
526
729
  sdkUrl: effectiveSdkUrl,
527
730
  blockCookies,
528
731
  previewCss
529
732
  };
530
- const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
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,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@allegrocdp/preview",
4
- "version": "0.1.0-dev.13",
4
+ "version": "0.1.0-dev.15",
5
5
  "description": "Local dev server for previewing Allegro templates",
6
6
  "type": "module",
7
7
  "bin": {
@@ -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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}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();