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

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}