package theme // import "git.griefed.de/griefed/repoman/internal/theme"
Package theme implements repoman's theming model: a theme is a named set of
values for a fixed, known list of CSS custom properties (colors, the corner
radius, the card shadow). Structural CSS lives in app.css and only ever
references these variables, so swapping the variable set re-skins the whole UI.
Themes come from two places and meet in one pipeline:
- Built-in themes ship as .css files under frontend/web/themes/ — one bare
`:root { --var: value; }` block per file (see Parse for the exact format).
Adding a built-in theme is dropping a file in that directory.
- Custom themes are stored in the database (see internal/db.ThemeRepo) and
managed by admins in the web UI. Export produces the same .css file format,
so a custom theme can be shared and imported elsewhere.
Both kinds are rendered into one generated stylesheet (Stylesheet) that scopes
each theme to :root[data-theme="…"]; the client selects a theme by setting the
data-theme attribute on <html>.
Security: theme values are served as CSS to every user, so Parse and
ValidateValue are strict — only a small safe character set is accepted,
which rules out closing the declaration/block ( ; { } ), embedding markup (< >),
strings (quotes/backslash), and external requests (url() needs : or / for any
absolute URL, both rejected).
CONSTANTS
const (
StatusGlobal = "global"
StatusPersonal = "personal"
StatusPending = "pending"
)
Theme status constants for DB themes.
VARIABLES
var KnownVars = []VarSpec{
{Name: "--bg", Label: "Page background", Kind: KindColor},
{Name: "--bg-card", Label: "Card background", Kind: KindColor},
{Name: "--fg", Label: "Text", Kind: KindColor},
{Name: "--fg-muted", Label: "Muted text", Kind: KindColor},
{Name: "--border", Label: "Borders", Kind: KindColor},
{Name: "--nav-bg", Label: "Nav background", Kind: KindColor},
{Name: "--nav-fg", Label: "Nav text", Kind: KindColor},
{Name: "--nav-fg-muted", Label: "Nav muted text", Kind: KindColor},
{Name: "--accent", Label: "Accent", Kind: KindColor},
{Name: "--danger", Label: "Danger", Kind: KindColor},
{Name: "--warning", Label: "Warning", Kind: KindColor},
{Name: "--success", Label: "Success", Kind: KindColor},
{Name: "--radius", Label: "Corner radius", Kind: KindDimension},
{Name: "--shadow", Label: "Card shadow", Kind: KindShadow},
}
KnownVars is the canonical, ordered list of variables a theme may define.
app.css references exactly these (directly or via color-mix() derivation);
adding a variable here means also using it in app.css and giving the
built-in themes a value for it. Order is the render and editor order.
FUNCTIONS
func CustomBlocks(customs []Theme) (string, error)
CustomBlocks renders ONLY the per-theme :root[data-theme="custom-<id>"]
blocks for the given themes — no built-ins, no base :root — for the
authenticated per-user stylesheet (/themes/me.css) that layers on top of the
global /themes.css. Values are re-validated, the same guard as Stylesheet.
func ExportCSS(t Theme) []byte
ExportCSS renders a theme back into the .css theme-file format Parse accepts
— the shareable artifact for the export/import feature. Variables render in
KnownVars order; undefined ones are omitted.
func Parse(src []byte) (map[string]string, error)
Parse reads the theme .css file format: optional comments, then exactly
one `:root { … }` block of `--name: value;` declarations. Unknown variable
names, invalid values, duplicate declarations, and trailing content are
all errors — imports are untrusted input, so strictness beats leniency.
Returns the parsed variable map (at least one variable required).
func Stylesheet(builtins, customs []Theme) (string, error)
Stylesheet renders every theme into the one stylesheet the server serves as
/themes.css:
- :root carries the light theme's values as the document default,
so a missing or deleted selection degrades to light, and partial themes
inherit light values for variables they leave undefined.
- Each built-in renders as :root[data-theme="<slug>"].
- data-theme="auto" follows the OS via a prefers-color-scheme media query
carrying the dark theme's values.
- Each custom theme renders as :root[data-theme="custom-<id>"].
Every value is re-validated before rendering; a failure means the stored
data was tampered with or a validation gap was introduced, so it is surfaced
as an error rather than skipped silently.
func ValidateName(name string, builtins []Theme) error
ValidateName checks a custom theme's display name: syntax via validName,
plus a reservation check so a custom theme cannot shadow the "auto" mode or
a built-in slug in the dropdown.
func ValidateValue(value string) error
ValidateValue checks that a variable value is safe to embed in the generated
stylesheet. The value must already be trimmed.
TYPES
type Theme struct {
// ID is the database row ID for custom themes; 0 for built-ins.
ID int64
// Name is the human-readable display name shown in the dropdown.
Name string
// Slug is the data-theme attribute value for built-ins (the theme
// file's base name, e.g. "dark"); empty for custom themes.
Slug string
// BuiltIn marks themes shipped in the binary (not editable/deletable).
BuiltIn bool
// Vars holds the variable values, keyed by custom-property name.
Vars map[string]string
// ── DB-theme sharing metadata (zero for built-ins) ──
// Status is "global", "personal", or "pending" for DB themes.
Status string
// OwnerUserID is the owning user for a personal/pending theme; nil for global.
OwnerUserID *int64
// SuggestedName is the submitter's proposed name while pending.
SuggestedName string
// SubmittedBy is the user who submitted the theme (kept for attribution after
// it is accepted to global); nil when never submitted.
SubmittedBy *int64
}
Theme is one selectable theme: either a built-in (loaded from an embedded
.css file, identified by its slug) or a custom one (stored in the DB,
identified by its ID). Vars maps known variable names to validated values;
a theme may define a subset — undefined variables fall back to the default
(light) values in the generated stylesheet's :root block.
func LoadBuiltins(fsys fs.FS, dir string) ([]Theme, error)
LoadBuiltins parses every *.css theme file in dir of fsys into a Theme.
The slug is the file's base name; the display name is the slug with the
first letter upper-cased. Order: "light" first, "dark" second, the rest
alphabetical — the dropdown and stylesheet order. Any malformed file is an
error so a bad embedded theme fails at startup, not per request.
func (t Theme) DataThemeValue() string
DataThemeValue returns the value the client sets as <html data-theme="…"> to
select this theme: the slug for built-ins, "custom-<id>" for custom themes
(IDs keep the attribute CSS-safe regardless of the display name).
type VarKind string
VarKind classifies a theme variable for the editor UI: colors get a color
input, dimensions and shadows get plain text inputs.
const (
// KindColor is a CSS color value (hex, rgb()/rgba(), hsl(), or a name).
KindColor VarKind = "color"
// KindDimension is a CSS length such as the corner radius ("6px").
KindDimension VarKind = "dimension"
// KindShadow is a CSS box-shadow value.
KindShadow VarKind = "shadow"
)
type VarSpec struct {
Name string
Label string
Kind VarKind
}
VarSpec describes one known theme variable: its CSS custom-property name,
a human label for the editor UI, and its kind.