feat: slideout editor new new new!

This commit is contained in:
Your Name 2026-08-03 01:24:54 -04:00
parent c175a610da
commit 48ac1249c8
10 changed files with 482 additions and 112 deletions

View file

@ -0,0 +1,3 @@
function handler(req, res) {
res.render("inspector/slideout", {});
}

View file

@ -0,0 +1,79 @@
#!/usr/bin/env node
// Install/update the live editor using Bliss's public CLI workflow. This file
// intentionally does not open the Bliss database directly.
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const repo = path.resolve(__dirname, "../..");
const cli = path.join(repo, "bliss-cli/bliss");
function bliss(...args) {
const result = spawnSync(process.execPath, [cli, ...args], {
cwd: repo,
env: process.env,
encoding: "utf8",
});
if (result.status !== 0) throw new Error(result.stderr || result.stdout);
return result.stdout.trim();
}
function json(...args) {
return JSON.parse(bliss(...args));
}
const structures = json("structures");
let structure = structures.find((item) => item.name === "__bliss_live_editor");
if (!structure) {
const created = json("create-structure", "__bliss_live_editor");
structure = { id: created.id };
}
const structureId = String(structure.id);
bliss("update-settings", structureId, "--route-prefix", "/_bliss");
let state = json("structure", structureId);
if (!state.dbs.some((db) => String(db.id) === "0" && db.alias === "bliss")) {
bliss("attach-db", structureId, "0", "bliss");
}
const templates = [
["inspector/slideout", "slideout.eta"],
["inspector/route_editor", "route-editor.eta"],
["inspector/template_editor", "template-editor.eta"],
];
for (const [name, filename] of templates) {
let template = state.templates.find((item) => item.name === name);
if (!template) {
template = json("create-template", structureId, name);
}
bliss(
"update-template",
structureId,
String(template.id),
"--content-file",
path.join(__dirname, filename),
);
}
const routes = [
["/inspector", "inspector.js"],
["/editor/route/:id", "route-editor.js"],
["/editor/template/:id", "template-editor.js"],
];
for (const [routePath, filename] of routes) {
let route = state.routes.find(
(item) => item.verb === "GET" && item.path === routePath,
);
if (!route) {
route = json("create-route", structureId, "GET", routePath);
}
bliss(
"update-route",
structureId,
String(route.id),
"--handler-file",
path.join(__dirname, filename),
);
}
console.log(`live editor installed as Structure ${structureId}`);

View file

@ -0,0 +1,28 @@
<div data-bliss-ui>
<div class="mb-2 flex items-center justify-between border-b-4 border-double border-black pb-2"><strong><%= it.sourceRoute.verb %> <%= it.sourceRoute.path %></strong><span data-status class="text-xs"></span></div>
<% if (it.sourceRoute.verb !== "GET") { %>
<div class="mb-2 rounded border border-amber-600 bg-amber-950 p-2 text-amber-200">Rerunning this <%= it.sourceRoute.verb %> request can repeat writes or external side effects. Bliss will not rerun it automatically.</div>
<% } %>
<div data-source class="h-[55vh] w-full border-2 border-black"><%= it.sourceRoute.handler %></div>
<button data-save type="button" class="mt-2 border-2 border-black bg-white px-3 py-2 font-bold shadow-[3px_3px_0_#000] active:translate-x-0.5 active:translate-y-0.5 active:shadow-none">Save Route</button>
</div>
<script>
(function () {
const box = document.currentScript.previousElementSibling;
if (!box) return;
const editor = ace.edit(box.querySelector("[data-source]"));
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/javascript");
box.querySelector("[data-save]").onclick = async () => {
const status = box.querySelector("[data-status]");
status.textContent = "Saving…";
const response = await fetch("/workshop/<%= it.sourceRoute.structure_id %>/route/<%= it.sourceRoute.id %>", {
method: "PUT", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handler: editor.getValue() })
});
if (!response.ok) return void (status.textContent = "Save failed");
status.textContent = "Saved";
document.dispatchEvent(new CustomEvent("bliss:source-saved", { detail: { kind: "route", id: "<%= it.sourceRoute.id %>", method: "<%= it.sourceRoute.verb %>" } }));
};
})();
</script>

View file

@ -0,0 +1,8 @@
function handler(req, res) {
const sql = require("db")("bliss").sql;
const sourceRoute = sql
.prepare("SELECT * FROM routes WHERE id = ?")
.get(req.params.id);
if (!sourceRoute) return res.status(404).send("Route not found");
res.render("inspector/route_editor", { sourceRoute });
}

