@allegrocdp/preview 0.1.0-dev.6 → 0.1.0

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/server.js CHANGED
@@ -3,116 +3,11 @@
3
3
  // src/server.ts
4
4
  import chokidar from "chokidar";
5
5
  import express from "express";
6
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
6
+ import { existsSync, readdirSync, readFileSync } from "fs";
7
7
  import open from "open";
8
8
  import { basename, isAbsolute, join, relative, resolve } from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  var __dirname = fileURLToPath(new URL(".", import.meta.url));
11
- var BOILERPLATE_HTML = `<div class="signup">
12
- <div class="signup__eyebrow">Newsletter</div>
13
- <h2 class="signup__headline">{% headline %}</h2>
14
- <p class="signup__sub">{% subheadline %}</p>
15
- <form class="signup__form" onsubmit="return false">
16
- <div class="signup__row">
17
- <input class="signup__input" type="email" placeholder="{% input_placeholder %}" />
18
- <button class="signup__btn" type="submit">{% button_text %}</button>
19
- </div>
20
- <p class="signup__note">{% privacy_note %}</p>
21
- </form>
22
- </div>
23
- `;
24
- var BOILERPLATE_CSS = `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
25
-
26
- .signup {
27
- font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
28
- -webkit-font-smoothing: antialiased;
29
- padding: 16px;
30
- }
31
-
32
- .signup__eyebrow {
33
- font-size: 11px;
34
- font-weight: 700;
35
- letter-spacing: 0.1em;
36
- text-transform: uppercase;
37
- color: #6366f1;
38
- margin-bottom: 8px;
39
- }
40
-
41
- .signup__headline {
42
- font-size: clamp(20px, 4vw, 26px);
43
- font-weight: 800;
44
- color: #0f172a;
45
- line-height: 1.2;
46
- letter-spacing: -0.02em;
47
- margin-bottom: 8px;
48
- }
49
-
50
- .signup__sub {
51
- font-size: 14px;
52
- color: #64748b;
53
- line-height: 1.6;
54
- margin-bottom: 20px;
55
- }
56
-
57
- .signup__row {
58
- display: flex;
59
- gap: 8px;
60
- flex-wrap: wrap;
61
- }
62
-
63
- .signup__input {
64
- flex: 1;
65
- min-width: 180px;
66
- height: 42px;
67
- padding: 0 12px;
68
- border: 1.5px solid #e2e8f0;
69
- border-radius: 8px;
70
- font-size: 14px;
71
- color: #0f172a;
72
- background: #f8fafc;
73
- outline: none;
74
- transition: border-color 0.15s, background 0.15s;
75
- }
76
-
77
- .signup__input:focus { border-color: #6366f1; background: #fff; }
78
- .signup__input::placeholder { color: #94a3b8; }
79
-
80
- .signup__btn {
81
- height: 42px;
82
- padding: 0 20px;
83
- background: #6366f1;
84
- color: #fff;
85
- border: none;
86
- border-radius: 8px;
87
- font-size: 14px;
88
- font-weight: 600;
89
- font-family: inherit;
90
- cursor: pointer;
91
- white-space: nowrap;
92
- transition: background 0.15s;
93
- }
94
-
95
- .signup__btn:hover { background: #4f46e5; }
96
- .signup__btn:active { opacity: 0.9; }
97
-
98
- .signup__note {
99
- margin-top: 10px;
100
- font-size: 12px;
101
- color: #94a3b8;
102
- }
103
- `;
104
- var BOILERPLATE_FIELDS = [
105
- { slug: "headline", label: "Headline", type: "text", default_value: "Stay in the know" },
106
- {
107
- slug: "subheadline",
108
- label: "Subheadline",
109
- type: "textarea",
110
- default_value: "Get the latest stories delivered to your inbox."
111
- },
112
- { slug: "input_placeholder", label: "Input placeholder", type: "text", default_value: "Enter your email" },
113
- { slug: "button_text", label: "Button text", type: "text", default_value: "Subscribe" },
114
- { slug: "privacy_note", label: "Privacy note", type: "text", default_value: "No spam. Unsubscribe anytime." }
115
- ];
116
11
  var sseClients = /* @__PURE__ */ new Set();
117
12
  function broadcast(event) {
118
13
  for (const client of sseClients) {
@@ -225,36 +120,6 @@ async function startServer(templatePath, port = 0, tenant = "") {
225
120
  }
226
121
  res.json(readTemplateFiles(resolvedDir));
227
122
  });
228
- app.post("/api/template/create", express.json(), (req, res) => {
229
- const body = req.body;
230
- const slug = body.slug;
231
- if (typeof slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
232
- res.status(400).json({
233
- error: "Slug must be lowercase letters, numbers, and hyphens only (e.g. my-template)."
234
- });
235
- return;
236
- }
237
- const targetDir = join(baseDir, slug);
238
- if (existsSync(targetDir)) {
239
- res.status(400).json({ error: `A template named "${slug}" already exists.` });
240
- return;
241
- }
242
- try {
243
- mkdirSync(targetDir);
244
- writeFileSync(join(targetDir, "index.html"), BOILERPLATE_HTML);
245
- writeFileSync(join(targetDir, "index.css"), BOILERPLATE_CSS);
246
- writeFileSync(
247
- join(targetDir, "template.json"),
248
- JSON.stringify({ name: slug, fields: BOILERPLATE_FIELDS }, null, 2) + "\n"
249
- );
250
- } catch (err) {
251
- console.error("[allegro-preview] Failed to create template:", err);
252
- res.status(500).json({ error: "Failed to create template files." });
253
- return;
254
- }
255
- rescan();
256
- res.status(201).json({ slug });
257
- });
258
123
  app.get("/api/events", (req, res) => {
259
124
  res.setHeader("Content-Type", "text/event-stream");
260
125
  res.setHeader("Cache-Control", "no-cache");
@@ -270,9 +135,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
270
135
  if (existsSync(indexHtmlPath)) {
271
136
  res.type("html").send(readFileSync(indexHtmlPath));
272
137
  } else {
273
- res.status(503).send(
274
- 'Preview client build artifacts not found. Ensure the client is built (for example, by running "npm run build" in this package during development).'
275
- );
138
+ res.status(503).send('Preview client build artifacts not found. Ensure the client is built (for example, by running "npm run build" in this package during development).');
276
139
  }
277
140
  });
278
141
  const TEMPLATE_FILES = ["index.html", "index.css", "index.js", "template.json"];
@@ -287,14 +150,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
287
150
  console.log(`Changed: ${changedPath}`);
288
151
  broadcastReload();
289
152
  });
290
- fileWatcher.on("add", (addedPath) => {
291
- console.log(`Added: ${addedPath}`);
292
- if (basename(addedPath) === "index.html") {
293
- rescan();
294
- }
295
- broadcastReload();
296
- });
297
- const dirWatcher = chokidar.watch(baseDir, { depth: 1, ignoreInitial: true });
153
+ const dirWatcher = chokidar.watch(baseDir, { depth: 0, ignoreInitial: true });
298
154
  function rescan() {
299
155
  templates = scanTemplates(baseDir);
300
156
  console.log(`Templates updated: ${templates.map((t) => t.slug).join(", ") || "(none)"}`);
@@ -321,32 +177,18 @@ async function startServer(templatePath, port = 0, tenant = "") {
321
177
  fileWatcher.add(templateFilePaths(join(filePath, "..")));
322
178
  }
323
179
  });
