UNPKG

theme-o-rama

Version:

A TypeScript library for dynamic theme management in react + shadcn + tailwind applications

556 lines (466 loc) 13.9 kB
# Theme-o-rama A comprehensive TypeScript library for dynamic theme management in React applications with shadcn/ui and Tailwind CSS. Features advanced theme discovery, inheritance, custom theme support, and dynamic theme switching at runtime. ## Requirements - **React** 18.0+ or 19.0+ - **TypeScript** 5.0+ (optional, but recommended) The library uses only stable React APIs and is compatible with all React 18.x and 19.x versions. ## Installation ```bash npm install theme-o-rama # or pnpm add theme-o-rama # or yarn add theme-o-rama ``` ## Framework Compatibility Theme-o-rama is designed to work seamlessly with: - ✅ **Vite** + React - ✅ **Create React App** - ✅ **Next.js** (Pages Router & App Router) - ✅ **Remix** - ✅ Any React SSR framework The library is SSR-safe and includes proper guards for server-side rendering environments. ## Usage ### Basic Theme Definition ```typescript import { Theme } from "theme-o-rama"; const myTheme: Theme = { name: "my-theme", displayName: "My Custom Theme", schemaVersion: 1, colors: { background: "#ffffff", foreground: "#000000", primary: "#007bff", // ... other theme properties }, }; ``` ### Tailwind CSS Integration Theme-o-rama provides Tailwind CSS extensions that work with the dynamic theming system. To use them in your app: ```css @import "theme-o-rama/themes.css"; ``` ```javascript // tailwind.config.js import { themeExtensions } from "theme-o-rama/tailwind.config.js"; export default { darkMode: ["class"], content: ["src/**/*.{ts,tsx}", "index.html"], theme: { extend: { // Include Theme-o-rama theme extensions ...themeExtensions, // Add your app-specific extensions here // keyframes: { ... }, // animation: { ... }, }, }, plugins: [require("tailwindcss-animate")], }; ``` The `themeExtensions` include: - **Font families** with CSS variables (`--font-sans`, `--font-serif`, etc.) - **Border radius** with CSS variables (`--corner-sm`, `--corner-md`, etc.) - **Box shadows** with CSS variables (`--shadow-sm`, `--shadow-md`, etc.) - **Color system** with CSS variables (`--background`, `--foreground`, etc.) ### React Integration ```typescript import { ThemeProvider, useTheme } from 'theme-o-rama'; // Theme discovery function (optional) async function discoverThemes() { // Your theme discovery logic here // Can load themes from API, file system, etc. return [ { name: 'colorful', displayName: 'Colorful', schemaVersion: 1, colors: { /* ... */ } }, { name: 'amiga', displayName: 'Amiga', schemaVersion: 1, colors: { /* ... */ } }, // ... more themes ]; } // Image resolver function (optional) function resolveThemeImage(themeName: string, imagePath: string) { // Resolve theme background images return `/themes/${themeName}/${imagePath}`; } function App() { return ( <ThemeProvider discoverThemes={discoverThemes} imageResolver={resolveThemeImage} > <YourApp /> </ThemeProvider> ); } function YourComponent() { const { setTheme, availableThemes, isLoading, error, } = useTheme(); if (isLoading) return <div>Loading themes...</div>; if (error) return <div>Error: {error}</div>; return ( <div> <button onClick={() => setTheme('amiga')}> Switch to Amiga Theme </button> {/* Display available themes */} <div> {availableThemes.map(theme => ( <button key={theme.name} onClick={() => setTheme(theme.name)} className={currentTheme?.name === theme.name ? 'active' : ''} > {theme.displayName} </button> ))} </div> </div> ); } ``` ### Next.js Integration Theme-o-rama is fully compatible with Next.js (both Pages Router and App Router). The library is SSR-safe with proper guards for server-side rendering. #### Next.js App Router (13+) ```typescript // app/layout.tsx import { ThemeProvider } from 'theme-o-rama'; import 'theme-o-rama/themes.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <ThemeProvider> {children} </ThemeProvider> </body> </html> ); } ``` #### Next.js Pages Router ```typescript // pages/_app.tsx import type { AppProps } from 'next/app'; import { ThemeProvider } from 'theme-o-rama'; import 'theme-o-rama/themes.css'; export default function App({ Component, pageProps }: AppProps) { return ( <ThemeProvider> <Component {...pageProps} /> </ThemeProvider> ); } ``` #### Using Themes in Next.js Components ```typescript // app/components/ThemeSelector.tsx (or pages/components/ThemeSelector.tsx) 'use client'; // Only needed for App Router import { useTheme } from 'theme-o-rama'; export function ThemeSelector() { const { setTheme, currentTheme, availableThemes } = useTheme(); return ( <select value={currentTheme?.name || 'light'} onChange={(e) => setTheme(e.target.value)} > {availableThemes.map(theme => ( <option key={theme.name} value={theme.name}> {theme.displayName} </option> ))} </select> ); } ``` **Note:** The `ThemeProvider` component automatically includes the `'use client'` directive, making it compatible with Next.js App Router. Any component using the `useTheme` hook should also be a Client Component. #### Preventing Flash of Unstyled Content (FOUC) Since themes are applied client-side after hydration, you may notice a brief flash of default styling. To minimize this, add a blocking script that applies the theme before React hydrates: **Next.js App Router:** ```typescript // app/layout.tsx import { ThemeProvider } from 'theme-o-rama'; import 'theme-o-rama/themes.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> {/* Blocking script to prevent FOUC */} <script dangerouslySetInnerHTML={{ __html: ` (function() { try { const theme = localStorage.getItem('theme') || 'light'; // Apply theme class immediately document.documentElement.className = 'theme-' + theme; document.documentElement.setAttribute('data-theme', theme); // Set color-scheme for browser UI document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : 'light'; } catch (e) { console.error('Theme initialization error:', e); } })(); `, }} /> </head> <body> <ThemeProvider> {children} </ThemeProvider> </body> </html> ); } ``` **Next.js Pages Router:** ```typescript // pages/_document.tsx import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { return ( <Html suppressHydrationWarning> <Head> {/* Blocking script to prevent FOUC */} <script dangerouslySetInnerHTML={{ __html: ` (function() { try { const theme = localStorage.getItem('theme') || 'light'; // Apply theme class immediately document.documentElement.className = 'theme-' + theme; document.documentElement.setAttribute('data-theme', theme); // Set color-scheme for browser UI document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : 'light'; } catch (e) { console.error('Theme initialization error:', e); } })(); `, }} /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); } ``` **What this script does:** - Runs before React hydration (blocking) - Reads theme from localStorage - Sets `theme-{name}` class on `<html>` element - Sets `data-theme` attribute for CSS targeting - Sets `color-scheme` for native browser UI elements - Provides basic theme indication before full theme loads **Add supporting CSS for instant feedback:** ```css /* In your global CSS or theme-o-rama/themes.css */ :root { color-scheme: light; } :root[data-theme="dark"], :root.theme-dark { color-scheme: dark; } /* Optional: Add basic color transitions for smooth theme changes */ html { transition: background-color 0.2s ease, color 0.2s ease; } ``` The `suppressHydrationWarning` prop on `<html>` is needed because the blocking script modifies the DOM before React hydration, which would otherwise cause a warning. ### Theme Discovery and Image Resolution Theme-o-rama supports dynamic theme discovery through the `discoverThemes` prop. Standard `light` and `dark` themes are always included. This allows you to load themes from source of your choice: ```typescript // Example: Loading themes from a file system (Vite) async function discoverThemes() { const themeModules = import.meta.glob("../themes/*/theme.json", { eager: true, }); return Object.entries(themeModules) .map(([path, module]) => { const match = path.match(/\.\.\/themes\/([^/]+)\/theme\.json$/); if (match) { return module as Theme; } return null; }) .filter((theme): theme is Theme => theme !== null); } // Example: Loading themes from an API async function discoverThemesFromAPI() { const response = await fetch("/api/themes"); return response.json(); } // Example: Loading themes from static imports async function discoverStaticThemes() { return [await import("./themes/amiga.json"), await import("./themes/colorful.json")]; } ``` The `imageResolver` prop allows you to resolve theme background images: ```typescript // Example: Resolving local theme images function resolveThemeImage(themeName: string, imagePath: string) { const imageModules = import.meta.glob("../themes/*/*.{jpg,jpeg,png,gif,webp}", { eager: true }); const resolvedPath = `../themes/${themeName}/${imagePath}`; const imageModule = imageModules[resolvedPath]; if (imageModule) { return (imageModule as { default: string }).default; } return `../themes/${themeName}/${imagePath}`; } // Example: Resolving remote images function resolveRemoteThemeImage(themeName: string, imagePath: string) { return `https://your-cdn.com/themes/${themeName}/${imagePath}`; } ``` ### Advanced Usage #### Theme Inheritance Themes can inherit from other themes and override specific properties: ```typescript // Inherited theme const darkTheme: Theme = { name: "my-dark", displayName: "Dark Theme", schemaVersion: 1, inherits: "dark", // Inherits from dark theme colors: { background: "#1a1a1a", foreground: "#ffffff", // primary is inherited from dark theme }, // fonts and corners are inherited from dark theme }; ``` #### Custom Theme with Component Styling ```typescript const customTheme: Theme = { name: "cyberpunk", displayName: "Cyberpunk", schemaVersion: 1, mostLike: "dark", backgroundImage: "cyberpunk-bg.jpg", backgroundSize: "cover", colors: { background: "#0a0a0a", foreground: "#00ff41", primary: "#ff0080", secondary: "#00ffff", accent: "#ffff00", destructive: "#ff0040", border: "#00ff41", card: "rgba(0, 255, 65, 0.1)", cardForeground: "#00ff41", }, fonts: { sans: "Orbitron, monospace", mono: "JetBrains Mono, monospace", }, corners: { sm: "2px", md: "4px", lg: "8px", }, shadows: { sm: "0 0 10px #00ff41", md: "0 0 20px #00ff41", lg: "0 0 30px #00ff41", }, // Custom button styling buttons: { default: { background: "linear-gradient(45deg, #ff0080, #00ffff)", color: "#000000", border: "2px solid #00ff41", borderRadius: "4px", boxShadow: "0 0 15px #00ff41", hover: { background: "linear-gradient(45deg, #ff40a0, #40ffff)", transform: "scale(1.05)", boxShadow: "0 0 25px #00ff41", }, active: { transform: "scale(0.95)", }, }, outline: { background: "transparent", color: "#00ff41", border: "2px solid #00ff41", hover: { background: "rgba(0, 255, 65, 0.1)", boxShadow: "0 0 15px #00ff41", }, }, }, // Button style flags for CSS effects buttonStyles: ["gradient", "shimmer"], // Custom table styling tables: { background: "rgba(0, 255, 65, 0.05)", border: "1px solid #00ff41", borderRadius: "8px", boxShadow: "0 0 20px rgba(0, 255, 65, 0.3)", header: { background: "rgba(0, 255, 65, 0.1)", color: "#00ff41", border: "1px solid #00ff41", fontWeight: "bold", backdropFilter: "blur(10px)", }, row: { background: "transparent", color: "#ffffff", hover: { background: "rgba(0, 255, 65, 0.1)", color: "#00ff41", }, selected: { background: "rgba(255, 0, 128, 0.2)", color: "#ff0080", }, }, }, // Custom switch styling switches: { checked: { background: "linear-gradient(45deg, #ff0080, #00ffff)", }, unchecked: { background: "#333333", }, thumb: { background: "#ffffff", }, }, // Sidebar styling sidebar: { background: "rgba(0, 255, 65, 0.1)", border: "1px solid #00ff41", backdropFilter: "blur(20px)", }, }; ``` ## Development ```bash # Install dependencies npm install # Build the library npm run build # Watch for changes during development npm run dev # Clean build artifacts npm run clean ``` ## License Apache 2.0