View file

@ -0,0 +1,76 @@
<aside id="bliss-live-editor" data-bliss-ui
class="fixed inset-y-0 left-0 z-[2147483646] w-[min(28rem,90vw)] overflow-auto border-r-4 border-black bg-white text-black shadow-[8px_0_0_#000]"
style="font-family:ui-sans-serif,system-ui">
<header class="sticky top-0 flex items-center justify-between border-b-4 border-double border-black bg-white p-3">
<div><div class="text-lg font-black tracking-tight">Live Page</div><div class="text-xs">Objects currently on this card</div></div>
<button type="button" data-bliss-close class="h-7 w-7 border-2 border-black bg-white text-lg font-black leading-none shadow-[2px_2px_0_#000] active:translate-x-0.5 active:translate-y-0.5 active:shadow-none">×</button>
</header>
<div class="grid min-h-[calc(100vh-61px)] grid-rows-[auto_1fr]">
<nav data-bliss-tree class="border-b-4 border-double border-black p-2"></nav>
<section data-bliss-editor class="min-h-0 p-3 text-sm">Select a route or template to edit it.</section>
</div>
</aside>
<script>
(function () {
const root = document.getElementById("bliss-live-editor");
if (!root) return;
const tree = root.querySelector("[data-bliss-tree]");
const editor = root.querySelector("[data-bliss-editor]");
root.querySelector("[data-bliss-close]").onclick = () =>
document.dispatchEvent(new CustomEvent("bliss:inspector-close"));
function sourceButton(label, kind, id, count) {
const button = document.createElement("button");
button.type = "button";
button.className = "block w-full border border-transparent px-2 py-1 text-left hover:border-black hover:bg-black hover:text-white";
button.textContent = label + (count > 1 ? ` (${count})` : "");
button.onmouseenter = () => document.dispatchEvent(new CustomEvent("bliss:highlight", { detail: { kind, id, on: true } }));
button.onmouseleave = () => document.dispatchEvent(new CustomEvent("bliss:highlight", { detail: { kind, id, on: false } }));
if (kind !== "structure") button.onclick = () => {
editor.textContent = "Loading…";
htmx.ajax("GET", `/_bliss/editor/${kind}/${id}`, { target: editor, swap: "innerHTML" });
};
return button;
}
function render(items) {
tree.innerHTML = "";
const structures = new Map();
for (const item of items) {
if (!structures.has(item.structureId)) structures.set(item.structureId, { name: item.structureName, routes: new Map(), templates: new Map(), count: 0 });
const structure = structures.get(item.structureId);
structure.count++;
if (!structure.routes.has(item.routeId)) structure.routes.set(item.routeId, { name: item.routeName, count: 0 });
structure.routes.get(item.routeId).count++;
if (item.templateId) {
if (!structure.templates.has(item.templateId)) structure.templates.set(item.templateId, { name: item.templateName, count: 0 });
structure.templates.get(item.templateId).count++;
}
}
for (const [structureId, structure] of structures) {
const group = document.createElement("details");
group.open = true;
const heading = document.createElement("summary");
heading.className = "cursor-pointer list-none font-semibold";
heading.append(sourceButton(structure.name, "structure", structureId, structure.count));
group.append(heading);
for (const [label, kind, entries] of [["Routes", "route", structure.routes], ["Templates", "template", structure.templates]]) {
if (!entries.size) continue;
const section = document.createElement("details");
section.open = true;
section.className = "ml-3";
const summary = document.createElement("summary");
summary.className = "cursor-pointer py-1 text-xs font-bold uppercase tracking-widest";
summary.textContent = label;
section.append(summary);
for (const [id, entry] of entries) section.append(sourceButton(entry.name, kind, id, entry.count));
group.append(section);
}
tree.append(group);
}
}
document.addEventListener("bliss:inventory", (event) => render(event.detail));
document.dispatchEvent(new CustomEvent("bliss:inventory-request"));
})();
</script>

View file

