Appearance
Theme System
DoCurious has a comprehensive theme system that converts a ThemeConfig object into CSS custom properties applied to :root. It supports 7 built-in presets, two navigation modes (sidebar vs topnav), three density levels, and persistence via localStorage.
Architecture
ThemeConfig Interface
The complete theme configuration is defined in src/theme/types.ts:
typescript
interface ThemeConfig {
colors: ThemeColors
typography: ThemeTypography
spacing: ThemeSpacing
branding: ThemeBranding
style: ThemeStyle
layout: ThemeLayout
}Colors
typescript
interface ThemeColors {
// Core
primary: string // Main brand color
primaryHover: string
primaryActive: string
accent: string // Highlight/accent color
accentHover: string
// Backgrounds
background: string // Page background
backgroundSecondary: string
// Text
text: string // Primary text
textMuted: string // Secondary text
border: string
// Status
error: string
success: string
warning: string
// Sidebar
sidebarBackground: string
sidebarText: string
sidebarTextMuted: string
sidebarActiveBackground: string
sidebarActiveBorder: string
// Audience accents (optional)
accentB2C?: string // Parents & Kids
accentSchools?: string // Schools & Educators
accentProviders?: string // Activity Providers
// Secondary brand (optional)
secondary?: string
secondaryHover?: string
}Typography
typescript
interface ThemeTypography {
fontFamily: string // Body text font
fontFamilyHeading: string // Heading font
fontFamilyMono: string // Code/monospace font
}Style
Controls visual treatment of surfaces, cards, buttons, and spacing:
typescript
interface ThemeStyle {
// Card surface
cardShadow: string
cardBorderWidth: string
cardBorderRadius: string
// Headings
headingWeight: string
headingLetterSpacing: string
headingTextTransform: 'none' | 'uppercase' | 'capitalize'
// Buttons
buttonRadius: string
buttonWeight: string
// Sidebar / nav
sidebarWidth: string
navItemRadius: string
navItemBorderWidth: string
// Shadows
surfaceShadowSm: string
surfaceShadowMd: string
surfaceShadowLg: string
// Borders
borderWidth: string
dividerStyle: string
// Motion
transitionSpeed: string
// Background texture
backgroundTexture: string
// Badge / input
badgeRadius: string
inputRadius: string
inputBorderWidth: string
// Density
density: 'compact' | 'comfortable' | 'spacious'
// Layout structure
contentMaxWidth: string
cardGap: string
// Structural effects
sidebarInset: string
sidebarRadius: string
cardAccentBorder: string
cardBackground: string
cardBackdropBlur: string
headingBorderBottom: string
}Layout
typescript
type NavigationMode = 'sidebar' | 'topnav'
type FilterPlacement = 'sidebar' | 'topbar' | 'drawer'
interface ThemeLayout {
navigationMode: NavigationMode
exploreFilterPlacement: FilterPlacement
topNavHeight: string
}Default Values
The default theme (src/theme/defaults.ts) uses the DoCurious brand palette:
| Token | Color | Name |
|---|---|---|
| Primary | #3D5A3C | Scout Green |
| Primary Hover | #4A6B49 | |
| Accent | #D4A84B | Honey |
| Background | #FAF8F3 | Cream |
| Background Secondary | #F3EFE6 | |
| Text | #1A1A1A | Ink |
| Text Muted | #6B6B6B | Stone |
| Warning | #E07B5A | Coral |
| Secondary | #5B8FA8 | Sky |
Typography defaults:
- Body:
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif - Headings:
"Lexend", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif - Mono:
SFMono-Regular, Menlo, Monaco, Consolas, monospace
Theme Presets
Seven presets are defined in src/theme/presets/:
| ID | Name | Description | Dark? | Swatches |
|---|---|---|---|---|
trailhead | Trailhead | Home base -- warm Scout Green, Honey accents, Cream canvas | No | #3D5A3C #D4A84B #FAF8F3 |
ember | Ember | Dark and cozy -- Coral firelight glow, warm shadows, spacious | Yes | #E07B5A #D4A84B #1C1614 |
tidepool | Tidepool | Coastal and clean -- Sky blue, pill buttons, no shadows, compact | No | #5B8FA8 #D4A84B #F6F9FB |
canopy | Canopy | Deep forest -- rich greens, Violet wildflower accents, immersive | No | #2D4A2C #8B7BC8 #F4F7F2 |
expedition | Expedition | Bold adventure -- rugged borders, topo-map texture, uppercase headings | No | #8B5E2A #E07B5A #FBF7F0 |
stardust | Stardust | Mystical night -- Violet glow, Honey stars, dreamy transitions | Yes | #8B7BC8 #D4A84B #14121C |
fieldJournal | Field Journal | Warm and tactile -- dashed borders, paper texture, naturalist notebook feel | No | #5b7c3a #b8860b #3a3226 |
Each preset is a complete ThemeConfig object. Presets are accessed via:
typescript
import { themePresets, getPresetById } from '../theme/presets'
const ember = getPresetById('ember')
// ember.config → ThemeConfig
// ember.isDark → true
// ember.swatches → ['#E07B5A', '#D4A84B', '#1C1614']Layout Modes
Sidebar Navigation (navigationMode: 'sidebar')
The default mode. A fixed-width sidebar on the left with the main content area to the right. The sidebar width is controlled by style.sidebarWidth (default: 200px).
Top Navigation (navigationMode: 'topnav')
A horizontal navigation bar at the top. The height is controlled by layout.topNavHeight (default: 64px). The data-nav-mode attribute on <html> is set to 'topnav', which CSS can target for layout adjustments.
Filter Placement
The explore page filter panel placement is controlled by layout.exploreFilterPlacement:
'sidebar'-- filters in a left sidebar'topbar'-- filters in a horizontal bar above content'drawer'-- filters in a slide-out drawer
ThemeProvider
The ThemeProvider component (src/theme/ThemeProvider.tsx) wraps the entire app:
- Loads the saved theme from
themeApi.getTheme()on mount - Falls back to
defaultThemeConfigon error - Calls
applyThemeVars(config)on every config change to update CSS variables - Provides the config and a change callback via
ThemeConfigContext
tsx
// src/App.tsx (simplified)
import { ThemeProvider } from './theme'
function App() {
return (
<ThemeProvider>
<RouterProvider router={router} />
</ThemeProvider>
)
}CSS Variable Application
The applyThemeVars function (src/theme/applyTheme.ts) sets 60+ CSS custom properties on document.documentElement.style:
typescript
// Colors
--primary, --primary-hover, --primary-active, --primary-foreground
--accent, --accent-hover
--background, --background-secondary
--foreground, --foreground-muted
--border, --error, --success, --warning
--secondary, --secondary-hover
--sidebar-bg, --sidebar-text, --sidebar-text-muted
--sidebar-active-bg, --sidebar-active-border
--accent-b2c, --accent-schools, --accent-providers
// Typography
--font-body, --font-heading, --font-mono
// Card
--theme-card-shadow, --theme-card-border-width, --theme-card-radius
// Headings
--theme-heading-weight, --theme-heading-letter-spacing, --theme-heading-text-transform
// Buttons
--theme-button-radius, --theme-button-weight
// Sidebar/nav
--theme-sidebar-width, --theme-nav-item-radius, --theme-nav-item-border-width
// Shadows
--theme-shadow-sm, --theme-shadow-md, --theme-shadow-lg
// Borders
--theme-border-width, --theme-divider-style
// Motion
--theme-transition
// Density spacing (varies by compact/comfortable/spacious)
--theme-space-xs through --theme-space-2xl
--theme-section-gap, --theme-card-padding, --theme-page-padding
// Layout structure
--theme-content-max-width, --theme-card-gap
--theme-sidebar-inset, --theme-sidebar-radius
--theme-card-accent-border, --theme-card-bg, --theme-card-backdrop-blur
--theme-heading-border-bottom
--theme-topnav-heightThe data-nav-mode attribute on <html> is also set to enable CSS-based layout switching.
Density Spacing
Three density levels scale all spacing tokens:
| Token | Compact | Comfortable | Spacious |
|---|---|---|---|
--theme-space-xs | 0.2rem | 0.25rem | 0.375rem |
--theme-space-sm | 0.35rem | 0.5rem | 0.625rem |
--theme-space-md | 0.5rem | 0.75rem | 1rem |
--theme-space-lg | 0.65rem | 1rem | 1.25rem |
--theme-space-xl | 0.85rem | 1.25rem | 1.75rem |
--theme-card-padding | 0.75rem | 1rem | 1.25rem |
--theme-page-padding | 1rem | 1.5rem | 2rem |
localStorage Persistence
The themeApi (src/theme/themeApi.ts) provides a mock API that uses localStorage:
typescript
interface ThemeConfigAPI {
getTheme: () => Promise<ThemeConfig> // Read with version check
saveTheme: (config: ThemeConfig) => Promise<void> // Write
resetTheme: () => Promise<void> // Delete saved config
}- Storage key:
docurious-theme-config - Version key:
docurious-theme-version(current:'3') - Version mismatches automatically clear old config and fall back to defaults
- Saved configs are merged with defaults to ensure all keys exist
How to Create a New Preset
- Create
src/theme/presets/myPreset.ts:
typescript
import type { ThemeConfig } from '../types'
import { defaultThemeConfig } from '../defaults'
export const myPresetConfig: ThemeConfig = {
...defaultThemeConfig,
colors: {
...defaultThemeConfig.colors,
primary: '#...',
accent: '#...',
// ... override colors
},
style: {
...defaultThemeConfig.style,
cardBorderRadius: '1rem',
density: 'spacious',
// ... override style tokens
},
}- Register in
src/theme/presets/index.ts:
typescript
import { myPresetConfig } from './myPreset'
// Add to the themePresets array:
{
id: 'myPreset',
name: 'My Preset',
description: 'Description of the visual style',
config: myPresetConfig,
isDark: false,
swatches: ['#primary', '#accent', '#background'],
},- Export from
src/theme/presets/index.ts:
typescript
export { myPresetConfig } from './myPreset'Admin Theme Editor
The Design Token Editor at /admin/theme-editor (platform_admin only) provides a visual interface for editing all theme tokens in real time. Changes are persisted via themeApi.saveTheme().