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:
parent
49e5752633
commit
f9575a708a
11 changed files with 608 additions and 81 deletions
|
|
@ -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
311
js/studio.js
Normal 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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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 · Mon–Fri 9–18 · <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; });
|
||||
88
portal.html
88
portal.html
|
|
@ -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 & language</h3>
|
||||
<p class="hint">Font pill and language picker live in the top bar; both are remembered per browser.</p>
|
||||
<h3>Reading, language & 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
136
studio.html
Normal 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>
|
||||
25
vendor/grapesjs/LICENSE-blocks-basic.txt
vendored
Normal file
25
vendor/grapesjs/LICENSE-blocks-basic.txt
vendored
Normal 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
25
vendor/grapesjs/LICENSE-grapesjs.txt
vendored
Normal 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-preset-webpage.txt
vendored
Normal file
25
vendor/grapesjs/LICENSE-preset-webpage.txt
vendored
Normal 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
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
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
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
3
vendor/grapesjs/preset-webpage.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue