Skip to content

Latest commit

 

History

History
675 lines (559 loc) · 16.3 KB

File metadata and controls

675 lines (559 loc) · 16.3 KB
layout title parent nav_order
default
Chapter 6: Theme & Customization System
Chatbox Tutorial
6

Chapter 6: Theme & Customization System

Welcome to Chapter 6: Theme & Customization System. In this part of Chatbox Tutorial: Building Modern AI Chat Interfaces, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.

This chapter covers building a comprehensive theming system and customization options for chat applications.

🎨 Theme Architecture

Theme System Design

// Comprehensive theme system
interface Theme {
  name: string
  displayName: string
  version: string
  author: string
  colors: ThemeColors
  typography: ThemeTypography
  spacing: ThemeSpacing
  borderRadius: ThemeBorderRadius
  shadows: ThemeShadows
  animations: ThemeAnimations
  components: ThemeComponents
}

interface ThemeColors {
  primary: ColorPalette
  secondary: ColorPalette
  surface: ColorPalette
  background: ColorPalette
  text: TextColors
  border: BorderColors
  status: StatusColors
  custom: Record<string, string>
}

interface ColorPalette {
  main: string
  light: string
  dark: string
  contrastText: string
}

interface TextColors {
  primary: string
  secondary: string
  disabled: string
  hint: string
}

interface StatusColors {
  success: ColorPalette
  warning: ColorPalette
  error: ColorPalette
  info: ColorPalette
}

Theme Manager

// Theme management system
class ThemeManager {
  private themes: Map<string, Theme> = new Map()
  private currentTheme: string = 'default'
  private customThemes: Map<string, Theme> = new Map()

  registerTheme(theme: Theme) {
    this.themes.set(theme.name, theme)
  }

  setTheme(themeName: string) {
    if (this.themes.has(themeName)) {
      this.currentTheme = themeName
      this.applyTheme(this.themes.get(themeName)!)
      this.persistThemeChoice(themeName)
    }
  }

  getCurrentTheme(): Theme {
    return this.themes.get(this.currentTheme)!
  }

  getAvailableThemes(): Theme[] {
    return Array.from(this.themes.values())
  }

  createCustomTheme(baseTheme: string, overrides: Partial<Theme>): Theme {
    const base = this.themes.get(baseTheme)
    if (!base) throw new Error(`Base theme ${baseTheme} not found`)

    const customTheme: Theme = {
      ...base,
      ...overrides,
      name: `custom-${Date.now()}`,
      displayName: overrides.displayName || 'Custom Theme'
    }

    this.customThemes.set(customTheme.name, customTheme)
    return customTheme
  }

  private applyTheme(theme: Theme) {
    const root = document.documentElement

    // Apply CSS custom properties
    this.applyColors(root, theme.colors)
    this.applyTypography(root, theme.typography)
    this.applySpacing(root, theme.spacing)
    this.applyBorderRadius(root, theme.borderRadius)
    this.applyShadows(root, theme.shadows)
    this.applyAnimations(root, theme.animations)

    // Trigger theme change event
    window.dispatchEvent(new CustomEvent('themeChanged', { detail: theme }))
  }

  private applyColors(root: HTMLElement, colors: ThemeColors) {
    // Apply primary colors
    root.style.setProperty('--color-primary-main', colors.primary.main)
    root.style.setProperty('--color-primary-light', colors.primary.light)
    root.style.setProperty('--color-primary-dark', colors.primary.dark)

    // Apply text colors
    root.style.setProperty('--color-text-primary', colors.text.primary)
    root.style.setProperty('--color-text-secondary', colors.text.secondary)

    // Apply background colors
    root.style.setProperty('--color-background-primary', colors.background.main)
    root.style.setProperty('--color-background-secondary', colors.surface.main)
  }

  private persistThemeChoice(themeName: string) {
    localStorage.setItem('preferred-theme', themeName)
  }

