feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu

Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.

Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.

Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
This commit is contained in:
Local Dev 2026-09-17 01:35:21 +02:00
parent 49e5752633
commit f9575a708a
11 changed files with 608 additions and 81 deletions

View file

@ -123,9 +123,9 @@ window.addEventListener("storage", async (e) => {
})();
// ---------- navigation ----------
const PANES = ["overview", "domains", "domain", "tlds", "tld", "newtld", "wallet", "settings"];
const PANES = ["studio", "domains", "domain", "tlds", "tld", "newtld", "settings"];
function go(pane, arg) {
if (!PANES.includes(pane)) pane = "overview";
if (!PANES.includes(pane)) pane = "domains";
document.querySelectorAll(".pane[data-pane]").forEach((p) => { p.hidden = p.dataset.pane !== pane; });
const activeItem = pane === "domain" ? "domains" : pane === "tld" ? "tlds" : pane;
document.querySelectorAll(".side .item[data-go]").forEach((b) => b.classList.toggle("active", b.dataset.go === activeItem));
@ -135,11 +135,12 @@ function go(pane, arg) {
if (pane === "tld" && arg) openTld(arg);
if (pane === "tlds") renderTlds();
if (pane === "domains") renderDomains();
if (pane === "studio") renderStudio();
window.scrollTo({ top: 0, behavior: "smooth" });
}
function routeFromHash() {
const m = /^#([a-z]+)(?:\/(.+))?$/.exec(location.hash || "");
if (!m) return go("overview");
if (!m) return go("domains");
go(m[1], m[2] ? decodeURIComponent(m[2]) : undefined);
}
window.addEventListener("hashchange", () => { if (wallet) routeFromHash(); });
@ -175,16 +176,14 @@ async function loadHoldings() {
H.tlds = p.tlds || [];
H.wallet = p.wallet || null; H.chain = p.chain || null; H.loadedAt = Date.now();
// header numbers
$("stat-names").textContent = H.names.length; $("cnt-names").textContent = H.names.length;
$("stat-tlds").textContent = H.tlds.length; $("cnt-tlds").textContent = H.tlds.length;
const setText = (id, v) => { const el = $(id); if (el) el.textContent = v; };
setText("cnt-names", H.names.length); setText("cnt-tlds", H.tlds.length);
if (H.wallet?.sats != null) {
const s = fmtInt(H.wallet.sats);
$("stat-balance").textContent = s; $("side-balance").textContent = s; $("w-sats").textContent = `${s} sat`;
$("stat-balance-sub").textContent = `sat · ${H.wallet.utxo_count} UTXO${H.wallet.utxo_count === 1 ? "" : "s"}`;
$("w-utxos").textContent = String(H.wallet.utxo_count);
setText("side-balance", s); setText("w-sats", `${s} sat`); setText("w-utxos", String(H.wallet.utxo_count));
}
if (H.chain?.height != null) $("stat-height").textContent = fmtInt(H.chain.height);
renderOverview(); renderDomains(); renderTlds();
if (H.chain?.height != null) setText("stat-height", fmtInt(H.chain.height));
renderAttention(); renderDomains(); renderTlds(); renderStudio();
if (current.name) { const e = H.names.find((n) => n.name === current.name); if (e) { current.entry = e; fillSummary(e); } }
warmDnsCache();
} catch (e) {
@ -242,25 +241,47 @@ function pointsTo(rec) {
const siteUrl = (name) => `${API}/bns/${encodeURIComponent(name)}/`;
const txUrl = (txid) => `https://chipnet.imaginary.cash/tx/${txid}`;
// ---------- overview ----------
function renderOverview() {
const recent = [...H.names].sort((a, b) => (b.height || 1e12) - (a.height || 1e12)).slice(0, 6);
$("ov-recent").innerHTML = recent.length
? recent.map((e) => domainRow(e, { compact: true })).join("")
: `<div class="empty">No names yet. <a href="./#search-input">Register your first one →</a></div>`;
renderAttention();
}
// ---------- attention strip (top of Domain names) ----------
function renderAttention() {
const items = [];
for (const e of H.names) {
const pt = pointsTo(e.records);
const d = dnsCache.get(e.name);
if (pt.kind === "none") items.push({ e, why: "points nowhere yet", go: "content" });
else if (d && d.status !== "ok") items.push({ e, why: d.status === "nopointer" ? "DNS needs an s3 pointer" : "no DNS records", go: "dns" });
}
$("ov-attention").innerHTML = items.length
? items.slice(0, 8).map(({ e, why, go }) => `<div class="drow" data-open="${esc(e.name)}" data-sub="${go}"><div class="n">${nameHtml(e)}</div><span class="badge b-warn">${esc(why)}</span></div>`).join("")
: `<div class="empty">Everything points somewhere and has DNS. Nice.</div>`;
const box = $("ov-attention");
if (!items.length) { box.innerHTML = ""; return; }
box.innerHTML = `<div class="dim" style="margin:6px 0 4px">Needs attention</div>` +
items.slice(0, 5).map(({ e, why, go }) => `<div class="drow" data-open="${esc(e.name)}" data-sub="${go}"><div class="n">${nameHtml(e)}</div><span class="badge b-warn">${esc(why)}</span></div>`).join("");
}
// ---------- web-builder (Sirius Studio) ----------
function renderStudio() {
const status = $("studio-status"), list = $("studio-list");
if (!status || !list) return;
if (!H.names.length) {
status.className = "status";
status.innerHTML = `You need a name first. <a href="./#search-input">Register one →</a> — then come back here to build its site.`;
list.innerHTML = ""; return;
}
status.className = "status ok";
status.textContent = `Pick a name to build or edit its site.`;
list.innerHTML = [...H.names].sort((a, b) => a.name.localeCompare(b.name)).map((e) => {
const rec = e.records || {};
const studioFolder = `bns/${e.name}/`;
const st = rec.s3 === studioFolder ? { label: "built with Studio", cls: "b-ok" }
: rec.s3 ? { label: "site on Sia (external)", cls: "b-x" }
: rec.h ? { label: "inline HTML", cls: "b-x" }
: rec.u ? { label: "redirect", cls: "b-dim" }
: rec.p || rec.ip ? { label: "own server", cls: "b-dim" }
: { label: "no site yet", cls: "b-warn" };
return `<div class="drow" style="cursor:default">
<div class="n">${nameHtml(e)} <span class="badge ${st.cls}">${esc(st.label)}</span></div>
<div class="acts">
${rec.s3 || rec.h ? `<a class="btn small ghost" href="${siteUrl(e.name)}" target="_blank" rel="noopener">View →</a>` : ""}
<a class="btn small acid" href="./studio.html?name=${encodeURIComponent(e.name)}">${rec.s3 === studioFolder ? "Edit in Studio" : "Open in Studio"} </a>
</div>
</div>`;
}).join("");
}
// ---------- domains list ----------
@ -528,7 +549,7 @@ async function publishRecords(records, logId, msgId, btnId, doneText) {
: await BNS.setRecordsWithBuiltInWallet(el, { wallet, name: current.name, records, onProgress: (s) => push(s) });
push("broadcast: " + (res.txid || res.txId || "(no txid)"));
if (current.entry) { current.entry.records = records; current.entry.updated_txid = res.txid || res.txId || current.entry.updated_txid; }
fillSummary(current.entry); renderDomains(); renderOverview();
fillSummary(current.entry); renderDomains(); renderAttention(); renderStudio();
setMsg(msgId, doneText, "ok");
return true;
} catch (e) { setMsg(msgId, "Failed: " + (e.message || e), "err"); push("error: " + (e.message || e)); return false; }

311
js/studio.js Normal file
View file

@ -0,0 +1,311 @@
// Sirius Studio — a site builder for BCNR names.
//
// GrapesJS (BSD-3, vendored under ./vendor/grapesjs/) edits the page. On
// Publish the exported HTML+CSS becomes index.html in the name's own folder
// on Sia — bns/<name>/ — uploaded through the gateway's /api/site route,
// every request signed with the wallet key that holds the name's NFT. The
// editor's project data is saved next to it (_studio.json) so the site can
// be reopened and edited later. If the name's on-chain s3 record does not
// point at that folder yet, Publish also sends one UPD that sets it.
//
// The gateway never holds a key: it verifies each upload's signature
// against the current NFT owner and refuses writes outside bns/<name>/.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260916dns";
const API = "https://silentmode.st";
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const name = (new URLSearchParams(location.search).get("name") || "").toLowerCase().trim();
const folder = name ? `bns/${name}/` : null;
const readUrl = (path) => `${API}/api/site/${encodeURIComponent(name)}/${path}`;
const siteUrl = () => `${API}/bns/${encodeURIComponent(name)}/`;
let wallet = null;
let entry = null; // { records, category, ... } from /api/name
let editor = null;
let dirty = false;
function status(text, cls = "") { const el = $("status"); el.textContent = text; el.className = "st " + cls; }
// ---------- session ----------
async function restoreWallet() {
if (window.siriusWallet) return window.siriusWallet;
const S = window.siriusSession;
if (!S?.restore) return null;
try {
const s = await S.restore();
if (!s?.mnemonic) return null;
return await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.CHIPNET_PREFIX, s.accountPath || undefined);
} catch { return null; }
}
// ---------- signed uploads ----------
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
async function signedHeaders(path, bodyBytes) {
const ts = String(Date.now());
const bodyHash = hex(await crypto.subtle.digest("SHA-256", bodyBytes));
const msg = `BNS-SITE1\n${name}\n${path}\n${bodyHash}\n${ts}`;
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg)));
const sig = wallet.signMessage(digest);
return { "x-bns-ts": ts, "x-bns-sig": btoa(String.fromCharCode(...sig)) };
}
async function putFile(path, body, contentType) {
const bytes = body instanceof Uint8Array ? body : new Uint8Array(await new Blob([body]).arrayBuffer());
const headers = { "content-type": contentType, ...(await signedHeaders(path, bytes)) };
const r = await fetch(readUrl(path), { method: "PUT", headers, body: bytes });
const j = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(`${path}: ${j.error || r.status}`);
return j;
}
async function listFiles() {
const r = await fetch(`${API}/api/site/${encodeURIComponent(name)}`, { cache: "no-store" });
if (!r.ok) return [];
const j = await r.json().catch(() => ({}));
return j.files || [];
}
// ---------- templates ----------
const TEMPLATES = {
blank: { html: `<section style="padding:60px 20px;text-align:center"><h1>${esc(name)}</h1><p>Start building.</p></section>`, css: `body{font-family:system-ui,sans-serif;margin:0;color:#111}` },
landing: {
html: `
<header class="hero">
<h1>Hello from ${esc(name)}</h1>
<p>A name on the Bitcoin Cash chain, a site on Sia. No host, no renewal, no permission.</p>
<a class="cta" href="#more">Learn more</a>
</header>
<section id="more" class="features">
<div class="f"><h3>Yours</h3><p>The certificate sits in your wallet. Nobody can take it back.</p></div>
<div class="f"><h3>Fast</h3><p>Served from Sia through any BCNR resolver or the public gateway.</p></div>
<div class="f"><h3>Simple</h3><p>Edit this page in Sirius Studio and publish in one click.</p></div>
</section>
<footer class="foot">© ${new Date().getFullYear()} ${esc(name)} · built with Sirius Studio</footer>`,
css: `
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:#12161f;background:#fff}
.hero{padding:96px 24px 72px;text-align:center;background:linear-gradient(180deg,#0b0e14,#141a24);color:#f1f4fa}
.hero h1{font-size:44px;margin:0 0 12px;letter-spacing:-.01em}
.hero p{font-size:18px;color:#b8c2d4;max-width:560px;margin:0 auto 26px}
.cta{display:inline-block;background:#d6ff3d;color:#0b0e14;font-weight:700;padding:12px 22px;border-radius:10px;text-decoration:none}
.features{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:20px;max-width:960px;margin:0 auto;padding:56px 24px}
.f{background:#f5f7fb;border-radius:14px;padding:22px}
.f h3{margin:0 0 8px}
.f p{margin:0;color:#4a5568}
.foot{text-align:center;padding:28px;color:#6a7488;font-size:13px;border-top:1px solid #e6e9f0}`,
},
profile: {
html: `
<main class="card">
<div class="avatar"></div>
<h1>${esc(name)}</h1>
<p class="bio">One line about you. Edit me.</p>
<a class="link" href="https://">Website</a>
<a class="link" href="https://">Nostr</a>
<a class="link" href="https://">Bitcoin Cash tips</a>
<a class="link" href="mailto:">Email</a>
</main>`,
css: `
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 0,#1a2233,#050810);font-family:system-ui,sans-serif;color:#f1f4fa}
.card{width:min(420px,92vw);text-align:center;padding:36px 24px}
.avatar{width:88px;height:88px;border-radius:50%;background:#d6ff3d;color:#0b0e14;font-size:40px;line-height:88px;margin:0 auto 14px}
h1{margin:0 0 6px;font-size:26px}
.bio{color:#b8c2d4;margin:0 0 22px}
.link{display:block;background:#141a24;border:1px solid rgba(255,255,255,.1);color:#f1f4fa;text-decoration:none;padding:14px;border-radius:12px;margin:10px 0;font-weight:600}
.link:hover{border-color:#d6ff3d}`,
},
business: {
html: `
<nav class="nav"><b>${esc(name)}</b><span><a href="#about">About</a><a href="#services">Services</a><a href="#contact">Contact</a></span></nav>
<header class="top"><h1>We are open</h1><p>Say what you do in one sentence.</p></header>
<section id="about" class="sec"><h2>About</h2><p>Two or three sentences about the business, the people and the place.</p></section>
<section id="services" class="sec alt"><h2>Services</h2>
<ul class="grid"><li><b>Service one</b><span>Short description.</span></li><li><b>Service two</b><span>Short description.</span></li><li><b>Service three</b><span>Short description.</span></li></ul></section>
<section id="contact" class="sec"><h2>Contact</h2><p>Street 1, City · MonFri 918 · <a href="mailto:">hello@example</a></p></section>
<footer class="foot">© ${new Date().getFullYear()} ${esc(name)}</footer>`,
css: `
body{margin:0;font-family:Georgia,serif;color:#2b1d12;background:#fff8f0}
.nav{display:flex;justify-content:space-between;align-items:center;padding:16px 28px;border-bottom:1px solid #eadfd0;font-family:system-ui,sans-serif}
.nav a{margin-left:18px;color:#7a3b00;text-decoration:none}
.top{text-align:center;padding:80px 24px;background:#7a3b00;color:#fff4e6}
.top h1{font-size:42px;margin:0 0 10px}
.sec{max-width:820px;margin:0 auto;padding:48px 24px}
.sec.alt{max-width:none;background:#fff1e0}
.sec.alt h2,.sec.alt ul{max-width:820px;margin-left:auto;margin-right:auto}
.grid{list-style:none;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px}
.grid li{background:#fff;border-radius:12px;padding:18px;box-shadow:0 2px 10px rgba(0,0,0,.05)}
.grid b{display:block;margin-bottom:6px}
.foot{text-align:center;padding:24px;color:#8a6d55;font-size:13px}`,
},
};
// ---------- editor ----------
function plugin(nameOrObj) {
const p = window[nameOrObj];
return p && (p.default || p);
}
function initEditor() {
const plugins = [];
const pluginsOpts = {};
const basic = plugin("grapesjs-blocks-basic"); if (basic) { plugins.push(basic); pluginsOpts[basic] = { flexGrid: true }; }
const preset = plugin("grapesjs-preset-webpage"); if (preset) { plugins.push(preset); pluginsOpts[preset] = { modalImportTitle: "Import HTML", blocks: [] }; }
editor = grapesjs.init({
container: "#gjs",
height: "100%",
fromElement: false,
storageManager: false,
plugins,
pluginsOpts,
canvas: { styles: [] },
assetManager: {
upload: false,
uploadFile: async (e) => {
const files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
for (const f of files) await uploadAsset(f);
},
},
});
editor.on("update", () => { dirty = true; $("btn-draft").disabled = false; });
// Ctrl/Cmd+S saves a draft.
document.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); saveDraft(); } });
window.addEventListener("beforeunload", (e) => { if (dirty) { e.preventDefault(); e.returnValue = ""; } });
}
function loadTemplate(key) {
const t = TEMPLATES[key] || TEMPLATES.blank;
editor.setComponents(t.html);
editor.setStyle(t.css);
dirty = true; $("btn-draft").disabled = false;
}
async function uploadAsset(file) {
const safe = file.name.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/-+/g, "-");
const path = `assets/${Date.now().toString(36)}-${safe}`;
status(`Uploading ${file.name}`, "busy");
try {
const bytes = new Uint8Array(await file.arrayBuffer());
await putFile(path, bytes, file.type || "application/octet-stream");
editor.AssetManager.add({ src: readUrl(path), name: file.name, type: "image" });
status(`Uploaded ${file.name}`, "ok");
} catch (e) { status("Upload failed: " + (e.message || e), "err"); }
}
// Exported page: inline CSS, asset URLs rewritten to be relative to the
// site folder so the page works from Sia, any resolver and the gateway.
function exportHtml() {
const html = editor.getHtml();
const css = editor.getCss();
const rel = (s) => s.split(readUrl("")).join("");
return `<!doctype html>
<html lang="${esc(document.documentElement.lang || "en")}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(name)}</title>
<meta name="generator" content="Sirius Studio">
<style>
${rel(css)}
</style>
</head>
${rel(html)}
</html>
`;
}
// ---------- draft / publish ----------
function pushStep(t) { const d = document.createElement("div"); d.textContent = t; $("pub-steps").appendChild(d); $("pub-steps").scrollTop = 1e6; }
async function saveDraft() {
if (!editor || !wallet) return;
status("Saving draft…", "busy");
try {
const data = JSON.stringify({ v: 1, name, saved_at: new Date().toISOString(), project: editor.getProjectData() });
await putFile("_studio.json", data, "application/json");
dirty = false; $("btn-draft").disabled = true;
status("Draft saved on Sia", "ok");
} catch (e) { status("Draft not saved: " + (e.message || e), "err"); }
}
async function publish() {
if (!editor || !wallet) return;
$("pub-name").textContent = name; $("pub-steps").innerHTML = ""; $("pub-view").hidden = true; $("pub").hidden = false;
$("btn-publish").disabled = true;
try {
const html = exportHtml();
pushStep(`exported page · ${html.length.toLocaleString()} bytes`);
const res = await putFile("index.html", html, "text/html; charset=utf-8");
pushStep(`uploaded index.html → ${res.sia_key}`);
const data = JSON.stringify({ v: 1, name, saved_at: new Date().toISOString(), project: editor.getProjectData() });
await putFile("_studio.json", data, "application/json");
pushStep("saved editor project (_studio.json)");
dirty = false; $("btn-draft").disabled = true;
// Point the name at the folder if it does not already.
const rec = entry?.records || {};
if (rec.s3 !== folder) {
pushStep(`name points at ${rec.s3 ? `"${rec.s3}"` : "nothing"} — setting s3 = ${folder} on chain`);
const next = { ...rec, s3: folder };
delete next.h; // inline HTML would shadow the Sia site
let el = null;
try {
el = await BNS.connect();
const r = await BNS.setRecordsWithBuiltInWallet(el, { wallet, name, records: next, onProgress: (s) => pushStep(String(s)) });
pushStep(`broadcast ${r.txid || r.txId || ""}`);
entry = { ...(entry || {}), records: next };
} finally { try { el?.close?.(); } catch {} }
pushStep("resolvers switch to the new site within a block");
} else {
pushStep("name already points at this folder — live now");
}
$("pub-view").href = siteUrl(); $("pub-view").hidden = false;
$("btn-view").href = siteUrl(); $("btn-view").hidden = false;
status("Published", "ok");
} catch (e) {
pushStep("error: " + (e.message || e));
status("Publish failed", "err");
} finally { $("btn-publish").disabled = false; }
}
// ---------- boot ----------
(async function boot() {
if (!name) { location.replace("./portal.html#studio"); return; }
$("site-name").innerHTML = `${esc(name.split(".")[0])}.<span class="tld">${esc(name.split(".").slice(1).join("."))}</span>`;
$("picker-name").textContent = name;
document.title = `${name} — Sirius Studio`;
wallet = await restoreWallet();
if (!wallet) { $("gate-link").href = `./portal.html?next=${encodeURIComponent(location.pathname + location.search)}`; $("gate").hidden = false; status("Not signed in", "err"); return; }
status("Loading name…");
try { const r = await fetch(`${API}/api/name/${encodeURIComponent(name)}`, { cache: "no-store" }); entry = r.ok ? await r.json() : null; } catch { entry = null; }
initEditor();
$("btn-publish").disabled = false;
if (entry?.records?.s3 === folder) { $("btn-view").href = siteUrl(); $("btn-view").hidden = false; }
// Existing project?
const files = await listFiles();
const hasProject = files.some((f) => f.path === "_studio.json");
if (hasProject) {
try {
const j = await (await fetch(readUrl("_studio.json"), { cache: "no-store" })).json();
if (j?.project) { editor.loadProjectData(j.project); dirty = false; status(`Loaded draft from ${j.saved_at ? new Date(j.saved_at).toLocaleString() : "Sia"}`, "ok"); }
} catch (e) { status("Could not load the saved project: " + (e.message || e), "err"); $("picker").hidden = false; }
} else if (files.some((f) => f.path === "index.html")) {
// A site exists but was not made here — import its HTML as a starting point.
try {
const html = await (await fetch(readUrl("index.html"), { cache: "no-store" })).text();
editor.setComponents(html); status("Imported existing index.html", "ok");
} catch { $("picker").hidden = false; }
} else {
$("picker").hidden = false; status("New site");
}
})();
$("picker").addEventListener("click", (e) => {
const b = e.target.closest("[data-tpl]"); if (!b) return;
loadTemplate(b.dataset.tpl); $("picker").hidden = true; status("Template loaded — edit, then Publish");
});
$("tpl-select").addEventListener("change", (e) => {
const v = e.target.value; e.target.value = "";
if (!v) return;
if (!confirm("Replace the current page with this template?")) return;
loadTemplate(v);
});
$("btn-preview").addEventListener("click", () => {
const w = window.open("", "_blank"); if (!w) return;
w.document.open(); w.document.write(exportHtml()); w.document.close();
});
$("btn-draft").addEventListener("click", saveDraft);
$("btn-publish").addEventListener("click", publish);
$("pub-close").addEventListener("click", () => { $("pub").hidden = true; });

