Documentation
¶
Overview ¶
Package toolshell is the ZoolCoder app shell for server-rendered Go apps: a sidebar that folds to icons and slides in on phones, a Ctrl/Cmd-K command palette, keyboard jumps, tooltips, light/dark/auto themes, sortable and searchable tables, and a strict Content-Security-Policy. An app brings its pages and its nav; the shell, the stylesheet and the one script come from here.
Every asset is a file under /_ts/, so the CSP needs no 'unsafe-inline'. Theme and sidebar state live in cookies the server reads, so the first paint is already right without a script.
Wiring, in short:
ts := toolshell.New(toolshell.Config{Brand: toolshell.Brand{Name: "mail", Accent: "ctl"}, Cookie: "mailctl"})
tmpl, _ := ts.Templates(appFS, "templates/*.html", funcs)
ts.Mount(mux)
page := myPage{Chrome: ts.Chrome(r, "domains", nav)}
and each page template starts with {{template "ts-top" .}} and ends with {{template "ts-bottom" .}}.
Index ¶
- Constants
- Variables
- func Bars(values []int) template.HTML
- func Icon(name string) template.HTML
- func Ints(v ...int) []int
- func Meter(tones string, values []int) template.HTML
- func Ring(part, total int) template.HTML
- func SafeNext(next string) string
- func SetHeaders(h http.Header)
- type Brand
- type Chrome
- type Config
- type NavGroup
- type NavItem
- type PaletteItem
- type Shell
- type Status
Constants ¶
const ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self'; " +
"img-src 'self' data:; font-src 'self'; connect-src 'self'; " +
"form-action 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'"
ContentSecurityPolicy keeps a page to its own origin. The shell's styles and script are files, so nothing inline runs or applies.
const Prefix = "/_ts"
Prefix is where the shell serves its stylesheet, script and mark.
Variables ¶
var Icons = map[string]template.HTML{
"grid": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1.5" y="1.5" width="5.5" height="5.5" rx="1"/><rect x="9" y="1.5" width="5.5" height="5.5" rx="1"/><rect x="1.5" y="9" width="5.5" height="5.5" rx="1"/><rect x="9" y="9" width="5.5" height="5.5" rx="1"/></svg>`,
"box": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2.5 3.5h11v9h-11z"/><path d="M2.5 6.5h11"/></svg>`,
"share": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="3.5" cy="8" r="2"/><circle cx="12.5" cy="3.5" r="2"/><circle cx="12.5" cy="12.5" r="2"/><path d="M5.3 7.2l5.3-2.7M5.3 8.8l5.3 2.7"/></svg>`,
"merge": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="4" cy="3.5" r="1.8"/><circle cx="4" cy="12.5" r="1.8"/><circle cx="12" cy="12.5" r="1.8"/><path d="M4 5.3v5.4M12 10.7V7a2 2 0 0 0-2-2H8"/></svg>`,
"bot": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="5" width="10" height="8" rx="2"/><path d="M6 8.5h.01M10 8.5h.01M8 2v3M5 5l-1.5-1.5M11 5l1.5-1.5"/></svg>`,
"pulse": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1.5 8h3l2-5 3 10 2-5h3"/></svg>`,
"gear": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2.2"/><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M3.4 12.6l1.4-1.4M11.2 4.8l1.4-1.4"/></svg>`,
"globe": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.5"/><path d="M1.5 8h13M8 1.5c2.2 2 2.2 11 0 13M8 1.5c-2.2 2-2.2 11 0 13"/></svg>`,
"mail": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1.5" y="3.5" width="13" height="9" rx="1.5"/><path d="M2 4.5l6 4.5 6-4.5"/></svg>`,
"list": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 4h9M5 8h9M5 12h9M2 4h.01M2 8h.01M2 12h.01"/></svg>`,
"check": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.5"/><path d="M5 8.2l2 2 4-4.4"/></svg>`,
"cloud": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4.5 12.5h7a3 3 0 0 0 .4-6A4 4 0 0 0 4.2 8a2.3 2.3 0 0 0 .3 4.5z"/></svg>`,
"shield": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1.5l5.5 2v4c0 3.3-2.4 5.6-5.5 7-3.1-1.4-5.5-3.7-5.5-7v-4z"/></svg>`,
"terminal": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5"/><path d="M4.5 6l2.5 2-2.5 2M8.5 10h3"/></svg>`,
"arrow": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8h10M9 4l4 4-4 4"/></svg>`,
"plus": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v10M3 8h10"/></svg>`,
"edit": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.5 2.5l3 3-8 8h-3v-3z"/></svg>`,
"trash": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4h11M6 4V2.5h4V4M4 4l.7 9.5h6.6L12 4"/></svg>`,
"search": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5l3.5 3.5"/></svg>`,
"menu": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4h11M2.5 8h11M2.5 12h11"/></svg>`,
"fold": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5"/><path d="M6 2.5v11M10.5 6.5L9 8l1.5 1.5"/></svg>`,
"close": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>`,
"copy": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/></svg>`,
"external": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 2.5h4.5V7M13.5 2.5L7 9M11.5 9.5v3a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3"/></svg>`,
"info": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.5"/><path d="M8 7.5v4M8 4.8h.01"/></svg>`,
"keyboard": `<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="3.5" width="13" height="9" rx="1.5"/><path d="M4 6.5h.01M6.5 6.5h.01M9 6.5h.01M11.5 6.5h.01M5 9.5h6"/></svg>`,
}
Icons are 16×16 stroke glyphs that take the text colour. Nav entries name them; templates draw one with {{icon "name"}}.
var Themes = []string{"light", "dark", "auto"}
Themes are the choices in the sidebar switch; auto follows the OS.
Functions ¶
func Meter ¶
Meter draws a stacked bar of values, each segment classed by the matching word in tones, e.g. {{meter "ok warn bad" (ints 3 1 0)}}.
func SetHeaders ¶
SetHeaders sets the strict headers every toolshell page is meant to carry: the CSP, no framing, no sniffing, no referrer, no caching, no indexing.
Types ¶
type Brand ¶
type Brand struct {
Name, Accent, Tagline string
Mark template.HTML
Home string // where the brand links; "/" when empty
}
Brand is the sidebar wordmark: Name then Accent in the accent colour, with an optional Tagline under it. Mark replaces the ZoolCoder mark.
type Chrome ¶
type Chrome struct {
Brand Brand
Active string // Key of the current entry
Title string // the entry's label; a page may overwrite it
Theme string // light, dark or auto
Mini bool // the sidebar is folded to icons
ReturnTo string // where a form on this page sends the user back
Msg, Err string // flash from ?msg= and ?err=
Status Status
Palette bool // the app serves palette entries
Cookie string // the cookie prefix, for the script
}
Chrome is what the shell needs on every page. Page data embeds it.
type Config ¶
type Config struct {
Brand Brand
Status Status
Footer template.HTML
// Cookie prefixes the theme and sidebar cookies so two apps on one
// host keep their own choices. "ts" when empty.
Cookie string
// Palette adds entries to the command palette; nil leaves it with the
// nav pages only.
Palette func(*http.Request) []PaletteItem
}
Config is what an app tells the shell once.
type NavItem ¶
type NavItem struct {
NavItem is one sidebar entry. Icon names an entry of Icons. Count shows as a badge when non-zero. Hotkey is the second key of a "g then key" jump.
type PaletteItem ¶
type PaletteItem struct {
Label string `json:"label"`
Hint string `json:"hint,omitempty"`
Href string `json:"href,omitempty"`
Post string `json:"post,omitempty"`
Fields map[string]string `json:"fields,omitempty"`
}
PaletteItem is one palette entry the app adds beyond its pages: a link (Href) or a button that posts Fields to Post.
type Shell ¶
type Shell struct {
// contains filtered or unexported fields
}
Shell is a configured toolshell.
func (*Shell) Mount ¶
Mount registers the shell's routes: its assets, the theme switch and, when the app has palette entries, the palette's data.
func (*Shell) Templates ¶
func (k *Shell) Templates(app fs.FS, pattern string, funcs template.FuncMap) (*template.Template, error)
Templates parses the shared shell and the app's pages into one set, with the shell's helpers and the app's funcs (the app's win on a name clash). Pages are addressed by base name: "plan.html".