@allegrocdp/preview 0.1.0-dev.13 → 0.1.0-dev.14

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