@allegrocdp/preview 0.1.0-dev.6 → 0.1.0-dev.7

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
@@ -116,6 +116,101 @@ var BOILERPLATE_FIELDS = [
116
116
  { slug: "button_text", label: "Button text", type: "text", default_value: "Subscribe" },
117
117
  { slug: "privacy_note", label: "Privacy note", type: "text", default_value: "No spam. Unsubscribe anytime." }
118
118
  ];
119
+ var ARTICLE_PARAGRAPHS = [
120
+ "The city council voted unanimously Tuesday to approve a sweeping new infrastructure plan that would reshape public transit routes across the metropolitan area over the next decade.",
121
+ "Mayor Jordan Ellison called the decision a turning point for how the region thinks about getting around, noting that similar investments in peer cities have led to measurable reductions in traffic congestion and improved air quality.",
122
+ "The plan includes upgrades to 14 existing bus rapid transit lines, the introduction of three new light-rail corridors connecting the downtown core to outlying neighborhoods, and dedicated cycling infrastructure along six major arterials.",
123
+ "Critics, however, warn that the projected cost of $2.4 billion may be optimistic. Independent analysts suggest the final figure could climb to $3.8 billion once land acquisition, labor, and contingency costs are factored in over the full construction timeline.",
124
+ "Council member Dana Reyes, who voted in favor, acknowledged the fiscal concern but argued that inaction carries its own price, noting that congestion costs residents and businesses hundreds of millions in lost productivity each year.",
125
+ "Environmental groups praised the announcement, saying expanded transit capacity is essential to meet the stated goal of reducing transportation emissions by 45 percent before 2035.",
126
+ "Public comment sessions are scheduled for next month at community centers in all twelve districts. Construction is expected to begin no sooner than mid-next year, pending environmental review and final design approvals."
127
+ ];
128
+ function escapeAttr(s) {
129
+ return s.replace(/&/g, "&").replace(/"/g, """).replace(/>/g, ">");
130
+ }
131
+ function sdkScriptTag(sdkUrl) {
132
+ if (!sdkUrl) {
133
+ return "";
134
+ }
135
+ return `<script src="${escapeAttr(sdkUrl)}" data-api-url="https://${new URL(sdkUrl).host}"></script>`;
136
+ }
137
+ function normalizeTenantSdkUrl(raw) {
138
+ const trimmed = raw.trim();
139
+ if (!trimmed) {
140
+ return "";
141
+ }
142
+ const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
143
+ return withProtocol.replace(/\/+$/, "") + "/client.js";
144
+ }
145
+ 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;}}});})();`;
146
+ function buildPreviewData(options) {
147
+ const json = JSON.stringify({
148
+ slug: options.slug,
149
+ html: options.html,
150
+ css: options.css,
151
+ js: options.js,
152
+ fieldValues: options.fieldValues,
153
+ tab: options.tab
154
+ });
155
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
156
+ }
157
+ function standaloneShell(options) {
158
+ const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
159
+ return `<!DOCTYPE html>
160
+ <html>
161
+ <head>
162
+ <meta charset="UTF-8">
163
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
164
+ <script>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</script>
165
+ ${cookieScript}
166
+ <style>
167
+ * { box-sizing: border-box; }
168
+ body { margin: 0; }
169
+ [data-allegro-interaction] { display: block; }
170
+ </style>
171
+ </head>
172
+ <body>
173
+ <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
174
+ ${sdkScriptTag(options.sdkUrl)}
175
+ <script src="/preview-client.js"></script>
176
+ </body>
177
+ </html>`;
178
+ }
179
+ function articleShell(options) {
180
+ const paras = ARTICLE_PARAGRAPHS.map((p) => `<p>${p}</p>`).join("\n ");
181
+ const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
182
+ return `<!DOCTYPE html>
183
+ <html>
184
+ <head>
185
+ <meta charset="UTF-8">
186
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
187
+ <script>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</script>
188
+ ${cookieScript}
189
+ <style>
190
+ *, *::before, *::after { box-sizing: border-box; }
191
+ body { font-family: Georgia, 'Times New Roman', serif; background: #fff; color: #111;
192
+ max-width: 680px; margin: 0 auto; padding: 32px 24px 64px; line-height: 1.75;
193
+ -webkit-font-smoothing: antialiased; }
194
+ h1 { font-size: 1.6rem; line-height: 1.25; margin: 0 0 6px; font-weight: 700; }
195
+ .meta { color: #6b7280; font-size: 0.82rem; font-family: system-ui, sans-serif; margin-bottom: 24px; }
196
+ p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
197
+ [data-allegro-interaction] { display: block; margin: 24px 0; }
198
+ </style>
199
+ </head>
200
+ <body>
201
+ <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
202
+ <header>
203
+ <h1>Council Approves Decade-Long Transit Overhaul</h1>
204
+ <p class="meta">By Staff Reporter &nbsp;&middot;&nbsp; March 14, 2026</p>
205
+ </header>
206
+ <article id="allegro-preview-article">
207
+ ${paras}
208
+ </article>
209
+ ${sdkScriptTag(options.sdkUrl)}
210
+ <script src="/preview-client.js"></script>
211
+ </body>
212
+ </html>`;
213
+ }
119
214
  var sseClients = /* @__PURE__ */ new Set();
120
215
  function broadcast(event) {
121
216
  for (const client of sseClients) {
@@ -168,6 +263,11 @@ function scanTemplates(baseDir) {
168
263
  }
169
264
  return results;
170
265
  }
266
+ var templateFilesCache = /* @__PURE__ */ new Map();
267
+ function invalidateTemplateCache(filePath) {
268
+ const dir = filePath.replace(/[/\\][^/\\]+$/, "");
269
+ templateFilesCache.delete(dir);
270
+ }
171
271
  function readTemplateFiles(dir) {
172
272
  const read = (filename) => {
173
273
  const filePath = join(dir, filename);
@@ -186,7 +286,7 @@ function readTemplateFiles(dir) {
186
286
  fieldsParseError = err instanceof Error ? err.message : "Invalid JSON in template.json";
187
287
  }
188
288
  }
189
- return {
289
+ const files = {
190
290
  html: read("index.html"),
191
291
  css: read("index.css"),
192
292
  js: read("index.js"),
@@ -194,6 +294,11 @@ function readTemplateFiles(dir) {
194
294
  name,
195
295
  fieldsParseError
196
296
  };
297
+ templateFilesCache.set(dir, files);
298
+ return files;
299
+ }
300
+ function readTemplateFilesCached(dir) {
301
+ return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
197
302
  }
198
303
  async function startServer(templatePath, port = 0, tenant = "") {
199
304
  const baseDir = resolve(process.cwd(), templatePath);
@@ -226,7 +331,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
226
331
  res.status(404).json({ error: "Template not found" });
227
332
  return;
228
333
  }
229
- res.json(readTemplateFiles(resolvedDir));
334
+ res.json(readTemplateFilesCached(resolvedDir));
230
335
  });
231
336
  app.post("/api/template/create", express.json(), (req, res) => {
232
337
  const body = req.body;
@@ -268,6 +373,53 @@ async function startServer(templatePath, port = 0, tenant = "") {
268
373
  sseClients.delete(res);
269
374
  });
270
375
  });
376
+ app.get("/preview", (req, res) => {
377
+ const slug = typeof req.query.slug === "string" ? req.query.slug : "";
378
+ const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
379
+ const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
380
+ if (!slug) {
381
+ res.status(400).send("Missing slug");
382
+ return;
383
+ }
384
+ if (slug !== "." && !templates.some((t) => t.slug === slug)) {
385
+ res.status(404).send("Template not found");
386
+ return;
387
+ }
388
+ const targetDir = slug === "." ? baseDir : join(baseDir, slug);
389
+ const resolvedBaseDir = resolve(baseDir);
390
+ const resolvedDir = resolve(targetDir);
391
+ const relativePath = relative(resolvedBaseDir, resolvedDir);
392
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
393
+ res.status(400).send("Invalid template path");
394
+ return;
395
+ }
396
+ if (!existsSync(resolvedDir) || !isTemplateDir(resolvedDir)) {
397
+ res.status(404).send("Template not found");
398
+ return;
399
+ }
400
+ const templateFiles = readTemplateFilesCached(resolvedDir);
401
+ const fieldValues = [];
402
+ for (const [key, val] of Object.entries(req.query)) {
403
+ if (key.startsWith("f_") && typeof val === "string") {
404
+ fieldValues.push({ slug: key.slice(2), value: val });
405
+ }
406
+ }
407
+ const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
408
+ const blockCookies = req.query.blockCookies !== "false";
409
+ const shellOptions = {
410
+ slug,
411
+ html: templateFiles.html,
412
+ css: templateFiles.css,
413
+ js: templateFiles.js,
414
+ fieldValues,
415
+ tab,
416
+ sdkUrl: effectiveSdkUrl,
417
+ blockCookies
418
+ };
419
+ const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
420
+ res.setHeader("Cache-Control", "no-store");
421
+ res.type("html").send(rendered);
422
+ });
271
423
  const indexHtmlPath = join(clientDistDir, "index.html");
272
424
  app.get("/*path", (_req, res) => {
273
425
  if (existsSync(indexHtmlPath)) {
@@ -288,10 +440,12 @@ async function startServer(templatePath, port = 0, tenant = "") {
288
440
  );
289
441
  fileWatcher.on("change", (changedPath) => {
290
442
  console.log(`Changed: ${changedPath}`);
443
+ invalidateTemplateCache(changedPath);
291
444
  broadcastReload();
292
445
  });
293
446
  fileWatcher.on("add", (addedPath) => {
294
447
  console.log(`Added: ${addedPath}`);
448
+ invalidateTemplateCache(addedPath);
295
449
  if (basename(addedPath) === "index.html") {
296
450
  rescan();
297
451
  }
@@ -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)}})();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();
@@ -457,6 +457,195 @@
457
457
  background: #4ade80;
458
458
  }
459
459
 
460
+ /* Cookies section */
461
+
462
+ .cookie-section-wrap {
463
+ padding: 2px 10px 10px;
464
+ }
465
+
466
+ .cookie-checkbox-row {
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 8px;
470
+ margin-bottom: 6px;
471
+ }
472
+
473
+ .cookie-checkbox-row input[type='checkbox'] {
474
+ width: 14px;
475
+ height: 14px;
476
+ accent-color: var(--sb-accent);
477
+ flex-shrink: 0;
478
+ cursor: pointer;
479
+ }
480
+
481
+ .cookie-checkbox-label {
482
+ font-size: 12px;
483
+ color: var(--sb-text);
484
+ cursor: pointer;
485
+ }
486
+
487
+ .cookie-auth-area {
488
+ margin-top: 6px;
489
+ }
490
+
491
+ .cookie-auth-hint {
492
+ font-size: 10px;
493
+ color: var(--sb-label);
494
+ padding: 0 2px;
495
+ }
496
+
497
+ .cookie-list {
498
+ display: flex;
499
+ flex-direction: column;
500
+ gap: 4px;
501
+ margin-bottom: 5px;
502
+ }
503
+
504
+ .cookie-row {
505
+ display: flex;
506
+ align-items: center;
507
+ gap: 6px;
508
+ padding: 5px 7px;
509
+ background: rgba(74, 222, 128, 0.08);
510
+ border: 1px solid rgba(74, 222, 128, 0.2);
511
+ border-radius: 5px;
512
+ }
513
+
514
+ .cookie-auth-dot {
515
+ width: 5px;
516
+ height: 5px;
517
+ border-radius: 50%;
518
+ background: #4ade80;
519
+ flex-shrink: 0;
520
+ }
521
+
522
+ .cookie-row-info {
523
+ flex: 1;
524
+ min-width: 0;
525
+ }
526
+
527
+ .cookie-row-label {
528
+ font-size: 10px;
529
+ font-weight: 600;
530
+ color: var(--sb-label);
531
+ text-transform: uppercase;
532
+ letter-spacing: 0.05em;
533
+ }
534
+
535
+ .cookie-row-value {
536
+ font-size: 11px;
537
+ color: var(--sb-text-active);
538
+ font-family: 'SF Mono', 'Fira Code', monospace;
539
+ overflow: hidden;
540
+ text-overflow: ellipsis;
541
+ white-space: nowrap;
542
+ }
543
+
544
+ .cookie-view-btn {
545
+ background: none;
546
+ border: 1px solid var(--sb-border);
547
+ border-radius: 4px;
548
+ padding: 2px 6px;
549
+ font-size: 10px;
550
+ font-family: inherit;
551
+ color: var(--sb-text);
552
+ cursor: pointer;
553
+ white-space: nowrap;
554
+ transition:
555
+ color 0.12s,
556
+ border-color 0.12s;
557
+ }
558
+
559
+ .cookie-view-btn:hover {
560
+ color: var(--sb-text-active);
561
+ border-color: var(--sb-accent);
562
+ }
563
+
564
+ .cookie-clear-btn {
565
+ display: flex;
566
+ align-items: center;
567
+ gap: 4px;
568
+ background: none;
569
+ border: 1px solid var(--sb-border);
570
+ border-radius: 5px;
571
+ padding: 4px 8px;
572
+ font-size: 11px;
573
+ font-family: inherit;
574
+ color: var(--sb-text);
575
+ cursor: pointer;
576
+ width: 100%;
577
+ justify-content: center;
578
+ margin-top: 4px;
579
+ transition:
580
+ color 0.12s,
581
+ border-color 0.12s,
582
+ background 0.12s;
583
+ }
584
+
585
+ .cookie-clear-btn:hover {
586
+ color: #f87171;
587
+ border-color: rgba(248, 113, 113, 0.4);
588
+ background: rgba(248, 113, 113, 0.05);
589
+ }
590
+
591
+ /* JWT payload modal */
592
+
593
+ .jwt-modal {
594
+ background: var(--sb-bg);
595
+ border: 1px solid rgba(255, 255, 255, 0.12);
596
+ border-radius: 10px;
597
+ padding: 20px;
598
+ width: 480px;
599
+ max-width: 90vw;
600
+ max-height: 80vh;
601
+ display: flex;
602
+ flex-direction: column;
603
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
604
+ }
605
+
606
+ .jwt-modal-header {
607
+ display: flex;
608
+ align-items: center;
609
+ justify-content: space-between;
610
+ margin-bottom: 12px;
611
+ }
612
+
613
+ .jwt-modal-title {
614
+ font-size: 13px;
615
+ font-weight: 600;
616
+ color: var(--sb-text-active);
617
+ }
618
+
619
+ .jwt-modal-close {
620
+ background: none;
621
+ border: 1px solid var(--sb-border);
622
+ border-radius: 5px;
623
+ padding: 3px 7px;
624
+ font-size: 12px;
625
+ font-family: inherit;
626
+ color: var(--sb-text);
627
+ cursor: pointer;
628
+ transition: color 0.12s;
629
+ }
630
+
631
+ .jwt-modal-close:hover {
632
+ color: var(--sb-text-active);
633
+ }
634
+
635
+ .jwt-payload {
636
+ flex: 1;
637
+ overflow-y: auto;
638
+ background: var(--sb-input-bg);
639
+ border: 1px solid var(--sb-input-border);
640
+ border-radius: 6px;
641
+ padding: 10px 12px;
642
+ font-size: 11px;
643
+ font-family: 'SF Mono', 'Fira Code', monospace;
644
+ color: var(--sb-text-active);
645
+ white-space: pre;
646
+ line-height: 1.6;
647
+ }
648
+
460
649
  /* Fields section */
461
650
 
462
651
  .fields-scroll {
@@ -819,7 +1008,7 @@
819
1008
  display: block;
820
1009
  }
821
1010
  </style>
822
- <script type="module" crossorigin src="/assets/index-CDDVjT8Y.js"></script>
1011
+ <script type="module" crossorigin src="/assets/app-D1XS_DDX.js"></script>
823
1012
  </head>
824
1013
  <body>
825
1014
  <aside class="sidebar">
@@ -901,6 +1090,39 @@
901
1090
 
902
1091
  <div class="sb-divider"></div>
903
1092
 
1093
+ <div class="sb-section">
1094
+ <div class="sb-section-label">Cookies</div>
1095
+ <div class="cookie-section-wrap">
1096
+ <div class="cookie-checkbox-row">
1097
+ <input type="checkbox" id="block-cookies-checkbox" checked />
1098
+ <label class="cookie-checkbox-label" for="block-cookies-checkbox">Block cookies</label>
1099
+ </div>
1100
+ <div id="cookie-auth-area" class="cookie-auth-area" style="display: none">
1101
+ <div id="cookie-auth-user" class="cookie-list" style="display: none"></div>
1102
+ <div id="cookie-auth-hint" class="cookie-auth-hint">No session cookie found.</div>
1103
+ <button class="cookie-clear-btn" id="cookie-clear-btn">
1104
+ <svg
1105
+ width="10"
1106
+ height="10"
1107
+ viewBox="0 0 24 24"
1108
+ fill="none"
1109
+ stroke="currentColor"
1110
+ stroke-width="2.5"
1111
+ stroke-linecap="round"
1112
+ stroke-linejoin="round"
1113
+ >
1114
+ <polyline points="3 6 5 6 21 6"></polyline>
1115
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path>
1116
+ <path d="M10 11v6M14 11v6"></path>
1117
+ </svg>
1118
+ Clear cookies
1119
+ </button>
1120
+ </div>
1121
+ </div>
1122
+ </div>
1123
+
1124
+ <div class="sb-divider"></div>
1125
+
904
1126
  <div class="sb-section-label" style="padding-top: 12px">Fields</div>
905
1127
  <div class="fields-scroll" id="field-list">
906
1128
  <p class="no-fields">Select a template to edit fields.</p>
@@ -985,11 +1207,39 @@
985
1207
  Mobile
986
1208
  </button>
987
1209
  </div>
1210
+ <div class="toolbar-sep"></div>
1211
+ <button class="device-btn" id="new-window-btn" disabled title="Open in new window">
1212
+ <svg
1213
+ width="13"
1214
+ height="13"
1215
+ viewBox="0 0 24 24"
1216
+ fill="none"
1217
+ stroke="currentColor"
1218
+ stroke-width="2"
1219
+ stroke-linecap="round"
1220
+ stroke-linejoin="round"
1221
+ >
1222
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
1223
+ <polyline points="15 3 21 3 21 9" />
1224
+ <line x1="10" y1="14" x2="21" y2="3" />
1225
+ </svg>
1226
+ New Window
1227
+ </button>
988
1228
  </div>
989
1229
 
990
1230
  <div class="preview-area" id="preview-area"></div>
991
1231
  </div>
992
1232
 
1233
+ <div class="modal-backdrop hidden" id="jwt-modal-backdrop">
1234
+ <div class="jwt-modal" role="dialog" aria-modal="true" aria-labelledby="jwt-modal-title">
1235
+ <div class="jwt-modal-header">
1236
+ <div class="jwt-modal-title" id="jwt-modal-title">JWT Payload</div>
1237
+ <button class="jwt-modal-close" id="jwt-modal-close-btn">Close</button>
1238
+ </div>
1239
+ <pre class="jwt-payload" id="jwt-payload-content"></pre>
1240
+ </div>
1241
+ </div>
1242
+
993
1243
  <div class="modal-backdrop hidden" id="create-modal-backdrop">
994
1244
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
995
1245
  <div class="modal-title" id="modal-title">New Template</div>
@@ -0,0 +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)})});
package/dist/server.js CHANGED
@@ -113,6 +113,101 @@ var BOILERPLATE_FIELDS = [
113
113
  { slug: "button_text", label: "Button text", type: "text", default_value: "Subscribe" },
114
114
  { slug: "privacy_note", label: "Privacy note", type: "text", default_value: "No spam. Unsubscribe anytime." }
115
115
  ];
116
+ var ARTICLE_PARAGRAPHS = [
117
+ "The city council voted unanimously Tuesday to approve a sweeping new infrastructure plan that would reshape public transit routes across the metropolitan area over the next decade.",
118
+ "Mayor Jordan Ellison called the decision a turning point for how the region thinks about getting around, noting that similar investments in peer cities have led to measurable reductions in traffic congestion and improved air quality.",
119
+ "The plan includes upgrades to 14 existing bus rapid transit lines, the introduction of three new light-rail corridors connecting the downtown core to outlying neighborhoods, and dedicated cycling infrastructure along six major arterials.",
120
+ "Critics, however, warn that the projected cost of $2.4 billion may be optimistic. Independent analysts suggest the final figure could climb to $3.8 billion once land acquisition, labor, and contingency costs are factored in over the full construction timeline.",
121
+ "Council member Dana Reyes, who voted in favor, acknowledged the fiscal concern but argued that inaction carries its own price, noting that congestion costs residents and businesses hundreds of millions in lost productivity each year.",
122
+ "Environmental groups praised the announcement, saying expanded transit capacity is essential to meet the stated goal of reducing transportation emissions by 45 percent before 2035.",
123
+ "Public comment sessions are scheduled for next month at community centers in all twelve districts. Construction is expected to begin no sooner than mid-next year, pending environmental review and final design approvals."
124
+ ];
125
+ function escapeAttr(s) {
126
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/>/g, "&gt;");
127
+ }
128
+ function sdkScriptTag(sdkUrl) {
129
+ if (!sdkUrl) {
130
+ return "";
131
+ }
132
+ return `<script src="${escapeAttr(sdkUrl)}" data-api-url="https://${new URL(sdkUrl).host}"></script>`;
133
+ }
134
+ function normalizeTenantSdkUrl(raw) {
135
+ const trimmed = raw.trim();
136
+ if (!trimmed) {
137
+ return "";
138
+ }
139
+ const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
140
+ return withProtocol.replace(/\/+$/, "") + "/client.js";
141
+ }
142
+ 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;}}});})();`;
143
+ function buildPreviewData(options) {
144
+ const json = JSON.stringify({
145
+ slug: options.slug,
146
+ html: options.html,
147
+ css: options.css,
148
+ js: options.js,
149
+ fieldValues: options.fieldValues,
150
+ tab: options.tab
151
+ });
152
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
153
+ }
154
+ function standaloneShell(options) {
155
+ const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
156
+ return `<!DOCTYPE html>
157
+ <html>
158
+ <head>
159
+ <meta charset="UTF-8">
160
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
161
+ <script>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</script>
162
+ ${cookieScript}
163
+ <style>
164
+ * { box-sizing: border-box; }
165
+ body { margin: 0; }
166
+ [data-allegro-interaction] { display: block; }
167
+ </style>
168
+ </head>
169
+ <body>
170
+ <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
171
+ ${sdkScriptTag(options.sdkUrl)}
172
+ <script src="/preview-client.js"></script>
173
+ </body>
174
+ </html>`;
175
+ }
176
+ function articleShell(options) {
177
+ const paras = ARTICLE_PARAGRAPHS.map((p) => `<p>${p}</p>`).join("\n ");
178
+ const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
179
+ return `<!DOCTYPE html>
180
+ <html>
181
+ <head>
182
+ <meta charset="UTF-8">
183
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
184
+ <script>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</script>
185
+ ${cookieScript}
186
+ <style>
187
+ *, *::before, *::after { box-sizing: border-box; }
188
+ body { font-family: Georgia, 'Times New Roman', serif; background: #fff; color: #111;
189
+ max-width: 680px; margin: 0 auto; padding: 32px 24px 64px; line-height: 1.75;
190
+ -webkit-font-smoothing: antialiased; }
191
+ h1 { font-size: 1.6rem; line-height: 1.25; margin: 0 0 6px; font-weight: 700; }
192
+ .meta { color: #6b7280; font-size: 0.82rem; font-family: system-ui, sans-serif; margin-bottom: 24px; }
193
+ p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
194
+ [data-allegro-interaction] { display: block; margin: 24px 0; }
195
+ </style>
196
+ </head>
197
+ <body>
198
+ <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
199
+ <header>
200
+ <h1>Council Approves Decade-Long Transit Overhaul</h1>
201
+ <p class="meta">By Staff Reporter &nbsp;&middot;&nbsp; March 14, 2026</p>
202
+ </header>
203
+ <article id="allegro-preview-article">
204
+ ${paras}
205
+ </article>
206
+ ${sdkScriptTag(options.sdkUrl)}
207
+ <script src="/preview-client.js"></script>
208
+ </body>
209
+ </html>`;
210
+ }
116
211
  var sseClients = /* @__PURE__ */ new Set();
117
212
  function broadcast(event) {
118
213
  for (const client of sseClients) {
@@ -165,6 +260,11 @@ function scanTemplates(baseDir) {
165
260
  }
166
261
  return results;
167
262
  }
263
+ var templateFilesCache = /* @__PURE__ */ new Map();
264
+ function invalidateTemplateCache(filePath) {
265
+ const dir = filePath.replace(/[/\\][^/\\]+$/, "");
266
+ templateFilesCache.delete(dir);
267
+ }
168
268
  function readTemplateFiles(dir) {
169
269
  const read = (filename) => {
170
270
  const filePath = join(dir, filename);
@@ -183,7 +283,7 @@ function readTemplateFiles(dir) {
183
283
  fieldsParseError = err instanceof Error ? err.message : "Invalid JSON in template.json";
184
284
  }
185
285
  }
186
- return {
286
+ const files = {
187
287
  html: read("index.html"),
188
288
  css: read("index.css"),
189
289
  js: read("index.js"),
@@ -191,6 +291,11 @@ function readTemplateFiles(dir) {
191
291
  name,
192
292
  fieldsParseError
193
293
  };
294
+ templateFilesCache.set(dir, files);
295
+ return files;
296
+ }
297
+ function readTemplateFilesCached(dir) {
298
+ return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
194
299
  }
195
300
  async function startServer(templatePath, port = 0, tenant = "") {
196
301
  const baseDir = resolve(process.cwd(), templatePath);
@@ -223,7 +328,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
223
328
  res.status(404).json({ error: "Template not found" });
224
329
  return;
225
330
  }
226
- res.json(readTemplateFiles(resolvedDir));
331
+ res.json(readTemplateFilesCached(resolvedDir));
227
332
  });
228
333
  app.post("/api/template/create", express.json(), (req, res) => {
229
334
  const body = req.body;
@@ -265,6 +370,53 @@ async function startServer(templatePath, port = 0, tenant = "") {
265
370
  sseClients.delete(res);
266
371
  });
267
372
  });
373
+ app.get("/preview", (req, res) => {
374
+ const slug = typeof req.query.slug === "string" ? req.query.slug : "";
375
+ const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
376
+ const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
377
+ if (!slug) {
378
+ res.status(400).send("Missing slug");
379
+ return;
380
+ }
381
+ if (slug !== "." && !templates.some((t) => t.slug === slug)) {
382
+ res.status(404).send("Template not found");
383
+ return;
384
+ }
385
+ const targetDir = slug === "." ? baseDir : join(baseDir, slug);
386
+ const resolvedBaseDir = resolve(baseDir);
387
+ const resolvedDir = resolve(targetDir);
388
+ const relativePath = relative(resolvedBaseDir, resolvedDir);
389
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
390
+ res.status(400).send("Invalid template path");
391
+ return;
392
+ }
393
+ if (!existsSync(resolvedDir) || !isTemplateDir(resolvedDir)) {
394
+ res.status(404).send("Template not found");
395
+ return;
396
+ }
397
+ const templateFiles = readTemplateFilesCached(resolvedDir);
398
+ const fieldValues = [];
399
+ for (const [key, val] of Object.entries(req.query)) {
400
+ if (key.startsWith("f_") && typeof val === "string") {
401
+ fieldValues.push({ slug: key.slice(2), value: val });
402
+ }
403
+ }
404
+ const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
405
+ const blockCookies = req.query.blockCookies !== "false";
406
+ const shellOptions = {
407
+ slug,
408
+ html: templateFiles.html,
409
+ css: templateFiles.css,
410
+ js: templateFiles.js,
411
+ fieldValues,
412
+ tab,
413
+ sdkUrl: effectiveSdkUrl,
414
+ blockCookies
415
+ };
416
+ const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
417
+ res.setHeader("Cache-Control", "no-store");
418
+ res.type("html").send(rendered);
419
+ });
268
420
  const indexHtmlPath = join(clientDistDir, "index.html");
269
421
  app.get("/*path", (_req, res) => {
270
422
  if (existsSync(indexHtmlPath)) {
@@ -285,10 +437,12 @@ async function startServer(templatePath, port = 0, tenant = "") {
285
437
  );
286
438
  fileWatcher.on("change", (changedPath) => {
287
439
  console.log(`Changed: ${changedPath}`);
440
+ invalidateTemplateCache(changedPath);
288
441
  broadcastReload();
289
442
  });
290
443
  fileWatcher.on("add", (addedPath) => {
291
444
  console.log(`Added: ${addedPath}`);
445
+ invalidateTemplateCache(addedPath);
292
446
  if (basename(addedPath) === "index.html") {
293
447
  rescan();
294
448
  }
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.6",
4
+ "version": "0.1.0-dev.7",
5
5
  "description": "Local dev server for previewing Allegro templates",
6
6
  "type": "module",
7
7
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "build": "npm run build:client && npm run build:cli",
15
15
  "build:client": "vite build",
16
16
  "build:cli": "tsup",
17
- "dev": "vite",
17
+ "dev": "npm run build:client -- --watch & tsup --watch",
18
18
  "types": "tsc --noEmit",
19
19
  "format": "prettier --write src/",
20
20
  "format:check": "prettier --check src/"
@@ -1,137 +0,0 @@
1
- (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))o(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const a of s.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&o(a)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function o(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();function A(e,t){let n=e;for(const o of t){const i=new RegExp(`\\{%\\s*${o.slug}\\s*%\\}`,"g");n=n.replace(i,o.value)}return n}const O="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js",K={full:0,desktop:1280,tablet:768,mobile:390},Q=`
2
- *, *::before, *::after { box-sizing: border-box; }
3
- * { margin: 0; }
4
- body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.5; -webkit-font-smoothing: antialiased; }
5
- img, picture, video, canvas, svg { display: block; max-width: 100%; }
6
- input, button, textarea, select { font: inherit; }
7
- p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
8
- `,P=["The city council voted unanimously Tuesday to approve a sweeping new infrastructure plan that would reshape public transit routes across the metropolitan area over the next decade.","Mayor Jordan Ellison called the decision a turning point for how the region thinks about getting around, noting that similar investments in peer cities have led to measurable reductions in traffic congestion and improved air quality.","The plan includes upgrades to 14 existing bus rapid transit lines, the introduction of three new light-rail corridors connecting the downtown core to outlying neighborhoods, and dedicated cycling infrastructure along six major arterials.","Critics, however, warn that the projected cost of $2.4 billion may be optimistic. Independent analysts suggest the final figure could climb to $3.8 billion once land acquisition, labor, and contingency costs are factored in over the full construction timeline.","Council member Dana Reyes, who voted in favor, acknowledged the fiscal concern but argued that inaction carries its own price, noting that congestion costs residents and businesses hundreds of millions in lost productivity each year.","Environmental groups praised the announcement, saying expanded transit capacity is essential to meet the stated goal of reducing transportation emissions by 45 percent before 2035.","Public comment sessions are scheduled for next month at community centers in all twelve districts. Construction is expected to begin no sooner than mid-next year, pending environmental review and final design approvals."];function F(e){return e?`<script src="${e}"><\/script>`:""}function R(e,t,n,o){const i=JSON.stringify(e),s=JSON.stringify(t),a=JSON.stringify(n);return`(function() {
9
- var html = ${i};
10
- var css = ${s};
11
- var js = ${a};
12
-
13
- var host = document.createElement('div');
14
- host.setAttribute('data-allegro-preview', '');
15
- var shadow = host.attachShadow({ mode: 'open' });
16
-
17
- if (css) {
18
- var style = document.createElement('style');
19
- style.textContent = css;
20
- shadow.appendChild(style);
21
- }
22
-
23
- var container = document.createElement('div');
24
- container.setAttribute('x-data', JSON.stringify({ session: null, audience_member: null }));
25
- container.innerHTML = html;
26
- shadow.appendChild(container);
27
-
28
- if (js) {
29
- try { (new Function('shadowRoot', 'hostElement', js))(shadow, host); }
30
- catch(e) { console.error('[allegro-preview] JS error:', e); }
31
- }
32
-
33
- var article = document.getElementById('article-body');
34
- if (!article) return;
35
-
36
- var blockTags = new Set(['P','H1','H2','H3','H4','H5','H6','LI','BLOCKQUOTE','DIV','SECTION','FIGURE','PRE']);
37
- var blocks = Array.from(article.children).filter(function(c) { return blockTags.has(c.tagName); });
38
-
39
- ${o==="end"?"article.appendChild(host);":`var cutIndex = Math.min(${o}, blocks.length);
40
- var pivot = blocks[cutIndex - 1] || blocks[0];
41
- pivot.after(host);
42
- blocks.slice(cutIndex).forEach(function(el) { el.remove(); });`}
43
-
44
- if (window.Alpine && typeof window.Alpine.initTree === 'function') {
45
- window.Alpine.initTree(shadow);
46
- }
47
- })();`}function X(e,t,n){const o=JSON.stringify(e),i=JSON.stringify(t),s=JSON.stringify(n);return`(function() {
48
- var html = ${o};
49
- var css = ${i};
50
- var js = ${s};
51
-
52
- var host = document.createElement('div');
53
- host.setAttribute('data-allegro-preview', '');
54
- var shadow = host.attachShadow({ mode: 'open' });
55
-
56
- if (css) {
57
- var style = document.createElement('style');
58
- style.textContent = css;
59
- shadow.appendChild(style);
60
- }
61
-
62
- var container = document.createElement('div');
63
- container.setAttribute('x-data', JSON.stringify({ session: null, audience_member: null }));
64
- container.innerHTML = html;
65
- shadow.appendChild(container);
66
-
67
- if (js) {
68
- try { (new Function('shadowRoot', 'hostElement', js))(shadow, host); }
69
- catch(e) { console.error('[allegro-preview] JS error:', e); }
70
- }
71
-
72
- document.body.appendChild(host);
73
-
74
- if (window.Alpine && typeof window.Alpine.initTree === 'function') {
75
- window.Alpine.initTree(shadow);
76
- }
77
- })();`}function Z(e,t,n,o){const i=X(e,t,n);return`<!DOCTYPE html>
78
- <html>
79
- <head>
80
- <meta charset="UTF-8">
81
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
82
- <script>window.__ALLEGRO_PREVIEW__ = true;<\/script>
83
- ${F(o)}
84
- <script src="${O}"><\/script>
85
- <style>
86
- * { box-sizing: border-box; }
87
- body { margin: 0; }
88
- [data-allegro-preview] { display: block; }
89
- </style>
90
- </head>
91
- <body>
92
- <script>${i}<\/script>
93
- </body>
94
- </html>`}function _(e,t,n){const o=e.map(i=>`<p>${i}</p>`).join(`
95
- `);return`<!DOCTYPE html>
96
- <html>
97
- <head>
98
- <meta charset="UTF-8">
99
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
100
- ${F(n)}
101
- <script src="${O}"><\/script>
102
- <style>
103
- * { box-sizing: border-box; }
104
- *, *::before, *::after { box-sizing: border-box; }
105
- body { font-family: Georgia, 'Times New Roman', serif; background: #fff; color: #111;
106
- max-width: 680px; margin: 0 auto; padding: 32px 24px 64px; line-height: 1.75;
107
- -webkit-font-smoothing: antialiased; }
108
- h1 { font-size: 1.6rem; line-height: 1.25; margin: 0 0 6px; font-weight: 700; }
109
- .meta { color: #6b7280; font-size: 0.82rem; font-family: system-ui, sans-serif; margin-bottom: 24px; }
110
- p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
111
- [data-allegro-preview] { display: block; margin: 24px 0; }
112
- </style>
113
- </head>
114
- <body>
115
- <header>
116
- <h1>Council Approves Decade-Long Transit Overhaul</h1>
117
- <p class="meta">By Staff Reporter &nbsp;&middot;&nbsp; March 14, 2026</p>
118
- </header>
119
- <article id="article-body">
120
- ${o}
121
- </article>
122
- <script>${t}<\/script>
123
- </body>
124
- </html>`}function ee(e,t,n){const o=A(e.html,t);return Z(o,Q+e.css,e.js,n)}function te(e,t,n){const o=A(e.html,t),i=R(o,e.css,e.js,"end");return _(P,i,n)}function ne(e,t,n){const o=A(e.html,t),i=R(o,e.css,e.js,3);return _(P,i,n)}function oe(e){return K[e]}let c=[],u=null,b="standalone",w="full",m=null,v={},S;const M=document.getElementById("template-nav"),p=document.getElementById("field-list"),d=document.getElementById("preview-area"),I=document.getElementById("tab-group"),T=document.getElementById("device-group"),k=document.getElementById("toolbar-badge"),ie=document.getElementById("theme-toggle"),ae=document.getElementById("theme-icon-sun"),se=document.getElementById("theme-icon-moon"),L=document.getElementById("tenant-input"),C=document.getElementById("tenant-status"),re=document.getElementById("new-template-btn"),E=document.getElementById("create-modal-backdrop"),l=document.getElementById("modal-slug-input"),f=document.getElementById("modal-error"),le=document.getElementById("modal-cancel-btn"),y=document.getElementById("modal-create-btn");function J(e){document.documentElement.setAttribute("data-theme",e?"dark":"light"),ae.style.display=e?"none":"",se.style.display=e?"":"none"}function ce(){const e=localStorage.getItem("allegro-preview-theme"),t=window.matchMedia("(prefers-color-scheme: dark)").matches;J(e?e==="dark":t)}ie.addEventListener("click",()=>{const e=document.documentElement.getAttribute("data-theme")!=="dark";J(e),localStorage.setItem("allegro-preview-theme",e?"dark":"light")});function de(e){const t=e.trim();return t?(/^https?:\/\//.test(t)?t:`https://${t}`).replace(/\/+$/,"")+"/client.js":""}function D(e){const t=de(e);S=t||void 0,t?(C.className="tenant-active-badge",C.textContent=t):(C.className="tenant-hint",C.textContent="Loads /client.js on the preview page"),m&&h()}L.addEventListener("input",()=>{localStorage.setItem("allegro-preview-tenant",L.value),D(L.value)});function ue(e=""){const n=localStorage.getItem("allegro-preview-tenant")??e;n&&(L.value=n,D(n))}function pe(){const e=window.location.hash.slice(1);return e?decodeURIComponent(e):null}function me(e){history.replaceState(null,"",`#${encodeURIComponent(e)}`)}function fe(){history.replaceState(null,"",window.location.pathname)}function q(e){I.querySelectorAll(".tab-btn").forEach(t=>{t.disabled=!e}),T.querySelectorAll(".device-btn").forEach(t=>{t.disabled=!e})}I.addEventListener("click",e=>{const t=e.target.closest(".tab-btn");!t||t.disabled||(b=t.dataset.tab,I.querySelectorAll(".tab-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-tab",b),h())});T.addEventListener("click",e=>{const t=e.target.closest(".device-btn");!t||t.disabled||(w=t.dataset.device,T.querySelectorAll(".device-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-device",w),h())});function x(){M.innerHTML="";for(const e of c){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">${j(e.name)}</span>`,t.addEventListener("click",()=>void B(e.slug)),M.appendChild(t)}}async function B(e){u=e,me(e),x(),await H(e)}function z(){m=null,u=null,fe(),x(),q(!1),k.textContent="No template selected",k.style.opacity="0.4",p.innerHTML='<p class="no-fields">Select a template to edit fields.</p>';const e=document.createElement("div");if(e.className="selector-screen",c.length===0){e.innerHTML=`
125
- <div class="selector-empty">
126
- <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>
127
- <span>No templates found in this directory.</span>
128
- </div>`,d.innerHTML="",d.appendChild(e);return}e.innerHTML=`
129
- <div class="selector-eyebrow">Allegro Preview</div>
130
- <h2 class="selector-heading">Choose a template</h2>
131
- <p class="selector-sub">Select a template to start previewing.</p>
132
- <div class="selector-grid" id="selector-grid"></div>`,d.innerHTML="",d.appendChild(e);const t=e.querySelector("#selector-grid");for(const n of c){const o=document.createElement("button");o.className="selector-card",o.innerHTML=`
133
- <div class="selector-card-icon">
134
- <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>
135
- </div>
136
- <div class="selector-card-name">${j(n.name)}</div>
137
- <div class="selector-card-slug">${j(n.slug)}</div>`,o.addEventListener("click",()=>void B(n.slug)),t.appendChild(o)}}const N=["text","number","checkbox","textarea"];function U(e){if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.slug=="string"&&t.slug.length>0&&N.includes(t.type)}function he(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'),!N.includes(t.type)){const i=t.type!==void 0?`"${String(t.type)}"`:"none";n.push(`invalid type ${i} — valid types: ${N.join(", ")}`)}return`${typeof t.slug=="string"&&t.slug.length>0?`Field "${t.slug}"`:"Field"}: ${n.join("; ")}.`}function ge(e){v={};for(const t of e)U(t)&&(v[t.slug]=t.default_value??"")}function ve(e,t){if(p.innerHTML="",t){const n=document.createElement("div");n.className="field-error",n.textContent=`template.json parse error: ${t}`,p.appendChild(n);return}if(e.length===0){p.innerHTML='<p class="no-fields">No fields in template.json.</p>';return}for(const n of e){if(!U(n)){const a=document.createElement("div");a.className="field-error",a.textContent=he(n),p.appendChild(a);continue}const o=n;if(o.type==="checkbox"){const a=document.createElement("div");a.className="field-checkbox-row";const r=document.createElement("input");r.type="checkbox",r.id=`field-${o.slug}`,r.checked=o.default_value==="true"||o.default_value==="1",r.addEventListener("change",()=>{v[o.slug]=r.checked?"true":"false",h()});const g=document.createElement("label");g.className="field-label",g.htmlFor=`field-${o.slug}`,g.textContent=o.label||o.slug,a.appendChild(r),a.appendChild(g),p.appendChild(a);continue}const i=document.createElement("div");i.className="field-group";const s=document.createElement("label");if(s.className="field-label",s.htmlFor=`field-${o.slug}`,s.textContent=o.label||o.slug,i.appendChild(s),o.type==="textarea"){const a=document.createElement("textarea");a.className="field-input",a.id=`field-${o.slug}`,a.rows=3,a.value=o.default_value??"",a.addEventListener("input",()=>{v[o.slug]=a.value,h()}),i.appendChild(a)}else{const a=document.createElement("input");a.type=o.type==="number"?"number":"text",a.className="field-input",a.id=`field-${o.slug}`,a.value=o.default_value??"",a.addEventListener("input",()=>{v[o.slug]=a.value,h()}),i.appendChild(a)}p.appendChild(i)}}function h(){if(!m)return;const e=Object.entries(v).map(([g,Y])=>({slug:g,value:Y})),t=oe(w),n=t===0,o=b==="standalone"?ee(m,e,S):b==="append"?te(m,e,S):ne(m,e,S);d.innerHTML="";const i=document.createElement("div");i.className=n?"preview-stage":"preview-stage preview-stage--breakpoint";const s=document.createElement("div");s.className="preview-scaler";const a=document.createElement("div");a.className="preview-frame-wrapper";const r=document.createElement("iframe");r.style.height="calc(100vh - 48px)",w==="mobile"&&(r.style.maxHeight="812px"),r.setAttribute("sandbox","allow-scripts allow-same-origin"),r.srcdoc=o,n?(a.style.width="100%",r.style.width="100%",s.style.width="100%"):(a.style.width=`${t}px`,r.style.width=`${t}px`),a.appendChild(r),s.appendChild(a),i.appendChild(s),d.appendChild(i)}async function H(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();m=n,k.textContent=`Template: ${n.name}`,k.style.opacity="1",q(!0),ge(n.fields),ve(n.fields,n.fieldsParseError),h()}catch(t){console.error("[allegro-preview] Failed to load template:",t),d.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}".`,d.appendChild(n)}}async function ye(){const t=await(await fetch("/api/config")).json();c=t.templates,ue(t.tenant??"");const n=pe(),o=n&&c.find(i=>i.slug===n)?n:null;o?(u=o,x(),await H(o)):z()}function be(){const e=localStorage.getItem("allegro-preview-tab"),t=localStorage.getItem("allegro-preview-device");e&&["standalone","append","cut"].includes(e)&&(b=e,I.querySelectorAll(".tab-btn").forEach(n=>{n.classList.toggle("active",n.dataset.tab===e)})),t&&["full","desktop","tablet","mobile"].includes(t)&&(w=t,T.querySelectorAll(".device-btn").forEach(n=>{n.classList.toggle("active",n.dataset.device===t)}))}function V(){const e=new EventSource("/api/events");e.addEventListener("reload",()=>{u&&H(u)}),e.addEventListener("config",()=>{we()}),e.onerror=()=>{e.close(),setTimeout(V,2e3)}}async function we(){c=(await(await fetch("/api/config")).json()).templates,x(),u&&!c.find(n=>n.slug===u)&&z()}function j(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const Ee=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;function xe(){l.value="",f.textContent="",l.classList.remove("invalid"),y.disabled=!1,E.classList.remove("hidden"),l.focus()}function $(){E.classList.add("hidden")}function G(e){return e?Ee.test(e)?null:"Use lowercase letters, numbers, and hyphens only (e.g. my-template).":"Slug is required."}re.addEventListener("click",xe);le.addEventListener("click",$);E.addEventListener("click",e=>{e.target===E&&$()});document.addEventListener("keydown",e=>{e.key==="Escape"&&!E.classList.contains("hidden")&&$()});l.addEventListener("input",()=>{const e=G(l.value);e?(l.classList.add("invalid"),f.textContent=e):(l.classList.remove("invalid"),f.textContent="")});l.addEventListener("keydown",e=>{e.key==="Enter"&&W()});y.addEventListener("click",()=>void W());async function W(){const e=l.value.trim(),t=G(e);if(t){l.classList.add("invalid"),f.textContent=t,l.focus();return}y.disabled=!0,f.textContent="";try{const n=await fetch("/api/template/create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:e})}),o=await n.json();if(!n.ok){f.textContent=o.error??"Failed to create template.",y.disabled=!1;return}$(),c=[...c,{slug:e,name:e}],x(),await B(e)}catch{f.textContent="Network error. Please try again.",y.disabled=!1}}ce();be();ye();V();