@allegrocdp/preview 0.1.0-dev.6 → 0.1.0-dev.8
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 +156 -2
- package/dist/client/assets/app-D1XS_DDX.js +14 -0
- package/dist/client/index.html +251 -1
- package/dist/client/preview-client.js +1 -0
- package/dist/server.js +156 -2
- package/package.json +2 -2
- package/dist/client/assets/index-CDDVjT8Y.js +0 -137
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 · 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
|
-
|
|
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(
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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();
|
package/dist/client/index.html
CHANGED
|
@@ -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/
|
|
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, "&").replace(/"/g, """).replace(/>/g, ">");
|
|
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 · 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
|
-
|
|
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(
|
|
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.
|
|
4
|
+
"version": "0.1.0-dev.8",
|
|
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": "
|
|
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 · 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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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();
|