@ -0,0 +1,25 @@
<div data-bliss-ui>
<div class="mb-2 flex items-center justify-between border-b-4 border-double border-black pb-2"><strong><%= it.sourceTemplate.name %></strong><span data-status class="text-xs"></span></div>
<div data-source class="h-[55vh] w-full border-2 border-black"><%= it.sourceTemplate.content %></div>
<button data-save type="button" class="mt-2 border-2 border-black bg-white px-3 py-2 font-bold shadow-[3px_3px_0_#000] active:translate-x-0.5 active:translate-y-0.5 active:shadow-none">Save Template</button>
</div>
<script>
(function () {
const box = document.currentScript.previousElementSibling;
if (!box) return;
const editor = ace.edit(box.querySelector("[data-source]"));
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/html");
box.querySelector("[data-save]").onclick = async () => {
const status = box.querySelector("[data-status]");
status.textContent = "Saving…";
const response = await fetch("/workshop/<%= it.sourceTemplate.structure_id %>/template/<%= it.sourceTemplate.id %>", {
method: "PUT", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: editor.getValue() })
});
if (!response.ok) return void (status.textContent = "Save failed");
status.textContent = "Saved";
document.dispatchEvent(new CustomEvent("bliss:source-saved", { detail: { kind: "template", id: "<%= it.sourceTemplate.id %>" } }));
};
})();
</script>

View file

@ -0,0 +1,8 @@
function handler(req, res) {
const sql = require("db")("bliss").sql;
const sourceTemplate = sql
.prepare("SELECT * FROM templates WHERE id = ?")
.get(req.params.id);
if (!sourceTemplate) return res.status(404).send("Template not found");
res.render("inspector/template_editor", { sourceTemplate });
}

47
db.js
View file

@ -3,6 +3,7 @@ const path = require("path");
const betterSqlite3 = require("better-sqlite3"); const betterSqlite3 = require("better-sqlite3");
const { LRUCache } = require("lru-cache"); const { LRUCache } = require("lru-cache");
const { Eta } = require("eta"); const { Eta } = require("eta");
const cheerio = require("cheerio");
const db = betterSqlite3("./dbs/0.sqlite"); const db = betterSqlite3("./dbs/0.sqlite");
@ -91,6 +92,37 @@ function getAllRoutes() {
const templateCache = new LRUCache({ max: 100 }); const templateCache = new LRUCache({ max: 100 });
function annotateTemplateHtml(html, template) {
function annotate($, element) {
const current = $(element).attr("data-bliss-templates");
let templates = [];
if (current) {
try {
templates = JSON.parse(current);
} catch (_) {}
}
if (!templates.some((item) => String(item.id) === String(template.id))) {
templates.push({ id: String(template.id), name: template.name });
}
$(element).attr("data-bliss-templates", JSON.stringify(templates));
if (!$(element).attr("data-bliss-template-id")) {
$(element).attr({
"data-bliss-template-id": String(template.id),
"data-bliss-template-name": template.name,
});
}
}
const lower = html.trimStart().toLowerCase();
if (lower.startsWith("<html") || lower.startsWith("<!doctype")) {
const $ = cheerio.load(html);
annotate($, $("body")[0]);
return $.html();
}
const $ = cheerio.load(html, null, false);
for (const child of $.root().children()) annotate($, child);
return $.html();
}
function getTemplater(structId) { function getTemplater(structId) {
let etaInstance = templateCache.get(structId); let etaInstance = templateCache.get(structId);
@ -102,6 +134,13 @@ function getTemplater(structId) {
etaInstance.readFile = function (templateAlias) { etaInstance.readFile = function (templateAlias) {
return getTemplateContentByName(structId, templateAlias).content; return getTemplateContentByName(structId, templateAlias).content;
}; };
const etaRender = etaInstance.render;
etaInstance.render = function (templateName, data, meta) {
const html = etaRender.call(this, templateName, data, meta);
if (typeof templateName !== "string") return html;
const template = getTemplateByName(structId, templateName);
return template ? annotateTemplateHtml(html, template) : html;
};
templateCache.set(structId, etaInstance); templateCache.set(structId, etaInstance);
} }
@ -245,6 +284,7 @@ function updateStruct(struct) {
function updateTemplate(template) { function updateTemplate(template) {
update("templates", ["content", "name", "test_object"], template); update("templates", ["content", "name", "test_object"], template);
templateCache.delete(template.structure_id);
recordVersion("template", template.id, template.structure_id, template); recordVersion("template", template.id, template.structure_id, template);
} }
@ -266,6 +306,12 @@ function getTemplateContentByName(structId, name) {
.get(structId, name); .get(structId, name);
} }
function getTemplateByName(structId, name) {
return db
.prepare("SELECT * from templates where structure_id = ? AND name = ?")
.get(structId, name);
}
function createTemplate(structureId, name, content, testObjectString) { function createTemplate(structureId, name, content, testObjectString) {
const id = db const id = db
.prepare( .prepare(
@ -686,6 +732,7 @@ module.exports = {
getTemplates, getTemplates,
getTemplate, getTemplate,
getTemplateContentByName, getTemplateContentByName,
getTemplateByName,
createTemplate, createTemplate,
getDbsForStructure, getDbsForStructure,
getDb, getDb,

View file

@ -16,7 +16,7 @@ const app = express();
const _expressWs = require("express-ws")(app); const _expressWs = require("express-ws")(app);
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
const model = require("./db"); const model = require("./db");
const PORT = 3000; const PORT = process.env.PORT || 3000;
const db = model.db; const db = model.db;
const wsRouter = express.Router() const wsRouter = express.Router()
@ -340,6 +340,7 @@ function headChrome(headInjection) {
<script src="/js/hyperscript.js"></script> <script src="/js/hyperscript.js"></script>
<script src="/js/tailwind.js"></script> <script src="/js/tailwind.js"></script>
<script src="/js/htmx.js"></script> <script src="/js/htmx.js"></script>
<script src="/js/ace/ace.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/ws.js"></script> <script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/ws.js"></script>
<script src="/js/bliss_inspector.js"></script> <script src="/js/bliss_inspector.js"></script>
<script> <script>
@ -362,7 +363,17 @@ function blissAttrs(source) {
const attrs = { const attrs = {
"data-bliss-route": `/workshop/${source.structureId}/route/${source.routeId}`, "data-bliss-route": `/workshop/${source.structureId}/route/${source.routeId}`,
"data-bliss-clone": `/workshop/${source.structureId}/clone_modal/`, "data-bliss-clone": `/workshop/${source.structureId}/clone_modal/`,
"data-bliss-structure-id": String(source.structureId),
"data-bliss-structure-name": source.structureName,
"data-bliss-route-id": String(source.routeId),
"data-bliss-route-name": `${source.verb} ${source.path}`,
"data-bliss-method": source.verb,
"data-bliss-request-url": source.requestUrl,
}; };
if (source.templateId) {
attrs["data-bliss-template-id"] = String(source.templateId);
attrs["data-bliss-template-name"] = source.templateName;
}
if (source.copyUrl) attrs["data-bliss-copy"] = source.copyUrl; if (source.copyUrl) attrs["data-bliss-copy"] = source.copyUrl;
return attrs; return attrs;
} }
@ -435,10 +446,20 @@ function renderTemplate(structureId, templateName, context) {
// Provenance for a rendered response: GET routes are re-embeddable, so they // Provenance for a rendered response: GET routes are re-embeddable, so they
// carry the copy-embed snippet; other verbs don't. // carry the copy-embed snippet; other verbs don't.
function sourceFor(route, req) { function sourceFor(route, req, templateName = null) {
const structure = model.getStructure(route.structure_id);
const template = templateName
? model.getTemplateByName(route.structure_id, templateName)
: null;
return { return {
structureId: route.structure_id, structureId: route.structure_id,
structureName: structure.name,
routeId: route.id, routeId: route.id,
verb: route.verb,
path: route.path,
requestUrl: req.originalUrl,
templateId: template?.id,
templateName: template?.name,
copyUrl: route.verb === "GET" ? embedHTML(req.originalUrl) : null, copyUrl: route.verb === "GET" ? embedHTML(req.originalUrl) : null,
}; };
} }
@ -862,7 +883,7 @@ app.all("*", async (req, res) => {
res.send( res.send(
decorate(renderTemplate(route.structure_id, template, context), { decorate(renderTemplate(route.structure_id, template, context), {
headInjection: structure.head_injection, headInjection: structure.head_injection,
source: sourceFor(route, req), source: sourceFor(route, req, template),
fragment: req.headers["hx-request"], fragment: req.headers["hx-request"],
}), }),
); );

View file

@ -1,120 +1,195 @@
document.addEventListener("DOMContentLoaded", function () { (function () {
// Create the magnifying glass emoji button if (window.__blissInspectorLoaded) return;
const magnifyingGlass = document.createElement("div"); window.__blissInspectorLoaded = true;
magnifyingGlass.innerHTML = "🔍";
magnifyingGlass.className =
"fixed bottom-2 right-2 cursor-pointer text-2xl z-50";
document.body.appendChild(magnifyingGlass);
let highlighted = false; const ROUTE_SELECTOR = "[data-bliss-structure-id][data-bliss-route-id]";
const TEMPLATE_SELECTOR = "[data-bliss-template-id]";
let root = null;
let open = false;
// Function to highlight elements function visibleElements(selector) {
function highlightElements() { return [...document.querySelectorAll(selector)].filter(
const modal = document.createElement("div"); (element) => !element.closest("[data-bliss-ui]"),
modal.id = "inspector-modal";
document.body.appendChild(modal);
const elements = document.querySelectorAll("[data-bliss-route]");
elements.forEach((el) => {
if (!el.classList.contains("highlighted")) {
el.classList.add(
"border-2",
"border-yellow-500",
"highlighted",
"p-2",
); );
let controls = document.createElement("div");
controls.className =
"flex absolute gap-1 -bottom-3 right-0 inspector-controls";
// Create the edit icon
const editIcon = document.createElement("a");
editIcon.href = el.getAttribute("data-bliss-route");
editIcon.innerHTML = "✏️";
editIcon.className =
"text-lg bg-white rounded-full p-1 shadow edit-icon";
controls.appendChild(editIcon);
// Create the copy icon
if (el.getAttribute("data-bliss-copy")) {
const copyIcon = document.createElement("button");
copyIcon.innerHTML = "📋";
copyIcon.className =
"text-lg bg-white rounded-full p-1 shadow copy-icon";
controls.appendChild(copyIcon);
copyIcon.addEventListener("click", () => {
navigator.clipboard
.writeText(el.getAttribute("data-bliss-copy"))
.then(() => {
copyIcon.innerHTML = "✅";
})
.catch((err) => {
copyIcon.innerHTML = "❌";
});
});
} }
// Create the clone icon function routeElements() {
const cloneIcon = document.createElement("div"); return visibleElements(ROUTE_SELECTOR);
cloneIcon.innerHTML = "👯‍♀️"; }
cloneIcon.className =
"text-lg bg-white rounded-full cursor-pointer p-1 shadow clone-icon";
cloneIcon.setAttribute("hx-get", el.getAttribute("data-bliss-clone"));
cloneIcon.setAttribute("hx-target", "#inspector-modal");
controls.setAttribute( function templateElements() {
"_", return visibleElements(TEMPLATE_SELECTOR);
`on mouseover set my.style.zIndex to 10000 }
on mouseout set my.style.zIndex to "initial"
`, function templatesFor(element) {
try {
const templates = JSON.parse(element.dataset.blissTemplates || "[]");
if (templates.length) return templates;
} catch (_) {}
return element.dataset.blissTemplateId
? [
{
id: element.dataset.blissTemplateId,
name: element.dataset.blissTemplateName,
},
]
: [];
}
function inventory() {
const items = [];
for (const owner of routeElements()) {
const templates = templateElements().filter(
(element) => element.closest(ROUTE_SELECTOR) === owner,
); );
controls.appendChild(cloneIcon); if (!templates.length) templates.push(null);
for (const templateElement of templates) {
el.appendChild(controls); const renderedTemplates = templateElement
htmx.process(controls); ? templatesFor(templateElement)
_hyperscript.processNode(controls); : [null];
} for (const template of renderedTemplates) {
items.push({
structureId: owner.dataset.blissStructureId,
structureName: owner.dataset.blissStructureName,
routeId: owner.dataset.blissRouteId,
routeName: owner.dataset.blissRouteName,
templateId: template?.id || null,
templateName: template?.name || null,
}); });
} }
}
}
return items;
}
// Function to remove highlights function publishInventory() {
function removeHighlights() { document.dispatchEvent(
const elements = document.querySelectorAll( new CustomEvent("bliss:inventory", { detail: inventory() }),
"[data-bliss-route].highlighted",
); );
elements.forEach((el) => { }
el.classList.remove(
"border-2", function matches(element, { kind, id }) {
"border-yellow-500", if (kind === "template") {
"relative", return templatesFor(element).some(
"highlighted", (template) => String(template.id) === String(id),
"p-2",
); );
el.querySelectorAll(".inspector-controls").forEach((el) => el.remove()); }
}); const field = {
structure: "blissStructureId",
route: "blissRouteId",
}[kind];
return field && element.dataset[field] === String(id);
} }
magnifyingGlass.addEventListener("click", function () { function highlight(detail) {
if (!highlighted) { const candidates =
highlightElements(); detail.kind === "template" ? templateElements() : routeElements();
magnifyingGlass.innerHTML = "❌"; for (const element of candidates.filter((el) => matches(el, detail))) {
} else { element.classList.toggle("bliss-source-highlight", detail.on);
removeHighlights();
magnifyingGlass.innerHTML = "🔍";
} }
highlighted = !highlighted; }
async function refreshElement(element) {
if (element.dataset.blissMethod !== "GET") return;
const url = element.dataset.blissRequestUrl;
if (!url) return;
if (element === document.body) {
const ui = [...document.body.querySelectorAll(":scope > [data-bliss-ui]")];
const response = await fetch(url, { headers: { "HX-Request": "true" } });
if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`);
const next = new DOMParser().parseFromString(await response.text(), "text/html");
for (const attribute of [...document.body.attributes]) {
document.body.removeAttribute(attribute.name);
}
for (const attribute of [...next.body.attributes]) {
document.body.setAttribute(attribute.name, attribute.value);
}
document.body.innerHTML = next.body.innerHTML;
for (const node of ui) document.body.appendChild(node);
htmx.process(document.body);
return;
}
await htmx.ajax("GET", url, { target: element, swap: "outerHTML" });
}
async function refreshSources({ kind, id, method }) {
if (method && method !== "GET") return;
const elements = routeElements().filter((element) => {
if (kind === "template") {
return templateElements().some(
(template) =>
matches(template, { kind: "template", id }) &&
template.closest(ROUTE_SELECTOR) === element,
);
}
return matches(element, { kind, id });
});
const settled = await Promise.allSettled(elements.map(refreshElement));
const failure = settled.find((result) => result.status === "rejected");
if (failure) console.error("Bliss live refresh failed", failure.reason);
publishInventory();
}
function show() {
if (open) return;
open = true;
document.documentElement.classList.add("bliss-inspector-open");
root = document.createElement("div");
root.id = "bliss-inspector-root";
root.dataset.blissUi = "";
root.setAttribute("hx-get", "/_bliss/inspector");
root.setAttribute("hx-trigger", "load");
root.setAttribute("hx-swap", "innerHTML");
document.body.appendChild(root);
htmx.process(root);
}
function hide() {
open = false;
document.documentElement.classList.remove("bliss-inspector-open");
root?.remove();
root = null;
for (const element of [...routeElements(), ...templateElements()]) {
element.classList.remove("bliss-source-highlight");
}
}
document.addEventListener("DOMContentLoaded", () => {
const style = document.createElement("style");
style.dataset.blissUi = "";
style.textContent = `
html.bliss-inspector-open body > :not([data-bliss-ui]) {
transform: translateX(min(28rem, 90vw));
max-width: calc(100vw - min(28rem, 90vw));
}
[data-bliss-structure-id], [data-bliss-template-id] {
transition: outline-color 80ms linear, background-color 80ms linear;
}
.bliss-source-highlight {
outline: 4px solid #ff00a8 !important;
outline-offset: 3px !important;
background-color: rgba(255, 0, 168, .08) !important;
}
`;
document.head.appendChild(style);
const trigger = document.createElement("button");
trigger.type = "button";
trigger.textContent = "🔍";
trigger.title = "Open Bliss live editor";
trigger.dataset.blissUi = "";
trigger.style.cssText =
"position:fixed;right:.5rem;bottom:.5rem;z-index:2147483647;border:0;background:white;border-radius:999px;padding:.45rem;font-size:1.25rem;cursor:pointer;box-shadow:0 2px 8px #0004";
trigger.onclick = () => (open ? hide() : show());
document.body.appendChild(trigger);
}); });
// Observe the document for changes document.addEventListener("bliss:highlight", (event) => highlight(event.detail));
// const observer = new MutationObserver(function (mutations) { document.addEventListener("bliss:source-saved", (event) => refreshSources(event.detail));
// if (highlighted) { document.addEventListener("bliss:inventory-request", publishInventory);
// highlightElements(); document.addEventListener("bliss:inspector-close", hide);
// } document.addEventListener("htmx:afterSwap", () => {
// }); if (open) publishInventory();
// // Configure the observer
// observer.observe(document.body, {
// childList: true,
// subtree: true,
// });
}); });
})();