Skip to content

Conversation

@VAIBHAVSING
Copy link

@VAIBHAVSING VAIBHAVSING commented Dec 30, 2025

Summary

Implements a production-ready theme system with zero-config auto-detection, virtual modules, and runtime theme switching via data-theme attributes.

Architecture Highlights

  • Zero-config Vite plugin - Auto-detects site.config.js, no manual config needed
  • Virtual modules - Build-time theme parsing, no runtime file system access
  • data-theme switching - Instant theme changes via HTML attribute
  • localStorage persistence - User preference saved across sessions
  • No SSR needed - Pure CSS cascade, works in static sites
  • ThemeSelector component - Optional dropdown UI for theme switching

Closes #101


screenshot

Screenshot from 2026-01-02 23-27-36 image

Key Features

🔌 Vite Plugin (statue-ssg/vite-plugin)

Zero-config auto-detection:

// vite.config.js
import { statueThemesPlugin } from 'statue-ssg/vite-plugin';

plugins: [
  statueThemesPlugin(), // Auto-detects site.config.js
  sveltekit()
]

What it does:

  1. Reads site.config.js and finds theme configuration
  2. Parses each theme CSS file (supports both @theme {} and :root {})
  3. Extracts --color-* variables
  4. Generates virtual modules:
    • virtual:statue-themes - Theme metadata (JS)
    • virtual:statue-themes.css - Combined CSS with :root + html[data-theme="..."] selectors

🎨 Theme Configuration

In site.config.js:

export const siteConfig = {
  theme: {
    default: 'Blue',
    themes: [
      { name: 'Blue', path: 'statue-ssg/themes/blue.css' },
      { name: 'Red', path: 'statue-ssg/themes/red.css' },
      { name: 'Custom', path: './src/lib/themes/custom.css' }
    ]
  }
};

🎯 Usage

In layout:

<script>
  import { ThemeSelector } from 'statue-ssg';
  import 'virtual:statue-themes.css';
</script>

<ThemeSelector />
<slot />

Theme switching:

  • User selects theme from dropdown
  • Component sets document.documentElement.dataset.theme = 'blue'
  • CSS cascade instantly applies new theme
  • Preference saved to localStorage

Implementation Details

Files Changed

Vite Plugin:

  • vite-plugin/index.js - Theme parsing and virtual module generation
  • vite-plugin/README.md - Plugin documentation
  • vite-plugin/package.json - Module configuration

Components:

  • src/lib/components/ThemeSelector.svelte - Dropdown theme switcher
  • src/lib/components/NavigationBar.svelte - Integrated ThemeSelector
  • src/declarations.d.ts - TypeScript declarations for virtual modules

Configuration:

  • vite.config.js - Added statueThemesPlugin()
  • site.config.js - Theme configuration
  • package.json - Added ./vite-plugin export

Themes:

  • src/lib/themes/*.css - 9 built-in themes
  • src/lib/themes/README.md - Theme documentation

Documentation:

  • content/docs/themes.md - User-facing documentation
  • vite-plugin/README.md - Plugin usage guide

Layout & Routing:

  • src/routes/+layout.svelte - Imports virtual theme CSS
  • src/hooks.server.js - Removed unnecessary SSR theme injection

Removed (no longer needed):

  • src/lib/themes/index.ts - Replaced by virtual module
  • src/routes/api/save-theme/+server.js - Not needed (localStorage)

How It Works

Build Time:

site.config.js → Vite Plugin → Parses CSS → Generates:
  - virtual:statue-themes (JS with theme metadata)
  - virtual:statue-themes.css (Combined CSS)

Generated CSS structure:

:root {
  --color-background: #000; /* Default theme */
  --color-primary: #fff;
  /* ... */
}

html[data-theme="blue"] {
  --color-background: #1e3a8a;
  --color-primary: #3b82f6;
  /* ... */
}

html[data-theme="red"] {
  --color-background: #7f1d1d;
  --color-primary: #ef4444;
  /* ... */
}

Runtime:

User clicks theme → Sets data-theme → CSS cascade applies → localStorage saves

Developer Experience Improvements

Before (PR #106 - had issues):

  • ❌ Used Node.js fs at runtime (breaks in browser)
  • ❌ Had /api/save-theme endpoint (dev-only)
  • ❌ Required manual config passing to plugin
  • ❌ Mixed build-time and runtime concerns

After (this PR):

  • ✅ Pure virtual modules (no runtime fs access)
  • ✅ localStorage for persistence (works everywhere)
  • ✅ Zero-config auto-detection
  • ✅ Clean separation: build-time parsing, runtime CSS cascade

Zero-Config Example

vite.config.js (simplified):

import { statueThemesPlugin } from 'statue-ssg/vite-plugin';

plugins: [
  statueThemesPlugin() // No args needed!
]

Plugin automatically:

  1. Finds site.config.js
  2. Imports siteConfig.theme
  3. Parses themes
  4. Generates virtual modules

Testing

Manual Testing (Completed)

Dev Server:

npm run dev

✅ All 9 themes load correctly
✅ Auto-detection works
✅ Theme switching instant
✅ localStorage persistence works

Production Build:

npm run build

✅ Static build succeeds
✅ All theme CSS in bundle
html[data-theme="..."] selectors present
✅ No runtime errors

User Repository Test:

  • Linked local statue-ssg to test repo (/home/vsing/code/osp/new/)
  • Added plugin + config + virtual CSS import
  • ✅ Dev server works
  • ✅ Production build works
  • ✅ Theme switching works
  • ✅ Path resolution works (statue-ssg/themes/* → correct files)

Test Checklist

  • Zero-config auto-detection works
  • All 9 themes load in dev
  • All 9 themes load in production build
  • Theme switching works (data-theme attribute)
  • localStorage persistence works
  • No FOUC (CSS loads before JS)
  • Works with static site generation
  • ThemeSelector auto-hides for single theme
  • Custom themes work (local paths)
  • npm package paths resolve correctly
  • TypeScript declarations work
  • Documentation accurate

Migration Guide

For users upgrading from old @import approach:

Before:

/* src/lib/index.css */
@import "statue-ssg/themes/blue.css";

After:

  1. Remove theme import from src/lib/index.css
  2. Add plugin to vite.config.js (see above)
  3. Configure themes in site.config.js (see above)
  4. Import virtual:statue-themes.css in layout

Documentation

  • vite-plugin/README.md - Plugin usage and API
  • src/lib/themes/README.md - Theme system overview
  • content/docs/themes.md - User-facing docs (rendered at /docs/themes)

All docs updated to reflect new architecture.


Breaking Changes

⚠️ This replaces the old @import approach

Users need to:

  1. Add Vite plugin
  2. Configure themes in site.config.js
  3. Import virtual CSS in layout

Migration is straightforward (see Migration Guide above).

@VAIBHAVSING VAIBHAVSING marked this pull request as draft December 30, 2025 07:57
@VAIBHAVSING VAIBHAVSING force-pushed the feature/theme-selector-101 branch from 1c83a69 to ecb1e14 Compare December 30, 2025 07:58
@fredxfred
Copy link
Contributor

it says this is a draft, what are we waiting for here?

@VAIBHAVSING VAIBHAVSING changed the title Add runtime theme switcher component feat: Zero-config theme system with virtual modules and runtime switching (fixes #101) Jan 2, 2026
@VAIBHAVSING VAIBHAVSING marked this pull request as ready for review January 2, 2026 17:54
@VAIBHAVSING
Copy link
Author

VAIBHAVSING commented Jan 2, 2026

some architectural and developer exprience improvement needed

- Changed import from './vite-plugin/index.js' to 'statue-ssg/vite-plugin'
- This allows vite.config.js to work for both development and when distributed via npm
- Fixes Docker test failure where installed package couldn't resolve local path
- Also updated content/docs/themes.md with comprehensive theme system docs
@fredxfred
Copy link
Contributor

Let's hold off on further reviewing until #93 is ready

VAIBHAVSING and others added 5 commits January 3, 2026 17:47
This reverts commit b112aa1.
- Fix FOUC (Flash of Unstyled Content):
  - Moved script injection to `transformPageChunk` hook for earlier execution.
  - Added runtime sanitization to `src/hooks.server.js` to handle legacy localStorage values (e.g. collapsing 'black--white' to 'black-white').
- Improve Accessibility:
  - Added ARIA roles (listbox, option) to ThemeSelector.
  - Implemented full keyboard navigation (Arrow keys, Enter, Escape, Tab).
- Fix Build Issues:
  - Refactored `Stats.svelte` to use CSS variables for colors, resolving Tailwind dynamic class extraction warnings.
- Fix Logic:
  - Updated `toKebabCase` in `vite-plugin` to correctly collapse multiple dashes.
  - Shared `THEME_STORAGE_KEY` constant across files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Theme selector

2 participants