1// Package theme is the first look of gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home.2//3// It draws a header card as inline SVG, a divider and a footer, on top of the4// slot machinery the home realm provides. Everything else on the page comes5// from slots, so this file only decides HOW things are drawn, never WHAT is6// said.7//8// It is a candidate, not the page. Deploying it registers it (see init); the9// authority accepts it in a separate transaction. To change the look in a way10// slots and style knobs cannot express, copy this directory to theme/v1, edit,11// deploy, Accept. The home realm and every slot stay exactly where they are.12//13// Style knobs read from slots (all optional):14//15// style.accent #RRGGBB brass by default16// style.bg #RRGGBB card background17// style.fg #RRGGBB card text18// style.header card | plain | none19//20// Widgets this theme adds to the layout, beside every slot and the home21// realm's computed placeholders:22//23// :header: the card (or a plain heading, or nothing, per style.header)24// :divider: a thin accent rule25// :footer: realm · revision · block · chain · theme · links26package theme2728import (29 "encoding/base64"30 "strconv"31 "strings"3233 "chain/runtime"3435 "gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home"36)3738const (39 defaultAccent = "#D9A441"40 defaultBg = "#14161C"41 defaultFg = "#F4EFE6"42)4344// DefaultLayout is what renders when no layout slot is set. content/layout.md45// starts as a copy of it; push that slot to depart from it.46const DefaultLayout = `:header:4748<gno-columns>4950<gno-columns-sep />51:intro:5253:links:54</gno-columns>5556## Now5758:now:5960## Previously6162:previously:6364## Stack6566:stack:6768## Before the rabbit hole6970:before:7172## Numbers7374:numbers:7576:divider:7778:about:7980:footer:81`8283type clockwork struct{}8485var instance = &clockwork{}8687// Instance returns the theme object, for a caller that would rather register88// it explicitly than rely on this realm's init having done it.89func Instance() home.Theme { return instance }9091// init nominates this theme. Deploying is what registers it; it serves92// nothing until the authority runs Accept("<this path>").93func init(cur realm) {94 home.Register(cross(cur), instance)95}9697// Render draws the profile page and hands every other path to the built-in98// views, so the editor and the system page look the same under any theme.99func (clockwork) Render(path string) string {100 if path != "" {101 return home.Fallback(path)102 }103 layout := home.Layout()104 if layout == "" {105 layout = DefaultLayout106 }107 f := home.NewFiller()108 f.Add("header", header)109 f.Add("divider", divider)110 f.Add("footer", footer)111 return f.Fill(layout)112}113114// ---------------------------------------------------------------------------115// Widgets116117func header() string {118 name := home.Get("name")119 if name == "" {120 name = home.Path()121 }122 tagline := home.Get("tagline")123 switch home.Style("header", "card") {124 case "none":125 return ""126 case "plain":127 out := "# " + name + "\n"128 if tagline != "" {129 out += "\n_" + tagline + "_\n"130 }131 return out132 }133 meta := home.Get("handle")134 if meta != "" {135 meta += " · "136 }137 meta += "rev " + strconv.Itoa(home.Revision()) +138 " · block " + strconv.FormatInt(runtime.ChainHeight(), 10)139 return image(name, card(name, tagline, meta))140}141142func divider() string {143 accent := color("accent", defaultAccent)144 svg := `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 10" width="1200" height="10">` +145 `<defs><linearGradient id="g" x1="0" x2="1"><stop offset="0" stop-color="` + accent + `"/>` +146 `<stop offset="1" stop-color="` + accent + `" stop-opacity="0"/></linearGradient></defs>` +147 `<rect width="1200" height="10" rx="5" fill="url(#g)"/></svg>`148 return image("", svg)149}150151func footer() string {152 theme := home.LivePath()153 if theme == "" {154 theme = "built-in"155 }156 return "`" + home.Path() + "` · rev " + strconv.Itoa(home.Revision()) +157 " · block " + strconv.FormatInt(runtime.ChainHeight(), 10) +158 " · " + runtime.ChainID() +159 " · theme `" + theme + "`" +160 " · [slots](" + home.Link("slots") + ")" +161 " · [edit](" + home.Link("edit") + ")" +162 " · [system](" + home.Link("system") + ")"163}164165// ---------------------------------------------------------------------------166// SVG167168// card draws the header: name, tagline and a meta line on a rounded panel,169// with a gear on the right. Text is clipped, not wrapped, so keep the170// tagline to one line.171func card(name, tagline, meta string) string {172 accent := color("accent", defaultAccent)173 bg := color("bg", defaultBg)174 fg := color("fg", defaultFg)175176 var b strings.Builder177 b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 300" width="1200" height="300" role="img" aria-label="`)178 b.WriteString(esc(name))179 b.WriteString(`"><defs><clipPath id="c"><rect width="1200" height="300" rx="24"/></clipPath>`)180 b.WriteString(`<clipPath id="t"><rect x="64" y="30" width="820" height="250"/></clipPath></defs>`)181 b.WriteString(`<g clip-path="url(#c)">`)182 b.WriteString(`<rect width="1200" height="300" fill="` + bg + `"/>`)183 b.WriteString(`<rect width="14" height="300" fill="` + accent + `"/>`)184 // The gear: a thick dashed ring reads as teeth without a single path command.185 b.WriteString(`<g transform="translate(1030 150)" fill="none" stroke="` + accent + `" opacity="0.9">`)186 b.WriteString(`<circle r="82" stroke-width="30" stroke-dasharray="21 15.8"/>`)187 b.WriteString(`<circle r="42" stroke-width="12"/>`)188 b.WriteString(`<circle r="11" fill="` + accent + `" stroke="none"/>`)189 b.WriteString(`</g>`)190 b.WriteString(`<g clip-path="url(#t)" fill="` + fg + `" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">`)191 b.WriteString(`<text x="64" y="132" font-size="76" font-weight="700">` + esc(name) + `</text>`)192 if tagline != "" {193 b.WriteString(`<text x="64" y="188" font-size="30" opacity="0.85">` + esc(tagline) + `</text>`)194 }195 b.WriteString(`<text x="64" y="246" font-size="24" fill="` + accent + `" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">` + esc(meta) + `</text>`)196 b.WriteString(`</g></g></svg>`)197 return b.String()198}199200// image wraps an SVG document as a markdown image with a data: URI. gnoweb201// rejects every data: image except image/svg+xml, and gno.land's CSP allows202// data: in img-src, so this is the one way to draw without hosting anything.203func image(alt, svg string) string {204 alt = strings.NewReplacer("[", "", "]", "", "\n", " ").Replace(alt)205 return ") + ")"207}208209// color returns the style knob when it is a well-formed #RRGGBB, else the210// default. Knobs are admin-written, but a value is still pasted into markup,211// so the shape is checked rather than trusted.212func color(key, fallback string) string {213 v := home.Style(key, fallback)214 if isHexColor(v) {215 return v216 }217 return fallback218}219220func isHexColor(s string) bool {221 if len(s) != 7 || s[0] != '#' {222 return false223 }224 for i := 1; i < 7; i++ {225 c := s[i]226 switch {227 case c >= '0' && c <= '9', c >= 'a' && c <= 'f', c >= 'A' && c <= 'F':228 default:229 return false230 }231 }232 return true233}234235// esc escapes text for an SVG element body or attribute.236func esc(s string) string {237 return strings.NewReplacer(238 "&", "&",239 "<", "<",240 ">", ">",241 "\"", """,242 "\n", " ",243 ).Replace(s)244}245Instance() interface {Render func(string) string}
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
Render unavailable
rate limit exceeded — the render query is served by the Gnoland archive RPC.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.