diff --git a/branding/README.md b/branding/README.md index a68e8b9..00aac49 100644 --- a/branding/README.md +++ b/branding/README.md @@ -47,12 +47,16 @@ When both `logo.svg` and `logo.png` are present, SVG wins. "name": "AIO", // required — drives PWA manifest name "shortName": "AIO", // optional — PWA home-screen label; defaults to `name` "themeColor": "#1f2937", // optional — PWA chrome color override (otherwise each standalone keeps its accent) - "backgroundColor": "#fff" // optional — PWA splash background + "backgroundColor": "#fff", // optional — PWA splash background + "theme": "light", // optional — default in-app mode: light | dark | system + "palette": "darkmatter" // optional — default in-app palette (see PALETTES) } ``` `themeColor` and `backgroundColor` are *overrides*, not defaults. When unset, each standalone's own accent applies (wallet yellow `#eab308`, chat green `#16a34a`, …) — so the default brand kit preserves the per-app visual identity, and a deployer who wants unified chrome adds the override. +`theme` and `palette` set the **in-app** color scheme — distinct from `themeColor` (which is only the PWA chrome / status-bar color). They define the *initial* default a fresh visitor sees; once a user picks a theme in-app it's stored in `localStorage` and always wins. `palette` must be one of the names in `src/components/theme-provider` (`PALETTES`): `catppuccin` (the built-in default), `countrysidecastle`, `darkmatter`, `emeraldforest`, `lightgreen`, `neobrut`, `starrynight`. Each palette has both a light and a dark variant, so e.g. "darkmatter light" is `{ "theme": "light", "palette": "darkmatter" }`. Unset → the app's built-ins (`dark` + `catppuccin`). Applies app-wide (hub + every standalone). + ## Per-standalone overrides Place a logo at `branding//icons//logo.{svg,png}` to override the brand's primary logo for a single standalone build. diff --git a/src/components/theme-provider/index.ts b/src/components/theme-provider/index.ts index 36b9079..bfbb2b6 100644 --- a/src/components/theme-provider/index.ts +++ b/src/components/theme-provider/index.ts @@ -21,10 +21,28 @@ export const PALETTES: Palette[] = [ 'starrynight', ] -const DEFAULT_PALETTE: Palette = 'catppuccin' +// The palette styled by the bare `:root` in index.css (no `data-theme` +// attribute). This is a fixed invariant of the CSS, NOT the configurable +// default — applyPalette() removes the attribute only for this one. +const BASE_PALETTE: Palette = 'catppuccin' + +// Brand-configurable initial defaults, surfaced from brand.json via +// vite-branding.ts (VITE_BRAND_THEME / VITE_BRAND_PALETTE). These set the +// first-load value when the user has no saved preference; a stored choice +// always overrides (see onMounted). Invalid/unset values fall back to the +// app's built-ins ('dark' + catppuccin). +const BRAND_THEME = (import.meta.env.VITE_BRAND_THEME as string) || '' +const BRAND_PALETTE = (import.meta.env.VITE_BRAND_PALETTE as string) || '' +const DEFAULT_THEME: Theme = + BRAND_THEME === 'dark' || BRAND_THEME === 'light' || BRAND_THEME === 'system' + ? BRAND_THEME + : 'dark' +const DEFAULT_PALETTE: Palette = (PALETTES as string[]).includes(BRAND_PALETTE) + ? (BRAND_PALETTE as Palette) + : BASE_PALETTE const useTheme = () => { - const theme = ref('dark') + const theme = ref(DEFAULT_THEME) const systemTheme = ref<'dark' | 'light'>('light') const palette = ref(DEFAULT_PALETTE) @@ -45,7 +63,7 @@ const useTheme = () => { } const applyPalette = () => { - if (palette.value === DEFAULT_PALETTE) { + if (palette.value === BASE_PALETTE) { document.documentElement.removeAttribute('data-theme') } else { document.documentElement.setAttribute('data-theme', palette.value) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 64fa22a..b743dac 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -24,4 +24,8 @@ interface ImportMetaEnv { readonly VITE_APP_NAME?: string /** '1' when the active brand ships a banner, '' otherwise. */ readonly VITE_APP_BANNER?: string + /** Brand default theme mode ('light'|'dark'|'system'), set from brand.json. */ + readonly VITE_BRAND_THEME?: string + /** Brand default palette name, set from brand.json. */ + readonly VITE_BRAND_PALETTE?: string } diff --git a/vite-branding.ts b/vite-branding.ts index 7e8eda1..55ec901 100644 --- a/vite-branding.ts +++ b/vite-branding.ts @@ -28,12 +28,35 @@ export interface Brand { * config keeps its hardcoded value. */ backgroundColor?: string + /** + * Optional default in-app theme mode (light / dark / system). Sets the + * INITIAL value the theme-provider uses when the user has no saved + * preference; a user's later choice still wins and persists. Unset → + * the app's built-in default ('dark'). Distinct from `themeColor`, + * which is PWA chrome only. + */ + theme?: 'light' | 'dark' | 'system' + /** + * Optional default in-app color palette (e.g. 'darkmatter'). Same + * initial-default semantics as `theme`. Must be one of the palettes in + * src/components/theme-provider (PALETTES). Unset → 'catppuccin'. + */ + palette?: string } export const brand: Brand = JSON.parse( readFileSync(resolve(BRAND_DIR, 'brand.json'), 'utf-8'), ) +// Surface the brand's in-app theme defaults to the client as VITE_* +// env vars (read by the theme-provider). Set here at module load — every +// vite..config.ts imports this file — so the default applies +// app-wide (hub + all standalones) without per-config wiring. Always +// assigned (empty when unset) so a prior brand's value can't leak into a +// later build in the same process. +process.env.VITE_BRAND_THEME = brand.theme ?? '' +process.env.VITE_BRAND_PALETTE = brand.palette ?? '' + /** * Spread into a vite config's `resolve.alias` map. Lets components * import deployer-provided assets via `@brand/` instead of