  loadSavedTheme() {
    const saved = localStorage.getItem('preferred-theme')
    if (saved && this.themes.has(saved)) {
      this.setTheme(saved)
    }
  }
}

🎯 Predefined Themes

Light Theme

// Default light theme
const lightTheme: Theme = {
  name: 'light',
  displayName: 'Light',
  version: '1.0.0',
  author: 'Chatbox Team',
  colors: {
    primary: {
      main: '#007bff',
      light: '#4dabf5',
      dark: '#0056b3',
      contrastText: '#ffffff'
    },
    secondary: {
      main: '#6c757d',
      light: '#9ca3af',
      dark: '#495057',
      contrastText: '#ffffff'
    },
    surface: {
      main: '#ffffff',
      light: '#f8f9fa',
      dark: '#e9ecef',
      contrastText: '#212529'
    },
    background: {
      main: '#f8f9fa',
      light: '#ffffff',
      dark: '#e9ecef',
      contrastText: '#212529'
    },
    text: {
      primary: '#212529',
      secondary: '#6c757d',
      disabled: '#adb5bd',
      hint: '#6c757d'
    },
    border: {
      light: '#e9ecef',
      main: '#dee2e6',
      dark: '#ced4da'
    },
    status: {
      success: {
        main: '#28a745',
        light: '#71dd8a',
        dark: '#1e7e34',
        contrastText: '#ffffff'
      },
      warning: {
        main: '#ffc107',
        light: '#ffda6a',
        dark: '#e0a800',
        contrastText: '#212529'
      },
      error: {
        main: '#dc3545',
        light: '#f28b94',
        dark: '#bd2130',
        contrastText: '#ffffff'
      },
      info: {
        main: '#17a2b8',
        light: '#6bd3e1',
        dark: '#117a8b',
        contrastText: '#ffffff'
      }
    }
  }
}

Dark Theme

// Dark theme
const darkTheme: Theme = {
  name: 'dark',
  displayName: 'Dark',
  version: '1.0.0',
  author: 'Chatbox Team',
  colors: {
    primary: {
      main: '#4dabf5',
      light: '#80d6ff',
      dark: '#0091ea',
      contrastText: '#000000'
    },
    surface: {
      main: '#1e1e1e',
      light: '#2d2d2d',
      dark: '#121212',
      contrastText: '#ffffff'
    },
    background: {
      main: '#121212',
      light: '#1e1e1e',
      dark: '#000000',
      contrastText: '#ffffff'
    },
    text: {
      primary: '#ffffff',
      secondary: '#b3b3b3',
      disabled: '#666666',
      hint: '#888888'
    },
    border: {
      light: '#333333',
      main: '#424242',
      dark: '#616161'
    }
  }
}

🔧 Theme Customization

Theme Builder

// Interactive theme builder
class ThemeBuilder {
  private baseTheme: Theme

  constructor(baseTheme: Theme) {
    this.baseTheme = { ...baseTheme }
  }

  setPrimaryColor(color: string) {
    this.baseTheme.colors.primary.main = color
    this.baseTheme.colors.primary.light = this.lightenColor(color, 0.2)
    this.baseTheme.colors.primary.dark = this.darkenColor(color, 0.2)
    return this
  }

  setBackgroundColor(color: string) {
    this.baseTheme.colors.background.main = color
    return this
  }

  setTextColor(color: string) {
    this.baseTheme.colors.text.primary = color
    return this
  }

  setBorderRadius(radius: string) {
    this.baseTheme.borderRadius = {
      sm: radius,
      md: `calc(${radius} * 1.5)`,
      lg: `calc(${radius} * 2)`,
      xl: `calc(${radius} * 3)`
    }
    return this
  }

  build(): Theme {
    return { ...this.baseTheme }
  }

  private lightenColor(color: string, amount: number): string {
    // Color manipulation logic
    return color // Simplified
  }

  private darkenColor(color: string, amount: number): string {
    // Color manipulation logic
    return color // Simplified
  }
}

