@allegrocdp/preview 0.1.0-dev.12 → 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
@@ -27,6 +27,11 @@ window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
27
27
  }
28
28
  };`;
29
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
+
30
35
  // src/server.ts
31
36
  var __dirname = fileURLToPath(new URL(".", import.meta.url));
32
37
  var BOILERPLATE_HTML = `<div class="signup">
@@ -127,7 +132,7 @@ var BOILERPLATE_FIELDS = [
127
132
  {
128
133
  slug: "subheadline",
129
134
  label: "Subheadline",
130
- type: "textarea",
135
+ type: "text",
131
136
  default_value: "Get the latest stories delivered to your inbox."
132
137
  },
133
138
  { slug: "input_placeholder", label: "Input placeholder", type: "text", default_value: "Enter your email" },
@@ -310,7 +315,9 @@ function readTemplateMeta(dir) {
310
315
  }
311
316
  try {
312
317
  const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
313
- return { name: parsed.name ?? basename(dir) };
318
+ return {
319
+ name: typeof parsed.name === "string" && parsed.name.trim() !== "" ? parsed.name : basename(dir)
320
+ };
314
321
  } catch {
315
322
  return { name: basename(dir) };
316
323
  }
@@ -412,16 +419,28 @@ async function startServer(templatePath, port = 0, tenant = "") {
412
419
  });
413
420
  app.post("/api/template/create", express.json(), (req, res) => {
414
421
  const body = req.body;
415
- const slug = body.slug;
416
- if (typeof slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
417
- res.status(400).json({
418
- error: "Slug must be lowercase letters, numbers, and hyphens only (e.g. my-template)."
419
- });
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.` });
420
439
  return;
421
440
  }
422
441
  const targetDir = join(baseDir, slug);
423
442
  if (existsSync(targetDir)) {
424
- 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.` });
425
444
  return;
426
445
  }
427
446
  try {
@@ -430,7 +449,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
430
449
  writeFileSync(join(targetDir, "index.css"), BOILERPLATE_CSS);
431
450
  writeFileSync(
432
451
  join(targetDir, "template.json"),
433
- 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"
434
461
  );
435
462
  } catch (err) {
436
463
  console.error("[allegro-preview] Failed to create template:", err);
@@ -438,7 +465,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
438
465
  return;
439
466
  }
440
467
  rescan();
441
- res.status(201).json({ slug });
468
+ res.status(201).json({ slug, name });
442
469
  });
443
470
  app.get("/api/events", (req, res) => {
444
471
  res.setHeader("Content-Type", "text/event-stream");
@@ -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
@@ -24,6 +24,11 @@ window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
24
24
  }
25
25
  };`;
26
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
+
27
32
  // src/server.ts
28
33
  var __dirname = fileURLToPath(new URL(".", import.meta.url));
29
34
  var BOILERPLATE_HTML = `<div class="signup">
@@ -124,7 +129,7 @@ var BOILERPLATE_FIELDS = [
124
129
  {
125
130
  slug: "subheadline",
126
131
  label: "Subheadline",
127
- type: "textarea",
132
+ type: "text",
128
133
  default_value: "Get the latest stories delivered to your inbox."
129
134
  },
130
135
  { slug: "input_placeholder", label: "Input placeholder", type: "text", default_value: "Enter your email" },
@@ -307,7 +312,9 @@ function readTemplateMeta(dir) {
307
312
  }
308
313
  try {
309
314
  const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
310
- return { name: parsed.name ?? basename(dir) };
315
+ return {
316
+ name: typeof parsed.name === "string" && parsed.name.trim() !== "" ? parsed.name : basename(dir)
317
+ };
311
318
  } catch {
312
319
  return { name: basename(dir) };
313
320
  }
@@ -409,16 +416,28 @@ async function startServer(templatePath, port = 0, tenant = "") {
409
416
  });
410
417
  app.post("/api/template/create", express.json(), (req, res) => {
411
418
  const body = req.body;
412
- const slug = body.slug;
413
- if (typeof slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
414
- res.status(400).json({
415
- error: "Slug must be lowercase letters, numbers, and hyphens only (e.g. my-template)."
416
- });
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.` });
417
436
  return;
418
437
  }
419
438
  const targetDir = join(baseDir, slug);
420
439
  if (existsSync(targetDir)) {
421
- 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.` });
422
441
  return;
423
442
  }
424
443
  try {
@@ -427,7 +446,15 @@ async function startServer(templatePath, port = 0, tenant = "") {
427
446
  writeFileSync(join(targetDir, "index.css"), BOILERPLATE_CSS);
428
447
  writeFileSync(
429
448
  join(targetDir, "template.json"),
430
- 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"
431
458
  );
432
459
  } catch (err) {
433
460
  console.error("[allegro-preview] Failed to create template:", err);
@@ -435,7 +462,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
435
462
  return;
436
463
  }
437
464
  rescan();
438
- res.status(201).json({ slug });
465
+ res.status(201).json({ slug, name });
439
466
  });
440
467
  app.get("/api/events", (req, res) => {
441
468
  res.setHeader("Content-Type", "text/event-stream");
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.12",
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();