View file

@ -262,50 +262,29 @@
<div class="addr" id="side-addr"></div>
<div class="net">chipnet · <span id="side-balance"></span> sat</div>
</div>
<button class="item active" data-go="overview"><span class="ic"></span>Overview</button>
<div class="grp">Manage</div>
<button class="item" data-go="domains"><span class="ic">🌐</span>Domain names<span class="cnt" id="cnt-names">0</span></button>
<button class="item" data-go="tlds"><span class="ic">🏷</span>My TLD list<span class="cnt" id="cnt-tlds">0</span></button>
<button class="item" data-go="wallet"><span class="ic">👛</span>Wallet</button>
<div class="grp">Get more</div>
<a class="item" href="./#search-input"><span class="ic"></span>Register a name</a>
<button class="item" data-go="newtld"><span class="ic"></span>Register a TLD</button>
<button class="item" data-go="studio"><span class="ic">🎨</span>Web-Builder</button>
<button class="item active" data-go="domains"><span class="ic">🌐</span>Domain names<span class="cnt" id="cnt-names">0</span></button>
<button class="item" data-go="tlds"><span class="ic">🏷</span>My TLDs<span class="cnt" id="cnt-tlds">0</span></button>
<div class="sep"></div>
<button class="item" data-go="settings"><span class="ic"></span>Settings</button>
<a class="item" href="./docs/" target="_blank" rel="noopener"><span class="ic">📚</span>Docs</a>
<button class="item" id="side-signout"><span class="ic">🚪</span>Sign out</button>
</aside>
<main class="content">
<!-- OVERVIEW -->
<div class="pane" data-pane="overview">
<div class="ph"><div><h2>Overview</h2><p>What this wallet holds on chain, and where to go next.</p></div>
<!-- WEB-BUILDER -->
<div class="pane" data-pane="studio" hidden>
<div class="ph"><div><h2>Web-Builder</h2><p>Sirius Studio builds a site for any name you hold: drag blocks, edit text, publish. Pages live in the name's own folder on Sia and the name points at them — no host, no renewal.</p></div>
<div class="row" style="margin:0"><button class="btn ghost small" id="refresh-btn">↻ Refresh</button></div></div>
<div class="stat-grid">
<div class="stat"><div class="lbl">Domain names</div><div class="val" id="stat-names">0</div><div class="sub">CashTokens NFT certificates</div></div>
<div class="stat"><div class="lbl">TLDs owned</div><div class="val" id="stat-tlds">0</div><div class="sub">Per-TLD registry NFTs</div></div>
<div class="stat"><div class="lbl">Wallet balance</div><div class="val" id="stat-balance"></div><div class="sub" id="stat-balance-sub">chipnet sats</div></div>
<div class="stat"><div class="lbl">Chain height</div><div class="val" id="stat-height"></div><div class="sub">Last seen block</div></div>
</div>
<div class="grid2" style="margin-top:14px">
<div class="card" style="margin-top:0">
<h3>Recent names</h3>
<p class="hint">The last few certificates that landed in this wallet.</p>
<div id="ov-recent" class="list"></div>
</div>
<div class="card" style="margin-top:0">
<h3>Attention</h3>
<p class="hint">Names that point nowhere yet, or have no DNS.</p>
<div id="ov-attention" class="list"></div>
</div>
</div>
<div class="status" id="studio-status">Loading names from the chain…</div>
<div id="studio-list" class="list"></div>
</div>
<!-- DOMAINS -->
<div class="pane" data-pane="domains" hidden>
<div class="pane" data-pane="domains">
<div class="ph"><div><h2>Domain names</h2><p>Every BCNR name this wallet holds. Open one to manage DNS, content, redirects or transfer it.</p></div>
<a class="btn acid small" href="./#search-input"> Register a name</a></div>
<div id="ov-attention" class="list" style="margin-bottom:6px"></div>
<div class="toolbar">
<input type="text" id="dom-search" placeholder="Filter names…" autocomplete="off">
<select id="dom-sort"><option value="name">Sort: name</option><option value="tld">Sort: TLD</option><option value="height">Sort: newest</option></select>
@ -462,7 +441,7 @@
<!-- TLDs -->
<div class="pane" data-pane="tlds" hidden>
<div class="ph"><div><h2>My TLD list</h2><p>Every per-TLD registry NFT this wallet holds. Set the price every name under a TLD sells for, switch it on or off (off = private: only you can register under it), and tune policy. Each change is one signed TUPD on chain; you receive 90% of every sale.</p></div>
<div class="ph"><div><h2>My TLDs</h2><p>Every per-TLD registry NFT this wallet holds. Set the price every name under a TLD sells for, switch it on or off (off = private: only you can register under it), and tune policy. Each change is one signed TUPD on chain; you receive 90% of every sale.</p></div>
<button class="btn acid small" data-go="newtld"> Register a TLD</button></div>
<div class="status" id="tlds-status">Loading TLDs from the chain…</div>
<div id="tlds-list" class="list"></div>
@ -470,7 +449,7 @@
<!-- TLD DETAIL -->
<div class="pane" data-pane="tld" hidden>
<div class="crumbs"><a href="#tlds" data-go="tlds">My TLD list</a> <span id="td-crumb"></span></div>
<div class="crumbs"><a href="#tlds" data-go="tlds">My TLDs</a> <span id="td-crumb"></span></div>
<div class="dhead">
<h2 id="td-name">.—</h2>
<span class="badge" id="td-state">on</span>
@ -520,10 +499,22 @@
</div>
</div>
<!-- WALLET -->
<div class="pane" data-pane="wallet" hidden>
<div class="ph"><div><h2>Wallet</h2><p>Your addresses and current balance. Keys stay in this browser.</p></div></div>
<!-- SETTINGS -->
<div class="pane" data-pane="settings" hidden>
<div class="ph"><div><h2>Settings</h2><p>Session, security and wallet details for this browser. Nothing here changes what is on chain.</p></div></div>
<div class="card" style="margin-top:0">
<h3>Session</h3>
<div class="toggle"><input type="checkbox" id="set-stay"><div class="txt"><b>Stay signed in on this device</b><span>Your recovery phrase is kept encrypted under a key this browser holds and cannot export. Next visits open the dashboard straight away. Turn off to be asked for your password or PIN every time.</span></div></div>
<div class="toggle"><input type="checkbox" id="set-pinpay"><div class="txt"><b>Ask for PIN before payments</b><span>Adds a PIN prompt when registering a name or TLD. Off by default so signed-in sessions run uninterrupted.</span></div></div>
<div class="row">
<button class="btn ghost small" id="set-setpin">Set / change PIN</button>
<button class="btn ghost small" id="set-clearpin">Remove PIN</button>
<span class="dim" id="set-pinstate"></span>
</div>
</div>
<div class="card">
<h3>Wallet</h3>
<p class="hint">Your addresses and current balance. Keys stay in this browser.</p>
<label>Receive address <span class="dim" style="margin-left:6px">chipnet</span></label>
<div><code id="me-addr"></code> <button class="btn ghost small" data-copy="me-addr">Copy</button></div>
<label style="margin-top:14px">Token address <span class="dim" style="margin-left:6px">holds your name certificates</span></label>
@ -536,28 +527,11 @@
</span>
</div>
</div>
<div class="card">
<h3>Balance</h3>
<div class="kv"><div class="k">Spendable</div><div class="v" id="w-sats"></div><div class="k">UTXOs</div><div class="v" id="w-utxos"></div><div class="k">Watched addresses</div><div class="v" id="w-watched"></div></div>
</div>
</div>
<!-- SETTINGS -->
<div class="pane" data-pane="settings" hidden>
<div class="ph"><div><h2>Settings</h2><p>Session and security choices for this browser. Nothing here changes what is on chain.</p></div></div>
<div class="card" style="margin-top:0">
<h3>Session</h3>
<div class="toggle"><input type="checkbox" id="set-stay"><div class="txt"><b>Stay signed in on this device</b><span>Your recovery phrase is kept encrypted under a key this browser holds and cannot export. Next visits open the dashboard straight away. Turn off to be asked for your password or PIN every time.</span></div></div>
<div class="toggle"><input type="checkbox" id="set-pinpay"><div class="txt"><b>Ask for PIN before payments</b><span>Adds a PIN prompt when registering a name or TLD. Off by default so signed-in sessions run uninterrupted.</span></div></div>
<div class="row">
<button class="btn ghost small" id="set-setpin">Set / change PIN</button>
<button class="btn ghost small" id="set-clearpin">Remove PIN</button>
<span class="dim" id="set-pinstate"></span>
</div>
<div class="kv" style="margin-top:14px"><div class="k">Spendable</div><div class="v" id="w-sats"></div><div class="k">UTXOs</div><div class="v" id="w-utxos"></div><div class="k">Watched addresses</div><div class="v" id="w-watched"></div><div class="k">Chain height</div><div class="v" id="stat-height"></div></div>
</div>
<div class="card">
<h3>Reading &amp; language</h3>
<p class="hint">Font pill and language picker live in the top bar; both are remembered per browser.</p>
<h3>Reading, language &amp; help</h3>
<p class="hint">Font pill and language picker live in the top bar; both are remembered per browser. The technical documentation is at <a href="./docs/" target="_blank" rel="noopener">sirius-x/docs</a>.</p>
</div>
<div class="card">
<h3>This device</h3>
@ -633,7 +607,7 @@
<script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script>
<script src="./js/pricing.js?v=20260916private"></script>
<script type="module" src="./js/dashboard.js?v=20260917dash"></script>
<script type="module" src="./js/dashboard.js?v=20260917studio"></script>
<script defer src="./js/theme.js?v=20260910font"></script>
<script defer src="./js/site-footer.js?v=20260907rel"></script>
<script type="module" src="./js/register-flow.js?v=20260917dash"></script>

