Skip to content

feat: Visual theming engine — dark/light mode, color palette presets, forge theme command #30

@alohays

Description

@alohays

Problem

monitor-forge's visual customization is currently limited to branding.primaryColor only:

  1. Dark mode only: src/styles/base.css has hard-coded dark theme variables in :root with no light mode
  2. --accent not synced with config: base.css hard-codes --accent: #0052CC, not synchronized with branding.primaryColor
  3. No theme CLI command: There's no forge theme command for changing colors/fonts/layout
  4. Fixed panel layout: Sidebar width (--panel-width: 380px) and position are hard-coded

As a result, every monitor-forge dashboard looks identical. Visual differentiation — essential to the "your own dashboard" value proposition — is impossible.

Solution

1. Extend Config Schema (ThemeSchema)

Extend the existing BrandingSchema with comprehensive theme settings:

export const ThemeSchema = z.object({
  mode: z.enum(['dark', 'light', 'auto']).default('dark'),
  palette: z.enum([
    'default', 'ocean', 'forest', 'sunset', 'midnight',
    'cyberpunk', 'minimal', 'custom'
  ]).default('default'),
  colors: z.object({
    primary: z.string().regex(/^#[0-9A-Fa-f]{6}$/).default('#0052CC'),
    background: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
    foreground: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
    accent: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
    panel: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
    border: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
  }).default({}),
  font: z.enum(['system', 'mono', 'inter', 'roboto']).default('system'),
  panelPosition: z.enum(['right', 'left', 'bottom']).default('right'),
  panelWidth: z.number().min(280).max(600).default(380),
  compactMode: z.boolean().default(false),
}).default({});

Backward compatible: existing branding.primaryColor maps to theme.colors.primary.

2. Palette Preset System

const PALETTES = {
  default:   { bg: '#0a0a0f', fg: '#e0e0e0', panel: '#1a1a2e', border: '#2a2a3e', accent: '#0052CC' },
  ocean:     { bg: '#0a192f', fg: '#ccd6f6', panel: '#112240', border: '#233554', accent: '#64ffda' },
  forest:    { bg: '#0d1117', fg: '#c9d1d9', panel: '#161b22', border: '#30363d', accent: '#3fb950' },
  sunset:    { bg: '#1a1020', fg: '#f0e6d3', panel: '#251830', border: '#3d2a4a', accent: '#ff6b6b' },
  midnight:  { bg: '#020817', fg: '#f8fafc', panel: '#0f172a', border: '#1e293b', accent: '#818cf8' },
  cyberpunk: { bg: '#0a0a0a', fg: '#00ff41', panel: '#111111', border: '#1a1a1a', accent: '#ff00ff' },
  minimal:   { bg: '#ffffff', fg: '#1a1a1a', panel: '#f5f5f5', border: '#e0e0e0', accent: '#000000' },
};

3. forge theme CLI Command

# Apply a palette preset
forge theme set --palette ocean

# Set individual colors
forge theme set --primary "#64ffda" --mode dark

# Change layout
forge theme set --panel-position left --panel-width 320 --compact

# View current theme
forge theme status

# Switch dark/light/auto mode
forge theme set --mode light
forge theme set --mode auto   # follows OS preference

4. Dynamic CSS Variable Injection

In App.ts initialize(), inject config theme values into :root CSS variables:

const theme = config.theme ?? {};
document.documentElement.style.setProperty('--accent', theme.colors?.primary ?? '#0052CC');
document.documentElement.setAttribute('data-theme', theme.mode ?? 'dark');

5. Light Mode CSS

Add [data-theme="light"] selector to base.css:

[data-theme="light"] {
  --fg: #1a1a1a;
  --bg: #ffffff;
  --bg-secondary: #f8f9fa;
  --bg-panel: #ffffff;
  --border: #e0e0e0;
  --text-muted: #666;
}

@media (prefers-color-scheme: light) {
  [data-theme="auto"] { /* light values */ }
}

Implementation Order

  1. forge/src/config/schema.ts — Add ThemeSchema
  2. forge/src/theme/palettes.ts — Define palette presets (new file)
  3. forge/src/commands/theme.tsforge theme set/status commands (new file)
  4. forge/bin/forge.ts — Register registerThemeCommands(program)
  5. src/styles/base.css — Add light mode, data-theme attribute-based variable switching
  6. src/App.ts — Dynamic theme variable injection logic
  7. forge/src/generators/manifest-generator.ts — Include theme info in config-resolved.ts

Acceptance Criteria

  • MonitorForgeConfigSchema has an optional theme field, backward compatible with existing branding.primaryColor
  • forge theme set --palette <name> applies one of 7 palette presets
  • forge theme set --mode light/dark/auto switches theme mode
  • forge theme set --panel-position left/right/bottom changes panel position
  • forge theme status displays current theme settings
  • forge dev / forge build applies theme settings as CSS variables to the dashboard
  • --mode auto automatically switches based on OS prefers-color-scheme
  • All forge theme commands support --format json, --non-interactive, --dry-run flags
  • Existing config files without theme field continue to work (defaults applied)
  • Tests added: forge/src/commands/__tests__/theme.test.ts

Priority Rationale

Visual differentiation is essential for the "your own dashboard" value proposition. Forking worldmonitor still requires manual CSS editing to change appearance, but with monitor-forge, forge theme set --palette cyberpunk should give a completely different look in one command. The CSS variable structure is already well-prepared in base.css, making implementation relatively straightforward. If Issue #28 (onboarding) and Issue #29 (presets) attract users, Issue #30 (theming) gives each dashboard its identity and sense of ownership.

Metadata

Metadata

Assignees

No one assigned

    Labels

    dxDeveloper experience improvementsenhancementNew feature or requestfrontendFrontend engine changeshigh-impactHigh-impact feature for project growthpriority: mediumShould fix, but not urgent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions