diff --git a/bliss-cli/live-editor/slideout.eta b/bliss-cli/live-editor/slideout.eta index 4acf7b3..e3fe878 100644 --- a/bliss-cli/live-editor/slideout.eta +++ b/bliss-cli/live-editor/slideout.eta @@ -7,33 +7,61 @@
-
Select a route or template to edit it.
+
Select a route or template to edit it, or use the โœ๏ธ ๐Ÿ“„ ๐Ÿ“‹ ๐Ÿ‘ฏ buttons on the page.
diff --git a/public/js/bliss_inspector.js b/public/js/bliss_inspector.js index 7f68b6c..5b76cea 100644 --- a/public/js/bliss_inspector.js +++ b/public/js/bliss_inspector.js @@ -39,7 +39,8 @@ function contextIdFor(element, templateId) { try { const ids = JSON.parse(element.dataset.blissTemplateContextIds || "{}"); - if (Object.hasOwn(ids, String(templateId))) return ids[String(templateId)]; + if (Object.hasOwn(ids, String(templateId))) + return ids[String(templateId)]; } catch (_) {} return element.dataset.blissTemplateContextId || null; } @@ -116,7 +117,8 @@ function syncAttributes(element, next) { for (const attribute of [...element.attributes]) { - if (!next.hasAttribute(attribute.name)) element.removeAttribute(attribute.name); + if (!next.hasAttribute(attribute.name)) + element.removeAttribute(attribute.name); } for (const attribute of [...next.attributes]) { if (element.getAttribute(attribute.name) !== attribute.value) { @@ -144,7 +146,8 @@ return; } if (current.nodeType !== Node.ELEMENT_NODE) { - if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue; + if (current.nodeValue !== next.nodeValue) + current.nodeValue = next.nodeValue; return; } @@ -178,7 +181,8 @@ } for (const child of currentChildren) { - if (!used.has(child) && child.isConnected && !keepLiveNode(child)) child.remove(); + if (!used.has(child) && child.isConnected && !keepLiveNode(child)) + child.remove(); } } @@ -210,7 +214,8 @@ // A page root must be fetched as a document. Asking for an HTMX // fragment here used to parse an empty and erase the page. const response = await fetch(url); - if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`); + if (!response.ok) + throw new Error(`GET ${url} returned ${response.status}`); await replaceBody(await response.text()); return; } @@ -230,7 +235,11 @@ }); } - async function refreshTemplateElement(element, templateId, previousRoots = []) { + async function refreshTemplateElement( + element, + templateId, + previousRoots = [], + ) { const response = await fetch("/_bliss/render-template", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -244,7 +253,24 @@ throw new Error(`Template ${templateId} returned ${response.status}`); } const html = await response.text(); - if (element === document.body) return replaceBody(html, { morph: true }); + // The render-template endpoint re-stamps template provenance but not the + // route/structure provenance (that comes from the route pipeline). Carry it + // over from the element being replaced so the inspector overlay and tree + // stay complete after a live template edit, instead of losing the element's + // route/copy/clone identity. + const provenance = [...element.attributes] + .filter((attribute) => attribute.name.startsWith("data-bliss-")) + .map((attribute) => ({ name: attribute.name, value: attribute.value })); + const carry = (target) => { + for (const { name, value } of provenance) { + if (!target.hasAttribute(name)) target.setAttribute(name, value); + } + }; + if (element === document.body) { + await replaceBody(html, { morph: true }); + carry(document.body); + return; + } const fragment = document.createElement("template"); fragment.innerHTML = html; // A template may legally have several top-level nodes. They share one @@ -253,6 +279,7 @@ for (const root of previousRoots) { if (root !== element && root.isConnected) root.remove(); } + for (const root of fragment.content.children) carry(root); element.replaceWith(fragment.content); htmx.process(document.body); } @@ -328,6 +355,11 @@ [data-bliss-structure-id], [data-bliss-template-id] { transition: outline-color 80ms linear, background-color 80ms linear; } + html.bliss-inspector-open [data-bliss-structure-id], + html.bliss-inspector-open [data-bliss-template-id] { + outline: 1px dashed rgba(255, 0, 168, .5); + outline-offset: -1px; + } .bliss-source-highlight { outline: 4px solid #ff00a8 !important; outline-offset: 3px !important; @@ -346,8 +378,12 @@ document.body.appendChild(trigger); }); - document.addEventListener("bliss:highlight", (event) => highlight(event.detail)); - document.addEventListener("bliss:source-saved", (event) => refreshSources(event.detail)); + document.addEventListener("bliss:highlight", (event) => + highlight(event.detail), + ); + document.addEventListener("bliss:source-saved", (event) => + refreshSources(event.detail), + ); document.addEventListener("bliss:inventory-request", publishInventory); document.addEventListener("bliss:inspector-close", hide); document.addEventListener("htmx:afterSwap", () => {