feat: slideout editor new new new!
This commit is contained in:
parent
c175a610da
commit
48ac1249c8
10 changed files with 482 additions and 112 deletions
3
bliss-cli/live-editor/inspector.js
Normal file
3
bliss-cli/live-editor/inspector.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
function handler(req, res) {
|
||||
res.render("inspector/slideout", {});
|
||||
}
|
||||
79
bliss-cli/live-editor/install.js
Normal file
79
bliss-cli/live-editor/install.js
Normal 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}`);
|
||||
28
bliss-cli/live-editor/route-editor.eta
Normal file
28
bliss-cli/live-editor/route-editor.eta
Normal 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>
|
||||
8
bliss-cli/live-editor/route-editor.js
Normal file
8
bliss-cli/live-editor/route-editor.js
Normal 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 });
|
||||
}
|
||||
76
bliss-cli/live-editor/slideout.eta
Normal file
76
bliss-cli/live-editor/slideout.eta
Normal 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>
|
||||
25
bliss-cli/live-editor/template-editor.eta
Normal file
25
bliss-cli/live-editor/template-editor.eta
Normal 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>
|
||||
8
bliss-cli/live-editor/template-editor.js
Normal file
8
bliss-cli/live-editor/template-editor.js
Normal 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 });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue