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

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/dist/cli.js CHANGED
@@ -7,6 +7,8 @@ import { Command } from "commander";
7
7
  import chokidar from "chokidar";
8
8
  import express from "express";
9
9
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
10
+ import { get as httpGet } from "http";
11
+ import { get as httpsGet } from "https";
10
12
  import open from "open";
11
13
  import { basename, isAbsolute, join, relative, resolve } from "path";
12
14
  import { fileURLToPath } from "url";
@@ -25,6 +27,11 @@ window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
25
27
  }
26
28
  };`;
27
29
 
30
+ // src/slugify.ts
31
+ function slugify(name) {
32
+ return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
33
+ }
34
+
28
35
  // src/server.ts
29
36
  var __dirname = fileURLToPath(new URL(".", import.meta.url));
30
37
  var BOILERPLATE_HTML = `<div class="signup">
@@ -125,7 +132,7 @@ var BOILERPLATE_FIELDS = [
125
132
  {
126
133
  slug: "subheadline",
127
134
  label: "Subheadline",
128
- type: "textarea",
135
+ type: "text",
129
136
  default_value: "Get the latest stories delivered to your inbox."
130
137
  },
131
138
  { slug: "input_placeholder", label: "Input placeholder", type: "text", default_value: "Enter your email" },
@@ -158,6 +165,60 @@ function normalizeTenantSdkUrl(raw) {
158
165
  const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
159
166
  return withProtocol.replace(/\/+$/, "") + "/client.js";
160
167
  }
168
+ var previewCssCache = /* @__PURE__ */ new Map();
169
+ var PREVIEW_CSS_TTL_MS = 6e4;
170
+ function escapeStyle(css) {
171
+ return css.replace(/<\/(style)/gi, "<\\/$1");
172
+ }
173
+ function isLocalHost(hostname) {
174
+ const name = hostname.toLowerCase();
175
+ return name === "localhost" || name.endsWith(".localhost") || name.endsWith(".test") || name === "127.0.0.1" || name === "::1";
176
+ }
177
+ function getJson(target) {
178
+ return new Promise((resolve2, reject) => {
179
+ const handleResponse = (res) => {
180
+ const status = res.statusCode ?? 0;
181
+ if (status < 200 || status >= 300) {
182
+ res.resume();
183
+ reject(new Error(`HTTP ${status}`));
184
+ return;
185
+ }
186
+ let body = "";
187
+ res.setEncoding("utf-8");
188
+ res.on("data", (chunk) => body += chunk);
189
+ res.on("end", () => {
190
+ try {
191
+ resolve2(JSON.parse(body));
192
+ } catch (err) {
193
+ reject(err);
194
+ }
195
+ });
196
+ };
197
+ const req = target.protocol === "https:" ? httpsGet(target, { rejectUnauthorized: !isLocalHost(target.hostname) }, handleResponse) : httpGet(target, handleResponse);
198
+ req.on("error", reject);
199
+ req.setTimeout(5e3, () => req.destroy(new Error("Request timed out")));
200
+ });
201
+ }
202
+ async function fetchPreviewCss(sdkUrl) {
203
+ let endpoint;
204
+ try {
205
+ endpoint = new URL("/api/preview-css", sdkUrl);
206
+ } catch {
207
+ return "";
208
+ }
209
+ const cached = previewCssCache.get(endpoint.origin);
210
+ if (cached && Date.now() - cached.fetchedAt < PREVIEW_CSS_TTL_MS) {
211
+ return cached.css;
212
+ }
213
+ try {
214
+ const data = await getJson(endpoint);
215
+ const css = typeof data.css === "string" ? data.css : "";
216
+ previewCssCache.set(endpoint.origin, { css, fetchedAt: Date.now() });
217
+ return css;
218
+ } catch {
219
+ return cached?.css ?? "";
220
+ }
221
+ }
161
222
  var COOKIE_NOOP_SCRIPT = `(function(){var _jar={};Object.defineProperty(document,'cookie',{configurable:true,get:function(){return Object.keys(_jar).map(function(k){return k+'='+_jar[k];}).join('; ');},set:function(str){var parts=str.split(';');var kv=parts[0].trim();var eq=kv.indexOf('=');var name=kv.slice(0,eq).trim();var val=kv.slice(eq+1).trim();var maxAge=null;for(var i=1;i<parts.length;i++){var p=parts[i].trim().toLowerCase();if(p.indexOf('max-age=')===0){maxAge=parseInt(p.split('=')[1],10);}}if(maxAge!==null&&maxAge<=0){delete _jar[name];}else{_jar[name]=val;}}});})();`;
162
223
  function buildPreviewData(options) {
163
224
  const json = JSON.stringify({
@@ -170,6 +231,9 @@ function buildPreviewData(options) {
170
231
  });
171
232
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
172
233
  }
234
+ function previewCssTag(css) {
235
+ return css ? `<style data-allegro-preview-css>${escapeStyle(css)}</style>` : "";
236
+ }
173
237
  function standaloneShell(options) {
174
238
  const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
175
239
  return `<!DOCTYPE html>
@@ -184,6 +248,7 @@ function standaloneShell(options) {
184
248
  body { margin: 0; }
185
249
  [data-allegro-interaction] { display: block; }
186
250
  </style>
251
+ ${previewCssTag(options.previewCss)}
187
252
  </head>
188
253
  <body>
189
254
  <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
@@ -212,6 +277,7 @@ function articleShell(options) {
212
277
  p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
213
278
  [data-allegro-interaction] { display: block; margin: 24px 0; }
214
279
  </style>
280
+ ${previewCssTag(options.previewCss)}
215
281
  </head>
216
282
  <body>
217
283
  <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
@@ -249,7 +315,9 @@ function readTemplateMeta(dir) {
249
315
  }
250
316
  try {
251
317
  const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
252
- return { name: parsed.name ?? basename(dir) };
318
+ return {
319
+ name: typeof parsed.name === "string" && parsed.name.trim() !== "" ? parsed.name : basename(dir)
320
+ };
253
321
  } catch {
254
322
  return { name: basename(dir) };
255
323
  }
@@ -351,16 +419,28 @@ async function startServer(templatePath, port = 0, tenant = "") {
351
419
  });
352
420
  app.post("/api/template/create", express.json(), (req, res) => {
353
421
  const body = req.body;
354
- const slug = body.slug;
355
- if (typeof slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
356
- res.status(400).json({
357
- error: "Slug must be lowercase letters, numbers, and hyphens only (e.g. my-template)."
358
- });
422
+ const name = typeof body.name === "string" ? body.name.trim() : "";
423
+ if (name === "") {
424
+ res.status(400).json({ error: "Template name is required." });
425
+ return;
426
+ }
427
+ const slug = slugify(name);
428
+ if (slug === "") {
429
+ res.status(400).json({ error: "Template name must contain at least one letter or number." });
430
+ return;
431
+ }
432
+ const existing = scanTemplates(baseDir);
433
+ if (existing.some((t) => t.slug === slug)) {
434
+ res.status(400).json({ error: `A template with the slug "${slug}" already exists.` });
435
+ return;
436
+ }
437
+ if (existing.some((t) => t.name.toLowerCase() === name.toLowerCase())) {
438
+ res.status(400).json({ error: `A template named "${name}" already exists.` });
359
439
  return;
360
440
  }
361
441
  const targetDir = join(baseDir, slug);
362
442
  if (existsSync(targetDir)) {
363
- res.status(400).json({ error: `A template named "${slug}" already exists.` });
443
+ res.status(400).json({ error: `A template with the slug "${slug}" already exists.` });
364
444
  return;
365
445
  }
366
446
  try {
@@ -369,7 +449,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
369
449
  writeFileSync(join(targetDir, "index.css"), BOILERPLATE_CSS);
370
450
  writeFileSync(
371
451
  join(targetDir, "template.json"),
372
- JSON.stringify({ name: slug, fields: BOILERPLATE_FIELDS }, null, 2) + "\n"
452
+ JSON.stringify(
453
+ {
454
+ $schema: "https://docs.allegrocdp.com/template.schema.json",
455
+ name,
456
+ fields: BOILERPLATE_FIELDS
457
+ },
458
+ null,
459
+ 2
460
+ ) + "\n"
373
461
  );
374
462
  } catch (err) {
375
463
  console.error("[allegro-preview] Failed to create template:", err);
@@ -377,7 +465,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
377
465
  return;
378
466
  }
379
467
  rescan();
380
- res.status(201).json({ slug });
468
+ res.status(201).json({ slug, name });
381
469
  });
382
470
  app.get("/api/events", (req, res) => {
383
471
  res.setHeader("Content-Type", "text/event-stream");
@@ -389,7 +477,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
389
477
  sseClients.delete(res);
390
478
  });
391
479
  });
392
- app.get("/preview", (req, res) => {
480
+ app.get("/preview", async (req, res) => {
393
481
  const slug = typeof req.query.slug === "string" ? req.query.slug : "";
394
482
  const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
395
483
  const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
@@ -397,6 +485,14 @@ async function startServer(templatePath, port = 0, tenant = "") {
397
485
  res.status(400).send("Missing slug");
398
486
  return;
399
487
  }
488
+ if (sdkParam !== void 0) {
489
+ try {
490
+ new URL(sdkParam);
491
+ } catch {
492
+ res.status(400).send("Invalid sdk URL");
493
+ return;
494
+ }
495
+ }
400
496
  if (slug !== "." && !templates.some((t) => t.slug === slug)) {
401
497
  res.status(404).send("Template not found");
402
498
  return;
@@ -422,6 +518,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
422
518
  }
423
519
  const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
424
520
  const blockCookies = req.query.blockCookies !== "false";
521
+ const previewCss = effectiveSdkUrl ? await fetchPreviewCss(effectiveSdkUrl) : "";
425
522
  const shellOptions = {
426
523
  slug,
427
524
  html: templateFiles.html,
@@ -430,7 +527,8 @@ async function startServer(templatePath, port = 0, tenant = "") {
430
527
  fieldValues,
431
528
  tab,
432
529
  sdkUrl: effectiveSdkUrl,
433
- blockCookies
530
+ blockCookies,
531
+ previewCss
434
532
  };
435
533
  const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
436
534
  res.setHeader("Cache-Control", "no-store");
@@ -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 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();
@@ -283,6 +283,12 @@
283
283
  border-color: #f87171;
284
284
  }
285
285
 
286
+ .modal-hint {
287
+ margin-top: 6px;
288
+ font-size: 12px;
289
+ color: #94a3b8;
290
+ }
291
+
286
292
  .modal-error {
287
293
  font-size: 11px;
288
294
  color: #f87171;
@@ -1008,7 +1014,7 @@
1008
1014
  display: block;
1009
1015
  }
1010
1016
  </style>
1011
- <script type="module" crossorigin src="/assets/app-D1XS_DDX.js"></script>
1017
+ <script type="module" crossorigin src="/assets/app-B0ybFOKs.js"></script>
1012
1018
  </head>
1013
1019
  <body>
1014
1020
  <aside class="sidebar">
@@ -1250,15 +1256,16 @@
1250
1256
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
1251
1257
  <div class="modal-title" id="modal-title">New Template</div>
1252
1258
  <div class="modal-field">
1253
- <label class="modal-label" for="modal-slug-input">Template slug</label>
1259
+ <label class="modal-label" for="modal-name-input">Template name</label>
1254
1260
  <input
1255
1261
  class="modal-input"
1256
- id="modal-slug-input"
1262
+ id="modal-name-input"
1257
1263
  type="text"
1258
- placeholder="my-template"
1264
+ placeholder="My template"
1259
1265
  autocomplete="off"
1260
1266
  spellcheck="false"
1261
1267
  />
1268
+ <div class="modal-hint" id="modal-slug-hint"></div>
1262
1269
  <div class="modal-error" id="modal-error"></div>
1263
1270
  </div>
1264
1271
  <div class="modal-actions">
package/dist/server.js CHANGED
@@ -4,6 +4,8 @@
4
4
  import chokidar from "chokidar";
5
5
  import express from "express";
6
6
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { get as httpGet } from "http";
8
+ import { get as httpsGet } from "https";
7
9
  import open from "open";
8
10
  import { basename, isAbsolute, join, relative, resolve } from "path";
9
11
  import { fileURLToPath } from "url";
@@ -22,6 +24,11 @@ window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
22
24
  }
23
25
  };`;
24
26
 
27
+ // src/slugify.ts
28
+ function slugify(name) {
29
+ return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
30
+ }
31
+
25
32
  // src/server.ts
26
33
  var __dirname = fileURLToPath(new URL(".", import.meta.url));
27
34
  var BOILERPLATE_HTML = `<div class="signup">
@@ -122,7 +129,7 @@ var BOILERPLATE_FIELDS = [
122
129
  {
123
130
  slug: "subheadline",
124
131
  label: "Subheadline",
125
- type: "textarea",
132
+ type: "text",
126
133
  default_value: "Get the latest stories delivered to your inbox."
127
134
  },
128
135
  { slug: "input_placeholder", label: "Input placeholder", type: "text", default_value: "Enter your email" },
@@ -155,6 +162,60 @@ function normalizeTenantSdkUrl(raw) {
155
162
  const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
156
163
  return withProtocol.replace(/\/+$/, "") + "/client.js";
157
164
  }
165
+ var previewCssCache = /* @__PURE__ */ new Map();
166
+ var PREVIEW_CSS_TTL_MS = 6e4;
167
+ function escapeStyle(css) {
168
+ return css.replace(/<\/(style)/gi, "<\\/$1");
169
+ }
170
+ function isLocalHost(hostname) {
171
+ const name = hostname.toLowerCase();
172
+ return name === "localhost" || name.endsWith(".localhost") || name.endsWith(".test") || name === "127.0.0.1" || name === "::1";
173
+ }
174
+ function getJson(target) {
175
+ return new Promise((resolve2, reject) => {
176
+ const handleResponse = (res) => {
177
+ const status = res.statusCode ?? 0;
178
+ if (status < 200 || status >= 300) {
179
+ res.resume();
180
+ reject(new Error(`HTTP ${status}`));
181
+ return;
182
+ }
183
+ let body = "";
184
+ res.setEncoding("utf-8");
185
+ res.on("data", (chunk) => body += chunk);
186
+ res.on("end", () => {
187
+ try {
188
+ resolve2(JSON.parse(body));
189
+ } catch (err) {
190
+ reject(err);
191
+ }
192
+ });
193
+ };
194
+ const req = target.protocol === "https:" ? httpsGet(target, { rejectUnauthorized: !isLocalHost(target.hostname) }, handleResponse) : httpGet(target, handleResponse);
195
+ req.on("error", reject);
196
+ req.setTimeout(5e3, () => req.destroy(new Error("Request timed out")));
197
+ });
198
+ }
199
+ async function fetchPreviewCss(sdkUrl) {
200
+ let endpoint;
201
+ try {
202
+ endpoint = new URL("/api/preview-css", sdkUrl);
203
+ } catch {
204
+ return "";
205
+ }
206
+ const cached = previewCssCache.get(endpoint.origin);
207
+ if (cached && Date.now() - cached.fetchedAt < PREVIEW_CSS_TTL_MS) {
208
+ return cached.css;
209
+ }
210
+ try {
211
+ const data = await getJson(endpoint);
212
+ const css = typeof data.css === "string" ? data.css : "";
213
+ previewCssCache.set(endpoint.origin, { css, fetchedAt: Date.now() });
214
+ return css;
215
+ } catch {
216
+ return cached?.css ?? "";
217
+ }
218
+ }
158
219
  var COOKIE_NOOP_SCRIPT = `(function(){var _jar={};Object.defineProperty(document,'cookie',{configurable:true,get:function(){return Object.keys(_jar).map(function(k){return k+'='+_jar[k];}).join('; ');},set:function(str){var parts=str.split(';');var kv=parts[0].trim();var eq=kv.indexOf('=');var name=kv.slice(0,eq).trim();var val=kv.slice(eq+1).trim();var maxAge=null;for(var i=1;i<parts.length;i++){var p=parts[i].trim().toLowerCase();if(p.indexOf('max-age=')===0){maxAge=parseInt(p.split('=')[1],10);}}if(maxAge!==null&&maxAge<=0){delete _jar[name];}else{_jar[name]=val;}}});})();`;
159
220
  function buildPreviewData(options) {
160
221
  const json = JSON.stringify({
@@ -167,6 +228,9 @@ function buildPreviewData(options) {
167
228
  });
168
229
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
169
230
  }
231
+ function previewCssTag(css) {
232
+ return css ? `<style data-allegro-preview-css>${escapeStyle(css)}</style>` : "";
233
+ }
170
234
  function standaloneShell(options) {
171
235
  const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
172
236
  return `<!DOCTYPE html>
@@ -181,6 +245,7 @@ function standaloneShell(options) {
181
245
  body { margin: 0; }
182
246
  [data-allegro-interaction] { display: block; }
183
247
  </style>
248
+ ${previewCssTag(options.previewCss)}
184
249
  </head>
185
250
  <body>
186
251
  <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
@@ -209,6 +274,7 @@ function articleShell(options) {
209
274
  p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
210
275
  [data-allegro-interaction] { display: block; margin: 24px 0; }
211
276
  </style>
277
+ ${previewCssTag(options.previewCss)}
212
278
  </head>
213
279
  <body>
214
280
  <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
@@ -246,7 +312,9 @@ function readTemplateMeta(dir) {
246
312
  }
247
313
  try {
248
314
  const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
249
- return { name: parsed.name ?? basename(dir) };
315
+ return {
316
+ name: typeof parsed.name === "string" && parsed.name.trim() !== "" ? parsed.name : basename(dir)
317
+ };
250
318
  } catch {
251
319
  return { name: basename(dir) };
252
320
  }
@@ -348,16 +416,28 @@ async function startServer(templatePath, port = 0, tenant = "") {
348
416
  });
349
417
  app.post("/api/template/create", express.json(), (req, res) => {
350
418
  const body = req.body;
351
- const slug = body.slug;
352
- if (typeof slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
353
- res.status(400).json({
354
- error: "Slug must be lowercase letters, numbers, and hyphens only (e.g. my-template)."
355
- });
419
+ const name = typeof body.name === "string" ? body.name.trim() : "";
420
+ if (name === "") {
421
+ res.status(400).json({ error: "Template name is required." });
422
+ return;
423
+ }
424
+ const slug = slugify(name);
425
+ if (slug === "") {
426
+ res.status(400).json({ error: "Template name must contain at least one letter or number." });
427
+ return;
428
+ }
429
+ const existing = scanTemplates(baseDir);
430
+ if (existing.some((t) => t.slug === slug)) {
431
+ res.status(400).json({ error: `A template with the slug "${slug}" already exists.` });
432
+ return;
433
+ }
434
+ if (existing.some((t) => t.name.toLowerCase() === name.toLowerCase())) {
435
+ res.status(400).json({ error: `A template named "${name}" already exists.` });
356
436
  return;
357
437
  }
358
438
  const targetDir = join(baseDir, slug);
359
439
  if (existsSync(targetDir)) {
360
- res.status(400).json({ error: `A template named "${slug}" already exists.` });
440
+ res.status(400).json({ error: `A template with the slug "${slug}" already exists.` });
361
441
  return;
362
442
  }
363
443
  try {
@@ -366,7 +446,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
366
446
  writeFileSync(join(targetDir, "index.css"), BOILERPLATE_CSS);
367
447
  writeFileSync(
368
448
  join(targetDir, "template.json"),
369
- JSON.stringify({ name: slug, fields: BOILERPLATE_FIELDS }, null, 2) + "\n"
449
+ JSON.stringify(
450
+ {
451
+ $schema: "https://docs.allegrocdp.com/template.schema.json",
452
+ name,
453
+ fields: BOILERPLATE_FIELDS
454
+ },
455
+ null,
456
+ 2
457
+ ) + "\n"
370
458
  );
371
459
  } catch (err) {
372
460
  console.error("[allegro-preview] Failed to create template:", err);
@@ -374,7 +462,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
374
462
  return;
375
463
  }
376
464
  rescan();
377
- res.status(201).json({ slug });
465
+ res.status(201).json({ slug, name });
378
466
  });
379
467
  app.get("/api/events", (req, res) => {
380
468
  res.setHeader("Content-Type", "text/event-stream");
@@ -386,7 +474,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
386
474
  sseClients.delete(res);
387
475
  });
388
476
  });
389
- app.get("/preview", (req, res) => {
477
+ app.get("/preview", async (req, res) => {
390
478
  const slug = typeof req.query.slug === "string" ? req.query.slug : "";
391
479
  const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
392
480
  const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
@@ -394,6 +482,14 @@ async function startServer(templatePath, port = 0, tenant = "") {
394
482
  res.status(400).send("Missing slug");
395
483
  return;
396
484
  }
485
+ if (sdkParam !== void 0) {
486
+ try {
487
+ new URL(sdkParam);
488
+ } catch {
489
+ res.status(400).send("Invalid sdk URL");
490
+ return;
491
+ }
492
+ }
397
493
  if (slug !== "." && !templates.some((t) => t.slug === slug)) {
398
494
  res.status(404).send("Template not found");
399
495
  return;
@@ -419,6 +515,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
419
515
  }
420
516
  const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
421
517
  const blockCookies = req.query.blockCookies !== "false";
518
+ const previewCss = effectiveSdkUrl ? await fetchPreviewCss(effectiveSdkUrl) : "";
422
519
  const shellOptions = {
423
520
  slug,
424
521
  html: templateFiles.html,
@@ -427,7 +524,8 @@ async function startServer(templatePath, port = 0, tenant = "") {
427
524
  fieldValues,
428
525
  tab,
429
526
  sdkUrl: effectiveSdkUrl,
430
- blockCookies
527
+ blockCookies,
528
+ previewCss
431
529
  };
432
530
  const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
433
531
  res.setHeader("Cache-Control", "no-store");
@@ -522,5 +620,6 @@ allegro-preview \u2192 ${url}
522
620
  void open(url);
523
621
  }
524
622
  export {
623
+ escapeStyle,
525
624
  startServer
526
625
  };
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.11",
4
+ "version": "0.1.0-dev.13",
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)}})();const te={full:0,desktop:1280,tablet:768,mobile:390};function ne(e){return te[e]}let u=[],d=null,N="standalone",y="full",L=null,E={},H,F=!0,T=null,f=null,h=null;const q=document.getElementById("template-nav"),g=document.getElementById("field-list"),m=document.getElementById("preview-area"),$=document.getElementById("tab-group"),M=document.getElementById("device-group"),j=document.getElementById("toolbar-badge"),le=document.getElementById("theme-toggle"),oe=document.getElementById("theme-icon-sun"),ae=document.getElementById("theme-icon-moon"),B=document.getElementById("tenant-input"),x=document.getElementById("tenant-status"),se=document.getElementById("new-template-btn"),J=document.getElementById("new-window-btn"),k=document.getElementById("create-modal-backdrop"),c=document.getElementById("modal-slug-input"),v=document.getElementById("modal-error"),ie=document.getElementById("modal-cancel-btn"),w=document.getElementById("modal-create-btn"),P=document.getElementById("block-cookies-checkbox"),V=document.getElementById("cookie-auth-area"),S=document.getElementById("cookie-auth-user"),R=document.getElementById("cookie-auth-hint"),ce=document.getElementById("cookie-clear-btn"),b=document.getElementById("jwt-modal-backdrop"),re=document.getElementById("jwt-payload-content"),de=document.getElementById("jwt-modal-close-btn");function W(e){document.documentElement.setAttribute("data-theme",e?"dark":"light"),oe.style.display=e?"none":"",ae.style.display=e?"":"none"}function ue(){const e=localStorage.getItem("allegro-preview-theme"),t=window.matchMedia("(prefers-color-scheme: dark)").matches;W(e?e==="dark":t)}le.addEventListener("click",()=>{const e=document.documentElement.getAttribute("data-theme")!=="dark";W(e),localStorage.setItem("allegro-preview-theme",e?"dark":"light")});function me(e){const t=e.trim();return t?(/^https?:\/\//.test(t)?t:`https://${t}`).replace(/\/+$/,"")+"/client.js":""}function G(e){const t=me(e);H=t||void 0,t?(x.className="tenant-active-badge",x.textContent=t):(x.className="tenant-hint",x.textContent="Loads /client.js on the preview page"),L&&p()}B.addEventListener("input",()=>{localStorage.setItem("allegro-preview-tenant",B.value),G(B.value)});function pe(e=""){const t=localStorage.getItem("allegro-preview-tenant"),n=e||t||"";n&&(B.value=n,e&&localStorage.setItem("allegro-preview-tenant",n),G(n))}const fe=[{name:"_ALLEGROT",label:"Session JWT",isJwt:!0},{name:"_ALLEGROD",label:"Device ID"},{name:"_ALLEGROS",label:"Session ID"}];function ge(e){const t=document.cookie.split("; ").find(n=>n.startsWith(e+"="));if(!t)return null;try{return decodeURIComponent(t.split("=").slice(1).join("="))}catch{return t.split("=").slice(1).join("=")}}function ve(e){try{const t=e.split(".");if(t.length!==3)return null;const n=t[1].replace(/-/g,"+").replace(/_/g,"/"),l=atob(n);return JSON.parse(l)}catch{return null}}function C(){const e=fe.map(t=>({...t,value:ge(t.name)})).filter(t=>t.value!==null);if(S.innerHTML="",e.length===0){S.style.display="none",R.style.display="";return}R.style.display="none",S.style.display="";for(const t of e){const n=document.createElement("div");n.className="cookie-row";const l=document.createElement("div");l.className="cookie-auth-dot";const a=document.createElement("div");a.className="cookie-row-info";const s=document.createElement("div");s.className="cookie-row-label",s.textContent=t.label;const o=document.createElement("div");if(o.className="cookie-row-value",o.textContent=t.value.length>24?t.value.slice(0,24)+"…":t.value,a.appendChild(s),a.appendChild(o),n.appendChild(l),n.appendChild(a),t.isJwt){const i=document.createElement("button");i.className="cookie-view-btn",i.textContent="Decode",i.onclick=()=>{const r=ve(t.value);re.textContent=r?JSON.stringify(r,null,2):t.value,b.classList.remove("hidden")},n.appendChild(i)}S.appendChild(n)}}function he(e){F=e,localStorage.setItem("allegro-preview-block-cookies",e?"true":"false"),V.style.display=e?"none":"",e?h&&(clearInterval(h),h=null):(C(),h||(h=setInterval(C,2e3))),L&&p()}function ye(){const t=localStorage.getItem("allegro-preview-block-cookies")!=="false";P.checked=t,F=t,V.style.display=t?"none":"",t||(C(),h=setInterval(C,2e3))}P.addEventListener("change",()=>{he(P.checked)});ce.addEventListener("click",()=>{const e=document.cookie.split("; ");for(const t of e){const n=t.split("=")[0];document.cookie=`${n}=; max-age=0; path=/`}C()});de.addEventListener("click",()=>{b.classList.add("hidden")});b.addEventListener("click",e=>{e.target===b&&b.classList.add("hidden")});function Ee(){const e=window.location.hash.slice(1);return e?decodeURIComponent(e):null}function we(e){history.replaceState(null,"",`#${encodeURIComponent(e)}`)}function be(){history.replaceState(null,"",window.location.pathname)}function z(e){$.querySelectorAll(".tab-btn").forEach(t=>{t.disabled=!e}),M.querySelectorAll(".device-btn").forEach(t=>{t.disabled=!e}),J.disabled=!e}$.addEventListener("click",e=>{const t=e.target.closest(".tab-btn");!t||t.disabled||(N=t.dataset.tab,$.querySelectorAll(".tab-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-tab",N),p())});M.addEventListener("click",e=>{const t=e.target.closest(".device-btn");!t||t.disabled||(y=t.dataset.device,M.querySelectorAll(".device-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-device",y),p())});function I(){q.innerHTML="";for(const e of u){const t=document.createElement("button");t.className="template-nav-item"+(e.slug===d?" active":""),t.innerHTML=`<span class="template-nav-dot"></span><span class="template-nav-name">${_(e.name)}</span>`,t.addEventListener("click",()=>void D(e.slug)),q.appendChild(t)}}async function D(e){d=e,we(e),I(),await U(e)}function K(){L=null,d=null,T=null,f=null,be(),I(),z(!1),j.textContent="No template selected",j.style.opacity="0.4",g.innerHTML='<p class="no-fields">Select a template to edit fields.</p>';const e=document.createElement("div");if(e.className="selector-screen",u.length===0){e.innerHTML=`
2
- <div class="selector-empty">
3
- <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
4
- <span>No templates found in this directory.</span>
5
- </div>`,m.innerHTML="",m.appendChild(e);return}e.innerHTML=`
6
- <div class="selector-eyebrow">Allegro Preview</div>
7
- <h2 class="selector-heading">Choose a template</h2>
8
- <p class="selector-sub">Select a template to start previewing.</p>
9
- <div class="selector-grid" id="selector-grid"></div>`,m.innerHTML="",m.appendChild(e);const t=e.querySelector("#selector-grid");for(const n of u){const l=document.createElement("button");l.className="selector-card",l.innerHTML=`
10
- <div class="selector-card-icon">
11
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
12
- </div>
13
- <div class="selector-card-name">${_(n.name)}</div>
14
- <div class="selector-card-slug">${_(n.slug)}</div>`,l.addEventListener("click",()=>void D(n.slug)),t.appendChild(l)}}const O=["text","number","checkbox","textarea"];function Y(e){if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.slug=="string"&&t.slug.length>0&&O.includes(t.type)}function ke(e){if(typeof e!="object"||e===null)return`Invalid field: expected an object, got ${JSON.stringify(e)}.`;const t=e,n=[];if((typeof t.slug!="string"||t.slug.length===0)&&n.push('missing required "slug" property'),!O.includes(t.type)){const a=t.type!==void 0?`"${String(t.type)}"`:"none";n.push(`invalid type ${a} — valid types: ${O.join(", ")}`)}return`${typeof t.slug=="string"&&t.slug.length>0?`Field "${t.slug}"`:"Field"}: ${n.join("; ")}.`}function Ce(e){E={};for(const t of e)Y(t)&&(E[t.slug]=t.default_value??"")}function Le(e,t){if(g.innerHTML="",t){const n=document.createElement("div");n.className="field-error",n.textContent=`template.json parse error: ${t}`,g.appendChild(n);return}if(e.length===0){g.innerHTML='<p class="no-fields">No fields in template.json.</p>';return}for(const n of e){if(!Y(n)){const o=document.createElement("div");o.className="field-error",o.textContent=ke(n),g.appendChild(o);continue}const l=n;if(l.type==="checkbox"){const o=document.createElement("div");o.className="field-checkbox-row";const i=document.createElement("input");i.type="checkbox",i.id=`field-${l.slug}`,i.checked=l.default_value==="true"||l.default_value==="1",i.addEventListener("change",()=>{E[l.slug]=i.checked?"true":"false",p()});const r=document.createElement("label");r.className="field-label",r.htmlFor=`field-${l.slug}`,r.textContent=l.label||l.slug,o.appendChild(i),o.appendChild(r),g.appendChild(o);continue}const a=document.createElement("div");a.className="field-group";const s=document.createElement("label");if(s.className="field-label",s.htmlFor=`field-${l.slug}`,s.textContent=l.label||l.slug,a.appendChild(s),l.type==="textarea"){const o=document.createElement("textarea");o.className="field-input",o.id=`field-${l.slug}`,o.rows=3,o.value=l.default_value??"",o.addEventListener("input",()=>{E[l.slug]=o.value,p()}),a.appendChild(o)}else{const o=document.createElement("input");o.type=l.type==="number"?"number":"text",o.className="field-input",o.id=`field-${l.slug}`,o.value=l.default_value??"",o.addEventListener("input",()=>{E[l.slug]=o.value,p()}),a.appendChild(o)}g.appendChild(a)}}function Ie(){const e=new URLSearchParams;e.set("slug",d),e.set("tab",N),H&&e.set("sdk",H),F||e.set("blockCookies","false");for(const[t,n]of Object.entries(E))e.set(`f_${t}`,n);return`/preview?${e.toString()}`}function p(){if(!L||!d)return;const e=Ie();T=e;const t=ne(y),n=t===0;if(f){const i=f.parentElement,r=i.parentElement,ee=r.parentElement;ee.className=n?"preview-stage":"preview-stage preview-stage--breakpoint",r.style.width=n?"100%":"",i.style.width=n?"100%":`${t}px`,f.style.width=n?"100%":`${t}px`,f.style.maxHeight=y==="mobile"?"812px":"",f.src=e;return}m.innerHTML="";const l=document.createElement("div");l.className=n?"preview-stage":"preview-stage preview-stage--breakpoint";const a=document.createElement("div");a.className="preview-scaler";const s=document.createElement("div");s.className="preview-frame-wrapper";const o=document.createElement("iframe");o.style.height="calc(100vh - 48px)",n?(a.style.width="100%",s.style.width="100%",o.style.width="100%"):(s.style.width=`${t}px`,o.style.width=`${t}px`,y==="mobile"&&(o.style.maxHeight="812px")),o.src=e,s.appendChild(o),a.appendChild(s),l.appendChild(a),m.appendChild(l),f=o}async function U(e){try{const t=await fetch(`/api/template?slug=${encodeURIComponent(e)}`);if(!t.ok)throw new Error(`HTTP ${t.status}`);const n=await t.json();L=n,j.textContent=`Template: ${n.name}`,j.style.opacity="1",z(!0),Ce(n.fields),Le(n.fields,n.fieldsParseError),p()}catch(t){console.error("[allegro-preview] Failed to load template:",t),m.innerHTML="";const n=document.createElement("div");n.style.padding="40px",n.style.color="var(--text-muted)",n.style.fontSize="13px",n.textContent=`Failed to load template "${e}".`,m.appendChild(n)}}async function xe(){const t=await(await fetch("/api/config")).json();u=t.templates,pe(t.tenant??"");const n=Ee(),l=n&&u.find(a=>a.slug===n)?n:null;l?(d=l,I(),await U(l)):K()}function Se(){const e=localStorage.getItem("allegro-preview-tab"),t=localStorage.getItem("allegro-preview-device");e&&["standalone","append","cut"].includes(e)&&(N=e,$.querySelectorAll(".tab-btn").forEach(n=>{n.classList.toggle("active",n.dataset.tab===e)})),t&&["full","desktop","tablet","mobile"].includes(t)&&(y=t,M.querySelectorAll(".device-btn").forEach(n=>{n.classList.toggle("active",n.dataset.device===t)}))}function Q(){const e=new EventSource("/api/events");e.addEventListener("reload",()=>{d&&U(d)}),e.addEventListener("config",()=>{Be()}),e.onerror=()=>{e.close(),setTimeout(Q,2e3)}}async function Be(){u=(await(await fetch("/api/config")).json()).templates,I(),d&&!u.find(n=>n.slug===d)&&K()}function _(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const Ne=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;function Te(){c.value="",v.textContent="",c.classList.remove("invalid"),w.disabled=!1,k.classList.remove("hidden"),c.focus()}function A(){k.classList.add("hidden")}function X(e){return e?Ne.test(e)?null:"Use lowercase letters, numbers, and hyphens only (e.g. my-template).":"Slug is required."}J.addEventListener("click",()=>{T&&window.open(T,"_blank")});se.addEventListener("click",Te);ie.addEventListener("click",A);k.addEventListener("click",e=>{e.target===k&&A()});document.addEventListener("keydown",e=>{e.key==="Escape"&&!k.classList.contains("hidden")&&A()});c.addEventListener("input",()=>{const e=X(c.value);e?(c.classList.add("invalid"),v.textContent=e):(c.classList.remove("invalid"),v.textContent="")});c.addEventListener("keydown",e=>{e.key==="Enter"&&Z()});w.addEventListener("click",()=>void Z());async function Z(){const e=c.value.trim(),t=X(e);if(t){c.classList.add("invalid"),v.textContent=t,c.focus();return}w.disabled=!0,v.textContent="";try{const n=await fetch("/api/template/create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:e})}),l=await n.json();if(!n.ok){v.textContent=l.error??"Failed to create template.",w.disabled=!1;return}A(),u=[...u,{slug:e,name:e}],I(),await D(e)}catch{v.textContent="Network error. Please try again.",w.disabled=!1}}ue();Se();ye();xe();Q();