324
- const bindServer = (p) => new Promise((resolve2, reject) => {
325
- const server = app.listen(p);
326
- server.once("listening", () => {
180
+ await new Promise((resolvePromise) => {
181
+ const server = app.listen(port, () => {
327
182
  const addr = server.address();
328
- const assignedPort2 = typeof addr === "object" && addr ? addr.port : p;
329
- resolve2({ server, assignedPort: assignedPort2 });
330
- });
331
- server.once("error", reject);
332
- });
333
- let assignedPort;
334
- try {
335
- ({ assignedPort } = await bindServer(port));
336
- } catch (err) {
337
- const nodeErr = err;
338
- if (nodeErr.code === "EADDRINUSE" && port !== 0) {
339
- console.log(`Port ${port} is in use, using a random available port instead.`);
340
- ({ assignedPort } = await bindServer(0));
341
- } else {
342
- throw err;
343
- }
344
- }
345
- const url = `http://localhost:${assignedPort}`;
346
- console.log(`
183
+ const assignedPort = typeof addr === "object" && addr ? addr.port : port;
184
+ const url = `http://localhost:${assignedPort}`;
185
+ console.log(`
347
186
  allegro-preview \u2192 ${url}
348
187
  `);
349
- void open(url);
188
+ void open(url);
189
+ resolvePromise();
190
+ });
191
+ });
350
192
  }