Theme Import/Export

// Theme serialization
class ThemeSerializer {
  static exportTheme(theme: Theme): string {
    return JSON.stringify(theme, null, 2)
  }

  static importTheme(jsonString: string): Theme {
    const parsed = JSON.parse(jsonString)

    // Validate theme structure
    this.validateTheme(parsed)

    return parsed as Theme
  }

  static exportAsCSS(theme: Theme): string {
    let css = ':root {\n'

    // Colors
    css += this.themeColorsToCSS(theme.colors)

    // Typography
    css += this.themeTypographyToCSS(theme.typography)

    // Spacing
    css += this.themeSpacingToCSS(theme.spacing)

    css += '}\n'
    return css
  }

  private static validateTheme(theme: any): void {
    const required = ['name', 'colors', 'typography']
    required.forEach(field => {
      if (!theme[field]) {
        throw new Error(`Theme missing required field: ${field}`)
      }
    })
  }

  private static themeColorsToCSS(colors: ThemeColors): string {
    let css = ''

    css += `  --color-primary: ${colors.primary.main};\n`
    css += `  --color-text-primary: ${colors.text.primary};\n`
    css += `  --color-background: ${colors.background.main};\n`

    return css
  }
}

🎨 Component Styling

Styled Components

// Theme-aware component styling
const StyledMessageBubble = styled.div`
  background-color: ${props => props.theme.colors.surface.main};
  color: ${props => props.theme.colors.text.primary};
  border: 1px solid ${props => props.theme.colors.border.main};
  border-radius: ${props => props.theme.borderRadius.md};
  padding: ${props => props.theme.spacing.md};

  &:hover {
    background-color: ${props => props.theme.colors.surface.light};
  }
`

const StyledButton = styled.button`
  background-color: ${props => props.theme.colors.primary.main};
  color: ${props => props.theme.colors.primary.contrastText};
  border: none;
  border-radius: ${props => props.theme.borderRadius.sm};
  padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md};

  &:hover {
    background-color: ${props => props.theme.colors.primary.dark};
  }

  &:disabled {
    background-color: ${props => props.theme.colors.text.disabled};
    cursor: not-allowed;
  }
`

Theme Provider

// React theme provider
const ThemeContext = React.createContext<ThemeContextType | null>(null)

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<Theme>(lightTheme)
  const themeManager = useRef(new ThemeManager())

  useEffect(() => {
    themeManager.current.loadSavedTheme()
  }, [])

  const changeTheme = useCallback((themeName: string) => {
    themeManager.current.setTheme(themeName)
    setTheme(themeManager.current.getCurrentTheme())
  }, [])

  const value = {
    theme,
    changeTheme,
    availableThemes: themeManager.current.getAvailableThemes()
  }

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

export const useTheme = () => {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}

📱 Responsive Themes

Breakpoint System

// Responsive theme adjustments
const responsiveTheme = {
  breakpoints: {
    mobile: '576px',
    tablet: '768px',
    desktop: '1024px',
    wide: '1440px'
  },

  responsiveStyles: {
    container: {
      padding: {
        mobile: '1rem',
        tablet: '1.5rem',
        desktop: '2rem'
      }
    },

    messageBubble: {
      maxWidth: {
        mobile: '100%',
        tablet: '80%',
        desktop: '70%'
      }
    }
  }
}

// CSS-in-JS responsive utilities
const responsiveCSS = `
  .container {
    padding: 1rem;
  }

  @media (min-width: 768px) {
    .container {
      padding: 1.5rem;
    }
  }

  @media (min-width: 1024px) {
    .container {
      padding: 2rem;
    }
  }
`

🎯 Theme Extensions

Plugin System

// Theme plugin architecture
interface ThemePlugin {
  name: string
  extend(theme: Theme): Theme
}

class ThemePluginManager {
  private plugins: ThemePlugin[] = []

  registerPlugin(plugin: ThemePlugin) {
    this.plugins.push(plugin)
  }

