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 ;
2026-09-21 01:19:40 +02:00
let pages = [ ] ; // [{ path, title, project, importedCss, extraHeadLinks, importedHtmlAttrs, dirty }]
let current = 0 ; // index in pages of the page on the canvas
let removed = [ ] ; // paths deleted since the last publish — swept off Sia on the next one
let loading = false ; // true while a page is being poured into the editor, so it does not count as an edit
let dirty = false ; // the page on the canvas has unsaved edits (mirrors pages[current].dirty)
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 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 ;
}
2026-09-21 01:19:40 +02:00
async function delFile ( path ) {
const headers = await signedHeaders ( path , new Uint8Array ( 0 ) ) ; // the gateway signs DELETE over an empty body
const r = await fetch ( readUrl ( path ) , { method : "DELETE" , headers } ) ;
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 ) ;
2026-09-21 01:19:40 +02:00
markDirty ( true ) ;
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
$ ( "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 ; }
2026-09-21 01:19:40 +02:00
// ---------- pages ----------
// A name's folder holds a whole site, not one index.html. Each page is one
// entry here and one file in the bucket. The editor only ever holds the page
// being looked at, so switching parks the current one back into the array
// first; Draft and Publish both walk the whole array, which is what keeps the
// pages of a site from drifting out of step with each other.
//
// The home page is pinned at index 0: it is what the name serves at /, so it
// cannot be deleted and its path cannot move.
const newPage = ( path , title ) => ( { path , title , project : null , importedCss : "" , extraHeadLinks : [ ] , importedHtmlAttrs : { } , dirty : false } ) ;
// A title becomes a folder with its own index.html, so the page answers to a
// directory URL (/about/) the way the gateway and every resolver expect.
function slugToPath ( s ) {
const slug = String ( s || "" ) . toLowerCase ( ) . trim ( )
. replace ( /[^a-z0-9/\- ]+/g , "" ) . replace ( /[\s_]+/g , "-" ) . replace ( /-{2,}/g , "-" )
. replace ( /^[-/]+|[-/]+$/g , "" ) . replace ( /\/{2,}/g , "/" ) ;
return slug ? ` ${ slug } /index.html ` : "" ;
}
const pageDepth = ( path ) => ( path . match ( /\//g ) || [ ] ) . length ;
const anyDirty = ( ) => pages . some ( ( p ) => p . dirty ) ;
function markDirty ( v = true ) {
dirty = v ;
if ( pages [ current ] ) pages [ current ] . dirty = v ;
if ( ! v ) for ( const p of pages ) p . dirty = false ;
$ ( "btn-draft" ) . disabled = ! anyDirty ( ) ;
renderPages ( ) ;
}
function capturePage ( ) {
const p = pages [ current ] ;
if ( ! p || ! editor ) return ;
p . project = editor . getProjectData ( ) ;
p . importedCss = importedCss ;
p . extraHeadLinks = extraHeadLinks ;
p . importedHtmlAttrs = importedHtmlAttrs ;
}
function applyPage ( i ) {
const p = pages [ i ] ;
if ( ! p || ! editor ) return ;
current = i ;
// Pouring a page in fires a storm of update events; none of them is an edit.
loading = true ;
if ( p . project ) editor . loadProjectData ( p . project ) ;
else { editor . setComponents ( "" ) ; editor . setStyle ( "" ) ; }
importedCss = p . importedCss || "" ;
extraHeadLinks = p . extraHeadLinks || [ ] ;
importedHtmlAttrs = p . importedHtmlAttrs || { } ;
injectImportedCss ( ) ;
const settle = ( ) => { loading = false ; dirty = ! ! p . dirty ; $ ( "btn-draft" ) . disabled = ! anyDirty ( ) ; renderPages ( ) ; } ;
editor . once ( "canvas:frame:load" , ( ) => { injectImportedCss ( ) ; settle ( ) ; } ) ;
setTimeout ( settle , 250 ) ; // the frame does not always reload; this is the backstop
renderPages ( ) ;
}
function selectPage ( i ) {
if ( i === current || ! pages [ i ] ) return ;
capturePage ( ) ;
applyPage ( i ) ;
status ( ` Editing ${ pages [ i ] . path } ` ) ;
}
function addPage ( ) {
const title = ( prompt ( "What is the new page called? Its address follows from the name — \"About\" becomes /about/." , "About" ) || "" ) . trim ( ) ;
if ( ! title ) return ;
const path = slugToPath ( title ) ;
if ( ! path ) { status ( "That name has no letters or digits in it" , "err" ) ; return ; }
if ( pages . some ( ( p ) => p . path === path ) ) { status ( ` There is already a page at ${ path } ` , "err" ) ; return ; }
capturePage ( ) ;
removed = removed . filter ( ( x ) => x !== path ) ; // re-adding a deleted page cancels its removal
pages . push ( { ... newPage ( path , title ) , dirty : true } ) ;
applyPage ( pages . length - 1 ) ;
editor . setComponents ( ` <section class="pg-start"><h1> ${ esc ( title ) } </h1><p>Start building this page, or press Templates to drop a whole layout in.</p></section> ` ) ;
editor . setStyle ( ` body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:#15181d} \n .pg-start{max-width:760px;margin:0 auto;padding:72px 24px} \n .pg-start h1{font-size:40px;letter-spacing:-.02em;margin:0 0 12px} \n .pg-start p{color:#5a6069;font-size:17px;margin:0} ` ) ;
markDirty ( true ) ;
status ( ` Added ${ path } — edit it, then Publish ` ) ;
}
function renamePage ( i ) {
const p = pages [ i ] ;
if ( ! p ) return ;
// The title only. A published address that moves is a link somebody else
// already wrote down and is now broken, so paths are fixed once made.
const title = ( prompt ( ` What should this page be called? Its address stays ${ p . path } . ` , p . title ) || "" ) . trim ( ) ;
if ( ! title || title === p . title ) return ;
p . title = title ;
p . dirty = true ;
if ( i === current ) dirty = true ;
$ ( "btn-draft" ) . disabled = false ;
renderPages ( ) ;
}
function deletePage ( i ) {
const p = pages [ i ] ;
if ( ! p || i === 0 || pages . length < 2 ) return ;
if ( ! confirm ( ` Delete " ${ p . title } "? The next Publish takes ${ p . path } off Sia as well. ` ) ) return ;
if ( i !== current ) capturePage ( ) ;
pages . splice ( i , 1 ) ;
removed . push ( p . path ) ;
applyPage ( i < current ? current - 1 : Math . min ( i === current ? i : current , pages . length - 1 ) ) ;
$ ( "btn-draft" ) . disabled = false ;
status ( ` Removed ${ p . path } — Publish to take it off Sia ` ) ;
}
function renderPages ( ) {
const ul = $ ( "page-list" ) ;
if ( ! ul ) return ;
ul . textContent = "" ;
pages . forEach ( ( p , i ) => {
const li = document . createElement ( "li" ) ;
li . className = "pg" + ( i === current ? " on" : "" ) ;
const go = document . createElement ( "button" ) ;
go . type = "button" ; go . className = "pg-go" ; go . dataset . i = String ( i ) ;
if ( i === current ) go . setAttribute ( "aria-current" , "true" ) ;
// Titles and paths are the owner's words, not ours: never translated.
const t = document . createElement ( "span" ) ; t . className = "pg-t" ; t . textContent = p . title ; t . setAttribute ( "data-i18n-skip" , "" ) ;
// The address a visitor types, not the file behind it: /about/, not
// about/index.html. It is shorter, and it is what the owner has to know.
const path = document . createElement ( "span" ) ; path . className = "pg-p" ; path . textContent = "/" + p . path . replace ( /(^|\/)index\.html$/ , "$1" ) ; path . setAttribute ( "data-i18n-skip" , "" ) ;
go . append ( t , path ) ;
li . appendChild ( go ) ;
if ( p . dirty ) { const d = document . createElement ( "i" ) ; d . className = "pg-dot" ; d . title = "Unsaved changes" ; li . appendChild ( d ) ; }
const acts = document . createElement ( "span" ) ;
acts . className = "pg-acts" ;
const act = ( kind , glyph , label ) => {
const b = document . createElement ( "button" ) ;
b . type = "button" ; b . className = "pg-act" ; b . dataset . act = kind ; b . dataset . i = String ( i ) ;
b . textContent = glyph ; b . title = label ; b . setAttribute ( "aria-label" , label ) ;
return b ;
} ;
acts . appendChild ( act ( "rename" , "✎" , "Rename" ) ) ;
if ( i > 0 ) acts . appendChild ( act ( "delete" , "× " , "Delete" ) ) ;
li . appendChild ( acts ) ;
ul . appendChild ( li ) ;
} ) ;
}
// A page's public address: index.html is the folder itself, so /about/index.html
// is advertised as /about/.
const pageUrl = ( path ) => ` https:// ${ name } / ` + path . replace ( /(^|\/)index\.html$/ , "$1" ) ;
function sitemapXml ( ) {
const day = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
return ` <?xml version="1.0" encoding="UTF-8"?> \n <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> \n `
+ pages . map ( ( p ) => ` <url><loc> ${ esc ( pageUrl ( p . path ) ) } </loc><lastmod> ${ day } </lastmod></url> ` ) . join ( "\n" )
+ ` \n </urlset> \n ` ;
}
function draftJson ( ) {
return JSON . stringify ( {
v : 2 , name , saved _at : new Date ( ) . toISOString ( ) , sitemap : pages . length > 1 ,
pages : pages . map ( ( p ) => ( { path : p . path , title : p . title , project : p . project , importedCss : p . importedCss , extraHeadLinks : p . extraHeadLinks , importedHtmlAttrs : p . importedHtmlAttrs } ) ) ,
} ) ;
}
// v1 drafts held one project at the top level. It becomes the home page, so a
// site built before this existed opens unchanged and gains pages when asked.
function loadDraft ( j ) {
const norm = ( p ) => ( {
path : typeof p ? . path === "string" && p . path ? p . path : "index.html" ,
title : typeof p ? . title === "string" && p . title ? p . title : "Home" ,
project : p ? . project || null ,
importedCss : typeof p ? . importedCss === "string" ? p . importedCss : "" ,
extraHeadLinks : Array . isArray ( p ? . extraHeadLinks ) ? p . extraHeadLinks : [ ] ,
importedHtmlAttrs : p ? . importedHtmlAttrs && typeof p . importedHtmlAttrs === "object" ? p . importedHtmlAttrs : { } ,
dirty : false ,
} ) ;
pages = Array . isArray ( j ? . pages ) && j . pages . length ? j . pages . map ( norm ) : [ norm ( j ) ] ;
removed = [ ] ;
current = 0 ;
applyPage ( 0 ) ;
}
$ ( "page-add" ) . addEventListener ( "click" , addPage ) ;
$ ( "page-list" ) . addEventListener ( "click" , ( e ) => {
const a = e . target . closest ( ".pg-act" ) ;
if ( a ) { const i = Number ( a . dataset . i ) ; a . dataset . act === "rename" ? renamePage ( i ) : deletePage ( i ) ; return ; }
const go = e . target . closest ( ".pg-go" ) ;
if ( go ) selectPage ( Number ( go . dataset . i ) ) ;
} ) ;
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 ----------
2026-09-21 00:48:35 +02:00
// Every entry carries its own card text: `label` and `tag` are what the
// picker shows, `cat` is the filter it answers to. The bodies are written to
// be thrown away — each line is a prompt for the owner to overwrite — so they
// deliberately have no translation entries; the studio's chrome does.
//
// Class names are prefixed per template (.pf-, .bi-, .rs-, …) so a template
// and any sx-* section dropped on top of it never fight over a selector.
// Palettes rotate light-neutral / dark-neutral / warm / acid so the gallery
// never shows four of the same page in a row.
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 TEMPLATES = {
2026-09-21 00:48:35 +02:00
blank : {
label : "Blank page" , tag : "Start from nothing." , cat : "special" ,
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} ` ,
} ,
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
landing : {
2026-09-21 00:48:35 +02:00
label : "Landing page" , tag : "Hero, three features, call to action." , cat : "landing" ,
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
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 : {
2026-09-21 00:48:35 +02:00
label : "Profile / link hub" , tag : "Avatar, bio and a stack of links." , cat : "special" ,
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
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 : {
2026-09-21 00:48:35 +02:00
label : "Business one-pager" , tag : "About, services, hours, contact." , cat : "business" ,
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
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 } ` ,
} ,
2026-09-21 00:48:35 +02:00
// --- light neutral ---
portfolio : {
label : "Portfolio" , tag : "A grid of work, one line about you." , cat : "landing" ,
html : `
< header class = "pf-top" >
< div class = "pf-in" >
< b > $ { esc ( name ) } < / b >
< nav > < a href = "#work" > Work < / a > < a h r e f = " # a b o u t " > A b o u t < / a > < a h r e f = " m a i l t o : " > H i r e m e < / a > < / n a v >
< / d i v >
< / h e a d e r >
< section class = "pf-hero" >
< h1 > I design things that < br > hold up under use . < / h 1 >
< p > Two sentences on what you make and who you make it for . Leave out the adjectives you would not say out loud . < / p >
< / s e c t i o n >
< section id = "work" class = "pf-work" >
< article class = "pf-item" > < div class = "pf-shot" > < / d i v > < h 3 > P r o j e c t o n e < / h 3 > < p > W h a t i t w a s , w h a t y o u d i d , w h a t c h a n g e d . < / p > < / a r t i c l e >
< article class = "pf-item" > < div class = "pf-shot" > < / d i v > < h 3 > P r o j e c t t w o < / h 3 > < p > W h a t i t w a s , w h a t y o u d i d , w h a t c h a n g e d . < / p > < / a r t i c l e >
< article class = "pf-item" > < div class = "pf-shot" > < / d i v > < h 3 > P r o j e c t t h r e e < / h 3 > < p > W h a t i t w a s , w h a t y o u d i d , w h a t c h a n g e d . < / p > < / a r t i c l e >
< article class = "pf-item" > < div class = "pf-shot" > < / d i v > < h 3 > P r o j e c t f o u r < / h 3 > < p > W h a t i t w a s , w h a t y o u d i d , w h a t c h a n g e d . < / p > < / a r t i c l e >
< / s e c t i o n >
< section id = "about" class = "pf-about" >
< h2 > About < / h 2 >
< p > Where you are , what you use , what you are looking for next . A working email beats a contact form . < / p >
< a class = "pf-mail" href = "mailto:" > hello @ example < / a >
< / s e c t i o n >
< footer class = "pf-foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } < / f o o t e r > ` ,
css : `
body { margin : 0 ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; color : # 15181 d ; background : # fafafa ; line - height : 1.6 }
. pf - top { border - bottom : 1 px solid # e6e7ea ; background : # fff }
. pf - in { max - width : 1040 px ; margin : 0 auto ; padding : 18 px 24 px ; display : flex ; justify - content : space - between ; align - items : center }
. pf - in b { font - size : 16 px ; letter - spacing : - . 01 em }
. pf - in a { margin - left : 20 px ; color : # 5 a6069 ; text - decoration : none ; font - size : 14 px }
. pf - in a : hover { color : # 15181 d }
. pf - hero { max - width : 1040 px ; margin : 0 auto ; padding : 88 px 24 px 56 px }
. pf - hero h1 { font - size : 52 px ; line - height : 1.08 ; letter - spacing : - . 025 em ; margin : 0 0 20 px ; font - weight : 600 }
. pf - hero p { max - width : 560 px ; color : # 5 a6069 ; font - size : 18 px ; margin : 0 }
. pf - work { max - width : 1040 px ; margin : 0 auto ; padding : 0 24 px 64 px ; display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 300 px , 1 fr ) ) ; gap : 36 px 28 px }
. pf - shot { aspect - ratio : 4 / 3 ; border - radius : 12 px ; background : linear - gradient ( 135 deg , # eceef1 , # dfe2e7 ) ; margin - bottom : 16 px }
. pf - item h3 { margin : 0 0 6 px ; font - size : 18 px ; font - weight : 600 }
. pf - item p { margin : 0 ; color : # 5 a6069 ; font - size : 15 px }
. pf - about { border - top : 1 px solid # e6e7ea ; background : # fff }
. pf - about h2 { font - size : 15 px ; text - transform : uppercase ; letter - spacing : . 12 em ; color : # 8 b9099 ; margin : 0 0 14 px }
. pf - about , . pf - about > * { max - width : 1040 px ; margin - left : auto ; margin - right : auto }
. pf - about { padding : 56 px 24 px }
. pf - about p { max - width : 620 px ; margin : 0 0 18 px ; font - size : 17 px }
. pf - mail { color : # 15181 d ; font - weight : 600 ; border - bottom : 2 px solid # 15181 d ; text - decoration : none ; padding - bottom : 1 px }
. pf - foot { max - width : 1040 px ; margin : 0 auto ; padding : 28 px 24 px ; color : # 8 b9099 ; font - size : 13 px }
@ media ( max - width : 600 px ) { . pf - hero h1 { font - size : 34 px } . pf - in nav a { margin - left : 12 px } } ` ,
} ,
"blog-post" : {
label : "Blog post" , tag : "One article — title, byline, body, replies." , cat : "blog" ,
html : `
< header class = "bp-top" > < a href = "/" > $ { esc ( name ) } < / a > < a h r e f = " / a r c h i v e / " > A r c h i v e < / a > < / h e a d e r >
< article class = "bp" >
< p class = "bp-kicker" > Essays < / p >
< h1 > The title goes here , and it can run to two lines < / h 1 >
< p class = "bp-meta" > By an author · $ { new Date ( ) . toLocaleDateString ( "en-GB" , { day : "numeric" , month : "long" , year : "numeric" } ) } · 6 min < / p >
< p class = "bp-lead" > The opening paragraph does the work of a subtitle . Say what the reader gets if they stay . < / p >
< p > A normal paragraph . Long - form type wants a measure of around 70 characters and generous leading — that is what the CSS below is doing , and it is the one thing worth keeping when you rewrite this . < / p >
< h2 > A section heading < / h 2 >
< p > Another paragraph . Links look < a href = "#" > like this < / a > , a n d a p h r a s e c a n b e < e m > e m p h a s i s e d < / e m > o r m a d e < s t r o n g > s t r o n g < / s t r o n g > w i t h o u t c h a n g i n g t h e r h y t h m o f t h e l i n e . < / p >
< blockquote > A pulled quote earns its indent when it is someone else ' s sentence , not a louder version of your own . < / b l o c k q u o t e >
< p > Close on something concrete . What changed , what you would do differently , where the reader goes next . < / p >
< / a r t i c l e >
< section class = "bp-replies" >
< h2 > Replies < / h 2 >
< p class = "bp-none" > No replies yet . < / p >
< / s e c t i o n >
< footer class = "bp-foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } < / f o o t e r > ` ,
css : `
body { margin : 0 ; background : # fffefb ; color : # 1 b1a17 ; font - family : Georgia , 'Times New Roman' , serif ; line - height : 1.7 }
. bp - top { display : flex ; gap : 20 px ; max - width : 720 px ; margin : 0 auto ; padding : 22 px 24 px ; font - family : system - ui , sans - serif ; font - size : 14 px }
. bp - top a { color : # 6 f6a60 ; text - decoration : none }
. bp - top a : first - child { font - weight : 600 ; color : # 1 b1a17 }
. bp { max - width : 680 px ; margin : 0 auto ; padding : 20 px 24 px 56 px }
. bp - kicker { font - family : system - ui , sans - serif ; font - size : 12 px ; letter - spacing : . 14 em ; text - transform : uppercase ; color : # b5561f ; margin : 0 0 14 px }
. bp h1 { font - size : 42 px ; line - height : 1.15 ; letter - spacing : - . 015 em ; margin : 0 0 16 px }
. bp - meta { font - family : system - ui , sans - serif ; font - size : 14 px ; color : # 8 a8579 ; margin : 0 0 32 px }
. bp - lead { font - size : 21 px ; color : # 3 d3a34 ; margin : 0 0 28 px }
. bp p { font - size : 18 px ; margin : 0 0 24 px }
. bp h2 { font - size : 26 px ; margin : 40 px 0 14 px ; letter - spacing : - . 01 em }
. bp a { color : # b5561f }
. bp blockquote { margin : 32 px 0 ; padding : 4 px 0 4 px 24 px ; border - left : 3 px solid # e6dfd2 ; color : # 57524 a ; font - style : italic ; font - size : 19 px }
. bp - replies { max - width : 680 px ; margin : 0 auto ; padding : 32 px 24 px ; border - top : 1 px solid # e6dfd2 }
. bp - replies h2 { font - family : system - ui , sans - serif ; font - size : 14 px ; letter - spacing : . 1 em ; text - transform : uppercase ; color : # 8 a8579 ; margin : 0 0 12 px }
. bp - none { color : # 8 a8579 ; font - size : 16 px ; margin : 0 }
. bp - foot { max - width : 680 px ; margin : 0 auto ; padding : 24 px ; color : # a8a294 ; font - size : 13 px ; font - family : system - ui , sans - serif }
@ media ( max - width : 600 px ) { . bp h1 { font - size : 30 px } . bp - lead { font - size : 18 px } } ` ,
} ,
resume : {
label : "Résumé" , tag : "One page: skills, history, a way to reach you." , cat : "special" ,
html : `
< main class = "cv" >
< header class = "cv-head" >
< div > < h1 > Your Name < / h 1 > < p c l a s s = " c v - r o l e " > W h a t y o u d o · C i t y < / p > < / d i v >
< p class = "cv-reach" > < a href = "mailto:" > hello @ example < /a><br><a href="https:/ / " > $ { esc ( name ) } < / a > < / p >
< / h e a d e r >
< section class = "cv-sec" >
< h2 > Summary < / h 2 >
< p > Three lines at most . What you build , how long you have been doing it , and the kind of problem you want next . < / p >
< / s e c t i o n >
< section class = "cv-sec" >
< h2 > Skills < / h 2 >
< ul class = "cv-tags" > < li > One < / l i > < l i > T w o < / l i > < l i > T h r e e < / l i > < l i > F o u r < / l i > < l i > F i v e < / l i > < l i > S i x < / l i > < / u l >
< / s e c t i o n >
< section class = "cv-sec" >
< h2 > Experience < / h 2 >
< div class = "cv-row" > < b > Role < / b > < s p a n > C o m p a n y · 2 0 2 4 — n o w < / s p a n > < p > W h a t y o u o w n e d a n d w h a t m e a s u r a b l y c h a n g e d b e c a u s e o f i t . < / p > < / d i v >
< div class = "cv-row" > < b > Role < / b > < s p a n > C o m p a n y · 2 0 2 1 — 2 0 2 4 < / s p a n > < p > W h a t y o u o w n e d a n d w h a t m e a s u r a b l y c h a n g e d b e c a u s e o f i t . < / p > < / d i v >
< div class = "cv-row" > < b > Role < / b > < s p a n > C o m p a n y · 2 0 1 8 — 2 0 2 1 < / s p a n > < p > W h a t y o u o w n e d a n d w h a t m e a s u r a b l y c h a n g e d b e c a u s e o f i t . < / p > < / d i v >
< / s e c t i o n >
< section class = "cv-sec" >
< h2 > Education < / h 2 >
< div class = "cv-row" > < b > Degree < / b > < s p a n > I n s t i t u t i o n · 2 0 1 8 < / s p a n > < / d i v >
< / s e c t i o n >
< p class = "cv-print" > Ctrl / Cmd + P prints this page straight to a PDF — the print rules below drop the shadow and the margins . < / p >
< / m a i n > ` ,
css : `
body { margin : 0 ; background : # f4f5f7 ; color : # 15181 d ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.6 }
. cv { max - width : 760 px ; margin : 40 px auto ; background : # fff ; padding : 48 px 52 px ; border - radius : 4 px ; box - shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , . 08 ) }
. cv - head { display : flex ; justify - content : space - between ; align - items : flex - start ; gap : 20 px ; padding - bottom : 24 px ; border - bottom : 2 px solid # 15181 d ; margin - bottom : 8 px }
. cv - head h1 { margin : 0 ; font - size : 32 px ; letter - spacing : - . 02 em }
. cv - role { margin : 4 px 0 0 ; color : # 5 a6069 }
. cv - reach { margin : 0 ; text - align : right ; font - size : 14 px ; line - height : 1.7 }
. cv - reach a { color : # 15181 d }
. cv - sec { padding : 24 px 0 ; border - bottom : 1 px solid # eceef1 }
. cv - sec h2 { font - size : 12 px ; letter - spacing : . 14 em ; text - transform : uppercase ; color : # 8 b9099 ; margin : 0 0 14 px }
. cv - sec > p { margin : 0 ; max - width : 60 ch }
. cv - tags { list - style : none ; display : flex ; flex - wrap : wrap ; gap : 8 px ; padding : 0 ; margin : 0 }
. cv - tags li { background : # f0f1f4 ; border - radius : 6 px ; padding : 5 px 11 px ; font - size : 14 px }
. cv - row { margin - bottom : 18 px }
. cv - row : last - child { margin - bottom : 0 }
. cv - row b { font - size : 16 px }
. cv - row span { display : block ; color : # 8 b9099 ; font - size : 14 px ; margin - bottom : 5 px }
. cv - row p { margin : 0 ; color : # 41464 e ; max - width : 62 ch }
. cv - print { margin : 24 px 0 0 ; font - size : 14 px ; color : # 8 b9099 }
@ media print { body { background : # fff } . cv { box - shadow : none ; margin : 0 ; padding : 0 } . cv - print { display : none } }
@ media ( max - width : 620 px ) { . cv { margin : 0 ; padding : 28 px 22 px } . cv - head { flex - direction : column } . cv - reach { text - align : left } } ` ,
} ,
// --- dark neutral ---
"blog-index" : {
label : "Blog" , tag : "A list of posts with tags down the side." , cat : "blog" ,
html : `
< header class = "bi-top" >
< div class = "bi-in" > < b > $ { esc ( name ) } < /b><nav><a href="/ ">Posts</a><a href=" / about / ">About</a><a href=" / feed . xml " > RSS < / a > < / n a v > < / d i v >
< / h e a d e r >
< div class = "bi-body" >
< main class = "bi-list" >
< article class = "bi-post" > < time > 12 September $ { new Date ( ) . getFullYear ( ) } < / t i m e > < h 2 > < a h r e f = " # " > T h e m o s t r e c e n t p o s t < / a > < / h 2 > < p > O n e o r t w o s e n t e n c e s o f t h e o p e n i n g , e n o u g h t o d e c i d e w h e t h e r t o c l i c k . < / p > < / a r t i c l e >
< article class = "bi-post" > < time > 28 August $ { new Date ( ) . getFullYear ( ) } < / t i m e > < h 2 > < a h r e f = " # " > S o m e t h i n g e l s e w o r t h w r i t i n g d o w n < / a > < / h 2 > < p > O n e o r t w o s e n t e n c e s o f t h e o p e n i n g , e n o u g h t o d e c i d e w h e t h e r t o c l i c k . < / p > < / a r t i c l e >
< article class = "bi-post" > < time > 03 August $ { new Date ( ) . getFullYear ( ) } < / t i m e > < h 2 > < a h r e f = " # " > A s h o r t e r n o t e < / a > < / h 2 > < p > O n e o r t w o s e n t e n c e s o f t h e o p e n i n g , e n o u g h t o d e c i d e w h e t h e r t o c l i c k . < / p > < / a r t i c l e >
< article class = "bi-post" > < time > 17 July $ { new Date ( ) . getFullYear ( ) } < / t i m e > < h 2 > < a h r e f = " # " > N o t e s f r o m a t h i n g t h a t b r o k e < / a > < / h 2 > < p > O n e o r t w o s e n t e n c e s o f t h e o p e n i n g , e n o u g h t o d e c i d e w h e t h e r t o c l i c k . < / p > < / a r t i c l e >
< / m a i n >
< aside class = "bi-side" >
< h3 > Tags < / h 3 >
< ul class = "bi-tags" > < li > < a href = "#" > writing < / a > < / l i > < l i > < a h r e f = " # " > b i t c o i n c a s h < / a > < / l i > < l i > < a h r e f = " # " > s e l f - h o s t i n g < / a > < / l i > < l i > < a h r e f = " # " > s i a < / a > < / l i > < l i > < a h r e f = " # " > n o t e s < / a > < / l i > < / u l >
< h3 > Elsewhere < / h 3 >
< ul class = "bi-tags" > < li > < a href = "https://" > Nostr < / a > < / l i > < l i > < a h r e f = " m a i l t o : " > E m a i l < / a > < / l i > < / u l >
< / a s i d e >
< / d i v >
< footer class = "bi-foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } · no trackers , no newsletter pop - up < / f o o t e r > ` ,
css : `
body { margin : 0 ; background : # 0 d1015 ; color : # e3e6eb ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.65 }
a { color : # e3e6eb }
. bi - top { border - bottom : 1 px solid # 1 f242c }
. bi - in { max - width : 980 px ; margin : 0 auto ; padding : 20 px 24 px ; display : flex ; justify - content : space - between ; align - items : center }
. bi - in b { font - size : 16 px }
. bi - in a { margin - left : 20 px ; color : # 8 d96a5 ; text - decoration : none ; font - size : 14 px }
. bi - in a : hover { color : # fff }
. bi - body { max - width : 980 px ; margin : 0 auto ; padding : 48 px 24 px ; display : grid ; grid - template - columns : 1 fr 220 px ; gap : 56 px ; align - items : start }
. bi - post { padding - bottom : 30 px ; margin - bottom : 30 px ; border - bottom : 1 px solid # 1 f242c }
. bi - post : last - child { border : 0 ; margin : 0 ; padding : 0 }
. bi - post time { font - size : 13 px ; color : # 6 d7787 ; letter - spacing : . 02 em }
. bi - post h2 { margin : 6 px 0 8 px ; font - size : 24 px ; letter - spacing : - . 015 em ; line - height : 1.25 }
. bi - post h2 a { text - decoration : none }
. bi - post h2 a : hover { color : # 8 ab4ff }
. bi - post p { margin : 0 ; color : # 98 a1b0 }
. bi - side h3 { font - size : 12 px ; letter - spacing : . 14 em ; text - transform : uppercase ; color : # 6 d7787 ; margin : 0 0 12 px }
. bi - side h3 + ul + h3 { margin - top : 28 px }
. bi - tags { list - style : none ; padding : 0 ; margin : 0 ; display : flex ; flex - wrap : wrap ; gap : 7 px }
. bi - tags a { display : inline - block ; background : # 161 b22 ; border : 1 px solid # 232933 ; border - radius : 6 px ; padding : 4 px 10 px ; font - size : 13 px ; color : # 98 a1b0 ; text - decoration : none }
. bi - tags a : hover { border - color : # 3 a434f ; color : # fff }
. bi - foot { border - top : 1 px solid # 1 f242c ; padding : 24 px ; text - align : center ; color : # 6 d7787 ; font - size : 13 px }
@ media ( max - width : 760 px ) { . bi - body { grid - template - columns : 1 fr ; gap : 40 px } } ` ,
} ,
docs : {
label : "Documentation" , tag : "Sidebar contents, one long article." , cat : "special" ,
html : `
< header class = "dc-top" > < b > $ { esc ( name ) } docs < / b > < i n p u t c l a s s = " d c - f i n d " t y p e = " s e a r c h " p l a c e h o l d e r = " S e a r c h ( w i r e t h i s u p l a t e r ) " > < / h e a d e r >
< div class = "dc-body" >
< nav class = "dc-nav" >
< h4 > Getting started < / h 4 >
< a class = "on" href = "#install" > Install < / a > < a h r e f = " # c o n f i g " > C o n f i g u r e < / a > < a h r e f = " # f i r s t " > F i r s t r u n < / a >
< h4 > Reference < / h 4 >
< a href = "#cli" > Commands < / a > < a h r e f = " # a p i " > H T T P A P I < / a > < a h r e f = " # e r r o r s " > E r r o r s < / a >
< h4 > Help < / h 4 >
< a href = "#faq" > FAQ < / a >
< / n a v >
< main class = "dc-main" >
< h1 id = "install" > Install < / h 1 >
< p > One paragraph saying what the reader is about to do and how long it takes . Then the command . < / p >
< pre class = "dc-pre" > < code > curl - fsSL https : //${esc(name)}/install.sh | sh</code></pre>
< h2 id = "config" > Configure < / h 2 >
< p > What to edit and where it lives . Say what the defaults are so most readers can stop here . < / p >
2026-09-21 00:55:46 +02:00
< table class = "dc-tab" > < thead > < tr > < th data - gjs - type = "text" > Key < / t h > < t h d a t a - g j s - t y p e = " t e x t " > D e f a u l t < / t h > < t h d a t a - g j s - t y p e = " t e x t " > W h a t i t d o e s < / t h > < / t r > < / t h e a d >
< tbody > < tr > < td data - gjs - type = "text" > port < / t d > < t d d a t a - g j s - t y p e = " t e x t " > 8 0 8 0 < / t d > < t d d a t a - g j s - t y p e = " t e x t " > W h e r e i t l i s t e n s . < / t d > < / t r > < t r > < t d d a t a - g j s - t y p e = " t e x t " > d a t a _ d i r < / t d > < t d d a t a - g j s - t y p e = " t e x t " > . / d a t a < / t d > < t d d a t a - g j s - t y p e = " t e x t " > W h e r e s t a t e i s k e p t . < / t d > < / t r > < t r > < t d d a t a - g j s - t y p e = " t e x t " > l o g _ l e v e l < / t d > < t d d a t a - g j s - t y p e = " t e x t " > i n f o < / t d > < t d d a t a - g j s - t y p e = " t e x t " > H o w l o u d i t i s . < / t d > < / t r > < / t b o d y > < / t a b l e >
2026-09-21 00:48:35 +02:00
< h2 id = "first" > First run < / h 2 >
< p > The happy path , end to end , with the output the reader should actually see . < / p >
< div class = "dc-note" > < b > Note < / b > A n y t h i n g t h a t w i l l b i t e a r e a d e r a t 2 a m g o e s i n a b o x l i k e t h i s o n e . < / d i v >
< / m a i n >
< / d i v > ` ,
css : `
body { margin : 0 ; background : # 0 f1216 ; color : # dfe3e9 ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.7 }
. dc - top { display : flex ; justify - content : space - between ; align - items : center ; gap : 16 px ; padding : 14 px 22 px ; border - bottom : 1 px solid # 1 d222a ; background : # 0b0 e12 ; position : sticky ; top : 0 }
. dc - top b { font - size : 15 px }
. dc - find { background : # 151 a21 ; border : 1 px solid # 252 b35 ; border - radius : 8 px ; padding : 7 px 12 px ; color : # dfe3e9 ; font : inherit ; font - size : 13 px ; width : min ( 280 px , 45 vw ) }
. dc - body { display : grid ; grid - template - columns : 230 px 1 fr ; max - width : 1120 px ; margin : 0 auto }
. dc - nav { padding : 28 px 18 px ; border - right : 1 px solid # 1 d222a ; position : sticky ; top : 57 px ; align - self : start ; max - height : calc ( 100 vh - 57 px ) ; overflow : auto }
. dc - nav h4 { font - size : 11 px ; letter - spacing : . 14 em ; text - transform : uppercase ; color : # 68727 f ; margin : 22 px 0 8 px }
. dc - nav h4 : first - child { margin - top : 0 }
. dc - nav a { display : block ; padding : 5 px 10 px ; border - radius : 7 px ; color : # 98 a1af ; text - decoration : none ; font - size : 14 px }
. dc - nav a : hover { background : # 151 a21 ; color : # fff }
. dc - nav a . on { background : # 1 b2230 ; color : # 8 ab4ff }
. dc - main { padding : 36 px 40 px 80 px ; max - width : 760 px }
. dc - main h1 { font - size : 34 px ; letter - spacing : - . 02 em ; margin : 0 0 14 px }
. dc - main h2 { font - size : 23 px ; letter - spacing : - . 01 em ; margin : 44 px 0 12 px ; padding - top : 12 px ; border - top : 1 px solid # 1 d222a }
. dc - main p { color : # aeb6c2 ; margin : 0 0 18 px }
. dc - pre { background : # 0 a0d11 ; border : 1 px solid # 1 d222a ; border - radius : 10 px ; padding : 14 px 16 px ; overflow : auto ; font - size : 13.5 px ; color : # a7e3b0 }
. dc - tab { width : 100 % ; border - collapse : collapse ; font - size : 14 px ; margin : 0 0 18 px }
. dc - tab th { text - align : left ; color : # 68727 f ; font - weight : 600 ; font - size : 12 px ; letter - spacing : . 08 em ; text - transform : uppercase ; padding : 0 12 px 8 px 0 ; border - bottom : 1 px solid # 1 d222a }
. dc - tab td { padding : 9 px 12 px 9 px 0 ; border - bottom : 1 px solid # 161 b22 ; color : # aeb6c2 }
. dc - note { border - left : 3 px solid # 8 ab4ff ; background : # 131 a26 ; border - radius : 0 8 px 8 px 0 ; padding : 12 px 16 px ; color : # aeb6c2 ; font - size : 14.5 px }
. dc - note b { color : # 8 ab4ff ; margin - right : 6 px }
@ media ( max - width : 820 px ) { . dc - body { grid - template - columns : 1 fr } . dc - nav { position : static ; max - height : none ; border - right : 0 ; border - bottom : 1 px solid # 1 d222a } . dc - main { padding : 28 px 22 px 64 px } } ` ,
} ,
agency : {
label : "Agency" , tag : "Case studies, services, the people behind them." , cat : "business" ,
html : `
< header class = "ag-top" > < div class = "ag-in" > < b > $ { esc ( name ) } < / b > < n a v > < a h r e f = " # w o r k " > W o r k < / 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 = " # t e a m " > T e a m < / a > < a c l a s s = " a g - c t a " h r e f = " # c o n t a c t " > S t a r t a p r o j e c t < / a > < / n a v > < / d i v > < / h e a d e r >
< section class = "ag-hero" >
< h1 > We take the awkward half of the problem . < / h 1 >
< p > One sentence on what you do , one on who for . Resist listing technologies here — the work below does that better . < / p >
< / s e c t i o n >
< section id = "work" class = "ag-work" >
< h2 class = "ag-lab" > Selected work < / h 2 >
< div class = "ag-cases" >
< a class = "ag-case" href = "#" > < div class = "ag-shot" > < / d i v > < h 3 > C l i e n t o n e < / h 3 > < p > T h e t h i n g t h a t w a s b r o k e n , a n d t h e n u m b e r t h a t m o v e d . < / p > < / a >
< a class = "ag-case" href = "#" > < div class = "ag-shot" > < / d i v > < h 3 > C l i e n t t w o < / h 3 > < p > T h e t h i n g t h a t w a s b r o k e n , a n d t h e n u m b e r t h a t m o v e d . < / p > < / a >
< a class = "ag-case" href = "#" > < div class = "ag-shot" > < / d i v > < h 3 > C l i e n t t h r e e < / h 3 > < p > T h e t h i n g t h a t w a s b r o k e n , a n d t h e n u m b e r t h a t m o v e d . < / p > < / a >
< / d i v >
< / s e c t i o n >
< section id = "services" class = "ag-serv" >
< h2 class = "ag-lab" > What we do < / h 2 >
< div class = "ag-grid" >
< div > < b > Strategy < / b > < p > D e c i d i n g w h a t n o t t o b u i l d . < / p > < / d i v >
< div > < b > Design < / b > < p > I n t e r f a c e s t h a t s u r v i v e a r e a l u s e r . < / p > < / d i v >
< div > < b > Engineering < / b > < p > S h i p p i n g i t , t h e n k e e p i n g i t u p . < / p > < / d i v >
< div > < b > Support < / b > < p > B e i n g r e a c h a b l e a f t e r l a u n c h . < / p > < / d i v >
< / d i v >
< / s e c t i o n >
< section id = "team" class = "ag-team" >
< h2 class = "ag-lab" > Who you would work with < / h 2 >
< div class = "ag-people" > < div > < span class = "ag-face" > < / s p a n > < b > A . P e r s o n < / b > < i > P a r t n e r < / i > < / d i v > < d i v > < s p a n c l a s s = " a g - f a c e " > < / s p a n > < b > B . P e r s o n < / b > < i > D e s i g n < / i > < / d i v > < d i v > < s p a n c l a s s = " a g - f a c e " > < / s p a n > < b > C . P e r s o n < / b > < i > E n g i n e e r i n g < / i > < / d i v > < / d i v >
< / s e c t i o n >
< footer id = "contact" class = "ag-foot" > < h2 > Tell us what is stuck . < / h 2 > < a c l a s s = " a g - c t a b i g " h r e f = " m a i l t o : " > h e l l o @ e x a m p l e < / a > < p > © $ { n e w D a t e ( ) . g e t F u l l Y e a r ( ) } $ { e s c ( n a m e ) } < / p > < / f o o t e r > ` ,
css : `
body { margin : 0 ; background : # 101114 ; color : # eceef2 ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.6 }
. ag - top { border - bottom : 1 px solid # 1e2026 }
. ag - in { max - width : 1080 px ; margin : 0 auto ; padding : 18 px 24 px ; display : flex ; justify - content : space - between ; align - items : center ; gap : 16 px ; flex - wrap : wrap }
. ag - in b { font - size : 16 px }
. ag - in a { margin - left : 22 px ; color : # 9 aa0ad ; text - decoration : none ; font - size : 14 px }
. ag - in a : hover { color : # fff }
. ag - cta { background : # eceef2 ; color : # 101114 ! important ; padding : 8 px 16 px ; border - radius : 999 px ; font - weight : 600 }
. ag - hero { max - width : 1080 px ; margin : 0 auto ; padding : 96 px 24 px 72 px }
. ag - hero h1 { font - size : 56 px ; line - height : 1.05 ; letter - spacing : - . 03 em ; margin : 0 0 20 px ; max - width : 14 ch }
. ag - hero p { max - width : 540 px ; color : # 9 aa0ad ; font - size : 18 px ; margin : 0 }
. ag - lab { font - size : 12 px ; letter - spacing : . 16 em ; text - transform : uppercase ; color : # 6 a7280 ; margin : 0 0 24 px ; font - weight : 600 }
. ag - work , . ag - serv , . ag - team { max - width : 1080 px ; margin : 0 auto ; padding : 56 px 24 px ; border - top : 1 px solid # 1e2026 }
. ag - cases { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 280 px , 1 fr ) ) ; gap : 28 px }
. ag - case { text - decoration : none ; color : inherit }
. ag - shot { aspect - ratio : 16 / 10 ; border - radius : 10 px ; background : linear - gradient ( 135 deg , # 1 c1f26 , # 272 b34 ) ; margin - bottom : 14 px }
. ag - case h3 { margin : 0 0 5 px ; font - size : 18 px }
. ag - case p { margin : 0 ; color : # 9 aa0ad ; font - size : 15 px }
. ag - case : hover . ag - shot { background : linear - gradient ( 135 deg , # 242832 , # 333844 ) }
. ag - grid { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 210 px , 1 fr ) ) ; gap : 28 px }
. ag - grid b { display : block ; margin - bottom : 5 px ; font - size : 17 px }
. ag - grid p { margin : 0 ; color : # 9 aa0ad ; font - size : 15 px }
. ag - people { display : flex ; gap : 40 px ; flex - wrap : wrap }
. ag - face { display : block ; width : 72 px ; height : 72 px ; border - radius : 50 % ; background : # 272 b34 ; margin - bottom : 10 px }
. ag - people b { display : block ; font - size : 15 px }
. ag - people i { color : # 6 a7280 ; font - size : 13.5 px ; font - style : normal }
. ag - foot { border - top : 1 px solid # 1e2026 ; text - align : center ; padding : 72 px 24 px 40 px }
. ag - foot h2 { font - size : 38 px ; letter - spacing : - . 02 em ; margin : 0 0 24 px }
. ag - cta . big { display : inline - block ; margin - left : 0 ; padding : 13 px 26 px ; font - size : 16 px }
. ag - foot p { color : # 6 a7280 ; font - size : 13 px ; margin : 40 px 0 0 }
@ media ( max - width : 640 px ) { . ag - hero { padding : 56 px 24 px 40 px } . ag - hero h1 { font - size : 36 px } . ag - foot h2 { font - size : 26 px } . ag - in nav a { margin - left : 14 px } } ` ,
} ,
// --- warm ---
restaurant : {
label : "Restaurant" , tag : "Menu, hours, the address, a table." , cat : "business" ,
html : `
< header class = "rs-hero" >
< p class = "rs-kick" > Since $ { new Date ( ) . getFullYear ( ) - 7 } < / p >
< h1 > $ { esc ( name ) } < / h 1 >
< p class = "rs-sub" > Small kitchen , short menu , everything cooked the day you eat it . < / p >
< a class = "rs-btn" href = "#book" > Book a table < / a >
< / h e a d e r >
< section class = "rs-menu" >
< h2 > Menu < / h 2 >
< div class = "rs-cols" >
< div >
< h3 > To start < / h 3 >
< div class = "rs-item" > < span > Bread , olive oil , salt < / s p a n > < b > 4 < / b > < / d i v >
< div class = "rs-item" > < span > Soup of the day < / s p a n > < b > 7 < / b > < / d i v >
< div class = "rs-item" > < span > Grilled vegetables , yoghurt < / s p a n > < b > 9 < / b > < / d i v >
< h3 > Mains < / h 3 >
< div class = "rs-item" > < span > Fish of the day < / s p a n > < b > 1 9 < / b > < / d i v >
< div class = "rs-item" > < span > Slow lamb , beans < / s p a n > < b > 2 1 < / b > < / d i v >
< div class = "rs-item" > < span > Mushroom orzo < / s p a n > < b > 1 6 < / b > < / d i v >
< / d i v >
< div >
< h3 > Sweet < / h 3 >
< div class = "rs-item" > < span > Yoghurt , honey , walnut < / s p a n > < b > 6 < / b > < / d i v >
< div class = "rs-item" > < span > Chocolate , olive oil < / s p a n > < b > 7 < / b > < / d i v >
< h3 > To drink < / h 3 >
< div class = "rs-item" > < span > House red , glass < / s p a n > < b > 5 < / b > < / d i v >
< div class = "rs-item" > < span > House white , glass < / s p a n > < b > 5 < / b > < / d i v >
< div class = "rs-item" > < span > Coffee < / s p a n > < b > 3 < / b > < / d i v >
< / d i v >
< / d i v >
< p class = "rs-fine" > Prices include tax . Tell us about allergies when you book — the menu changes weekly . < / p >
< / s e c t i o n >
< section class = "rs-when" >
< div > < h3 > Hours < / h 3 > < p > T u e s d a y – S a t u r d a y < b r > 1 8 : 0 0 – 2 3 : 0 0 < b r > S u n d a y l u n c h 1 3 : 0 0 – 1 6 : 0 0 < b r > C l o s e d M o n d a y < / p > < / d i v >
< div > < h3 > Where < /h3><p>Street 1<br>City<br><a href="https:/ / " > Open in maps → < / a > < / p > < / d i v >
< div id = "book" > < h3 > Book < / h 3 > < p > < a c l a s s = " r s - b t n s m a l l " h r e f = " t e l : " > C a l l u s < / a > < b r > < a h r e f = " m a i l t o : " > O r s e n d a n e m a i l < / a > < / p > < / d i v >
< / s e c t i o n >
< footer class = "rs-foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } < / f o o t e r > ` ,
css : `
body { margin : 0 ; background : # fdf6ec ; color : # 3 a2a1c ; font - family : Georgia , 'Times New Roman' , serif ; line - height : 1.65 }
. rs - hero { text - align : center ; padding : 88 px 24 px 72 px ; background : # 5 c2f16 ; color : # fdf6ec }
. rs - kick { letter - spacing : . 24 em ; text - transform : uppercase ; font - size : 11 px ; margin : 0 0 16 px ; color : # d9a86c ; font - family : system - ui , sans - serif }
. rs - hero h1 { font - size : 54 px ; margin : 0 0 14 px ; letter - spacing : . 01 em ; font - weight : 400 }
. rs - sub { max - width : 460 px ; margin : 0 auto 28 px ; color : # e8cfb3 ; font - size : 17 px }
. rs - btn { display : inline - block ; background : # d9a86c ; color : # 3 a2a1c ; padding : 12 px 26 px ; border - radius : 2 px ; text - decoration : none ; font - family : system - ui , sans - serif ; font - weight : 600 ; font - size : 14 px ; letter - spacing : . 04 em }
. rs - btn . small { padding : 9 px 18 px ; font - size : 13 px ; margin - bottom : 8 px }
. rs - menu { max - width : 860 px ; margin : 0 auto ; padding : 64 px 24 px 48 px }
. rs - menu h2 { text - align : center ; font - size : 13 px ; letter - spacing : . 2 em ; text - transform : uppercase ; color : # 9 a7550 ; font - family : system - ui , sans - serif ; margin : 0 0 36 px ; font - weight : 600 }
. rs - cols { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 20 px 56 px }
. rs - cols h3 { font - size : 20 px ; font - weight : 400 ; margin : 28 px 0 12 px ; font - style : italic ; color : # 5 c2f16 }
. rs - cols h3 : first - child { margin - top : 0 }
. rs - item { display : flex ; justify - content : space - between ; align - items : baseline ; gap : 12 px ; padding : 6 px 0 ; border - bottom : 1 px dotted # d8c3a8 }
. rs - item b { font - weight : 400 ; color : # 9 a7550 }
. rs - fine { text - align : center ; color : # 9 a7550 ; font - size : 14 px ; margin : 36 px 0 0 ; font - family : system - ui , sans - serif }
. rs - when { max - width : 860 px ; margin : 0 auto ; padding : 40 px 24 px 64 px ; display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 200 px , 1 fr ) ) ; gap : 36 px ; border - top : 1 px solid # e6d5bf }
. rs - when h3 { font - size : 12 px ; letter - spacing : . 16 em ; text - transform : uppercase ; color : # 9 a7550 ; font - family : system - ui , sans - serif ; margin : 0 0 10 px }
. rs - when p { margin : 0 }
. rs - when a { color : # 5 c2f16 }
. rs - foot { text - align : center ; padding : 24 px ; background : # 5 c2f16 ; color : # c9a883 ; font - size : 13 px ; font - family : system - ui , sans - serif }
@ media ( max - width : 620 px ) { . rs - hero h1 { font - size : 38 px } . rs - cols { grid - template - columns : 1 fr ; gap : 0 } } ` ,
} ,
event : {
label : "Event" , tag : "Date, venue, agenda, speakers, RSVP." , cat : "business" ,
html : `
< header class = "ev-top" >
< p class = "ev-date" > 14 November $ { new Date ( ) . getFullYear ( ) } · City < / p >
< h1 > A one - day thing < br > worth travelling for < / h 1 >
< p class = "ev-sub" > Eight talks , one room , no parallel tracks . Lunch is included and the coffee is good . < / p >
< a class = "ev-btn" href = "#rsvp" > Reserve a seat < / a >
< p class = "ev-left" > 62 of 120 seats left < / p >
< / h e a d e r >
< section class = "ev-sec" >
< h2 > Agenda < / h 2 >
< div class = "ev-line" > < time > 09 : 30 < / t i m e > < d i v > < b > D o o r s a n d c o f f e e < / b > < p > C o m e e a r l y , t h e g o o d s e a t s g o f i r s t . < / p > < / d i v > < / d i v >
< div class = "ev-line" > < time > 10 : 00 < / t i m e > < d i v > < b > O p e n i n g t a l k < / b > < p > S p e a k e r n a m e — t h e t i t l e o f t h e t a l k . < / p > < / d i v > < / d i v >
< div class = "ev-line" > < time > 11 : 15 < / t i m e > < d i v > < b > S e c o n d t a l k < / b > < p > S p e a k e r n a m e — t h e t i t l e o f t h e t a l k . < / p > < / d i v > < / d i v >
< div class = "ev-line" > < time > 12 : 30 < / t i m e > < d i v > < b > L u n c h < / b > < p > I n c l u d e d . S a y w h a t y o u c a n n o t e a t w h e n y o u R S V P . < / p > < / d i v > < / d i v >
< div class = "ev-line" > < time > 14 : 00 < / t i m e > < d i v > < b > W o r k s h o p s < / b > < p > T h r e e r o o m s , p i c k o n e o n t h e d a y . < / p > < / d i v > < / d i v >
< div class = "ev-line" > < time > 17 : 00 < / t i m e > < d i v > < b > C l o s e a n d d r i n k s < / b > < p > A r o u n d t h e c o r n e r , o n u s . < / p > < / d i v > < / d i v >
< / s e c t i o n >
< section class = "ev-sec alt" >
< h2 > Speakers < / h 2 >
< div class = "ev-spk" > < div > < span class = "ev-face" > < / s p a n > < b > A . P e r s o n < / b > < i > W h a t t h e y w o r k o n < / i > < / d i v > < d i v > < s p a n c l a s s = " e v - f a c e " > < / s p a n > < b > B . P e r s o n < / b > < i > W h a t t h e y w o r k o n < / i > < / d i v > < d i v > < s p a n c l a s s = " e v - f a c e " > < / s p a n > < b > C . P e r s o n < / b > < i > W h a t t h e y w o r k o n < / i > < / d i v > < d i v > < s p a n c l a s s = " e v - f a c e " > < / s p a n > < b > D . P e r s o n < / b > < i > W h a t t h e y w o r k o n < / i > < / d i v > < / d i v >
< / s e c t i o n >
< section id = "rsvp" class = "ev-sec" >
< h2 > RSVP < / h 2 >
< form class = "ev-form" method = "post" >
< input type = "text" name = "name" placeholder = "Your name" required >
< input type = "email" name = "email" placeholder = "Email" required >
< input type = "text" name = "diet" placeholder = "Anything you cannot eat" >
2026-09-21 00:55:46 +02:00
< button type = "submit" class = "ev-btn" data - gjs - type = "text" > Hold my seat < / b u t t o n >
2026-09-21 00:48:35 +02:00
< / f o r m >
< p class = "ev-fine" > This form needs somewhere to post to . Until you set one up , put an email address here instead . < / p >
< / s e c t i o n >
< footer class = "ev-foot" > $ { esc ( name ) } · Venue , Street 1 , City · © $ { new Date ( ) . getFullYear ( ) } < / f o o t e r > ` ,
css : `
body { margin : 0 ; background : # fff9f2 ; color : # 2 f2418 ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.6 }
. ev - top { text - align : center ; padding : 80 px 24 px 64 px ; background : linear - gradient ( 160 deg , # c2410c , # 7 c2d12 ) ; color : # fff4e8 }
. ev - date { letter - spacing : . 18 em ; text - transform : uppercase ; font - size : 12 px ; margin : 0 0 18 px ; color : # fdba74 }
. ev - top h1 { font - size : 50 px ; line - height : 1.08 ; letter - spacing : - . 02 em ; margin : 0 0 18 px ; font - weight : 700 }
. ev - sub { max - width : 520 px ; margin : 0 auto 30 px ; color : # fed7aa ; font - size : 17 px }
. ev - btn { display : inline - block ; background : # fff4e8 ; color : # 7 c2d12 ; padding : 13 px 28 px ; border - radius : 10 px ; text - decoration : none ; font - weight : 700 ; border : 0 ; font : inherit ; font - weight : 700 ; cursor : pointer }
. ev - left { margin : 14 px 0 0 ; font - size : 13 px ; color : # fdba74 }
. ev - sec { max - width : 760 px ; margin : 0 auto ; padding : 56 px 24 px }
. ev - sec . alt { max - width : none ; background : # fdefe0 }
. ev - sec . alt > * { max - width : 760 px ; margin - left : auto ; margin - right : auto }
. ev - sec h2 { font - size : 12 px ; letter - spacing : . 16 em ; text - transform : uppercase ; color : # a16207 ; margin : 0 0 24 px }
. ev - line { display : grid ; grid - template - columns : 78 px 1 fr ; gap : 18 px ; padding : 14 px 0 ; border - top : 1 px solid # f0dcc6 }
. ev - line time { color : # c2410c ; font - weight : 700 ; font - size : 15 px ; font - variant - numeric : tabular - nums }
. ev - line b { display : block ; font - size : 16 px }
. ev - line p { margin : 2 px 0 0 ; color : # 6 b5844 ; font - size : 15 px }
. ev - spk { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 150 px , 1 fr ) ) ; gap : 28 px }
. ev - face { display : block ; width : 76 px ; height : 76 px ; border - radius : 50 % ; background : # f3ddc4 ; margin - bottom : 10 px }
. ev - spk b { display : block ; font - size : 15 px }
. ev - spk i { font - style : normal ; color : # 8 a7358 ; font - size : 13.5 px }
. ev - form { display : grid ; gap : 10 px ; max - width : 420 px }
. ev - form input { padding : 12 px 14 px ; border : 1 px solid # e6d3ba ; border - radius : 10 px ; font : inherit ; background : # fff }
. ev - form input : focus { outline : 2 px solid # c2410c ; outline - offset : 1 px }
. ev - form . ev - btn { background : # c2410c ; color : # fff4e8 ; justify - self : start }
. ev - fine { color : # 8 a7358 ; font - size : 13.5 px ; margin : 14 px 0 0 ; max - width : 420 px }
. ev - foot { text - align : center ; padding : 26 px ; background : # 7 c2d12 ; color : # fdba74 ; font - size : 13 px }
@ media ( max - width : 620 px ) { . ev - top h1 { font - size : 34 px } . ev - line { grid - template - columns : 64 px 1 fr ; gap : 12 px } } ` ,
} ,
newsletter : {
label : "Newsletter" , tag : "One promise, one field, the back issues." , cat : "landing" ,
html : `
< main class = "nl" >
< header class = "nl-head" >
< p class = "nl-kick" > $ { esc ( name ) } < / p >
< h1 > One letter a week about the thing you actually care about . < / h 1 >
< p class = "nl-sub" > Say what is in it and how often it lands . Then say what you will never do with the address — people read that line . < / p >
< form class = "nl-form" method = "post" >
< input type = "email" name = "email" placeholder = "you@example" required >
2026-09-21 00:55:46 +02:00
< button type = "submit" data - gjs - type = "text" > Subscribe < / b u t t o n >
2026-09-21 00:48:35 +02:00
< / f o r m >
< p class = "nl-fine" > No tracking pixels . Unsubscribe from the bottom of any issue . < / p >
< / h e a d e r >
< section class = "nl-past" >
< h2 > Past issues < / h 2 >
< a class = "nl-item" href = "#" > < b > # 014 — The title of the issue < / b > < s p a n > 1 2 S e p t e m b e r $ { n e w D a t e ( ) . g e t F u l l Y e a r ( ) } < / s p a n > < p > T h e o n e - l i n e s u m m a r y t h a t m a k e s t h e a r c h i v e w o r t h s c r o l l i n g . < / p > < / a >
< a class = "nl-item" href = "#" > < b > # 013 — Another title < / b > < s p a n > 0 5 S e p t e m b e r $ { n e w D a t e ( ) . g e t F u l l Y e a r ( ) } < / s p a n > < p > T h e o n e - l i n e s u m m a r y t h a t m a k e s t h e a r c h i v e w o r t h s c r o l l i n g . < / p > < / a >
< a class = "nl-item" href = "#" > < b > # 012 — And another < / b > < s p a n > 2 9 A u g u s t $ { n e w D a t e ( ) . g e t F u l l Y e a r ( ) } < / s p a n > < p > T h e o n e - l i n e s u m m a r y t h a t m a k e s t h e a r c h i v e w o r t h s c r o l l i n g . < / p > < / a >
< / s e c t i o n >
< footer class = "nl-foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } · < a href = "mailto:" > write back < / a > < / f o o t e r >
< / m a i n > ` ,
css : `
body { margin : 0 ; background : # fbf3e9 ; color : # 2e261 c ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.65 }
. nl { max - width : 620 px ; margin : 0 auto ; padding : 0 24 px }
. nl - head { padding : 88 px 0 56 px }
. nl - kick { font - family : Georgia , serif ; font - style : italic ; color : # a8743a ; margin : 0 0 20 px ; font - size : 17 px }
. nl - head h1 { font - size : 40 px ; line - height : 1.18 ; letter - spacing : - . 02 em ; margin : 0 0 20 px ; font - weight : 700 }
. nl - sub { color : # 6 d5d49 ; font - size : 17 px ; margin : 0 0 28 px }
. nl - form { display : flex ; gap : 8 px ; flex - wrap : wrap }
. nl - form input { flex : 1 ; min - width : 200 px ; padding : 14 px 16 px ; border : 1 px solid # ddc9ab ; border - radius : 10 px ; font : inherit ; background : # fff }
. nl - form input : focus { outline : 2 px solid # a8743a ; outline - offset : 1 px }
. nl - form button { padding : 14 px 24 px ; border : 0 ; border - radius : 10 px ; background : # 2e261 c ; color : # fbf3e9 ; font : inherit ; font - weight : 600 ; cursor : pointer }
. nl - form button : hover { background : # 4 a3d2d }
. nl - fine { color : # 8 b7c66 ; font - size : 13.5 px ; margin : 14 px 0 0 }
. nl - past { padding : 8 px 0 48 px ; border - top : 1 px solid # e8d8c0 }
. nl - past h2 { font - size : 12 px ; letter - spacing : . 16 em ; text - transform : uppercase ; color : # a08a6c ; margin : 32 px 0 18 px }
. nl - item { display : block ; text - decoration : none ; color : inherit ; padding : 16 px 0 ; border - bottom : 1 px solid # eee1cd }
. nl - item b { display : block ; font - size : 17 px ; margin - bottom : 2 px }
. nl - item span { display : block ; font - size : 13 px ; color : # a08a6c ; margin - bottom : 6 px }
. nl - item p { margin : 0 ; color : # 6 d5d49 ; font - size : 15 px }
. nl - item : hover b { color : # a8743a }
. nl - foot { padding : 28 px 0 40 px ; color : # a08a6c ; font - size : 13 px }
. nl - foot a { color : # a8743a }
@ media ( max - width : 560 px ) { . nl - head { padding : 56 px 0 40 px } . nl - head h1 { font - size : 30 px } } ` ,
} ,
// --- acid ---
"shop-lite" : {
label : "Small shop" , tag : "A few products, paid to your own address." , cat : "business" ,
html : `
< header class = "sh-top" > < b > $ { esc ( name ) } < / b > < s p a n c l a s s = " s h - p a y " > P a i d i n B i t c o i n C a s h · s t r a i g h t t o y o u r w a l l e t < / s p a n > < / h e a d e r >
< section class = "sh-hero" > < h1 > Four things worth making . < / h 1 > < p > S a y w h a t y o u s e l l a n d w h y y o u m a k e i t . O n e h o n e s t p a r a g r a p h b e a t s a c a r o u s e l . < / p > < / s e c t i o n >
< section class = "sh-grid" >
< article class = "sh-card" > < div class = "sh-img" > < / d i v > < h 3 > P r o d u c t o n e < / h 3 > < p > W h a t i t i s , w h a t i t i s m a d e o f , h o w b i g i t i s . < / p > < d i v c l a s s = " s h - b u y " > < b > 0 . 0 5 B C H < / b > < a c l a s s = " s h - b t n " h r e f = " b i t c o i n c a s h : " > P a y < / a > < / d i v > < / a r t i c l e >
< article class = "sh-card" > < div class = "sh-img" > < / d i v > < h 3 > P r o d u c t t w o < / h 3 > < p > W h a t i t i s , w h a t i t i s m a d e o f , h o w b i g i t i s . < / p > < d i v c l a s s = " s h - b u y " > < b > 0 . 0 8 B C H < / b > < a c l a s s = " s h - b t n " h r e f = " b i t c o i n c a s h : " > P a y < / a > < / d i v > < / a r t i c l e >
< article class = "sh-card" > < div class = "sh-img" > < / d i v > < h 3 > P r o d u c t t h r e e < / h 3 > < p > W h a t i t i s , w h a t i t i s m a d e o f , h o w b i g i t i s . < / p > < d i v c l a s s = " s h - b u y " > < b > 0 . 1 2 B C H < / b > < a c l a s s = " s h - b t n " h r e f = " b i t c o i n c a s h : " > P a y < / a > < / d i v > < / a r t i c l e >
< article class = "sh-card" > < div class = "sh-img" > < / d i v > < h 3 > P r o d u c t f o u r < / h 3 > < p > W h a t i t i s , w h a t i t i s m a d e o f , h o w b i g i t i s . < / p > < d i v c l a s s = " s h - b u y " > < b > 0 . 2 0 B C H < / b > < a c l a s s = " s h - b t n " h r e f = " b i t c o i n c a s h : " > P a y < / a > < / d i v > < / a r t i c l e >
< / s e c t i o n >
< section class = "sh-how" >
< h2 > How ordering works < / h 2 >
< ol > < li > Tap Pay — your wallet opens with the address and amount filled in . < / l i > < l i > S e n d t h e p a y m e n t , t h e n e m a i l t h e t r a n s a c t i o n i d a n d w h e r e i t s h o u l d g o . < / l i > < l i > I t s h i p s w i t h i n t h r e e w o r k i n g d a y s , t r a c k e d . < / l i > < / o l >
< p class = "sh-note" > Put your real cashaddr in each Pay link ( < code > bitcoincash : q … ? amount = 0.05 < / c o d e > ) a n d a r e a l a d d r e s s t o e m a i l . N o c h e c k o u t s e r v e r , n o c a r d f e e s , n o c h a r g e b a c k s — a n d n o r e f u n d s u n l e s s y o u o f f e r t h e m , s o s a y w h a t y o u d o . < / p >
< / s e c t i o n >
< footer class = "sh-foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } · < a href = "mailto:" > orders @ example < / a > < / f o o t e r > ` ,
css : `
body { margin : 0 ; background : # 0 a0c08 ; color : # eef3e4 ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.6 }
. sh - top { display : flex ; justify - content : space - between ; align - items : center ; gap : 14 px ; flex - wrap : wrap ; padding : 16 px 26 px ; border - bottom : 1 px solid # 1 d2417 }
. sh - top b { font - size : 16 px }
. sh - pay { font - size : 12.5 px ; color : # c6ff4a ; letter - spacing : . 02 em }
. sh - hero { max - width : 900 px ; margin : 0 auto ; padding : 72 px 24 px 44 px }
. sh - hero h1 { font - size : 46 px ; letter - spacing : - . 025 em ; line - height : 1.08 ; margin : 0 0 16 px }
. sh - hero p { max - width : 520 px ; color : # 9 caa8c ; font - size : 17 px ; margin : 0 }
. sh - grid { max - width : 900 px ; margin : 0 auto ; padding : 0 24 px 56 px ; display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 220 px , 1 fr ) ) ; gap : 22 px }
. sh - card { background : # 11150 d ; border : 1 px solid # 1 d2417 ; border - radius : 14 px ; padding : 16 px }
. sh - img { aspect - ratio : 1 ; border - radius : 10 px ; background : linear - gradient ( 135 deg , # 1 a2012 , # 242 c19 ) ; margin - bottom : 14 px }
. sh - card h3 { margin : 0 0 5 px ; font - size : 17 px }
. sh - card p { margin : 0 0 14 px ; color : # 9 caa8c ; font - size : 14.5 px }
. sh - buy { display : flex ; justify - content : space - between ; align - items : center ; gap : 10 px }
. sh - buy b { color : # c6ff4a ; font - size : 15 px ; font - variant - numeric : tabular - nums }
. sh - btn { background : # c6ff4a ; color : # 0 a0c08 ; text - decoration : none ; font - weight : 700 ; font - size : 13.5 px ; padding : 8 px 16 px ; border - radius : 8 px }
. sh - btn : hover { background : # d9ff7e }
. sh - how { max - width : 900 px ; margin : 0 auto ; padding : 40 px 24 px 64 px ; border - top : 1 px solid # 1 d2417 }
. sh - how h2 { font - size : 12 px ; letter - spacing : . 16 em ; text - transform : uppercase ; color : # 6 f7d60 ; margin : 0 0 18 px }
. sh - how ol { margin : 0 0 20 px ; padding - left : 20 px ; color : # c3cdb7 ; max - width : 62 ch }
. sh - how li { margin - bottom : 7 px }
. sh - note { color : # 7 f8d70 ; font - size : 14 px ; margin : 0 ; max - width : 66 ch }
. sh - note code { background : # 11150 d ; border : 1 px solid # 1 d2417 ; border - radius : 5 px ; padding : 1 px 6 px ; font - size : 12.5 px ; color : # c6ff4a }
. sh - foot { padding : 24 px ; text - align : center ; color : # 6 f7d60 ; font - size : 13 px ; border - top : 1 px solid # 1 d2417 }
. sh - foot a { color : # 9 caa8c }
@ media ( max - width : 560 px ) { . sh - hero { padding : 48 px 24 px 32 px } . sh - hero h1 { font - size : 32 px } } ` ,
} ,
"coming-soon" : {
label : "Coming soon" , tag : "A date, a way to be told, nothing else." , cat : "landing" ,
html : `
< main class = "cs" >
< p class = "cs-kick" > $ { esc ( name ) } < / p >
< h1 > Something is being built here . < / h 1 >
< p class = "cs-sub" > One sentence on what it is . One on when . Vagueness reads as vapour , so give a real month . < / p >
< div class = "cs-clock" >
< div > < b > 14 < / b > < s p a n > d a y s < / s p a n > < / d i v >
< div > < b > 06 < / b > < s p a n > h o u r s < / s p a n > < / d i v >
< div > < b > 32 < / b > < s p a n > m i n u t e s < / s p a n > < / d i v >
< div > < b > 09 < / b > < s p a n > s e c o n d s < / s p a n > < / d i v >
< / d i v >
< p class = "cs-when" > Opening 14 November $ { new Date ( ) . getFullYear ( ) } — edit these numbers by hand , or drop in the Countdown widget from the Widgets panel to make them run . < / p >
< form class = "cs-form" method = "post" >
< input type = "email" name = "email" placeholder = "you@example" >
2026-09-21 00:55:46 +02:00
< button type = "submit" data - gjs - type = "text" > Tell me when < / b u t t o n >
2026-09-21 00:48:35 +02:00
< / f o r m >
< p class = "cs-or" > or follow along : < a href = "https://" > Nostr < /a> · <a href="https:/ / ">Mastodon</a> · <a href=" mailto : " > Email < / a > < / p >
< / m a i n >
< footer class = "cs-foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } < / f o o t e r > ` ,
css : `
body { margin : 0 ; min - height : 100 vh ; display : flex ; flex - direction : column ; background : # 070 a04 ; color : # e9f2dc ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.6 ; background - image : radial - gradient ( circle at 50 % 0 , rgba ( 198 , 255 , 74 , . 12 ) , transparent 60 % ) }
. cs { flex : 1 ; display : flex ; flex - direction : column ; align - items : center ; justify - content : center ; text - align : center ; padding : 72 px 24 px 48 px }
. cs - kick { font - family : ui - monospace , Menlo , monospace ; letter - spacing : . 2 em ; text - transform : uppercase ; font - size : 12 px ; color : # c6ff4a ; margin : 0 0 24 px }
. cs h1 { font - size : 46 px ; line - height : 1.1 ; letter - spacing : - . 025 em ; margin : 0 0 16 px ; max - width : 16 ch }
. cs - sub { max - width : 480 px ; color : # 93 a184 ; font - size : 17 px ; margin : 0 0 40 px }
. cs - clock { display : flex ; gap : 14 px ; flex - wrap : wrap ; justify - content : center ; margin - bottom : 18 px }
. cs - clock div { background : rgba ( 198 , 255 , 74 , . 07 ) ; border : 1 px solid rgba ( 198 , 255 , 74 , . 22 ) ; border - radius : 12 px ; padding : 16 px 20 px ; min - width : 84 px }
. cs - clock b { display : block ; font - size : 34 px ; font - variant - numeric : tabular - nums ; color : # c6ff4a ; line - height : 1.1 }
. cs - clock span { font - size : 11 px ; letter - spacing : . 14 em ; text - transform : uppercase ; color : # 7 c8a6d }
. cs - when { color : # 7 c8a6d ; font - size : 13.5 px ; max - width : 52 ch ; margin : 0 0 40 px }
. cs - form { display : flex ; gap : 8 px ; flex - wrap : wrap ; justify - content : center ; width : min ( 420 px , 100 % ) }
. cs - form input { flex : 1 ; min - width : 190 px ; padding : 13 px 16 px ; border - radius : 10 px ; border : 1 px solid rgba ( 255 , 255 , 255 , . 14 ) ; background : # 0 d1208 ; color : # e9f2dc ; font : inherit }
. cs - form input : focus { outline : 2 px solid # c6ff4a ; outline - offset : 1 px }
. cs - form button { padding : 13 px 22 px ; border : 0 ; border - radius : 10 px ; background : # c6ff4a ; color : # 070 a04 ; font : inherit ; font - weight : 700 ; cursor : pointer }
. cs - or { margin : 22 px 0 0 ; color : # 7 c8a6d ; font - size : 14 px }
. cs - or a { color : # c6ff4a ; text - decoration : none }
. cs - or a : hover { text - decoration : underline }
. cs - foot { text - align : center ; padding : 20 px ; color : # 5 d6a50 ; font - size : 12.5 px }
@ media ( max - width : 520 px ) { . cs h1 { font - size : 32 px } . cs - clock div { padding : 12 px 14 px ; min - width : 68 px } . cs - clock b { font - size : 26 px } } ` ,
} ,
changelog : {
label : "Changelog" , tag : "What shipped, when, and what broke." , cat : "special" ,
html : `
< header class = "cl-top" >
< div class = "cl-in" > < b > $ { esc ( name ) } < /b><nav><a href="/ ">Home</a><a href=" / docs / ">Docs</a><a href=" # " > Feed < / a > < / n a v > < / d i v >
< / h e a d e r >
< section class = "cl-intro" >
< h1 > Changelog < / h 1 >
< p > Every release , newest first . Dates are the day the build went out , not the day the work started . < / p >
< / s e c t i o n >
< main class = "cl" >
< article class = "cl-rel" >
< div class = "cl-ver" > < b > 0.4 . 0 < / b > < t i m e > 1 2 S e p $ { n e w D a t e ( ) . g e t F u l l Y e a r ( ) } < / t i m e > < / d i v >
< div class = "cl-body" >
< p class = "cl-tags" > < span class = "t add" > added < / s p a n > < s p a n c l a s s = " t c h g " > c h a n g e d < / s p a n > < / p >
< ul > < li > The thing people kept asking for , in one line . < / l i > < l i > A n o t h e r t h i n g , p h r a s e d a s w h a t t h e u s e r c a n n o w d o . < / l i > < l i > A s e t t i n g m o v e d . S a y w h e r e i t w e n t . < / l i > < / u l >
< / d i v >
< / a r t i c l e >
< article class = "cl-rel" >
< div class = "cl-ver" > < b > 0.3 . 2 < / b > < t i m e > 2 8 A u g $ { n e w D a t e ( ) . g e t F u l l Y e a r ( ) } < / t i m e > < / d i v >
< div class = "cl-body" >
< p class = "cl-tags" > < span class = "t fix" > fixed < / s p a n > < / p >
< ul > < li > The crash on the thing . Say what triggered it — people want to know if it was them . < / l i > < l i > A s l o w q u e r y t h a t i s n o l o n g e r s l o w . < / l i > < / u l >
< / d i v >
< / a r t i c l e >
< article class = "cl-rel" >
< div class = "cl-ver" > < b > 0.3 . 0 < / b > < t i m e > 1 7 J u l $ { n e w D a t e ( ) . g e t F u l l Y e a r ( ) } < / t i m e > < / d i v >
< div class = "cl-body" >
< p class = "cl-tags" > < span class = "t add" > added < / s p a n > < s p a n c l a s s = " t b r k " > b r e a k i n g < / s p a n > < / p >
< ul > < li > The new capability , in the user ' s words . < / l i > < l i > < b > B r e a k i n g : < / b > w h a t y o u h a v e t o c h a n g e b e f o r e u p g r a d i n g , a n d w h a t h a p p e n s i f y o u d o n o t . < / l i > < / u l >
< / d i v >
< / a r t i c l e >
< / m a i n >
< footer class = "cl-foot" > © $ { new Date ( ) . getFullYear ( ) } $ { esc ( name ) } < / f o o t e r > ` ,
css : `
body { margin : 0 ; background : # 08090 a ; color : # e8ecef ; font - family : system - ui , - apple - system , Segoe UI , Roboto , sans - serif ; line - height : 1.65 }
. cl - top { border - bottom : 1 px solid # 191 c1f }
. cl - in { max - width : 820 px ; margin : 0 auto ; padding : 18 px 24 px ; display : flex ; justify - content : space - between ; align - items : center }
. cl - in b { font - size : 15 px }
. cl - in a { margin - left : 20 px ; color : # 7e8792 ; text - decoration : none ; font - size : 14 px }
. cl - in a : hover { color : # fff }
. cl - intro { max - width : 820 px ; margin : 0 auto ; padding : 56 px 24 px 32 px }
. cl - intro h1 { font - size : 38 px ; letter - spacing : - . 025 em ; margin : 0 0 10 px }
. cl - intro p { margin : 0 ; color : # 8 b949e ; max - width : 56 ch }
. cl { max - width : 820 px ; margin : 0 auto ; padding : 0 24 px 40 px }
. cl - rel { display : grid ; grid - template - columns : 130 px 1 fr ; gap : 28 px ; padding : 28 px 0 ; border - top : 1 px solid # 191 c1f }
. cl - ver b { display : block ; font - size : 19 px ; font - variant - numeric : tabular - nums ; color : # d4ff4f }
. cl - ver time { display : block ; color : # 6e7681 ; font - size : 13 px ; margin - top : 3 px }
. cl - tags { margin : 0 0 10 px ; display : flex ; gap : 6 px ; flex - wrap : wrap }
. t { font - size : 11 px ; letter - spacing : . 08 em ; text - transform : uppercase ; font - weight : 600 ; padding : 3 px 9 px ; border - radius : 999 px ; border : 1 px solid }
. t . add { color : # d4ff4f ; border - color : rgba ( 212 , 255 , 79 , . 35 ) ; background : rgba ( 212 , 255 , 79 , . 08 ) }
. t . fix { color : # 7 ee3b0 ; border - color : rgba ( 126 , 227 , 176 , . 35 ) ; background : rgba ( 126 , 227 , 176 , . 08 ) }
. t . chg { color : # 8 ab4ff ; border - color : rgba ( 138 , 180 , 255 , . 35 ) ; background : rgba ( 138 , 180 , 255 , . 08 ) }
. t . brk { color : # ff8fa3 ; border - color : rgba ( 255 , 143 , 163 , . 35 ) ; background : rgba ( 255 , 143 , 163 , . 08 ) }
. cl - body ul { margin : 0 ; padding - left : 20 px ; color : # b6bec7 }
. cl - body li { margin - bottom : 7 px }
. cl - body b { color : # ff8fa3 }
. cl - foot { border - top : 1 px solid # 191 c1f ; padding : 24 px ; text - align : center ; color : # 6e7681 ; font - size : 13 px }
@ media ( max - width : 620 px ) { . cl - rel { grid - template - columns : 1 fr ; gap : 10 px } . cl - ver { display : flex ; align - items : baseline ; gap : 12 px } . cl - ver time { margin : 0 } } ` ,
} ,
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 ----------
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 ) ;
} ,
} ,
} ) ;
2026-09-21 01:19:40 +02:00
editor . on ( "update" , ( ) => { if ( ! loading ) markDirty ( true ) ; } ) ;
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 ( ) ; } } ) ;
2026-09-21 01:19:40 +02:00
window . addEventListener ( "beforeunload" , ( e ) => { if ( anyDirty ( ) ) { e . preventDefault ( ) ; e . returnValue = "" ; } } ) ;
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
}
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 ;
2026-09-21 00:55:46 +02:00
const sec = ( id , label , html , css , icon = SECTION _ICON ) => bm . add ( ` sx- ${ id } ` , { label , category : "Sections" , media : icon , content : ` <style> ${ css } </style> ${ html } ` } ) ;
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
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} ` ) ;
2026-09-21 00:55:46 +02:00
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" data-gjs-type="text">Send</button></form></div></section> ` ,
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
` .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} ` ) ;
2026-09-21 00:55:46 +02:00
// Ten more, added 2026-09: navigation, social proof and the small print
// that every real page ends up needing. Each block owns its own class
// namespace (.sx-nav1, .sx-faq, …) and never reuses a selector another
// block defines, so dropping two of them on one page cannot make the
// first one change shape. `data-gjs-type="text"` marks the elements the
// parser would otherwise treat as opaque — the editor eats the attribute,
// it never reaches the exported page.
sec ( "nav-simple" , "Nav — simple" , ` <header class="sx-nav1"><div class="sx-nav1-in"><a class="sx-nav1-logo" href="/">your name</a><nav class="sx-nav1-links"><a href="#">Work</a><a href="#">About</a><a href="#">Contact</a></nav></div></header> ` ,
` .sx-nav1{background:#fff;border-bottom:1px solid #e8eaee}.sx-nav1-in{max-width:1040px;margin:0 auto;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}.sx-nav1-logo{color:#111;font-weight:700;font-size:16px;text-decoration:none}.sx-nav1-links a{color:#5a6069;text-decoration:none;font-size:14.5px;margin-left:22px}.sx-nav1-links a:hover{color:#111}@media(max-width:480px){.sx-nav1-links a{margin:0 16px 0 0}} ` , NAV _ICON ) ;
sec ( "nav-cta" , "Nav — with button" , ` <header class="sx-nav2"><div class="sx-nav2-in"><a class="sx-nav2-logo" href="/">your name</a><nav class="sx-nav2-links"><a href="#">Product</a><a href="#">Pricing</a><a href="#">Docs</a><a href="#">Blog</a><a href="#">Support</a></nav><a class="sx-nav2-btn" href="#">Get started</a></div></header> ` ,
` .sx-nav2{background:#0b0e14;color:#f1f4fa}.sx-nav2-in{max-width:1100px;margin:0 auto;padding:14px 24px;display:flex;align-items:center;gap:18px;flex-wrap:wrap}.sx-nav2-logo{color:#f1f4fa;font-weight:700;font-size:16px;text-decoration:none;margin-right:10px}.sx-nav2-links{flex:1;display:flex;gap:22px;flex-wrap:wrap}.sx-nav2-links a{color:#9aa4b5;text-decoration:none;font-size:14.5px}.sx-nav2-links a:hover{color:#fff}.sx-nav2-btn{background:#d6ff3d;color:#0b0e14;font-weight:700;font-size:14px;padding:9px 18px;border-radius:9px;text-decoration:none;white-space:nowrap} ` , NAV _ICON ) ;
sec ( "video-hero" , "Video hero" , ` <section class="sx-vhero"><video class="sx-vhero-bg" data-gjs-controls="false" data-gjs-autoplay="true" data-gjs-loop="true" muted playsinline></video><div class="sx-vhero-in"><h1>Say it over moving pictures</h1><p>Click the video and set its source in the panel on the right — an MP4 you uploaded works, and so does any public URL. Until then the gradient stands in.</p><a class="sx-vhero-btn" href="#">Watch the film</a></div></section> ` ,
` .sx-vhero{position:relative;min-height:70vh;display:flex;align-items:center;justify-content:center;text-align:center;padding:80px 24px;overflow:hidden;background:linear-gradient(140deg,#131a2b,#2c1a3a 55%,#0b0e14)}.sx-vhero-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border:0}.sx-vhero-in{position:relative;max-width:720px;color:#fff;text-shadow:0 2px 24px rgba(0,0,0,.55)}.sx-vhero h1{font-size:46px;line-height:1.1;margin:0 0 14px;letter-spacing:-.02em}.sx-vhero p{font-size:17px;color:rgba(255,255,255,.82);margin:0 0 26px}.sx-vhero-btn{display:inline-block;background:#fff;color:#111;font-weight:700;padding:13px 26px;border-radius:999px;text-decoration:none}@media(max-width:600px){.sx-vhero h1{font-size:30px}} ` ) ;
sec ( "stats" , "Big numbers" , ` <section class="sx-stat"><div class="sx-stat-in"><div><b>12k</b><span data-gjs-type="text">names registered</span></div><div><b>99.9%</b><span data-gjs-type="text">gateway uptime</span></div><div><b>4</b><span data-gjs-type="text">chains supported</span></div><div><b> $ 0</b><span data-gjs-type="text">renewal fees, ever</span></div></div></section> ` ,
` .sx-stat{padding:56px 24px;background:#f6f7f9;color:#111}.sx-stat-in{max-width:1000px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:32px;text-align:center}.sx-stat b{display:block;font-size:44px;font-weight:700;letter-spacing:-.03em;line-height:1.05;font-variant-numeric:tabular-nums}.sx-stat span{display:block;margin-top:6px;color:#6b727c;font-size:14px} ` ) ;
sec ( "steps" , "Numbered steps" , ` <section class="sx-steps"><h2>How it works</h2><div class="sx-steps-in"><div class="sx-step"><span class="sx-step-n" data-gjs-type="text">1</span><h3>Pick a name</h3><p>Search, check it is free, pay once.</p></div><div class="sx-step"><span class="sx-step-n" data-gjs-type="text">2</span><h3>Build the page</h3><p>Drag sections in, rewrite the words.</p></div><div class="sx-step"><span class="sx-step-n" data-gjs-type="text">3</span><h3>Publish</h3><p>One click puts it on Sia, signed by you.</p></div></div></section> ` ,
` .sx-steps{padding:64px 24px;background:#fff;color:#111}.sx-steps>h2{max-width:1000px;margin:0 auto 32px;font-size:28px;letter-spacing:-.015em}.sx-steps-in{max-width:1000px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:28px}.sx-step-n{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;background:#111;color:#fff;font-weight:700;font-size:16px;margin-bottom:12px}.sx-step h3{margin:0 0 6px;font-size:18px}.sx-step p{margin:0;color:#5a6069;line-height:1.6} ` ) ;
sec ( "team" , "Team" , ` <section class="sx-team"><h2>Who you would be working with</h2><div class="sx-team-in"><div class="sx-member"><img src="https://silentmode.st/sirius-x/brand/avatar.svg" alt=""><b>A. Person</b><i data-gjs-type="text">Founder</i><p>One line on what they do and what they did before.</p></div><div class="sx-member"><img src="https://silentmode.st/sirius-x/brand/avatar.svg" alt=""><b>B. Person</b><i data-gjs-type="text">Design</i><p>One line on what they do and what they did before.</p></div><div class="sx-member"><img src="https://silentmode.st/sirius-x/brand/avatar.svg" alt=""><b>C. Person</b><i data-gjs-type="text">Engineering</i><p>One line on what they do and what they did before.</p></div></div></section> ` ,
` .sx-team{padding:64px 24px;background:#fff;color:#111}.sx-team>h2{max-width:980px;margin:0 auto 34px;font-size:28px;letter-spacing:-.015em}.sx-team-in{max-width:980px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:34px}.sx-member img{width:96px;height:96px;border-radius:50%;object-fit:cover;background:#eceef1;display:block;margin-bottom:14px}.sx-member b{display:block;font-size:16px}.sx-member i{display:block;font-style:normal;color:#8b9099;font-size:13.5px;margin-bottom:8px}.sx-member p{margin:0;color:#5a6069;font-size:15px;line-height:1.6} ` ) ;
sec ( "logos" , "Logo cloud" , ` <section class="sx-logos"><p class="sx-logos-lab" data-gjs-type="text">Trusted by people who read the source</p><div class="sx-logos-in"> ${ Array . from ( { length : 6 } , () => ` < img src = "https://silentmode.st/sirius-x/brand/avatar.svg" alt = "" > ` ).join("")}</div></section> ` ,
` .sx-logos{padding:48px 24px;background:#fafbfc;border-top:1px solid #eceef1;border-bottom:1px solid #eceef1}.sx-logos-lab{max-width:1000px;margin:0 auto 26px;text-align:center;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:#98a0ab}.sx-logos-in{max-width:1000px;margin:0 auto;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:24px 48px}.sx-logos img{height:34px;width:auto;filter:grayscale(1);opacity:.6;transition:filter .18s,opacity .18s}.sx-logos img:hover{filter:none;opacity:1} ` ) ;
sec ( "timeline" , "Timeline" , ` <section class="sx-tl"><h2>How we got here</h2><div class="sx-tl-in"><div class="sx-tl-row"><time data-gjs-type="text">2021</time><div><b>It started as a weekend thing</b><p>What happened, in one or two sentences.</p></div></div><div class="sx-tl-row"><time data-gjs-type="text">2023</time><div><b>The first real users</b><p>What happened, in one or two sentences.</p></div></div><div class="sx-tl-row"><time data-gjs-type="text">2024</time><div><b>It paid for itself</b><p>What happened, in one or two sentences.</p></div></div><div class="sx-tl-row"><time data-gjs-type="text">Today</time><div><b>Where it is now</b><p>What happened, in one or two sentences.</p></div></div></div></section> ` ,
` .sx-tl{padding:64px 24px;background:#0b0e14;color:#e8ecf2}.sx-tl>h2{max-width:720px;margin:0 auto 34px;font-size:28px;letter-spacing:-.015em}.sx-tl-in{max-width:720px;margin:0 auto;border-left:2px solid #232a36;padding-left:26px}.sx-tl-row{position:relative;padding-bottom:28px}.sx-tl-row:last-child{padding-bottom:0}.sx-tl-row::before{content:"";position:absolute;left:-33px;top:6px;width:12px;height:12px;border-radius:50%;background:#d6ff3d}.sx-tl-row time{display:block;font-size:13px;letter-spacing:.06em;color:#d6ff3d;margin-bottom:4px}.sx-tl-row b{display:block;font-size:17px;margin-bottom:4px}.sx-tl-row p{margin:0;color:#98a3b4;line-height:1.6} ` ) ;
sec ( "faq" , "FAQ" , ` <section class="sx-faq"><h2>Questions people actually ask</h2><div class="sx-faq-in"><details class="sx-faq-i"><summary data-gjs-type="text">What happens if this site goes away?</summary><p>Answer it straight. The shortest true answer builds more trust than the longest reassuring one.</p></details><details class="sx-faq-i"><summary data-gjs-type="text">Do I need a wallet?</summary><p>Answer it straight.</p></details><details class="sx-faq-i"><summary data-gjs-type="text">What does it cost, in total?</summary><p>Answer it straight.</p></details><details class="sx-faq-i"><summary data-gjs-type="text">Can I move it somewhere else later?</summary><p>Answer it straight.</p></details><details class="sx-faq-i"><summary data-gjs-type="text">Who can see what I publish?</summary><p>Answer it straight.</p></details><details class="sx-faq-i"><summary data-gjs-type="text">How do I get help?</summary><p>Answer it straight.</p></details></div></section> ` ,
` .sx-faq{padding:64px 24px;background:#fff;color:#111}.sx-faq>h2{max-width:720px;margin:0 auto 26px;font-size:28px;letter-spacing:-.015em}.sx-faq-in{max-width:720px;margin:0 auto}.sx-faq-i{border-bottom:1px solid #e8eaee}.sx-faq-i summary{cursor:pointer;padding:16px 28px 16px 0;font-size:17px;font-weight:600;position:relative;list-style:none}.sx-faq-i summary::-webkit-details-marker{display:none}.sx-faq-i summary::after{content:"+";position:absolute;right:4px;top:14px;font-size:20px;font-weight:400;color:#98a0ab}.sx-faq-i[open] summary::after{content:"– "}.sx-faq-i p{margin:0 0 18px;color:#5a6069;line-height:1.65;max-width:62ch} ` ) ;
sec ( "newsletter" , "Signup band" , ` <section class="sx-news"><div class="sx-news-in"><div><h2>One email a month, nothing else</h2><p>Say what is in it. People give an address to a promise, not to a form.</p></div><form class="sx-news-form" method="post"><input type="email" name="email" placeholder="you@example" required><button type="submit" data-gjs-type="text">Subscribe</button></form></div></section> ` ,
` .sx-news{padding:52px 24px;background:#d6ff3d;color:#0b0e14}.sx-news-in{max-width:1000px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:32px;flex-wrap:wrap}.sx-news h2{margin:0 0 6px;font-size:26px;letter-spacing:-.015em}.sx-news p{margin:0;font-size:15.5px;opacity:.75;max-width:46ch}.sx-news-form{display:flex;gap:8px;flex-wrap:wrap;flex:1;min-width:280px;max-width:440px}.sx-news-form input{flex:1;min-width:180px;padding:13px 15px;border:1px solid rgba(11,14,20,.22);border-radius:10px;font:inherit;background:#fff}.sx-news-form button{padding:13px 22px;border:0;border-radius:10px;background:#0b0e14;color:#d6ff3d;font:inherit;font-weight:700;cursor:pointer} ` ) ;
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
// 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 ) ; } ) ;
}
2026-09-21 00:55:46 +02:00
const NAV _ICON = ` <svg viewBox="0 0 24 24" width="36" height="36"><rect x="2" y="6" width="20" height="5" rx="1.5" fill="currentColor" opacity=".9"/><rect x="2" y="14" width="9" height="2" rx="1" fill="currentColor" opacity=".35"/><rect x="13" y="14" width="9" height="2" rx="1" fill="currentColor" opacity=".35"/></svg> ` ;
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 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 ) ;
2026-09-21 01:19:40 +02:00
markDirty ( 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
}
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.
2026-09-21 01:19:40 +02:00
// A page in a sub-folder needs as many ../ as it is deep, or every asset it
// shows in the editor turns into a 404 the moment it is published.
function exportHtml ( page = pages [ current ] || { path : "index.html" , title : name } ) {
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 html = editor . getHtml ( ) ;
const css = editor . getCss ( ) ;
2026-09-21 01:19:40 +02:00
const up = "../" . repeat ( pageDepth ( page . path ) ) ;
const rel = ( s ) => s . split ( readUrl ( "" ) ) . join ( up ) ;
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" >
2026-09-21 01:19:40 +02:00
< title > $ { esc ( page . path === "index.html" ? name : ` ${ page . title } · ${ name } ` ) } < / t i t l 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
< 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 ;
2026-09-21 01:19:40 +02:00
capturePage ( ) ;
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
status ( "Saving draft…" , "busy" ) ;
try {
2026-09-21 01:19:40 +02:00
await putFile ( "_studio.json" , draftJson ( ) , "application/json" ) ;
markDirty ( 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
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 ;
2026-09-21 01:19:40 +02:00
$ ( "pub-intro-multi" ) . hidden = pages . length < 2 ;
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
$ ( "btn-publish" ) . disabled = true ;
2026-09-21 01:19:40 +02:00
const wasOn = current ;
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 {
2026-09-21 01:19:40 +02:00
capturePage ( ) ;
// Every page, every time: a site whose pages link to each other is only
// coherent if they go up together.
for ( let i = 0 ; i < pages . length ; i ++ ) {
applyPage ( i ) ;
const html = exportHtml ( pages [ i ] ) ;
const res = await putFile ( pages [ i ] . path , html , "text/html; charset=utf-8" ) ;
pushStep ( ` uploaded ${ pages [ i ] . path } · ${ html . length . toLocaleString ( ) } bytes → ${ res . sia _key } ` ) ;
}
applyPage ( wasOn ) ;
if ( pages . length > 1 ) {
await putFile ( "sitemap.xml" , sitemapXml ( ) , "application/xml" ) ;
pushStep ( ` wrote sitemap.xml · ${ pages . length } pages ` ) ;
}
// Pages dropped since the last publish are swept now, rather than left
// serving something the owner believes they deleted.
for ( const path of removed . splice ( 0 ) ) {
try { await delFile ( path ) ; pushStep ( ` removed ${ path } ` ) ; }
catch ( e ) { pushStep ( ` could not remove ${ path } : ${ e . message || e } ` ) ; }
}
await putFile ( "_studio.json" , draftJson ( ) , "application/json" ) ;
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 ( "saved editor project (_studio.json)" ) ;
2026-09-21 01:19:40 +02:00
markDirty ( false ) ;
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 ( ) ;
2026-09-21 01:19:40 +02:00
pages = [ newPage ( "index.html" , "Home" ) ] ; // a site with no draft still has a home page in the list
current = 0 ;
renderPages ( ) ;
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
$ ( "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 ( ) ;
2026-09-21 01:19:40 +02:00
if ( j ? . project || Array . isArray ( j ? . pages ) ) {
loadDraft ( j ) ;
status ( ` Loaded draft from ${ j . saved _at ? new Date ( j . saved _at ) . toLocaleString ( ) : "Sia" } ` , "ok" ) ;
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
}
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." ) ;
2026-09-21 01:07:35 +02:00
} catch ( e ) { status ( "Could not load the saved project: " + ( e . message || e ) , "err" ) ; showPicker ( ) ; }
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 } ) ;
2026-09-21 01:19:40 +02:00
if ( ok ) { markDirty ( false ) ; if ( src . kind !== "studio" ) notice ( ` Imported the page the name serves from ${ src . label } . Publishing writes to ${ folder } and repoints the name there. ` ) ; }
2026-09-21 01:07:35 +02:00
else { showPicker ( ) ; }
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 {
2026-09-21 01:07:35 +02:00
showPicker ( ) ; status ( "New site" ) ;
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
$ ( "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
2026-09-21 01:07:35 +02:00
// ---------- template gallery ----------
// One renderer feeds both the first-run picker and the top-bar gallery, and
// the plain <select> behind them, so a new entry in TEMPLATES shows up in all
// three without being listed anywhere else.
const TPL _CATS = [ [ "all" , "All" ] , [ "landing" , "Landing" ] , [ "blog" , "Blog" ] , [ "business" , "Business" ] , [ "special" , "Special" ] ] ;
// The card preview is the template itself in a sandboxed iframe, scaled to a
// quarter. No screenshot pipeline to keep in sync, and a template that breaks
// shows itself broken instead of hiding behind a stale image.
const previewDoc = ( t ) => ` <!doctype html><html><head><meta charset="utf-8"><style>html{background:#fff;overflow:hidden} ${ t . css } </style></head><body> ${ t . html } </body></html> ` ;
function buildCards ( gridEl , tabsEl , onPick ) {
tabsEl . textContent = "" ;
for ( const [ cat , label ] of TPL _CATS ) {
const b = document . createElement ( "button" ) ;
b . type = "button" ; b . className = "gal-tab" + ( cat === "all" ? " on" : "" ) ;
b . dataset . cat = cat ; b . textContent = label ;
b . setAttribute ( "role" , "tab" ) ; b . setAttribute ( "aria-selected" , cat === "all" ? "true" : "false" ) ;
tabsEl . appendChild ( b ) ;
}
gridEl . textContent = "" ;
for ( const [ key , t ] of Object . entries ( TEMPLATES ) ) {
const card = document . createElement ( "button" ) ;
card . type = "button" ; card . className = "gal-card" ; card . dataset . tpl = key ; card . dataset . cat = t . cat ;
const prev = document . createElement ( "div" ) ; prev . className = "gal-prev" ;
const fr = document . createElement ( "iframe" ) ;
fr . setAttribute ( "sandbox" , "" ) ; // previews are inert: no scripts, no storage, no navigation
fr . setAttribute ( "tabindex" , "-1" ) ; // the card is the tab stop, not its picture
fr . setAttribute ( "aria-hidden" , "true" ) ;
// Not loading="lazy": the cards are built while the sheet is still hidden,
// so every preview counts as off-screen and none of them ever loads. They
// are inert 1-2 KB documents and the grid is only built once.
fr . srcdoc = previewDoc ( t ) ;
prev . appendChild ( fr ) ;
const b = document . createElement ( "b" ) ; b . textContent = t . label || key ;
const span = document . createElement ( "span" ) ; span . textContent = t . tag || "" ;
card . append ( prev , b , span ) ;
gridEl . appendChild ( card ) ;
}
tabsEl . addEventListener ( "click" , ( e ) => {
const tab = e . target . closest ( ".gal-tab" ) ; if ( ! tab ) return ;
const cat = tab . dataset . cat ;
for ( const t of tabsEl . children ) { const on = t === tab ; t . classList . toggle ( "on" , on ) ; t . setAttribute ( "aria-selected" , String ( on ) ) ; }
for ( const c of gridEl . children ) c . hidden = cat !== "all" && c . dataset . cat !== cat ;
} ) ;
gridEl . addEventListener ( "click" , ( e ) => {
const card = e . target . closest ( ".gal-card" ) ; if ( card ) onPick ( card . dataset . tpl ) ;
} ) ;
}
let galleryReturn = null ; // what had focus before the gallery took it
function openGallery ( ) {
const grid = $ ( "gal-grid" ) ;
if ( ! grid . children . length ) buildCards ( grid , $ ( "gal-tabs" ) , pickFromGallery ) ;
galleryReturn = document . activeElement ;
$ ( "gallery" ) . hidden = false ;
$ ( "gal-tabs" ) . firstElementChild ? . focus ( ) ;
}
function closeGallery ( ) {
$ ( "gallery" ) . hidden = true ;
try { galleryReturn ? . focus ? . ( ) ; } catch { }
galleryReturn = null ;
}
function pickFromGallery ( key ) {
if ( ! confirm ( "Replace the current page with this template?" ) ) return ;
loadTemplate ( key ) ;
closeGallery ( ) ;
status ( "Template loaded — edit, then Publish" ) ;
}
// The first-run picker has nothing to replace, so it skips the confirmation.
function showPicker ( ) {
const grid = $ ( "picker-grid" ) ;
if ( ! grid . children . length ) buildCards ( grid , $ ( "picker-tabs" ) , ( key ) => {
loadTemplate ( key ) ; $ ( "picker" ) . hidden = true ; status ( "Template loaded — edit, then Publish" ) ;
} ) ;
$ ( "picker" ) . hidden = false ;
}
$ ( "btn-templates" ) . addEventListener ( "click" , openGallery ) ;
$ ( "gal-close" ) . addEventListener ( "click" , closeGallery ) ;
$ ( "gallery" ) . addEventListener ( "click" , ( e ) => { if ( e . target === $ ( "gallery" ) ) closeGallery ( ) ; } ) ;
// Esc closes; Tab stays inside the sheet while it is open.
document . addEventListener ( "keydown" , ( e ) => {
if ( $ ( "gallery" ) . hidden ) return ;
if ( e . key === "Escape" ) { e . preventDefault ( ) ; closeGallery ( ) ; return ; }
if ( e . key !== "Tab" ) return ;
const stops = [ ... $ ( "gallery" ) . querySelectorAll ( "button" ) ] . filter ( ( el ) => el . getClientRects ( ) . length ) ;
if ( ! stops . length ) return ;
const first = stops [ 0 ] , last = stops [ stops . length - 1 ] ;
if ( e . shiftKey && document . activeElement === first ) { e . preventDefault ( ) ; last . focus ( ) ; }
else if ( ! e . shiftKey && document . activeElement === last ) { e . preventDefault ( ) ; first . focus ( ) ; }
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
} ) ;
2026-09-21 01:07:35 +02:00
// The plain list behind the gallery, filled from the same map.
2026-09-21 00:48:35 +02:00
( function fillTemplateSelect ( ) {
const sel = $ ( "tpl-select" ) ;
const first = sel . options [ 0 ] ; // "Templates…" — keep the node, i18n owns its text
2026-09-21 01:07:35 +02:00
sel . textContent = "" ;
2026-09-21 00:48:35 +02:00
sel . appendChild ( first ) ;
for ( const [ key , t ] of Object . entries ( TEMPLATES ) ) {
const o = document . createElement ( "option" ) ;
o . value = key ; o . textContent = t . label || key ;
sel . appendChild ( o ) ;
}
} ) ( ) ;
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
$ ( "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 ; } ) ;