The recipes.blue monorepo recipes.blue
recipes appview atproto

feat: dark mode(!!!)

+143 -26
+3 -1
apps/web/src/components/app-sidebar.tsx
··· 18 18 SidebarRail, 19 19 } from "@/components/ui/sidebar" 20 20 import { NavUserOpts } from "./nav-user-opts" 21 + import { ModeToggle } from "./mode-toggle" 21 22 22 23 const data = { 23 24 navMain: [ ··· 61 62 <Sidebar collapsible="icon" {...props}> 62 63 <SidebarHeader> 63 64 <SidebarMenu> 64 - <SidebarMenuItem> 65 + <SidebarMenuItem className="flex items-center justify-between gap-x-2"> 65 66 <SidebarMenuButton size="lg" asChild> 66 67 <a href="#"> 67 68 <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> ··· 72 73 </div> 73 74 </a> 74 75 </SidebarMenuButton> 76 + <ModeToggle /> 75 77 </SidebarMenuItem> 76 78 </SidebarMenu> 77 79 </SidebarHeader>
+37
apps/web/src/components/mode-toggle.tsx
··· 1 + import { Moon, Sun } from "lucide-react" 2 + 3 + import { Button } from "@/components/ui/button" 4 + import { 5 + DropdownMenu, 6 + DropdownMenuContent, 7 + DropdownMenuItem, 8 + DropdownMenuTrigger, 9 + } from "@/components/ui/dropdown-menu" 10 + import { useTheme } from "@/components/theme-provider" 11 + 12 + export function ModeToggle() { 13 + const { setTheme } = useTheme() 14 + 15 + return ( 16 + <DropdownMenu> 17 + <DropdownMenuTrigger asChild> 18 + <Button variant="outline" size="icon" className="size-8 aspect-square flex items-center justify-center rounded-lg"> 19 + <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> 20 + <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> 21 + <span className="sr-only">Toggle theme</span> 22 + </Button> 23 + </DropdownMenuTrigger> 24 + <DropdownMenuContent align="end"> 25 + <DropdownMenuItem onClick={() => setTheme("light")}> 26 + Light 27 + </DropdownMenuItem> 28 + <DropdownMenuItem onClick={() => setTheme("dark")}> 29 + Dark 30 + </DropdownMenuItem> 31 + <DropdownMenuItem onClick={() => setTheme("system")}> 32 + System 33 + </DropdownMenuItem> 34 + </DropdownMenuContent> 35 + </DropdownMenu> 36 + ) 37 + }
+7 -5
apps/web/src/components/sidebar-title.tsx
··· 4 4 SidebarMenu, 5 5 SidebarMenuItem, 6 6 } from "@/components/ui/sidebar" 7 + import { ModeToggle } from "./mode-toggle" 7 8 8 9 export function SidebarTitle() { 9 10 return ( 10 11 <SidebarMenu> 11 12 <SidebarMenuItem> 12 - <div className="flex items-center gap-2 p-2"> 13 - <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> 14 - <CookingPot className="size-4" /> 15 - </div> 16 - <span className="font-semibold text-sm flex-1 text-left leading-tight">Recipes</span> 13 + <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> 14 + <CookingPot className="size-4" /> 17 15 </div> 16 + <div className="flex flex-col gap-0.5 leading-none"> 17 + <span className="font-semibold">Recipes</span> 18 + </div> 19 + <ModeToggle /> 18 20 </SidebarMenuItem> 19 21 </SidebarMenu> 20 22 )
+73
apps/web/src/components/theme-provider.tsx
··· 1 + import { createContext, useContext, useEffect, useState } from "react" 2 + 3 + type Theme = "dark" | "light" | "system" 4 + 5 + type ThemeProviderProps = { 6 + children: React.ReactNode 7 + defaultTheme?: Theme 8 + storageKey?: string 9 + } 10 + 11 + type ThemeProviderState = { 12 + theme: Theme 13 + setTheme: (theme: Theme) => void 14 + } 15 + 16 + const initialState: ThemeProviderState = { 17 + theme: "system", 18 + setTheme: () => null, 19 + } 20 + 21 + const ThemeProviderContext = createContext<ThemeProviderState>(initialState) 22 + 23 + export function ThemeProvider({ 24 + children, 25 + defaultTheme = "system", 26 + storageKey = "vite-ui-theme", 27 + ...props 28 + }: ThemeProviderProps) { 29 + const [theme, setTheme] = useState<Theme>( 30 + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 31 + ) 32 + 33 + useEffect(() => { 34 + const root = window.document.documentElement 35 + 36 + root.classList.remove("light", "dark") 37 + 38 + if (theme === "system") { 39 + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 40 + .matches 41 + ? "dark" 42 + : "light" 43 + 44 + root.classList.add(systemTheme) 45 + return 46 + } 47 + 48 + root.classList.add(theme) 49 + }, [theme]) 50 + 51 + const value = { 52 + theme, 53 + setTheme: (theme: Theme) => { 54 + localStorage.setItem(storageKey, theme) 55 + setTheme(theme) 56 + }, 57 + } 58 + 59 + return ( 60 + <ThemeProviderContext.Provider {...props} value={value}> 61 + {children} 62 + </ThemeProviderContext.Provider> 63 + ) 64 + } 65 + 66 + export const useTheme = () => { 67 + const context = useContext(ThemeProviderContext) 68 + 69 + if (context === undefined) 70 + throw new Error("useTheme must be used within a ThemeProvider") 71 + 72 + return context 73 + }
+14 -14
apps/web/src/index.css
··· 45 45 --card-foreground: 0 0% 98%; 46 46 --popover: 0 0% 3.9%; 47 47 --popover-foreground: 0 0% 98%; 48 - --primary: 0 72.2% 50.6%; 49 - --primary-foreground: 0 85.7% 97.3%; 50 - --secondary: 0 0% 14.9%; 51 - --secondary-foreground: 0 0% 98%; 52 - --muted: 0 0% 14.9%; 53 - --muted-foreground: 0 0% 63.9%; 54 - --accent: 0 0% 14.9%; 55 - --accent-foreground: 0 0% 98%; 48 + --primary: 217.2 91.2% 59.8%; 49 + --primary-foreground: 222.2 47.4% 11.2%; 50 + --secondary: 217.2 32.6% 17.5%; 51 + --secondary-foreground: 210 40% 98%; 52 + --muted: 217.2 32.6% 17.5%; 53 + --muted-foreground: 215 20.2% 65.1%; 54 + --accent: 217.2 32.6% 17.5%; 55 + --accent-foreground: 210 40% 98%; 56 56 --destructive: 0 62.8% 30.6%; 57 - --destructive-foreground: 0 0% 98%; 58 - --border: 0 0% 14.9%; 59 - --input: 0 0% 14.9%; 60 - --ring: 0 72.2% 50.6%; 57 + --destructive-foreground: 210 40% 98%; 58 + --border: 217.2 32.6% 17.5%; 59 + --input: 217.2 32.6% 17.5%; 60 + --ring: 224.3 76.3% 48%; 61 61 --chart-1: 220 70% 50%; 62 62 --chart-2: 160 60% 45%; 63 63 --chart-3: 30 80% 55%; ··· 65 65 --chart-5: 340 75% 55%; 66 66 --sidebar-background: 240 5.9% 10%; 67 67 --sidebar-foreground: 240 4.8% 95.9%; 68 - --sidebar-primary: 224.3 76.3% 48%; 68 + --sidebar-primary: 217.2 91.2% 59.8%; 69 69 --sidebar-primary-foreground: 0 0% 100%; 70 - --sidebar-accent: 240 3.7% 15.9%; 70 + --sidebar-accent: 217.2 32.6% 17.5%; 71 71 --sidebar-accent-foreground: 240 4.8% 95.9%; 72 72 --sidebar-border: 240 3.7% 15.9%; 73 73 --sidebar-ring: 217.2 91.2% 59.8%;
+9 -6
apps/web/src/main.tsx
··· 7 7 import { configureOAuth } from '@atcute/oauth-browser-client'; 8 8 import './index.css' 9 9 import { AuthProvider, useAuth } from './state/auth'; 10 + import { ThemeProvider } from './components/theme-provider'; 10 11 11 12 const router = createRouter({ 12 13 routeTree, ··· 45 46 46 47 createRoot(document.getElementById('root')!).render( 47 48 <StrictMode> 48 - <AuthProvider> 49 - <QueryClientProvider client={queryClient}> 50 - <InnerApp /> 51 - <ReactQueryDevtools initialIsOpen={false} /> 52 - </QueryClientProvider> 53 - </AuthProvider> 49 + <ThemeProvider defaultTheme="dark" storageKey="recipes-theme"> 50 + <AuthProvider> 51 + <QueryClientProvider client={queryClient}> 52 + <InnerApp /> 53 + <ReactQueryDevtools initialIsOpen={false} /> 54 + </QueryClientProvider> 55 + </AuthProvider> 56 + </ThemeProvider> 54 57 </StrictMode>, 55 58 )