  applyPlugins(theme: Theme): Theme {
    return this.plugins.reduce((extendedTheme, plugin) => {
      return plugin.extend(extendedTheme)
    }, theme)
  }
}

// Example plugin: High contrast mode
const highContrastPlugin: ThemePlugin = {
  name: 'highContrast',
  extend(theme: Theme): Theme {
    return {
      ...theme,
      colors: {
        ...theme.colors,
        text: {
          ...theme.colors.text,
          primary: '#ffffff',
          secondary: '#cccccc'
        },
        background: {
          ...theme.colors.background,
          main: '#000000',
          secondary: '#111111'
        }
      }
    }
  }
}

💾 Theme Persistence

Local Storage

// Theme persistence
class ThemePersistence {
  private storageKey = 'chatbox-themes'

  saveTheme(theme: Theme) {
    const themes = this.getSavedThemes()
    themes[theme.name] = theme
    localStorage.setItem(this.storageKey, JSON.stringify(themes))
  }

  loadTheme(name: string): Theme | null {
    const themes = this.getSavedThemes()
    return themes[name] || null
  }

  getSavedThemes(): Record<string, Theme> {
    const saved = localStorage.getItem(this.storageKey)
    return saved ? JSON.parse(saved) : {}
  }

  deleteTheme(name: string) {
    const themes = this.getSavedThemes()
    delete themes[name]
    localStorage.setItem(this.storageKey, JSON.stringify(themes))
  }

  exportThemes(): string {
    return JSON.stringify(this.getSavedThemes(), null, 2)
  }

  importThemes(jsonString: string) {
    try {
      const themes = JSON.parse(jsonString)
      const existing = this.getSavedThemes()
      const merged = { ...existing, ...themes }
      localStorage.setItem(this.storageKey, JSON.stringify(merged))
    } catch (error) {
      throw new Error('Invalid theme format')
    }
  }
}

📝 Chapter Summary

  • ✅ Built comprehensive theme architecture
  • ✅ Created predefined light and dark themes
  • ✅ Developed theme customization system
  • ✅ Implemented theme-aware component styling
  • ✅ Added responsive design support
  • ✅ Created theme plugin system
  • ✅ Built theme persistence and import/export

Key Takeaways:

  • Theme system enables extensive customization
  • CSS custom properties provide dynamic theming
  • Responsive design ensures mobile compatibility
  • Plugins allow theme extensions
  • Persistence maintains user preferences
  • Builder pattern simplifies theme creation

What Problem Does This Solve?

Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for theme, colors, Theme so behavior stays predictable as complexity grows.

In practical terms, this chapter helps you avoid three common failures:

  • coupling core logic too tightly to one implementation path
  • missing the handoff boundaries between setup, execution, and validation
  • shipping changes without clear rollback or observability strategy

After working through this chapter, you should be able to reason about Chapter 6: Theme & Customization System as an operating subsystem inside Chatbox Tutorial: Building Modern AI Chat Interfaces, with explicit contracts for inputs, state transitions, and outputs.

Use the implementation notes around primary, color, main as your checklist when adapting these patterns to your own repository.

How it Works Under the Hood

Under the hood, Chapter 6: Theme & Customization System usually follows a repeatable control path:

  1. Context bootstrap: initialize runtime config and prerequisites for theme.
  2. Input normalization: shape incoming data so colors receives stable contracts.
  3. Core execution: run the main logic branch and propagate intermediate state through Theme.
  4. Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
  5. Output composition: return canonical result payloads for downstream consumers.
  6. Operational telemetry: emit logs/metrics needed for debugging and performance tuning.

When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.

Source Walkthrough

Use the following upstream sources to verify implementation details while reading this chapter:

  • View Repo Why it matters: authoritative reference on View Repo (github.com).

Suggested trace strategy:

  • search upstream code for theme and colors to map concrete implementation paths
  • compare docs claims against actual runtime/config code before reusing patterns in production

Chapter Connections