feat: introduce local-first SVG workbench

This commit is contained in:
2026-08-02 16:31:49 +02:00
commit 39d9802daa
97 changed files with 20702 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
(() => {
"use strict";
const controllerScript = document.currentScript;
const channel = document.documentElement.dataset.svgToolsChannel;
if (!channel) {
controllerScript?.remove();
return;
}
const send = (message) => parent.postMessage({ ...message, channel }, "*");
const find = (key) =>
typeof key === "string"
? document.querySelector(`[data-svg-tools-node="${CSS.escape(key)}"]`)
: null;
const reportBox = (key) => {
const element = find(key);
let box = null;
try {
if (element && typeof element.getBBox === "function") {
const candidate = element.getBBox();
if (
[candidate.x, candidate.y, candidate.width, candidate.height].every(
Number.isFinite,
)
) {
box = {
x: candidate.x,
y: candidate.y,
width: candidate.width,
height: candidate.height,
};
}
}
} catch {
// Some SVG elements do not expose geometry in every browser.
}
send({ type: "selection-box", key, box });
};
document.addEventListener("click", (event) => {
const target =
event.target instanceof Element
? event.target.closest("[data-svg-tools-node]")
: null;
const key = target?.getAttribute("data-svg-tools-node");
if (key) send({ type: "select", key });
if (event.target instanceof Element && event.target.closest("a")) {
event.preventDefault();
}
});
addEventListener("message", (event) => {
if (event.source !== parent) return;
const message = event.data;
if (!message || message.channel !== channel) return;
if (message.type === "ping") send({ type: "ready" });
if (message.type === "selection" && typeof message.key === "string") {
reportBox(message.key);
}
if (
message.type === "path" &&
typeof message.key === "string" &&
typeof message.data === "string"
) {
find(message.key)?.setAttribute("d", message.data);
reportBox(message.key);
}
if (
message.type === "transform" &&
typeof message.key === "string" &&
(typeof message.value === "string" || message.value === null)
) {
const element = find(message.key);
if (!element) return;
if (message.value) element.setAttribute("transform", message.value);
else element.removeAttribute("transform");
reportBox(message.key);
}
});
send({ type: "ready" });
controllerScript?.remove();
})();