136
studio.html Normal file
View file

@ -0,0 +1,136 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sirius Studio — build the site for your name</title>
<meta name="description" content="Sirius Studio: a drag-and-drop site builder for BCNR names. Pages live on Sia, the name on the chain — your keys, your site.">
<link rel="icon" href="./assets/favicon.svg" type="image/svg+xml">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600&family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./vendor/grapesjs/grapes.min.css?v=0.23.6">
<style>
:root{
--bg:#050810; --panel:#141a24; --panel2:#0a0f1c; --line:rgba(255,255,255,.08);
--ink:#f1f4fa; --mut:#b8c2d4; --dim:#6a7488; --acid:#d6ff3d; --ok:#4fd1a5; --taken:#f6768a; --warn:#ffc75f;
--serif:'Fraunces',Georgia,serif; --sans:'DM Sans',system-ui,sans-serif; --mono:'JetBrains Mono',ui-monospace,monospace;
/* GrapesJS theme hooks */
--gjs-primary-color:#0e131b; --gjs-secondary-color:#b8c2d4; --gjs-tertiary-color:#d6ff3d; --gjs-quaternary-color:#d6ff3d;
--gjs-font-color:#f1f4fa; --gjs-main-dark-color:#0a0f1c; --gjs-main-light-color:#141a24;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{background:var(--bg);color:var(--ink);font:14px/1.5 var(--sans);display:flex;flex-direction:column;overflow:hidden}
a{color:var(--acid)}
.bar{display:flex;align-items:center;gap:10px;padding:8px 14px;background:rgba(11,14,20,.95);border-bottom:1px solid var(--line);flex:none;flex-wrap:wrap}
.bar .brand{font-weight:600;color:var(--ink);text-decoration:none;font-size:15px;display:flex;align-items:center;gap:6px}
.bar .brand .g{color:var(--acid)}
.bar .name{font-family:var(--mono);font-size:14px;color:var(--ink);background:var(--panel);border:1px solid var(--line);padding:5px 10px;border-radius:8px}
.bar .name .tld{color:var(--acid)}
.bar .sp{flex:1}
.bar .st{font-size:12.5px;color:var(--dim);max-width:360px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar .st.ok{color:var(--ok)} .bar .st.err{color:var(--taken)} .bar .st.busy{color:var(--warn)}
.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;background:#4b7bec;color:#fff;font:inherit;font-size:13px;border:none;cursor:pointer;text-decoration:none;line-height:1.2}
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
.btn.acid{background:var(--acid);color:#0b0e14;font-weight:600}
.btn.small{padding:5px 10px;font-size:12px}
.btn:disabled{opacity:.45;cursor:not-allowed}
select.sel{padding:6px 28px 6px 10px;border-radius:9px;background:var(--panel);color:var(--ink);border:1px solid var(--line);font:inherit;font-size:13px}
#editor{flex:1;min-height:0}
#gjs{height:100%}
.gjs-one-bg{background-color:var(--panel2)}
.gjs-two-color{color:var(--mut)}
.gjs-three-bg{background-color:var(--acid);color:#0b0e14}
.gjs-four-color,.gjs-four-color-h:hover{color:var(--acid)}
.gjs-pn-panel{border-color:var(--line)}
.gjs-cv-canvas{background:#fff}
/* gate / picker overlays */
.overlay{position:fixed;inset:0;background:rgba(5,8,16,.92);backdrop-filter:blur(4px);z-index:100;display:flex;align-items:center;justify-content:center;padding:20px;overflow:auto}
.overlay[hidden]{display:none}
.sheet{background:var(--panel);border:1px solid var(--line);border-radius:16px;max-width:860px;width:100%;padding:26px 28px}
.sheet h2{font-family:var(--serif);margin:0 0 .3rem;font-size:1.5rem}
.sheet p{color:var(--mut);margin:.2rem 0 1rem}
.tpls{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-top:10px}
.tpl{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:0;overflow:hidden;cursor:pointer;text-align:left;color:inherit;font:inherit;transition:border-color .12s}
.tpl:hover{border-color:rgba(214,255,61,.4)}
.tpl .prev{height:110px;background:#fff;display:flex;align-items:center;justify-content:center;font-family:var(--serif);color:#111;font-size:22px}
.tpl .prev.dark{background:#0b0e14;color:#d6ff3d}
.tpl .prev.warm{background:#fff4e6;color:#7a3b00}
.tpl .prev.blue{background:#0f2a4a;color:#e6f0ff}
.tpl b{display:block;padding:10px 12px 2px;font-size:14px}
.tpl span{display:block;padding:0 12px 12px;color:var(--dim);font-size:12px}
.steps{margin-top:12px;font-family:var(--mono);font-size:12px;color:var(--mut);background:#0e131b;border:1px solid var(--line);border-radius:8px;padding:10px 12px;max-height:160px;overflow:auto}
.steps div::before{content:" ";color:var(--acid)}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:14px}
.row.end{justify-content:flex-end}
.muted{color:var(--mut);font-size:13px}
code{font-family:var(--mono);background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-size:12px}
</style>
<script src="./js/i18n.js?v=20260917e"></script>
</head>
<body>
<div class="bar">
<a class="brand" href="./portal.html#studio"><span class="g">Sirius</span> Studio</a>
<span class="name" id="site-name"></span>
<select class="sel" id="tpl-select" title="Start from a template">
<option value="">Templates…</option>
<option value="landing">Landing page</option>
<option value="profile">Profile / link hub</option>
<option value="business">Business one-pager</option>
<option value="blank">Blank page</option>
</select>
<button class="btn ghost small" id="btn-preview" title="Preview the exported page in a new tab">Preview</button>
<span class="sp"></span>
<span class="st" id="status">Loading…</span>
<button class="btn ghost small" id="btn-draft" disabled>Save draft</button>
<button class="btn acid small" id="btn-publish" disabled>Publish to Sia →</button>
<a class="btn ghost small" id="btn-view" href="#" target="_blank" rel="noopener" hidden>View site →</a>
</div>
<div id="editor"><div id="gjs"></div></div>
<!-- sign-in gate -->
<div class="overlay" id="gate" hidden>
<div class="sheet" style="max-width:520px">
<h2>Sign in first</h2>
<p>Sirius Studio publishes with your wallet's key. Sign in on the dashboard, then come back to this name.</p>
<div class="row end"><a class="btn acid" id="gate-link" href="./portal.html">Open the dashboard →</a></div>
</div>
</div>
<!-- template picker for a fresh site -->
<div class="overlay" id="picker" hidden>
<div class="sheet">
<h2>Build the site for <span id="picker-name" style="font-family:var(--mono);color:var(--acid)"></span></h2>
<p>Pick a starting point. Everything is editable — drag blocks in from the right, click any text to change it. Nothing leaves your browser until you press Publish; then the page is stored on Sia and the name points at it.</p>
<div class="tpls">
<button class="tpl" data-tpl="landing"><div class="prev">Hello, world</div><b>Landing page</b><span>Hero, three features, call to action.</span></button>
<button class="tpl" data-tpl="profile"><div class="prev dark">@you</div><b>Profile / link hub</b><span>Avatar, bio and a stack of links.</span></button>
<button class="tpl" data-tpl="business"><div class="prev warm">Open today</div><b>Business one-pager</b><span>About, services, hours, contact.</span></button>
<button class="tpl" data-tpl="blank"><div class="prev blue"></div><b>Blank page</b><span>Start from nothing.</span></button>
</div>
<div class="row end"><a class="btn ghost" href="./portal.html#studio">Back to dashboard</a></div>
</div>
</div>
<!-- publish dialog -->
<div class="overlay" id="pub" hidden>
<div class="sheet" style="max-width:620px">
<h2>Publish <span id="pub-name" style="font-family:var(--mono);color:var(--acid)"></span></h2>
<p id="pub-intro">The page and its assets are uploaded to the name's folder on Sia, signed with your key. If the name does not point at that folder yet, one on-chain update sets it (a few cents of chain dust).</p>
<div class="steps" id="pub-steps"></div>
<div class="row end">
<button class="btn ghost" id="pub-close">Close</button>
<a class="btn acid" id="pub-view" href="#" target="_blank" rel="noopener" hidden>View site →</a>
</div>
</div>
</div>
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script>
<script src="./js/session.js?v=20260917dash"></script>
<script src="./vendor/grapesjs/grapes.min.js?v=0.23.6"></script>
<script src="./vendor/grapesjs/blocks-basic.js?v=1.0.2"></script>
<script src="./vendor/grapesjs/preset-webpage.js?v=1.0.3"></script>
<script type="module" src="./js/studio.js?v=20260917studio"></script>
</body>
</html>

View file

@ -0,0 +1,25 @@
Copyright (c) 2017-current, Artur Arseniev
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
- Neither the name "GrapesJS" nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

25
vendor/grapesjs/LICENSE-grapesjs.txt vendored Normal file
View file

@ -0,0 +1,25 @@
Copyright (c) 2017-current, Artur Arseniev
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
- Neither the name "GrapesJS" nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,25 @@
Copyright (c) 2017, Artur Arseniev
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
- Neither the name "GrapesJS" nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
vendor/grapesjs/blocks-basic.js vendored Normal file

File diff suppressed because one or more lines are too long

1
vendor/grapesjs/grapes.min.css vendored Normal file

File diff suppressed because one or more lines are too long

3
vendor/grapesjs/grapes.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3
vendor/grapesjs/preset-webpage.js vendored Normal file

File diff suppressed because one or more lines are too long