351
193
  export {
352
194
  startServer
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@allegrocdp/preview",
4
- "version": "0.1.0-dev.6",
4
+ "version": "0.1.0",
5
5
  "description": "Local dev server for previewing Allegro templates",
6
6
  "type": "module",
7
7
  "bin": {
@@ -15,9 +15,7 @@
15
15
  "build:client": "vite build",
16
16
  "build:cli": "tsup",
17
17
  "dev": "vite",
18
- "types": "tsc --noEmit",
19
- "format": "prettier --write src/",
20
- "format:check": "prettier --check src/"
18
+ "types": "tsc --noEmit"
21
19
  },
22
20
  "dependencies": {
23
21
  "chokidar": "^4.0.3",
@@ -29,8 +27,6 @@
29
27
  "@alleyinteractive/allegro-platform": "*",
30
28
  "@types/express": "^5.0.1",
31
29
  "@types/node": "^22.13.5",
32
- "prettier": "^3.4.2",
33
- "prettier-plugin-organize-imports": "^4.1.0",
34
30
  "tsup": "^8.4.0",
35
31
  "typescript": "^5.7.2",
36
32
  "vite": "^7.0.4"
@@ -1,137 +0,0 @@
1
- (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))o(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const a of s.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&o(a)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function o(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();function A(e,t){let n=e;for(const o of t){const i=new RegExp(`\\{%\\s*${o.slug}\\s*%\\}`,"g");n=n.replace(i,o.value)}return n}const O="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js",K={full:0,desktop:1280,tablet:768,mobile:390},Q=`
2
- *, *::before, *::after { box-sizing: border-box; }
3
- * { margin: 0; }
4
- body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.5; -webkit-font-smoothing: antialiased; }
5
- img, picture, video, canvas, svg { display: block; max-width: 100%; }
6
- input, button, textarea, select { font: inherit; }
7
- p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
8
- `,P=["The city council voted unanimously Tuesday to approve a sweeping new infrastructure plan that would reshape public transit routes across the metropolitan area over the next decade.","Mayor Jordan Ellison called the decision a turning point for how the region thinks about getting around, noting that similar investments in peer cities have led to measurable reductions in traffic congestion and improved air quality.","The plan includes upgrades to 14 existing bus rapid transit lines, the introduction of three new light-rail corridors connecting the downtown core to outlying neighborhoods, and dedicated cycling infrastructure along six major arterials.","Critics, however, warn that the projected cost of $2.4 billion may be optimistic. Independent analysts suggest the final figure could climb to $3.8 billion once land acquisition, labor, and contingency costs are factored in over the full construction timeline.","Council member Dana Reyes, who voted in favor, acknowledged the fiscal concern but argued that inaction carries its own price, noting that congestion costs residents and businesses hundreds of millions in lost productivity each year.","Environmental groups praised the announcement, saying expanded transit capacity is essential to meet the stated goal of reducing transportation emissions by 45 percent before 2035.","Public comment sessions are scheduled for next month at community centers in all twelve districts. Construction is expected to begin no sooner than mid-next year, pending environmental review and final design approvals."];function F(e){return e?`<script src="${e}"><\/script>`:""}function R(e,t,n,o){const i=JSON.stringify(e),s=JSON.stringify(t),a=JSON.stringify(n);return`(function() {
9
- var html = ${i};
10
- var css = ${s};
11
- var js = ${a};
12
-
13
- var host = document.createElement('div');
14
- host.setAttribute('data-allegro-preview', '');
15
- var shadow = host.attachShadow({ mode: 'open' });
16
-
17
- if (css) {
18
- var style = document.createElement('style');
19
- style.textContent = css;
20
- shadow.appendChild(style);
21
- }
22
-
23
- var container = document.createElement('div');
24
- container.setAttribute('x-data', JSON.stringify({ session: null, audience_member: null }));
25
- container.innerHTML = html;
26
- shadow.appendChild(container);
27
-
28
- if (js) {
29
- try { (new Function('shadowRoot', 'hostElement', js))(shadow, host); }
30
- catch(e) { console.error('[allegro-preview] JS error:', e); }
31
- }
32
-
33
- var article = document.getElementById('article-body');
34
- if (!article) return;
35
-
36
- var blockTags = new Set(['P','H1','H2','H3','H4','H5','H6','LI','BLOCKQUOTE','DIV','SECTION','FIGURE','PRE']);
37
- var blocks = Array.from(article.children).filter(function(c) { return blockTags.has(c.tagName); });
38
-
39
- ${o==="end"?"article.appendChild(host);":`var cutIndex = Math.min(${o}, blocks.length);
40
- var pivot = blocks[cutIndex - 1] || blocks[0];
41
- pivot.after(host);
42
- blocks.slice(cutIndex).forEach(function(el) { el.remove(); });`}
43
-
44
- if (window.Alpine && typeof window.Alpine.initTree === 'function') {
45
- window.Alpine.initTree(shadow);
46
- }
47
- })();`}function X(e,t,n){const o=JSON.stringify(e),i=JSON.stringify(t),s=JSON.stringify(n);return`(function() {
48
- var html = ${o};
49
- var css = ${i};
50
- var js = ${s};
51
-
52
- var host = document.createElement('div');
53
- host.setAttribute('data-allegro-preview', '');
54
- var shadow = host.attachShadow({ mode: 'open' });
55
-
56
- if (css) {
57
- var style = document.createElement('style');
58
- style.textContent = css;
59
- shadow.appendChild(style);
60
- }
61
-
62
- var container = document.createElement('div');
63
- container.setAttribute('x-data', JSON.stringify({ session: null, audience_member: null }));
64
- container.innerHTML = html;
65
- shadow.appendChild(container);
66
-
67
- if (js) {
68
- try { (new Function('shadowRoot', 'hostElement', js))(shadow, host); }
69
- catch(e) { console.error('[allegro-preview] JS error:', e); }
70
- }
71
-
72
- document.body.appendChild(host);
73
-
74
- if (window.Alpine && typeof window.Alpine.initTree === 'function') {
75
- window.Alpine.initTree(shadow);
76
- }
77
- })();`}function Z(e,t,n,o){const i=X(e,t,n);return`<!DOCTYPE html>
78
- <html>
79
- <head>
80
- <meta charset="UTF-8">
81
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
82
- <script>window.__ALLEGRO_PREVIEW__ = true;<\/script>
83
- ${F(o)}
84
- <script src="${O}"><\/script>
85
- <style>
86
- * { box-sizing: border-box; }
87
- body { margin: 0; }
88
- [data-allegro-preview] { display: block; }
89
- </style>
90
- </head>
91
- <body>
92
- <script>${i}<\/script>
93
- </body>
94
- </html>`}function _(e,t,n){const o=e.map(i=>`<p>${i}</p>`).join(`
95
- `);return`<!DOCTYPE html>
96
- <html>
97
- <head>
98
- <meta charset="UTF-8">
99
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
100
- ${F(n)}
101
- <script src="${O}"><\/script>
102
- <style>
103
- * { box-sizing: border-box; }
104
- *, *::before, *::after { box-sizing: border-box; }
105
- body { font-family: Georgia, 'Times New Roman', serif; background: #fff; color: #111;
106
- max-width: 680px; margin: 0 auto; padding: 32px 24px 64px; line-height: 1.75;
107
- -webkit-font-smoothing: antialiased; }
108
- h1 { font-size: 1.6rem; line-height: 1.25; margin: 0 0 6px; font-weight: 700; }
109
- .meta { color: #6b7280; font-size: 0.82rem; font-family: system-ui, sans-serif; margin-bottom: 24px; }
110
- p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
111
- [data-allegro-preview] { display: block; margin: 24px 0; }
112
- </style>
113
- </head>
114
- <body>
115
- <header>
116
- <h1>Council Approves Decade-Long Transit Overhaul</h1>
117
- <p class="meta">By Staff Reporter &nbsp;&middot;&nbsp; March 14, 2026</p>
118
- </header>
119
- <article id="article-body">
120
- ${o}
121
- </article>
122
- <script>${t}<\/script>
123
- </body>
124
- </html>`}function ee(e,t,n){const o=A(e.html,t);return Z(o,Q+e.css,e.js,n)}function te(e,t,n){const o=A(e.html,t),i=R(o,e.css,e.js,"end");return _(P,i,n)}function ne(e,t,n){const o=A(e.html,t),i=R(o,e.css,e.js,3);return _(P,i,n)}function oe(e){return K[e]}let c=[],u=null,b="standalone",w="full",m=null,v={},S;const M=document.getElementById("template-nav"),p=document.getElementById("field-list"),d=document.getElementById("preview-area"),I=document.getElementById("tab-group"),T=document.getElementById("device-group"),k=document.getElementById("toolbar-badge"),ie=document.getElementById("theme-toggle"),ae=document.getElementById("theme-icon-sun"),se=document.getElementById("theme-icon-moon"),L=document.getElementById("tenant-input"),C=document.getElementById("tenant-status"),re=document.getElementById("new-template-btn"),E=document.getElementById("create-modal-backdrop"),l=document.getElementById("modal-slug-input"),f=document.getElementById("modal-error"),le=document.getElementById("modal-cancel-btn"),y=document.getElementById("modal-create-btn");function J(e){document.documentElement.setAttribute("data-theme",e?"dark":"light"),ae.style.display=e?"none":"",se.style.display=e?"":"none"}function ce(){const e=localStorage.getItem("allegro-preview-theme"),t=window.matchMedia("(prefers-color-scheme: dark)").matches;J(e?e==="dark":t)}ie.addEventListener("click",()=>{const e=document.documentElement.getAttribute("data-theme")!=="dark";J(e),localStorage.setItem("allegro-preview-theme",e?"dark":"light")});function de(e){const t=e.trim();return t?(/^https?:\/\//.test(t)?t:`https://${t}`).replace(/\/+$/,"")+"/client.js":""}function D(e){const t=de(e);S=t||void 0,t?(C.className="tenant-active-badge",C.textContent=t):(C.className="tenant-hint",C.textContent="Loads /client.js on the preview page"),m&&h()}L.addEventListener("input",()=>{localStorage.setItem("allegro-preview-tenant",L.value),D(L.value)});function ue(e=""){const n=localStorage.getItem("allegro-preview-tenant")??e;n&&(L.value=n,D(n))}function pe(){const e=window.location.hash.slice(1);return e?decodeURIComponent(e):null}function me(e){history.replaceState(null,"",`#${encodeURIComponent(e)}`)}function fe(){history.replaceState(null,"",window.location.pathname)}function q(e){I.querySelectorAll(".tab-btn").forEach(t=>{t.disabled=!e}),T.querySelectorAll(".device-btn").forEach(t=>{t.disabled=!e})}I.addEventListener("click",e=>{const t=e.target.closest(".tab-btn");!t||t.disabled||(b=t.dataset.tab,I.querySelectorAll(".tab-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-tab",b),h())});T.addEventListener("click",e=>{const t=e.target.closest(".device-btn");!t||t.disabled||(w=t.dataset.device,T.querySelectorAll(".device-btn").forEach(n=>n.classList.remove("active")),t.classList.add("active"),localStorage.setItem("allegro-preview-device",w),h())});function x(){M.innerHTML="";for(const e of c){const t=document.createElement("button");t.className="template-nav-item"+(e.slug===u?" active":""),t.innerHTML=`<span class="template-nav-dot"></span><span class="template-nav-name">${j(e.name)}</span>`,t.addEventListener("click",()=>void B(e.slug)),M.appendChild(t)}}async function B(e){u=e,me(e),x(),await H(e)}function z(){m=null,u=null,fe(),x(),q(!1),k.textContent="No template selected",k.style.opacity="0.4",p.innerHTML='<p class="no-fields">Select a template to edit fields.</p>';const e=document.createElement("div");if(e.className="selector-screen",c.length===0){e.innerHTML=`
125
- <div class="selector-empty">
126
- <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
127
- <span>No templates found in this directory.</span>
128
- </div>`,d.innerHTML="",d.appendChild(e);return}e.innerHTML=`
129
- <div class="selector-eyebrow">Allegro Preview</div>
130
- <h2 class="selector-heading">Choose a template</h2>
131
- <p class="selector-sub">Select a template to start previewing.</p>
132
- <div class="selector-grid" id="selector-grid"></div>`,d.innerHTML="",d.appendChild(e);const t=e.querySelector("#selector-grid");for(const n of c){const o=document.createElement("button");o.className="selector-card",o.innerHTML=`
133
- <div class="selector-card-icon">
134
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
135
- </div>
136
- <div class="selector-card-name">${j(n.name)}</div>
137
- <div class="selector-card-slug">${j(n.slug)}</div>`,o.addEventListener("click",()=>void B(n.slug)),t.appendChild(o)}}const N=["text","number","checkbox","textarea"];function U(e){if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.slug=="string"&&t.slug.length>0&&N.includes(t.type)}function he(e){if(typeof e!="object"||e===null)return`Invalid field: expected an object, got ${JSON.stringify(e)}.`;const t=e,n=[];if((typeof t.slug!="string"||t.slug.length===0)&&n.push('missing required "slug" property'),!N.includes(t.type)){const i=t.type!==void 0?`"${String(t.type)}"`:"none";n.push(`invalid type ${i} — valid types: ${N.join(", ")}`)}return`${typeof t.slug=="string"&&t.slug.length>0?`Field "${t.slug}"`:"Field"}: ${n.join("; ")}.`}function ge(e){v={};for(const t of e)U(t)&&(v[t.slug]=t.default_value??"")}function ve(e,t){if(p.innerHTML="",t){const n=document.createElement("div");n.className="field-error",n.textContent=`template.json parse error: ${t}`,p.appendChild(n);return}if(e.length===0){p.innerHTML='<p class="no-fields">No fields in template.json.</p>';return}for(const n of e){if(!U(n)){const a=document.createElement("div");a.className="field-error",a.textContent=he(n),p.appendChild(a);continue}const o=n;if(o.type==="checkbox"){const a=document.createElement("div");a.className="field-checkbox-row";const r=document.createElement("input");r.type="checkbox",r.id=`field-${o.slug}`,r.checked=o.default_value==="true"||o.default_value==="1",r.addEventListener("change",()=>{v[o.slug]=r.checked?"true":"false",h()});const g=document.createElement("label");g.className="field-label",g.htmlFor=`field-${o.slug}`,g.textContent=o.label||o.slug,a.appendChild(r),a.appendChild(g),p.appendChild(a);continue}const i=document.createElement("div");i.className="field-group";const s=document.createElement("label");if(s.className="field-label",s.htmlFor=`field-${o.slug}`,s.textContent=o.label||o.slug,i.appendChild(s),o.type==="textarea"){const a=document.createElement("textarea");a.className="field-input",a.id=`field-${o.slug}`,a.rows=3,a.value=o.default_value??"",a.addEventListener("input",()=>{v[o.slug]=a.value,h()}),i.appendChild(a)}else{const a=document.createElement("input");a.type=o.type==="number"?"number":"text",a.className="field-input",a.id=`field-${o.slug}`,a.value=o.default_value??"",a.addEventListener("input",()=>{v[o.slug]=a.value,h()}),i.appendChild(a)}p.appendChild(i)}}function h(){if(!m)return;const e=Object.entries(v).map(([g,Y])=>({slug:g,value:Y})),t=oe(w),n=t===0,o=b==="standalone"?ee(m,e,S):b==="append"?te(m,e,S):ne(m,e,S);d.innerHTML="";const i=document.createElement("div");i.className=n?"preview-stage":"preview-stage preview-stage--breakpoint";const s=document.createElement("div");s.className="preview-scaler";const a=document.createElement("div");a.className="preview-frame-wrapper";const r=document.createElement("iframe");r.style.height="calc(100vh - 48px)",w==="mobile"&&(r.style.maxHeight="812px"),r.setAttribute("sandbox","allow-scripts allow-same-origin"),r.srcdoc=o,n?(a.style.width="100%",r.style.width="100%",s.style.width="100%"):(a.style.width=`${t}px`,r.style.width=`${t}px`),a.appendChild(r),s.appendChild(a),i.appendChild(s),d.appendChild(i)}async function H(e){try{const t=await fetch(`/api/template?slug=${encodeURIComponent(e)}`);if(!t.ok)throw new Error(`HTTP ${t.status}`);const n=await t.json();m=n,k.textContent=`Template: ${n.name}`,k.style.opacity="1",q(!0),ge(n.fields),ve(n.fields,n.fieldsParseError),h()}catch(t){console.error("[allegro-preview] Failed to load template:",t),d.innerHTML="";const n=document.createElement("div");n.style.padding="40px",n.style.color="var(--text-muted)",n.style.fontSize="13px",n.textContent=`Failed to load template "${e}".`,d.appendChild(n)}}async function ye(){const t=await(await fetch("/api/config")).json();c=t.templates,ue(t.tenant??"");const n=pe(),o=n&&c.find(i=>i.slug===n)?n:null;o?(u=o,x(),await H(o)):z()}function be(){const e=localStorage.getItem("allegro-preview-tab"),t=localStorage.getItem("allegro-preview-device");e&&["standalone","append","cut"].includes(e)&&(b=e,I.querySelectorAll(".tab-btn").forEach(n=>{n.classList.toggle("active",n.dataset.tab===e)})),t&&["full","desktop","tablet","mobile"].includes(t)&&(w=t,T.querySelectorAll(".device-btn").forEach(n=>{n.classList.toggle("active",n.dataset.device===t)}))}function V(){const e=new EventSource("/api/events");e.addEventListener("reload",()=>{u&&H(u)}),e.addEventListener("config",()=>{we()}),e.onerror=()=>{e.close(),setTimeout(V,2e3)}}async function we(){c=(await(await fetch("/api/config")).json()).templates,x(),u&&!c.find(n=>n.slug===u)&&z()}function j(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const Ee=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;function xe(){l.value="",f.textContent="",l.classList.remove("invalid"),y.disabled=!1,E.classList.remove("hidden"),l.focus()}function $(){E.classList.add("hidden")}function G(e){return e?Ee.test(e)?null:"Use lowercase letters, numbers, and hyphens only (e.g. my-template).":"Slug is required."}re.addEventListener("click",xe);le.addEventListener("click",$);E.addEventListener("click",e=>{e.target===E&&$()});document.addEventListener("keydown",e=>{e.key==="Escape"&&!E.classList.contains("hidden")&&$()});l.addEventListener("input",()=>{const e=G(l.value);e?(l.classList.add("invalid"),f.textContent=e):(l.classList.remove("invalid"),f.textContent="")});l.addEventListener("keydown",e=>{e.key==="Enter"&&W()});y.addEventListener("click",()=>void W());async function W(){const e=l.value.trim(),t=G(e);if(t){l.classList.add("invalid"),f.textContent=t,l.focus();return}y.disabled=!0,f.textContent="";try{const n=await fetch("/api/template/create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:e})}),o=await n.json();if(!n.ok){f.textContent=o.error??"Failed to create template.",y.disabled=!1;return}$(),c=[...c,{slug:e,name:e}],x(),await B(e)}catch{f.textContent="Network error. Please try again.",y.disabled=!1}}ce();be();ye();V();