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