Theming & skins

Tiger separates how the app is structured from how it looks — two axes, deliberately different weights.

Theme Skin
Is a whole view layer (layouts, view scripts) a CSS-only override
Weight heavy, changes rarely light, swappable, structurally inert
Per-tenant? rare (white-label) yes — this is the tenant branding axis
Example puma (a future react theme) jaguar, cheetah, <tenant>.css

PUMA — the default theme, zero build

PUMA is vendored Bootstrap 5bootstrap.min.css + the bundle, dropped in. No npm, no Sass, no build step. composer install + an asset symlink and the UI renders. (Node only ever appears if a theme opts into a build — quarantined there.)

Skins are runtime CSS variables

A skin is a small :root { --bs-* } override (plus component --bs-btn-* vars). Because it's just CSS variables, it swaps at runtime — no recompile. Skins resolve per-user (a cookie) and per-org (a config row), and the admin ships a live skin switcher that hot-swaps them with no reload. Light / dark rides Bootstrap's native data-bs-theme with a browser/light/dark toggle and no-flash resolution.

Theme is just a path

The active theme + skin resolve from the config cascade at bootstrap; "active theme" is simply a path woven into the layout path, the view-script paths, and the asset base URL. No inheritance, no routing — the only fallback is theme → core default views, so a theme only provides what it wants to override. Because per-org theming is the config cascade (a tenant's tiger.skin is an org-scoped config row), the whole thing costs almost nothing.

Assets

Asset URLs are root-relative (/_theme/…, never a hardcoded domain) and cache-busted by $this->asset() (?v=<filemtime>), so a deploy's changed CSS/JS is picked up without a manifest or a hard refresh.

See also