← repoman internals

internal/theme

import "git.griefed.de/griefed/repoman/internal/theme"
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.