/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
/**
* Theme customization constants and types.
*
* Lives in `lib/` (not `context/`) so it can be imported alongside the
* provider without breaking React Fast Refresh boundaries.
*/
export const THEME_PRESETS = [
{
value: 'default',
name: 'Default',
swatches: ['oklch(0.13 0 0)', 'oklch(0.95 0 0)'],
},
{
// Inspired by Anthropic's official brand language: warm cream canvas
// (#faf9f5) paired with clay/coral (#d97757) as the single accent.
// Swatches preview the canvas → accent gradient that defines the system.
value: 'anthropic',
name: 'Anthropic',
swatches: ['oklch(0.984 0.005 95)', 'oklch(0.685 0.142 38)'],
},
{
value: 'underground',
name: 'Underground',
swatches: ['oklch(0.5315 0.0694 156.19)', 'oklch(0.5748 0.0862 336.52)'],
},
{
value: 'rose-garden',
name: 'Rose Garden',
swatches: ['oklch(0.5827 0.2418 12.23)', 'oklch(0.8131 0.1129 5.67)'],
},
{
value: 'lake-view',
name: 'Lake View',
swatches: ['oklch(0.765 0.177 163.22)', 'oklch(0.551 0.0899 200.52)'],
},
{
value: 'sunset-glow',
name: 'Sunset Glow',
swatches: ['oklch(0.5591 0.1882 25.33)', 'oklch(0.7938 0.1248 42.42)'],
},
{
value: 'forest-whisper',
name: 'Forest Whisper',
swatches: ['oklch(0.5276 0.1072 182.22)', 'oklch(0.5236 0.0505 250.18)'],
},
{
value: 'ocean-breeze',
name: 'Ocean Breeze',
swatches: ['oklch(0.5461 0.2152 262.88)', 'oklch(0.5854 0.2041 277.12)'],
},
{
value: 'lavender-dream',
name: 'Lavender Dream',
swatches: ['oklch(0.5709 0.1808 306.89)', 'oklch(0.811 0.0589 201.14)'],
},
] as const
export type ThemePreset = (typeof THEME_PRESETS)[number]['value']
export type ThemeRadius = 'default' | 'none' | 'sm' | 'md' | 'lg' | 'xl'
export type ThemeScale = 'default' | 'sm' | 'lg'
export type ContentLayout = 'full' | 'centered'
/**
* Font axis for the theme.
*
* - `default` — resolve at runtime from the active preset
* (see `PRESET_DEFAULT_FONT`). The shipped `default` and `anthropic`
* presets resolve to serif; other named color presets fall back to
* sans unless they list a different choice. Mirrors how
* `radius: 'default'` defers to a per-preset hint.
* - `sans` — humanist sans (Public Sans), the project's UI fallback.
* - `serif` — editorial serif (Lora + CJK fallbacks), the project's
* "soul" typography. Inherits across the whole UI; monospace contexts
* keep their own family via Tailwind preflight and `.font-mono`.
*/
export type ThemeFont = 'default' | 'sans' | 'serif'
/**
* The resolved (non-`default`) font value applied to the DOM. The provider
* always sets `data-theme-font` to one of these concrete values so CSS only
* needs simple attribute selectors (no `:not()` gymnastics, no per-preset
* font branches).
*/
export type ResolvedThemeFont = Exclude
export type ThemeCustomization = {
preset: ThemePreset
font: ThemeFont
radius: ThemeRadius
scale: ThemeScale
contentLayout: ContentLayout
}
export const DEFAULT_THEME_CUSTOMIZATION: ThemeCustomization = {
preset: 'default',
font: 'default',
radius: 'default',
scale: 'default',
contentLayout: 'full',
}
export const THEME_PRESET_VALUES = new Set(
THEME_PRESETS.map((p) => p.value)
) as ReadonlySet
export const THEME_FONT_VALUES: ReadonlySet = new Set([
'default',
'sans',
'serif',
])
export const THEME_RADIUS_VALUES: ReadonlySet = new Set([
'default',
'none',
'sm',
'md',
'lg',
'xl',
])
export const THEME_SCALE_VALUES: ReadonlySet = new Set([
'default',
'sm',
'lg',
])
export const CONTENT_LAYOUT_VALUES: ReadonlySet = new Set([
'full',
'centered',
])
export const THEME_COOKIE_KEYS = {
preset: 'theme_preset',
font: 'theme_font',
radius: 'theme_radius',
scale: 'theme_scale',
contentLayout: 'theme_content_layout',
} as const
/**
* Preset → default font mapping. Used by the provider to resolve the user's
* `font: 'default'` preference against the active preset.
*
* Co-located with the preset registry so a preset's signature typography
* is declared in one place. Presets not listed here fall back to the
* `resolveThemeFont` default of `sans`. The shipped `default` preset
* opts into serif so the editorial Lora voice is the out-of-the-box
* experience; vivid color presets stay on the humanist sans so their
* accents read clearly without competing with the body type.
*/
export const PRESET_DEFAULT_FONT: Partial<
Record
> = {
default: 'serif',
anthropic: 'serif',
}
/**
* Resolve a user font preference + active preset into the concrete font that
* should drive the DOM. Pure function so it's safe to call inside both the
* effect that applies the attribute and the UI preview that hints at what
* `default` will render as.
*/
export function resolveThemeFont(
font: ThemeFont,
preset: ThemePreset
): ResolvedThemeFont {
if (font === 'default') {
return PRESET_DEFAULT_FONT[preset] ?? 'sans'
}
return font
}