Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

theme.gno

7.29 Kb · 244 lines
  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 the
  4// slot machinery the home realm provides. Everything else on the page comes
  5// from slots, so this file only decides HOW things are drawn, never WHAT is
  6// said.
  7//
  8// It is a candidate, not the page. Deploying it registers it (see init); the
  9// authority accepts it in a separate transaction. To change the look in a way
 10// 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 default
 16//	style.bg      #RRGGBB   card background
 17//	style.fg      #RRGGBB   card text
 18//	style.header  card | plain | none
 19//
 20// Widgets this theme adds to the layout, beside every slot and the home
 21// realm's computed placeholders:
 22//
 23//	:header:   the card (or a plain heading, or nothing, per style.header)
 24//	:divider:  a thin accent rule
 25//	:footer:   realm · revision · block · chain · theme · links
 26package theme
 27
 28import (
 29	"encoding/base64"
 30	"strconv"
 31	"strings"
 32
 33	"chain/runtime"
 34
 35	"gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home"
 36)
 37
 38const (
 39	defaultAccent = "#D9A441"
 40	defaultBg     = "#14161C"
 41	defaultFg     = "#F4EFE6"
 42)
 43
 44// DefaultLayout is what renders when no layout slot is set. content/layout.md
 45// starts as a copy of it; push that slot to depart from it.
 46const DefaultLayout = `:header:
 47
 48<gno-columns>
 49![Clockwork](https://avatars.githubusercontent.com/u/6826762?s=400)
 50<gno-columns-sep />
 51:intro:
 52
 53:links:
 54</gno-columns>
 55
 56## Now
 57
 58:now:
 59
 60## Previously
 61
 62:previously:
 63
 64## Stack
 65
 66:stack:
 67
 68## Before the rabbit hole
 69
 70:before:
 71
 72## Numbers
 73
 74:numbers:
 75
 76:divider:
 77
 78:about:
 79
 80:footer:
 81`
 82
 83type clockwork struct{}
 84
 85var instance = &clockwork{}
 86
 87// Instance returns the theme object, for a caller that would rather register
 88// it explicitly than rely on this realm's init having done it.
 89func Instance() home.Theme { return instance }
 90
 91// init nominates this theme. Deploying is what registers it; it serves
 92// nothing until the authority runs Accept("<this path>").
 93func init(cur realm) {
 94	home.Register(cross(cur), instance)
 95}
 96
 97// Render draws the profile page and hands every other path to the built-in
 98// 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 = DefaultLayout
106	}
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}
113
114// ---------------------------------------------------------------------------
115// Widgets
116
117func 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 out
132	}
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}
141
142func 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}
150
151func 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}
164
165// ---------------------------------------------------------------------------
166// SVG
167
168// 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 the
170// 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)
175
176	var b strings.Builder
177	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}
199
200// image wraps an SVG document as a markdown image with a data: URI. gnoweb
201// rejects every data: image except image/svg+xml, and gno.land's CSP allows
202// 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 "![" + alt + "](data:image/svg+xml;base64," +
206		base64.StdEncoding.EncodeToString([]byte(svg)) + ")"
207}
208
209// color returns the style knob when it is a well-formed #RRGGBB, else the
210// 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 v
216	}
217	return fallback
218}
219
220func isHexColor(s string) bool {
221	if len(s) != 7 || s[0] != '#' {
222		return false
223	}
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 false
230		}
231	}
232	return true
233}
234
235// esc escapes text for an SVG element body or attribute.
236func esc(s string) string {
237	return strings.NewReplacer(
238		"&", "&amp;",
239		"<", "&lt;",
240		">", "&gt;",
241		"\"", "&quot;",
242		"\n", " ",
243	).Replace(s)
244}