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.
2026-09-17 01:35:21 +02:00
// 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>/.
2026-09-20 19:20:30 +02:00
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd" ;
feat(studio): AI assistant that runs on the user's own device
An Assistant drawer in Sirius Studio with two providers and no key, no
server and no spend. "This browser" runs an open model on the user's
GPU through WebGPU with WebLLM (vendored, Apache-2.0); weights download
once from the MLC mirror and stay in the browser cache. "Local
endpoint" talks to an OpenAI-compatible runtime on the user's machine
(Ollama, LM Studio), which unlocks larger models on a real GPU. Nothing
the user writes or builds leaves their device in either mode.
Three verbs: make a section, rewrite the selected text, restyle the
selection. The model returns HTML and CSS as data; Studio sanitises it
(no scripts, frames, handlers or imports) and inserts it through the
editor, with Undo. Model output is never executed.
The quantisation is chosen per GPU: q4f16 when the adapter exposes
shader-f16, q4f32 otherwise (Pascal-era cards lack it). Small models
often answer with bare HTML instead of JSON, so the parser accepts
both, prompts avoid literal placeholders one model echoed back, and an
out-of-memory or disposed runtime is reported as "pick a smaller
model" with the engine reset. Switching models starts a fresh worker.
Verified on an NVIDIA Pascal card: SmolLM2 360M rewrites text, Qwen2.5
Coder 0.5B builds a section; the 1.5B f32 build exceeded that card's
memory and now fails gracefully.
2026-09-20 15:22:34 +02:00
import { initAssistant } from "./studio-ai.js?v=20260920ai" ;
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.
2026-09-17 01:35:21 +02:00
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 ;
}
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
async function listFiles ( live = false ) {
const r = await fetch ( ` ${ API } /api/site/ ${ encodeURIComponent ( name ) } ${ live ? "?src=live" : "" } ` , { cache : "no-store" } ) ;
if ( ! r . ok ) return { files : [ ] , prefix : null , source : null } ;
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.
2026-09-17 01:35:21 +02:00
const j = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
return { files : j . files || [ ] , prefix : j . prefix ? ? null , source : j . source ? ? null } ;
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.
2026-09-17 01:35:21 +02:00
}
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
// ---------- import the page the name serves right now ----------
// Sites are hosted three ways: the Studio folder bns/<name>/, a CLI-published
// bucket the `s3` record points at (bns/<label>/ by convention), or inline
// HTML in the `h` record. The editor must start from whichever one is live,
// not from an empty Studio folder. Relative URLs are made absolute against
// the public site URL so the canvas shows the real images and links, and
// stylesheets are inlined so the imported page keeps its look.
let extraHeadLinks = [ ] ; // cross-origin stylesheets we could not inline (fonts CDNs)
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
// Rules the editor's CSS parser drops or cannot target — :root variables,
// html/body background and fonts, @import. Kept verbatim: injected into the
// canvas so the page looks like the live one, saved with the draft, and
// emitted ahead of the editor's CSS on export.
let importedCss = "" ;
// Attributes of the source page's <html> (lang, data-theme, class): themed
// sites key their variables on them, so they go on the canvas root and on
// the exported page.
let importedHtmlAttrs = { } ;
function splitRootCss ( css ) {
const root = [ ] , rest = [ ] ;
// Whole selectors that only address the document root — including themed
// variants such as html[data-theme="dark"] or :root:not([data-theme="light"])
// — but never descendants (body .hero stays with the editor).
const isRootSel = ( sel ) => / ^ ( : root | html | body ) [ ^ \ s , > + ~ ] * ( \ s * , \ s * ( : root | html | body ) [ ^ \ s , > + ~ ] * ) * $ / i . test ( sel . trim ( ) ) ;
let sheet ;
try { sheet = new CSSStyleSheet ( ) ; sheet . replaceSync ( css ) ; } catch { return { root : css , rest : "" } ; }
const walk = ( rules , into ) => {
for ( const r of rules ) {
if ( r . type === CSSRule . STYLE _RULE ) ( isRootSel ( r . selectorText ) ? into . root : into . rest ) . push ( r . cssText ) ;
else if ( r . type === CSSRule . MEDIA _RULE ) {
const sub = { root : [ ] , rest : [ ] } ; walk ( r . cssRules , sub ) ;
if ( sub . root . length ) into . root . push ( ` @media ${ r . conditionText } { ${ sub . root . join ( "\n" ) } } ` ) ;
if ( sub . rest . length ) into . rest . push ( ` @media ${ r . conditionText } { ${ sub . rest . join ( "\n" ) } } ` ) ;
} else if ( r . type === CSSRule . IMPORT _RULE ) into . root . push ( r . cssText ) ;
else into . rest . push ( r . cssText ) ;
}
} ;
walk ( sheet . cssRules , { root , rest } ) ;
return { root : root . join ( "\n" ) , rest : rest . join ( "\n" ) } ;
}
function injectImportedCss ( ) {
try {
const doc = editor . Canvas . getDocument ( ) ; if ( ! doc ) return ;
let st = doc . getElementById ( "sirius-imported" ) ;
if ( ! st ) { st = doc . createElement ( "style" ) ; st . id = "sirius-imported" ; }
2026-09-20 15:56:29 +02:00
// The editor's base sheet paints <body> white, which hides a page whose
// background lives on <html> (common in app bundles). Body stays
// transparent in the canvas; the export never had that rule anyway.
st . textContent = "html body{background-color:transparent}\n" + importedCss . replace ( /(^|[,{}\s])body(?=[\s,{.:\[#>])/g , "$1html body" ) ;
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
doc . head . appendChild ( st ) ;
for ( const [ k , v ] of Object . entries ( importedHtmlAttrs ) ) doc . documentElement . setAttribute ( k , v ) ;
for ( const href of extraHeadLinks ) if ( ! doc . head . querySelector ( ` link[href=" ${ href } "] ` ) ) { const l = doc . createElement ( "link" ) ; l . rel = "stylesheet" ; l . href = href ; doc . head . appendChild ( l ) ; }
} catch { }
}
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
function liveSource ( ) {
const rec = entry ? . records || { } ;
const s3 = typeof rec . s3 === "string" ? rec . s3 . trim ( ) . replace ( /\/?$/ , "/" ) : "" ;
if ( s3 && s3 !== folder ) return { kind : "s3" , label : s3 } ;
if ( ! s3 && typeof rec . h === "string" && rec . h ) return { kind : "inline" , label : "the inline h record" } ;
if ( s3 === folder ) return { kind : "studio" , label : folder } ;
return null ;
}
async function fetchLiveHtml ( ) {
const src = liveSource ( ) ;
if ( ! src ) return null ;
const url = src . kind === "studio" ? readUrl ( "index.html" ) : readUrl ( "index.html" ) + "?src=live" ;
const r = await fetch ( url , { cache : "no-store" } ) ;
if ( ! r . ok ) return null ;
return { html : await r . text ( ) , base : siteUrl ( ) , src } ;
}
async function importHtml ( html , base ) {
const doc = new DOMParser ( ) . parseFromString ( html , "text/html" ) ;
doc . querySelectorAll ( "script, base, noscript" ) . forEach ( ( n ) => n . remove ( ) ) ;
const isRel = ( u ) => ! ! u && ! /^(https?:|data:|blob:|mailto:|tel:|#|\/\/|javascript:)/i . test ( u . trim ( ) ) ;
const abs = ( u ) => { try { return new URL ( u . trim ( ) , base ) . href ; } catch { return u ; } } ;
doc . querySelectorAll ( "[src]" ) . forEach ( ( el ) => { const v = el . getAttribute ( "src" ) ; if ( isRel ( v ) ) el . setAttribute ( "src" , abs ( v ) ) ; } ) ;
doc . querySelectorAll ( "[poster]" ) . forEach ( ( el ) => { const v = el . getAttribute ( "poster" ) ; if ( isRel ( v ) ) el . setAttribute ( "poster" , abs ( v ) ) ; } ) ;
doc . querySelectorAll ( "a[href]" ) . forEach ( ( el ) => { const v = el . getAttribute ( "href" ) ; if ( isRel ( v ) ) el . setAttribute ( "href" , abs ( v ) ) ; } ) ;
doc . querySelectorAll ( "[srcset]" ) . forEach ( ( el ) => {
el . setAttribute ( "srcset" , el . getAttribute ( "srcset" ) . split ( "," ) . map ( ( part ) => { const [ u , d ] = part . trim ( ) . split ( /\s+/ ) ; return ( isRel ( u ) ? abs ( u ) : u ) + ( d ? " " + d : "" ) ; } ) . join ( ", " ) ) ;
} ) ;
let css = "" ;
extraHeadLinks = [ ] ;
for ( const link of [ ... doc . querySelectorAll ( 'link[rel~="stylesheet"][href]' ) ] ) {
const href = link . getAttribute ( "href" ) ;
const url = isRel ( href ) ? abs ( href ) : href ;
try {
const r = await fetch ( url , { cache : "no-store" } ) ;
if ( ! r . ok ) throw new Error ( String ( r . status ) ) ;
css += ` \n /* ${ url } */ \n ` + ( await r . text ( ) ) . replace ( /url\(\s*(['"]?)([^'")]+)\1\s*\)/g , ( m , q , u ) => ( isRel ( u ) ? ` url( ${ q } ${ new URL ( u . trim ( ) , url ) . href } ${ q } ) ` : m ) ) ;
} catch { extraHeadLinks . push ( url ) ; }
link . remove ( ) ;
}
doc . querySelectorAll ( "style" ) . forEach ( ( st ) => { css += "\n" + st . textContent ; st . remove ( ) ; } ) ;
css = css . replace ( /url\(\s*(['"]?)([^'")]+)\1\s*\)/g , ( m , q , u ) => ( isRel ( u ) ? ` url( ${ q } ${ abs ( u ) } ${ q } ) ` : m ) ) ;
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
const parts = splitRootCss ( css ) ;
importedCss = parts . root ;
importedHtmlAttrs = Object . fromEntries ( [ ... doc . documentElement . attributes ] . map ( ( a ) => [ a . name , a . value ] ) . filter ( ( [ k ] ) => k === "lang" || k === "class" || k . startsWith ( "data-" ) ) ) ;
const bodyClass = doc . body . getAttribute ( "class" ) ;
if ( bodyClass ) editor . getWrapper ( ) . addClass ( bodyClass . split ( /\s+/ ) . filter ( Boolean ) ) ;
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
editor . setComponents ( doc . body . innerHTML ) ;
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
editor . setStyle ( parts . rest ) ;
injectImportedCss ( ) ;
setTimeout ( injectImportedCss , 50 ) ;
editor . once ( "canvas:frame:load" , injectImportedCss ) ;
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
}
async function importLive ( { confirmIfDirty = true } = { } ) {
if ( confirmIfDirty && dirty && ! confirm ( "Replace your unsaved changes with the page the name serves right now?" ) ) return false ;
status ( "Importing the live site…" , "busy" ) ;
try {
const live = await fetchLiveHtml ( ) ;
if ( ! live ) { status ( "The name serves no page yet" , "err" ) ; return false ; }
await importHtml ( live . html , live . base ) ;
dirty = true ; $ ( "btn-draft" ) . disabled = false ;
$ ( "notice" ) . hidden = true ;
status ( ` Imported the live site from ${ live . src . label } ` , "ok" ) ;
return true ;
} catch ( e ) { status ( "Import failed: " + ( e . message || e ) , "err" ) ; return false ; }
}
function notice ( text ) { $ ( "notice-text" ) . textContent = text ; $ ( "notice" ) . hidden = false ; }
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.
2026-09-17 01:35:21 +02:00
// ---------- 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 ) } < / h 1 >
< 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 >
< / h e a d e r >
< section id = "more" class = "features" >
< div class = "f" > < h3 > Yours < / h 3 > < p > T h e c e r t i f i c a t e s i t s i n y o u r w a l l e t . N o b o d y c a n t a k e i t b a c k . < / p > < / d i v >
< div class = "f" > < h3 > Fast < / h 3 > < p > S e r v e d f r o m S i a t h r o u g h a n y B C N R r e s o l v e r o r t h e p u b l i c g a t e w a y . < / p > < / d i v >
< div class = "f" > < h3 > Simple < / h 3 > < p > E d i t t h i s p a g e i n S i r i u s S t u d i o a n d p u b l i s h i n o n e c l i c k . < / p > < / d i v >
< / s e c t i o n >
< footer class = "foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } · built with Sirius Studio < / f o o t e r > ` ,
css : `
body { margin : 0 ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; color : # 12161 f ; background : # fff }
. hero { padding : 96 px 24 px 72 px ; text - align : center ; background : linear - gradient ( 180 deg , # 0b0 e14 , # 141 a24 ) ; color : # f1f4fa }
. hero h1 { font - size : 44 px ; margin : 0 0 12 px ; letter - spacing : - . 01 em }
. hero p { font - size : 18 px ; color : # b8c2d4 ; max - width : 560 px ; margin : 0 auto 26 px }
. cta { display : inline - block ; background : # d6ff3d ; color : # 0b0 e14 ; font - weight : 700 ; padding : 12 px 22 px ; border - radius : 10 px ; text - decoration : none }
. features { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 220 px , 1 fr ) ) ; gap : 20 px ; max - width : 960 px ; margin : 0 auto ; padding : 56 px 24 px }
. f { background : # f5f7fb ; border - radius : 14 px ; padding : 22 px }
. f h3 { margin : 0 0 8 px }
. f p { margin : 0 ; color : # 4 a5568 }
. foot { text - align : center ; padding : 28 px ; color : # 6 a7488 ; font - size : 13 px ; border - top : 1 px solid # e6e9f0 } ` ,
} ,
profile : {
html : `
< main class = "card" >
< div class = "avatar" > ★ < / d i v >
< h1 > $ { esc ( name ) } < / h 1 >
< 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 >
< / m a i n > ` ,
css : `
body { margin : 0 ; min - height : 100 vh ; display : flex ; align - items : center ; justify - content : center ; background : radial - gradient ( circle at 50 % 0 , # 1 a2233 , # 050810 ) ; font - family : system - ui , sans - serif ; color : # f1f4fa }
. card { width : min ( 420 px , 92 vw ) ; text - align : center ; padding : 36 px 24 px }
. avatar { width : 88 px ; height : 88 px ; border - radius : 50 % ; background : # d6ff3d ; color : # 0b0 e14 ; font - size : 40 px ; line - height : 88 px ; margin : 0 auto 14 px }
h1 { margin : 0 0 6 px ; font - size : 26 px }
. bio { color : # b8c2d4 ; margin : 0 0 22 px }
. link { display : block ; background : # 141 a24 ; border : 1 px solid rgba ( 255 , 255 , 255 , . 1 ) ; color : # f1f4fa ; text - decoration : none ; padding : 14 px ; border - radius : 12 px ; margin : 10 px 0 ; font - weight : 600 }
. link : hover { border - color : # d6ff3d } ` ,
} ,
business : {
html : `
< nav class = "nav" > < b > $ { esc ( name ) } < / b > < s p a n > < a h r e f = " # a b o u t " > A b o u t < / a > < a h r e f = " # s e r v i c e s " > S e r v i c e s < / a > < a h r e f = " # c o n t a c t " > C o n t a c t < / a > < / s p a n > < / n a v >
< header class = "top" > < h1 > We are open < / h 1 > < p > S a y w h a t y o u d o i n o n e s e n t e n c e . < / p > < / h e a d e r >
< section id = "about" class = "sec" > < h2 > About < / h 2 > < p > T w o o r t h r e e s e n t e n c e s a b o u t t h e b u s i n e s s , t h e p e o p l e a n d t h e p l a c e . < / p > < / s e c t i o n >
< section id = "services" class = "sec alt" > < h2 > Services < / h 2 >
< ul class = "grid" > < li > < b > Service one < / b > < s p a n > S h o r t d e s c r i p t i o n . < / s p a n > < / l i > < l i > < b > S e r v i c e t w o < / b > < s p a n > S h o r t d e s c r i p t i o n . < / s p a n > < / l i > < l i > < b > S e r v i c e t h r e e < / b > < s p a n > S h o r t d e s c r i p t i o n . < / s p a n > < / l i > < / u l > < / s e c t i o n >
< section id = "contact" class = "sec" > < h2 > Contact < / h 2 > < p > S t r e e t 1 , C i t y · M o n – F r i 9 – 1 8 · < a h r e f = " m a i l t o : " > h e l l o @ e x a m p l e < / a > < / p > < / s e c t i o n >
< footer class = "foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } < / f o o t e r > ` ,
css : `
body { margin : 0 ; font - family : Georgia , serif ; color : # 2 b1d12 ; background : # fff8f0 }
. nav { display : flex ; justify - content : space - between ; align - items : center ; padding : 16 px 28 px ; border - bottom : 1 px solid # eadfd0 ; font - family : system - ui , sans - serif }
. nav a { margin - left : 18 px ; color : # 7 a3b00 ; text - decoration : none }
. top { text - align : center ; padding : 80 px 24 px ; background : # 7 a3b00 ; color : # fff4e6 }
. top h1 { font - size : 42 px ; margin : 0 0 10 px }
. sec { max - width : 820 px ; margin : 0 auto ; padding : 48 px 24 px }
. sec . alt { max - width : none ; background : # fff1e0 }
. sec . alt h2 , . sec . alt ul { max - width : 820 px ; margin - left : auto ; margin - right : auto }
. grid { list - style : none ; padding : 0 ; display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 200 px , 1 fr ) ) ; gap : 16 px }
. grid li { background : # fff ; border - radius : 12 px ; padding : 18 px ; box - shadow : 0 2 px 10 px rgba ( 0 , 0 , 0 , . 05 ) }
. grid b { display : block ; margin - bottom : 6 px }
. foot { text - align : center ; padding : 24 px ; color : # 8 a6d55 ; font - size : 13 px } ` ,
} ,
} ;
// ---------- editor ----------
function plugin ( nameOrObj ) {
const p = window [ nameOrObj ] ;
return p && ( p . default || p ) ;
}
function initEditor ( ) {
const plugins = [ ] ;
const pluginsOpts = { } ;
feat(studio): readable dark theme, resize handles, richer text tools, ready-made sections, more blocks
The editor panels were a muddy grey with pinkish text (4.7:1) because
the webpage preset injects its own demo palette after ours. That theme
is now switched off and the editor is themed through GrapesJS's CSS
variables: dark panels, near-white text (15:1 and better), acid accents,
readable inputs and toolbars.
Editing is closer to what people expect from Tilda: every box has drag
handles for width and height, the text toolbar gains size, colour,
highlight, alignment and heading controls, the font list has the site
families, and selecting anything opens the style panel. Nine ready-made
sections (hero, features, image+text, gallery, testimonial, pricing,
call to action, contact form, footer) drop in with their own styles.
Plugins vendored from npm (no CDN): forms, navbar, tabs, tooltip,
custom code, countdown, background styles, touch support; GrapesJS
itself is already current at 0.23.6. The basic blocks (columns, text,
image, video, map) had never loaded: the bundle registers itself as
gjs-blocks-basic, not grapesjs-blocks-basic. VERSIONS.txt records what
is vendored and how to update it.
2026-09-20 14:46:22 +02:00
const use = ( global , opts = { } ) => { const p = plugin ( global ) ; if ( p ) { plugins . push ( p ) ; pluginsOpts [ p ] = opts ; } } ;
use ( "gjs-blocks-basic" , { flexGrid : true , category : "Layout" } ) ; // the bundle registers itself as gjs-blocks-basic
// useCustomTheme:false — the preset's mauve demo palette used to override
// ours and made the panels low-contrast; the theme lives in studio.html.
use ( "grapesjs-preset-webpage" , { modalImportTitle : "Import HTML" , blocks : [ ] , useCustomTheme : false , showStylesOnChange : true } ) ;
use ( "grapesjs-plugin-forms" , { category : "Forms" } ) ;
use ( "grapesjs-navbar" ) ;
use ( "grapesjs-tabs" ) ;
use ( "grapesjs-tooltip" ) ;
use ( "grapesjs-custom-code" ) ;
use ( "grapesjs-component-countdown" ) ;
use ( "grapesjs-style-bg" ) ;
use ( "grapesjs-touch" ) ;
2026-09-20 16:18:11 +02:00
// PostCSS parser: the browser CSSOM path drops shorthands that contain
// var() (background: var(--surface) came back empty), which stripped
// imported pages of their panels and borders.
use ( "grapesjs-parser-postcss" ) ;
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.
2026-09-17 01:35:21 +02:00
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 ; } ) ;
feat(studio): readable dark theme, resize handles, richer text tools, ready-made sections, more blocks
The editor panels were a muddy grey with pinkish text (4.7:1) because
the webpage preset injects its own demo palette after ours. That theme
is now switched off and the editor is themed through GrapesJS's CSS
variables: dark panels, near-white text (15:1 and better), acid accents,
readable inputs and toolbars.
Editing is closer to what people expect from Tilda: every box has drag
handles for width and height, the text toolbar gains size, colour,
highlight, alignment and heading controls, the font list has the site
families, and selecting anything opens the style panel. Nine ready-made
sections (hero, features, image+text, gallery, testimonial, pricing,
call to action, contact form, footer) drop in with their own styles.
Plugins vendored from npm (no CDN): forms, navbar, tabs, tooltip,
custom code, countdown, background styles, touch support; GrapesJS
itself is already current at 0.23.6. The basic blocks (columns, text,
image, video, map) had never loaded: the bundle registers itself as
gjs-blocks-basic, not grapesjs-blocks-basic. VERSIONS.txt records what
is vendored and how to update it.
2026-09-20 14:46:22 +02:00
setupEditing ( ) ;
feat(studio): AI assistant that runs on the user's own device
An Assistant drawer in Sirius Studio with two providers and no key, no
server and no spend. "This browser" runs an open model on the user's
GPU through WebGPU with WebLLM (vendored, Apache-2.0); weights download
once from the MLC mirror and stay in the browser cache. "Local
endpoint" talks to an OpenAI-compatible runtime on the user's machine
(Ollama, LM Studio), which unlocks larger models on a real GPU. Nothing
the user writes or builds leaves their device in either mode.
Three verbs: make a section, rewrite the selected text, restyle the
selection. The model returns HTML and CSS as data; Studio sanitises it
(no scripts, frames, handlers or imports) and inserts it through the
editor, with Undo. Model output is never executed.
The quantisation is chosen per GPU: q4f16 when the adapter exposes
shader-f16, q4f32 otherwise (Pascal-era cards lack it). Small models
often answer with bare HTML instead of JSON, so the parser accepts
both, prompts avoid literal placeholders one model echoed back, and an
out-of-memory or disposed runtime is reported as "pick a smaller
model" with the engine reset. Switching models starts a fresh worker.
Verified on an NVIDIA Pascal card: SmolLM2 360M rewrites text, Qwen2.5
Coder 0.5B builds a section; the 1.5B f32 build exceeded that card's
memory and now fails gracefully.
2026-09-20 15:22:34 +02:00
try { initAssistant ( editor , { esc , name } ) ; } catch ( e ) { console . warn ( "assistant unavailable:" , e ) ; }
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.
2026-09-17 01:35:21 +02:00
// 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 = "" ; } } ) ;
}
feat(studio): readable dark theme, resize handles, richer text tools, ready-made sections, more blocks
The editor panels were a muddy grey with pinkish text (4.7:1) because
the webpage preset injects its own demo palette after ours. That theme
is now switched off and the editor is themed through GrapesJS's CSS
variables: dark panels, near-white text (15:1 and better), acid accents,
readable inputs and toolbars.
Editing is closer to what people expect from Tilda: every box has drag
handles for width and height, the text toolbar gains size, colour,
highlight, alignment and heading controls, the font list has the site
families, and selecting anything opens the style panel. Nine ready-made
sections (hero, features, image+text, gallery, testimonial, pricing,
call to action, contact form, footer) drop in with their own styles.
Plugins vendored from npm (no CDN): forms, navbar, tabs, tooltip,
custom code, countdown, background styles, touch support; GrapesJS
itself is already current at 0.23.6. The basic blocks (columns, text,
image, video, map) had never loaded: the bundle registers itself as
gjs-blocks-basic, not grapesjs-blocks-basic. VERSIONS.txt records what
is vendored and how to update it.
2026-09-20 14:46:22 +02:00
// ---------- Tilda-style editing: resize handles, richer text toolbar,
// ready-made sections, style panel on select ----------
function setupEditing ( ) {
// 1. Every box can be dragged to size: right, bottom and corner handles set
// width/height on the element (images keep their own ratio-aware resizer).
const rz = { tl : 0 , tc : 0 , tr : 0 , cl : 0 , cr : 1 , bl : 0 , bc : 1 , br : 1 , minDim : 16 } ;
for ( const t of [ "default" , "text" , "link" , "video" , "map" , "table" , "row" , "cell" , "svg" , "iframe" ] ) {
if ( editor . Components . getType ( t ) ) editor . Components . addType ( t , { model : { defaults : { resizable : rz } } } ) ;
}
// 2. Text toolbar: size, colour, highlight, alignment, headings.
const rte = editor . RichTextEditor ;
const wrapSel = ( r , style ) => { const t = String ( r . selection ( ) ) ; if ( t ) r . insertHTML ( ` <span style=" ${ style } "> ${ esc ( t ) } </span> ` ) ; } ;
rte . add ( "fontsize" , {
icon : ` <select title="Text size"> ${ [ "" , "12px" , "14px" , "16px" , "18px" , "20px" , "24px" , "28px" , "32px" , "40px" , "48px" , "64px" ] . map ( ( v ) => ` <option value=" ${ v } "> ${ v || "Size" } </option> ` ) . join ( "" ) } </select> ` ,
event : "change" ,
result : ( r , action ) => { const v = action . btn . querySelector ( "select" ) . value ; if ( v ) wrapSel ( r , ` font-size: ${ v } ` ) ; action . btn . querySelector ( "select" ) . value = "" ; } ,
} ) ;
rte . add ( "forecolor" , { icon : ` <input type="color" title="Text colour" value="#111111"> ` , event : "change" , result : ( r , action ) => r . exec ( "foreColor" , action . btn . querySelector ( "input" ) . value ) } ) ;
rte . add ( "hilite" , { icon : ` <input type="color" title="Highlight" value="#fff59d"> ` , event : "change" , result : ( r , action ) => r . exec ( "hiliteColor" , action . btn . querySelector ( "input" ) . value ) } ) ;
for ( const [ name , cmd , glyph , title ] of [ [ "alignL" , "justifyLeft" , "≡" , "Align left" ] , [ "alignC" , "justifyCenter" , "☰" , "Centre" ] , [ "alignR" , "justifyRight" , "≡" , "Align right" ] ] ) {
rte . add ( name , { icon : ` <b title=" ${ title } " style="font-style:normal"> ${ glyph } </b> ` , result : ( r ) => r . exec ( cmd ) } ) ;
}
rte . add ( "heading" , {
icon : ` <select title="Paragraph style"><option value="">Style</option><option value="h1">Heading 1</option><option value="h2">Heading 2</option><option value="h3">Heading 3</option><option value="p">Paragraph</option><option value="blockquote">Quote</option></select> ` ,
event : "change" ,
result : ( r , action ) => { const v = action . btn . querySelector ( "select" ) . value ; if ( v ) r . exec ( "formatBlock" , v ) ; action . btn . querySelector ( "select" ) . value = "" ; } ,
} ) ;
// 3. Fonts the exported page can rely on without loading anything.
const ff = editor . StyleManager . getProperty ( "typography" , "font-family" ) ;
if ( ff ) {
const extra = [
[ "'DM Sans', system-ui, sans-serif" , "DM Sans" ] , [ "Fraunces, Georgia, serif" , "Fraunces" ] , [ "Ubuntu, system-ui, sans-serif" , "Ubuntu" ] ,
[ "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" , "System" ] , [ "Georgia, 'Times New Roman', serif" , "Georgia" ] ,
[ "'JetBrains Mono', ui-monospace, Menlo, monospace" , "Mono" ] ,
] . map ( ( [ id , label ] ) => ( { id , label } ) ) ;
ff . set ( "options" , [ ... extra , ... ( ff . get ( "options" ) || [ ] ) ] ) ;
}
// 4. Ready-made sections, Tilda style: drop, then edit text and images in place.
const bm = editor . BlockManager ;
const sec = ( id , label , html , css ) => bm . add ( ` sx- ${ id } ` , { label , category : "Sections" , media : SECTION _ICON , content : ` <style> ${ css } </style> ${ html } ` } ) ;
sec ( "hero" , "Hero" , ` <section class="sx-hero"><div class="sx-wrap"><p class="sx-kicker">NEW</p><h1>A headline that earns the scroll</h1><p class="sx-lead">One sentence on what this is and who it is for. Keep it honest and short.</p><a class="sx-btn" href="#">Get started</a></div></section> ` ,
` .sx-hero{padding:96px 24px;background:#0b0e14;color:#f1f4fa;text-align:center}.sx-wrap{max-width:820px;margin:0 auto}.sx-kicker{letter-spacing:.2em;font-size:12px;color:#d6ff3d;margin:0 0 12px}.sx-hero h1{font-size:44px;line-height:1.1;margin:0 0 16px}.sx-lead{font-size:18px;color:#b8c2d4;margin:0 0 28px}.sx-btn{display:inline-block;background:#d6ff3d;color:#0b0e14;padding:12px 22px;border-radius:10px;text-decoration:none;font-weight:600} ` ) ;
sec ( "features" , "Three features" , ` <section class="sx-feat"><div class="sx-grid3"><div><h3>Fast</h3><p>Say what it does in one line.</p></div><div><h3>Simple</h3><p>Say why it is easy.</p></div><div><h3>Yours</h3><p>Say what the visitor keeps.</p></div></div></section> ` ,
` .sx-feat{padding:64px 24px;background:#fff;color:#111}.sx-grid3{max-width:1000px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:28px}.sx-feat h3{margin:0 0 8px;font-size:20px}.sx-feat p{margin:0;color:#555;line-height:1.6} ` ) ;
sec ( "split" , "Image + text" , ` <section class="sx-split"><div class="sx-split-in"><img src="https://silentmode.st/sirius-x/brand/banner.svg" alt=""><div><h2>Show, then tell</h2><p>A picture on one side, the explanation on the other. Swap the image by clicking it.</p><a class="sx-btn2" href="#">Learn more →</a></div></div></section> ` ,
` .sx-split{padding:64px 24px;background:#f6f7f9;color:#111}.sx-split-in{max-width:1000px;margin:0 auto;display:grid;grid-template-columns:1fr 1fr;gap:40px;align-items:center}.sx-split img{width:100%;border-radius:14px}.sx-split h2{font-size:32px;margin:0 0 12px}.sx-split p{color:#555;line-height:1.6}.sx-btn2{color:#111;font-weight:600;text-decoration:none}@media(max-width:700px){.sx-split-in{grid-template-columns:1fr}} ` ) ;
sec ( "gallery" , "Gallery" , ` <section class="sx-gal"><div class="sx-gal-grid"> ${ Array . from ( { length : 6 } , () => ` < img src = "https://silentmode.st/sirius-x/brand/avatar.svg" alt = "" > ` ).join("")}</div></section> ` ,
` .sx-gal{padding:48px 24px;background:#fff}.sx-gal-grid{max-width:1000px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.sx-gal img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:12px;background:#eee} ` ) ;
sec ( "quote" , "Testimonial" , ` <section class="sx-quote"><blockquote>“Put the kindest true thing a customer said right here.”</blockquote><p class="sx-who">— A real person, their role</p></section> ` ,
` .sx-quote{padding:72px 24px;background:#0b0e14;color:#f1f4fa;text-align:center}.sx-quote blockquote{max-width:760px;margin:0 auto 14px;font-size:26px;line-height:1.35;font-style:italic}.sx-who{color:#b8c2d4;margin:0} ` ) ;
sec ( "pricing" , "Pricing" , ` <section class="sx-price"><div class="sx-grid3"><div class="sx-plan"><h3>Basic</h3><p class="sx-amt"> $ 0</p><ul><li>One page</li><li>Your name</li></ul><a class="sx-btn" href="#">Choose</a></div><div class="sx-plan sx-hot"><h3>Pro</h3><p class="sx-amt"> $ 9</p><ul><li>Everything in Basic</li><li>Priority help</li></ul><a class="sx-btn" href="#">Choose</a></div><div class="sx-plan"><h3>Team</h3><p class="sx-amt"> $ 29</p><ul><li>Everything in Pro</li><li>Five seats</li></ul><a class="sx-btn" href="#">Choose</a></div></div></section> ` ,
` .sx-price{padding:64px 24px;background:#f6f7f9;color:#111}.sx-price .sx-grid3{max-width:1000px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px}.sx-plan{background:#fff;border:1px solid #e6e8ee;border-radius:16px;padding:28px 24px;text-align:center}.sx-hot{border-color:#111}.sx-amt{font-size:40px;font-weight:700;margin:8px 0 16px}.sx-plan ul{list-style:none;padding:0;margin:0 0 20px;color:#555;line-height:1.9}.sx-price .sx-btn{display:inline-block;background:#111;color:#fff;padding:10px 20px;border-radius:10px;text-decoration:none;font-weight:600} ` ) ;
sec ( "cta" , "Call to action" , ` <section class="sx-cta"><h2>Ready when you are</h2><p>One line that removes the last doubt.</p><a class="sx-btn" href="#">Do the thing →</a></section> ` ,
` .sx-cta{padding:72px 24px;text-align:center;background:#d6ff3d;color:#0b0e14}.sx-cta h2{font-size:34px;margin:0 0 8px}.sx-cta p{margin:0 0 24px;font-size:17px}.sx-cta .sx-btn{display:inline-block;background:#0b0e14;color:#d6ff3d;padding:12px 22px;border-radius:10px;text-decoration:none;font-weight:600} ` ) ;
sec ( "contact" , "Contact form" , ` <section class="sx-contact"><div class="sx-wrap"><h2>Get in touch</h2><form class="sx-form" method="post"><input type="text" name="name" placeholder="Your name"><input type="email" name="email" placeholder="Email"><textarea name="message" rows="4" placeholder="Message"></textarea><button type="submit" class="sx-btn">Send</button></form></div></section> ` ,
` .sx-contact{padding:64px 24px;background:#fff;color:#111}.sx-contact .sx-wrap{max-width:560px;margin:0 auto}.sx-contact h2{margin:0 0 16px}.sx-form{display:grid;gap:10px}.sx-form input,.sx-form textarea{width:100%;padding:12px;border:1px solid #d9dce3;border-radius:10px;font:inherit}.sx-contact .sx-btn{background:#111;color:#fff;border:0;padding:12px 20px;border-radius:10px;font-weight:600;cursor:pointer} ` ) ;
sec ( "footer" , "Footer" , ` <footer class="sx-footer"><div class="sx-wrap sx-foot-in"><span>© <b>your name</b> · on Bitcoin Cash</span><nav><a href="#">About</a><a href="#">Contact</a><a href="#">Privacy</a></nav></div></footer> ` ,
` .sx-footer{padding:28px 24px;background:#0b0e14;color:#b8c2d4;font-size:14px}.sx-foot-in{max-width:1000px;margin:0 auto;display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap}.sx-footer a{color:#b8c2d4;text-decoration:none;margin-left:16px}.sx-footer b{color:#f1f4fa} ` ) ;
// Group the plugin blocks under readable categories.
const cat = { navbar : "Sections" , tabs : "Widgets" , tooltip : "Widgets" , "custom-code" : "Widgets" , countdown : "Widgets" } ;
bm . getAll ( ) . forEach ( ( b ) => { const c = cat [ b . getId ( ) ] ; if ( c ) b . set ( "category" , c ) ; } ) ;
// 5. Selecting anything opens the style panel; the layer tree is one click away.
editor . on ( "component:selected" , ( ) => { const btn = editor . Panels . getButton ( "views" , "open-sm" ) ; if ( btn && ! btn . get ( "active" ) ) btn . set ( "active" , true ) ; } ) ;
}
const SECTION _ICON = ` <svg viewBox="0 0 24 24" width="36" height="36"><rect x="2" y="4" width="20" height="5" rx="1.5" fill="currentColor" opacity=".9"/><rect x="2" y="11" width="20" height="9" rx="1.5" fill="currentColor" opacity=".45"/></svg> ` ;
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.
2026-09-17 01:35:21 +02:00
function loadTemplate ( key ) {
const t = TEMPLATES [ key ] || TEMPLATES . blank ;
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
importedCss = "" ; extraHeadLinks = [ ] ; importedHtmlAttrs = { } ; injectImportedCss ( ) ;
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.
2026-09-17 01:35:21 +02:00
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 ( "" ) ;
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
const links = extraHeadLinks . map ( ( h ) => ` <link rel="stylesheet" href=" ${ esc ( h ) } "> ` ) . join ( "\n" ) ;
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
const htmlAttrs = Object . entries ( { lang : document . documentElement . lang || "en" , ... importedHtmlAttrs } ) . map ( ( [ k , v ] ) => ` ${ k } =" ${ esc ( v ) } " ` ) . join ( " " ) ;
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.
2026-09-17 01:35:21 +02:00
return ` <!doctype html>
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
< html $ { htmlAttrs } >
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.
2026-09-17 01:35:21 +02:00
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > $ { esc ( name ) } < / t i t l e >
< meta name = "generator" content = "Sirius Studio" >
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
$ { links }
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.
2026-09-17 01:35:21 +02:00
< style >
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
$ { rel ( importedCss ) }
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.
2026-09-17 01:35:21 +02:00
$ { rel ( css ) }
< / s t y l e >
< / h e a d >
$ { rel ( html ) }
< / h t m l >
` ;
}
// ---------- 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 {
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
const data = JSON . stringify ( { v : 1 , name , saved _at : new Date ( ) . toISOString ( ) , project : editor . getProjectData ( ) , importedCss , extraHeadLinks , importedHtmlAttrs } ) ;
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.
2026-09-17 01:35:21 +02:00
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 } ` ) ;
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
const data = JSON . stringify ( { v : 1 , name , saved _at : new Date ( ) . toISOString ( ) , project : editor . getProjectData ( ) , importedCss , extraHeadLinks , importedHtmlAttrs } ) ;
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.
2026-09-17 01:35:21 +02:00
await putFile ( "_studio.json" , data , "application/json" ) ;
pushStep ( "saved editor project (_studio.json)" ) ;
dirty = false ; $ ( "btn-draft" ) . disabled = true ;
2026-09-20 19:20:30 +02:00
// Point the name at the folder if it does not already. Re-read the
// registry first: `entry` from boot can be stale or missing (a name
// indexed after the page opened), and a stale view here means a
// needless on-chain transaction on every publish.
try { const r = await fetch ( ` ${ API } /api/name/ ${ encodeURIComponent ( name ) } ` , { cache : "no-store" } ) ; if ( r . ok ) entry = await r . json ( ) ; } catch { }
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.
2026-09-17 01:35:21 +02:00
const rec = entry ? . records || { } ;
2026-09-20 19:20:30 +02:00
const norm = ( v ) => String ( v || "" ) . trim ( ) . replace ( /\/+$/ , "" ) + "/" ;
const pointsHere = norm ( rec . s3 ) === folder ;
if ( ! pointsHere && ! entry ) {
pushStep ( "the registry has not indexed this name yet — files are up; pointing the name is skipped until it appears (or set Hosting in the dashboard)" ) ;
} else if ( ! pointsHere ) {
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.
2026-09-17 01:35:21 +02:00
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 ;
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
const src = liveSource ( ) ;
if ( src ) { $ ( "btn-view" ) . href = siteUrl ( ) ; $ ( "btn-view" ) . hidden = false ; $ ( "btn-import" ) . hidden = false ; }
// Start from the right thing: the saved Studio draft if there is one and
// it is what the name serves; otherwise the page the name serves right now
// (wherever it lives); otherwise the template picker.
const { files } = await listFiles ( ) ;
const draft = files . find ( ( f ) => f . path === "_studio.json" ) ;
const studioIndex = files . find ( ( f ) => f . path === "index.html" ) ;
if ( draft ) {
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.
2026-09-17 01:35:21 +02:00
try {
const j = await ( await fetch ( readUrl ( "_studio.json" ) , { cache : "no-store" } ) ) . json ( ) ;
fix(studio): imported pages keep their theme (root variables, body rules, html attributes)
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
2026-09-20 01:33:03 +02:00
if ( j ? . project ) {
editor . loadProjectData ( j . project ) ;
importedCss = typeof j . importedCss === "string" ? j . importedCss : "" ; extraHeadLinks = Array . isArray ( j . extraHeadLinks ) ? j . extraHeadLinks : [ ] ;
importedHtmlAttrs = j . importedHtmlAttrs && typeof j . importedHtmlAttrs === "object" ? j . importedHtmlAttrs : { } ;
editor . once ( "canvas:frame:load" , injectImportedCss ) ; injectImportedCss ( ) ;
dirty = false ; status ( ` Loaded draft from ${ j . saved _at ? new Date ( j . saved _at ) . toLocaleString ( ) : "Sia" } ` , "ok" ) ;
}
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
if ( src && src . kind !== "studio" ) notice ( ` This draft is not what visitors see: the name currently serves ${ src . label } . ` ) ;
else if ( studioIndex && j ? . saved _at && studioIndex . modified && Date . parse ( studioIndex . modified ) > Date . parse ( j . saved _at ) + 60_000 ) notice ( "index.html on Sia is newer than this draft — it was published by another tool." ) ;
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.
2026-09-17 01:35:21 +02:00
} catch ( e ) { status ( "Could not load the saved project: " + ( e . message || e ) , "err" ) ; $ ( "picker" ) . hidden = false ; }
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
} else if ( src ) {
const ok = await importLive ( { confirmIfDirty : false } ) ;
if ( ok ) { dirty = false ; $ ( "btn-draft" ) . disabled = true ; if ( src . kind !== "studio" ) notice ( ` Imported the page the name serves from ${ src . label } . Publishing writes to ${ folder } and repoints the name there. ` ) ; }
else { $ ( "picker" ) . hidden = false ; }
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.
2026-09-17 01:35:21 +02:00
} else {
$ ( "picker" ) . hidden = false ; status ( "New site" ) ;
}
} ) ( ) ;
fix(studio): open the page the name actually serves, not an empty folder
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
2026-09-20 01:22:21 +02:00
$ ( "btn-import" ) . addEventListener ( "click" , ( ) => importLive ( ) ) ;
$ ( "notice-import" ) . addEventListener ( "click" , ( ) => importLive ( ) ) ;
$ ( "notice-close" ) . addEventListener ( "click" , ( ) => { $ ( "notice" ) . hidden = true ; } ) ;
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.
2026-09-17 01:35:21 +02:00
$ ( "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 ; } ) ;