@@ -467,15 +467,94 @@ export function createTempAppWithRouting(tmpDir: string, routes: RouteInfo[], ap
467467 routeComponents . push ( ` <Route path="${ route . path } " element={<SchemaRenderer schema={${ schemaVarName } } />} />` ) ;
468468 } ) ;
469469
470+ // Create theme-provider.tsx
471+ const themeProviderTsx = `import { createContext, useContext, useEffect, useState } from "react"
472+
473+ type Theme = "dark" | "light" | "system"
474+
475+ type ThemeProviderProps = {
476+ children: React.ReactNode
477+ defaultTheme?: Theme
478+ storageKey?: string
479+ }
480+
481+ type ThemeProviderState = {
482+ theme: Theme
483+ setTheme: (theme: Theme) => void
484+ }
485+
486+ const initialState: ThemeProviderState = {
487+ theme: "system",
488+ setTheme: () => null,
489+ }
490+
491+ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
492+
493+ export function ThemeProvider({
494+ children,
495+ defaultTheme = "system",
496+ storageKey = "vite-ui-theme",
497+ ...props
498+ }: ThemeProviderProps) {
499+ const [theme, setTheme] = useState<Theme>(
500+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
501+ )
502+
503+ useEffect(() => {
504+ const root = window.document.documentElement
505+
506+ root.classList.remove("light", "dark")
507+
508+ if (theme === "system") {
509+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
510+ .matches
511+ ? "dark"
512+ : "light"
513+
514+ root.classList.add(systemTheme)
515+ return
516+ }
517+
518+ root.classList.add(theme)
519+ }, [theme])
520+
521+ const value = {
522+ theme,
523+ setTheme: (theme: Theme) => {
524+ localStorage.setItem(storageKey, theme)
525+ setTheme(theme)
526+ },
527+ }
528+
529+ return (
530+ <ThemeProviderContext.Provider {...props} value={value}>
531+ {children}
532+ </ThemeProviderContext.Provider>
533+ )
534+ }
535+
536+ export const useTheme = () => {
537+ const context = useContext(ThemeProviderContext)
538+
539+ if (context === undefined)
540+ throw new Error("useTheme must be used within a ThemeProvider")
541+
542+ return context
543+ }` ;
544+ writeFileSync ( join ( srcDir , 'theme-provider.tsx' ) , themeProviderTsx ) ;
545+
470546 // Create main.tsx
471547 const mainTsx = `import React from 'react';
472548import ReactDOM from 'react-dom/client';
473549import App from './App';
474550import './index.css';
551+ import { ThemeProvider } from "./theme-provider"
475552
476553ReactDOM.createRoot(document.getElementById('root')!).render(
477554 <React.StrictMode>
478- <App />
555+ <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
556+ <App />
557+ </ThemeProvider>
479558 </React.StrictMode>
480559);` ;
481560
@@ -495,8 +574,15 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
495574 const layoutCode = `
496575import { Link, useLocation } from 'react-router-dom';
497576import * as LucideIcons from 'lucide-react';
577+ import { Moon, Sun } from "lucide-react"
578+ import { useTheme } from "./theme-provider"
498579import {
499580 cn,
581+ Button,
582+ DropdownMenu,
583+ DropdownMenuContent,
584+ DropdownMenuItem,
585+ DropdownMenuTrigger,
500586 SidebarProvider,
501587 Sidebar,
502588 SidebarContent,
@@ -527,6 +613,42 @@ const DynamicIcon = ({ name, className }) => {
527613 return <Icon className={className} />;
528614};
529615
616+ export function ModeToggle() {
617+ const { setTheme } = useTheme()
618+
619+ return (
620+ <DropdownMenu>
621+ <DropdownMenuTrigger asChild>
622+ <SidebarMenuButton size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
623+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
624+ <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
625+ <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
626+ </div>
627+ <div className="grid flex-1 text-left text-sm leading-tight">
628+ <span className="truncate font-semibold">Switch Theme</span>
629+ <span className="truncate text-xs">Light / Dark</span>
630+ </div>
631+ <LucideIcons.ChevronsUpDown className="ml-auto size-4" />
632+ </SidebarMenuButton>
633+ </DropdownMenuTrigger>
634+ <DropdownMenuContent className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom" align="end" sideOffset={4}>
635+ <DropdownMenuItem onClick={() => setTheme("light")}>
636+ <LucideIcons.Sun className="mr-2 size-4" />
637+ Light
638+ </DropdownMenuItem>
639+ <DropdownMenuItem onClick={() => setTheme("dark")}>
640+ <LucideIcons.Moon className="mr-2 size-4" />
641+ Dark
642+ </DropdownMenuItem>
643+ <DropdownMenuItem onClick={() => setTheme("system")}>
644+ <LucideIcons.Monitor className="mr-2 size-4" />
645+ System
646+ </DropdownMenuItem>
647+ </DropdownMenuContent>
648+ </DropdownMenu>
649+ )
650+ }
651+
530652const AppLayout = ({ app, children }) => {
531653 const location = useLocation();
532654 const menu = app.menu || [];
@@ -600,6 +722,11 @@ const AppLayout = ({ app, children }) => {
600722 </SidebarGroup>
601723 </SidebarContent>
602724 <SidebarFooter>
725+ <SidebarMenu>
726+ <SidebarMenuItem>
727+ <ModeToggle />
728+ </SidebarMenuItem>
729+ </SidebarMenu>
603730 </SidebarFooter>
604731 <SidebarRail />
605732 </Sidebar>
0 commit comments