isokit: web Studio (engine as WASM, projects in the browser or a live folder)
- isokit-wasm: wasm-bindgen build of the engine; a project in memory behind the same JSON API as `isokit studio`, plus the MCP tools - isokit-project: Store trait (FsStore folder, MemStore memory) under a generic Workspace; Project::from_files; shared Studio API module (api.rs) used by serve.rs and the WASM build; web_time for clocks in the browser - studio.js runs on a pluggable backend (window.IsoKitBackend); no Studio keys while a dialog is open - web/: projects in IndexedDB, new/demo/delete, zip export/import (stored and deflated), folder import by drop or picker, folders opened live via File System Access with outside edits followed, code editor, page build and preview, two tabs in step, isokit.mcp() and WebMCP registration - tools/build_web.py -> dist/web (static), tools/check_web.py (44 checks)
This commit is contained in:
1 parent
564aeb75f6
commit
5eb60d2a0c
20 files changed
+1950
-178
No files matched your search
+1
-1
@@ -22,7 +22,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
<button type="button" id="redo" title="Redo (Ctrl+Y)" disabled>↷ Redo</button>
|
||||
<span class="st-save" id="save" title="Every change is saved to scene.json at once">Saved</span>
|
||||
<span class="st-sep"></span>
|
||||
<a class="st-btn" href="/view" target="_blank" rel="noopener" title="The scene as visitors see it (live)">Preview</a>
|
||||
<a class="st-btn" id="preview" href="/view" target="_blank" rel="noopener" title="The scene as visitors see it (live)">Preview</a>
|
||||
<button type="button" id="build" title="Write the self-contained HTML page">Build page</button>
|
||||
<button type="button" id="theme" title="Light / dark">◐</button>
|
||||
<button type="button" id="help" title="Keys and tips">?</button>
|
||||
|
||||
+16
-8
@@ -1,13 +1,18 @@
|
||||
/* isokit Studio: the scene editor (talks to `isokit studio` over /api). */
|
||||
/* isokit Studio: the scene editor. Talks to `isokit studio` over /api, or to
|
||||
* window.IsoKitBackend when a page provides one (the web Studio runs the
|
||||
* engine as WASM in the page and answers the same requests). */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
// { api(path, body) -> json | Promise, source(meta), agentBox(meta), preview(), saveTitle }
|
||||
var B = window.IsoKitBackend || null;
|
||||
var S = { meta: null, doc: null, version: null, sel: null, placing: null, view: null, boxes: {}, warnings: [], busy: false, canUndo: false, canRedo: false, port: location.port };
|
||||
var ICON_DEFAULT = "▪";
|
||||
|
||||
function esc(s) { return String(s === undefined || s === null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
||||
function api(path, body) {
|
||||
if (B) return Promise.resolve().then(function () { return B.api(path, body); });
|
||||
var o = body === undefined ? { cache: "no-store" } : { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) };
|
||||
return fetch(path, o).then(function (r) { return r.json(); });
|
||||
}
|
||||
@@ -46,7 +51,7 @@
|
||||
S.view.refresh();
|
||||
renderLists(); if (!quiet) renderRight();
|
||||
return r;
|
||||
}, function (e) { $("save").textContent = "Offline"; $("save").classList.add("st-err"); toast("Studio server not reachable: " + e, true); });
|
||||
}, function (e) { $("save").textContent = B ? "Not saved" : "Offline"; $("save").classList.add("st-err"); toast((B ? "Could not save: " : "Studio server not reachable: ") + e, true); });
|
||||
}
|
||||
function update(id, patch, quiet) { return op({ op: "update", id: id, patch: patch }, quiet); }
|
||||
function undo() { api("/api/undo", {}).then(function (r) { takeState(r); afterExternal(); }); }
|
||||
@@ -67,7 +72,7 @@
|
||||
// another editor or an agent changed the files: follow along
|
||||
function poll() {
|
||||
api("/api/version").then(function (r) {
|
||||
if (r.version !== S.version && !S.busy) { S.version = r.version; reloadAll("Scene updated outside the Studio (agent or file edit)"); }
|
||||
if (r.version !== S.version && !S.busy) { S.version = r.version; reloadAll(r.why || "Scene updated outside the Studio (agent or file edit)"); }
|
||||
}).catch(function () { /* server restarting */ }).then(function () { setTimeout(poll, 800); });
|
||||
}
|
||||
|
||||
@@ -75,7 +80,7 @@
|
||||
var drag = null;
|
||||
function moveEls(id) { return S.view.svg.querySelectorAll('[data-obj="' + id.replace(/"/g, "") + '"]'); }
|
||||
function mountView() {
|
||||
var src = IsoKit.httpSource("", toViewerMeta(S.meta));
|
||||
var src = B ? B.source(toViewerMeta(S.meta)) : IsoKit.httpSource("", toViewerMeta(S.meta));
|
||||
S.view = IsoKit.mount($("view"), src, {
|
||||
note: false, noSelect: true, wheelZoom: true,
|
||||
onRender: function (d) {
|
||||
@@ -120,7 +125,7 @@
|
||||
cancel: function () { if (drag && drag.els) S.view.refresh(); drag = null; }
|
||||
}
|
||||
});
|
||||
window.__studio = { S: S, view: S.view, op: op, select: select };
|
||||
window.__studio = { S: S, view: S.view, op: op, select: select, reloadAll: reloadAll, toast: toast };
|
||||
}
|
||||
function toViewerMeta(m) { return { title: m.title, note: m.note, levels: m.levels, defaultLevel: m.default_level, defaultRotation: m.default_rotation }; }
|
||||
function remount() {
|
||||
@@ -171,7 +176,7 @@
|
||||
}
|
||||
function keyHandler(e) {
|
||||
var t = e.target, typing = t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT");
|
||||
if (typing) return false;
|
||||
if (typing || document.querySelector("dialog[open]")) return false;
|
||||
var k = e.key, ctrl = e.ctrlKey || e.metaKey;
|
||||
if (ctrl && (k === "z" || k === "Z")) { if (e.shiftKey) redo(); else undo(); return true; }
|
||||
if (ctrl && (k === "y" || k === "Y")) { redo(); return true; }
|
||||
@@ -284,10 +289,11 @@
|
||||
return field(p.name, p.label || p.name, propInput({ key: p.name, type: p.type, default: p.default, choices: p.choices, min: p.min, max: p.max, step: p.step }, v).replace(/data-prop=/g, "data-cfg="));
|
||||
}).join("") + "</div>" : "") +
|
||||
(S.warnings.length ? "<h2>Warnings</h2>" + S.warnings.map(function (w) { return '<div class="st-warn">' + esc(w) + "</div>"; }).join("") : "") +
|
||||
(B && B.agentBox ? B.agentBox(m) :
|
||||
'<div class="st-box"><h2>Connect an AI agent (MCP)</h2>' +
|
||||
'<p class="st-help">Agents edit this scene with the same operations as the Studio; their changes appear here live.</p>' +
|
||||
"HTTP (while the Studio runs):<code>" + esc(base) + "/mcp</code>" +
|
||||
"stdio (any time):<code>isokit mcp " + esc(m.dir || ".") + "</code></div>" +
|
||||
"stdio (any time):<code>isokit mcp " + esc(m.dir || ".") + "</code></div>") +
|
||||
'<p class="st-help">Select an object to edit it, or pick one on the left to add it.</p>';
|
||||
}
|
||||
$("right").addEventListener("click", function (e) {
|
||||
@@ -344,9 +350,11 @@
|
||||
try { localStorage.setItem("isokit-theme", t); } catch (e) { /* private mode */ }
|
||||
});
|
||||
$("build").addEventListener("click", function () {
|
||||
api("/api/build", {}).then(function (r) { if (r.error) toast(r.error, true); else toast("Page written: " + r.path); });
|
||||
api("/api/build", {}).then(function (r) { if (r.error) toast(r.error, true); else toast(r.message || "Page written: " + r.path); }, function (e) { toast(String(e), true); });
|
||||
});
|
||||
$("warnbtn").addEventListener("click", function () { select(null); });
|
||||
if (B && B.preview) $("preview").addEventListener("click", function (e) { e.preventDefault(); B.preview(); });
|
||||
if (B && B.saveTitle) $("save").title = B.saveTitle;
|
||||
|
||||
// ------------------------------------------------------------ start
|
||||
Promise.all([api("/api/meta"), api("/api/doc")]).then(function (rs) {
|
||||
|
||||
Reference in new issue
Block a user