From 44616895cfb633eae975a1cf7e72c10e5712749b Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Fri, 19 Dec 2025 15:41:46 +0800 Subject: [PATCH] feat(client): add shadcn theme - Added Nix theme - Defaults to dark mode Signed-off-by: Noa Virellia --- client/src/App.tsx | 8 +- client/src/components/theme-provider.tsx | 53 ++++++ client/src/hooks/useTheme.ts | 24 +++ client/src/index.css | 198 +++++++++++++++-------- 4 files changed, 218 insertions(+), 65 deletions(-) create mode 100644 client/src/components/theme-provider.tsx create mode 100644 client/src/hooks/useTheme.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 6eb5d24..71c1efb 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,11 @@ +import { ThemeProvider } from '@/components/theme-provider'; + function App() { - return

Hello world

; + return ( + +

Hello world

+
+ ); } export { App }; diff --git a/client/src/components/theme-provider.tsx b/client/src/components/theme-provider.tsx new file mode 100644 index 0000000..14bc663 --- /dev/null +++ b/client/src/components/theme-provider.tsx @@ -0,0 +1,53 @@ +import type { Theme } from '@/hooks/useTheme'; +import { useEffect, useState } from 'react'; +import { ThemeProviderContext } from '@/hooks/useTheme'; + +interface ThemeProviderProps { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +} + +export function ThemeProvider({ + children, + defaultTheme = 'dark', + storageKey = 'vite-ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, + ); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove('light', 'dark'); + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') + .matches + ? 'dark' + : 'light'; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + // eslint-disable-next-line react/no-unstable-context-value + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} diff --git a/client/src/hooks/useTheme.ts b/client/src/hooks/useTheme.ts new file mode 100644 index 0000000..8479393 --- /dev/null +++ b/client/src/hooks/useTheme.ts @@ -0,0 +1,24 @@ +import { createContext, use } from 'react'; + +export type Theme = 'dark' | 'light' | 'system'; + +interface ThemeProviderState { + theme: Theme; + setTheme: (theme: Theme) => void; +} + +const initialState: ThemeProviderState = { + theme: 'system', + setTheme: () => null, +}; + +export const ThemeProviderContext = createContext(initialState); + +export function useTheme() { + const context = use(ThemeProviderContext); + + if (context === undefined) + throw new Error('useTheme must be used within a ThemeProvider'); + + return context; +} diff --git a/client/src/index.css b/client/src/index.css index d6a166c..04c2f50 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -42,75 +42,144 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --font-sans: 'Inter', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --font-serif: 'Lora', serif; + --radius: 0.5rem; + --tracking-tighter: calc(var(--tracking-normal) - 0.05em); + --tracking-tight: calc(var(--tracking-normal) - 0.025em); + --tracking-wide: calc(var(--tracking-normal) + 0.025em); + --tracking-wider: calc(var(--tracking-normal) + 0.05em); + --tracking-widest: calc(var(--tracking-normal) + 0.1em); + --tracking-normal: var(--tracking-normal); + --shadow-2xl: var(--shadow-2xl); + --shadow-xl: var(--shadow-xl); + --shadow-lg: var(--shadow-lg); + --shadow-md: var(--shadow-md); + --shadow: var(--shadow); + --shadow-sm: var(--shadow-sm); + --shadow-xs: var(--shadow-xs); + --shadow-2xs: var(--shadow-2xs); + --spacing: var(--spacing); + --letter-spacing: var(--letter-spacing); + --shadow-offset-y: var(--shadow-offset-y); + --shadow-offset-x: var(--shadow-offset-x); + --shadow-spread: var(--shadow-spread); + --shadow-blur: var(--shadow-blur); + --shadow-opacity: var(--shadow-opacity); + --color-shadow-color: var(--shadow-color); + --color-destructive-foreground: var(--destructive-foreground); } :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --radius: 0.5rem; + --background: oklch(0.9816 0.0017 247.8390); + --foreground: oklch(0.2621 0.0095 248.1897); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.2621 0.0095 248.1897); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.2621 0.0095 248.1897); + --primary: oklch(0.5502 0.1193 263.8209); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.7499 0.0898 239.3977); + --secondary-foreground: oklch(0.2621 0.0095 248.1897); + --muted: oklch(0.9417 0.0052 247.8790); + --muted-foreground: oklch(0.5575 0.0165 244.8933); + --accent: oklch(0.9417 0.0052 247.8790); + --accent-foreground: oklch(0.2621 0.0095 248.1897); + --destructive: oklch(0.5915 0.2020 21.2388); + --border: oklch(0.9109 0.0070 247.9014); + --input: oklch(1.0000 0 0); + --ring: oklch(0.5502 0.1193 263.8209); + --chart-1: oklch(0.5502 0.1193 263.8209); + --chart-2: oklch(0.7499 0.0898 239.3977); + --chart-3: oklch(0.4711 0.0998 264.0792); + --chart-4: oklch(0.6689 0.0699 240.3096); + --chart-5: oklch(0.5107 0.1098 263.6921); + --sidebar: oklch(0.9632 0.0034 247.8585); + --sidebar-foreground: oklch(0.2621 0.0095 248.1897); + --sidebar-primary: oklch(0.5502 0.1193 263.8209); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9417 0.0052 247.8790); + --sidebar-accent-foreground: oklch(0.2621 0.0095 248.1897); + --sidebar-border: oklch(0.9109 0.0070 247.9014); + --sidebar-ring: oklch(0.5502 0.1193 263.8209); + --destructive-foreground: oklch(1.0000 0 0); + --font-sans: 'Inter', sans-serif; + --font-serif: 'Lora', serif; + --font-mono: 'JetBrains Mono', monospace; + --shadow-color: #000000; + --shadow-opacity: 0.05; + --shadow-blur: 0.5rem; + --shadow-spread: 0rem; + --shadow-offset-x: 0rem; + --shadow-offset-y: 0.1rem; + --letter-spacing: 0em; + --spacing: 0.25rem; + --shadow-2xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.03); + --shadow-xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.03); + --shadow-sm: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 1px 2px -1px hsl(0 0% 0% / 0.05); + --shadow: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 1px 2px -1px hsl(0 0% 0% / 0.05); + --shadow-md: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 2px 4px -1px hsl(0 0% 0% / 0.05); + --shadow-lg: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 4px 6px -1px hsl(0 0% 0% / 0.05); + --shadow-xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 8px 10px -1px hsl(0 0% 0% / 0.05); + --shadow-2xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.13); + --tracking-normal: 0em; } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --background: oklch(0.2270 0.0120 270.8402); + --foreground: oklch(0.9067 0 0); + --card: oklch(0.2630 0.0127 258.3724); + --card-foreground: oklch(0.9067 0 0); + --popover: oklch(0.2630 0.0127 258.3724); + --popover-foreground: oklch(0.9067 0 0); + --primary: oklch(0.5774 0.1248 263.3770); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.7636 0.0866 239.8852); + --secondary-foreground: oklch(0.2621 0.0095 248.1897); + --muted: oklch(0.3006 0.0156 264.3078); + --muted-foreground: oklch(0.7137 0.0192 261.3246); + --accent: oklch(0.3006 0.0156 264.3078); + --accent-foreground: oklch(0.9067 0 0); + --destructive: oklch(0.5915 0.2020 21.2388); + --border: oklch(0.3451 0.0133 248.2124); + --input: oklch(0.2630 0.0127 258.3724); + --ring: oklch(0.5502 0.1193 263.8209); + --chart-1: oklch(0.5502 0.1193 263.8209); + --chart-2: oklch(0.7499 0.0898 239.3977); + --chart-3: oklch(0.4711 0.0998 264.0792); + --chart-4: oklch(0.6689 0.0699 240.3096); + --chart-5: oklch(0.5107 0.1098 263.6921); + --sidebar: oklch(0.2270 0.0120 270.8402); + --sidebar-foreground: oklch(0.9067 0 0); + --sidebar-primary: oklch(0.5502 0.1193 263.8209); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.3006 0.0156 264.3078); + --sidebar-accent-foreground: oklch(0.9067 0 0); + --sidebar-border: oklch(0.3451 0.0133 248.2124); + --sidebar-ring: oklch(0.5502 0.1193 263.8209); + --destructive-foreground: oklch(1.0000 0 0); + --radius: 0.5rem; + --font-sans: 'Inter', sans-serif; + --font-serif: 'Lora', serif; + --font-mono: 'JetBrains Mono', monospace; + --shadow-color: #000000; + --shadow-opacity: 0.3; + --shadow-blur: 0.5rem; + --shadow-spread: 0rem; + --shadow-offset-x: 0rem; + --shadow-offset-y: 0.1rem; + --letter-spacing: 0em; + --spacing: 0.25rem; + --shadow-2xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.15); + --shadow-xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.15); + --shadow-sm: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 1px 2px -1px hsl(0 0% 0% / 0.30); + --shadow: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 1px 2px -1px hsl(0 0% 0% / 0.30); + --shadow-md: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 2px 4px -1px hsl(0 0% 0% / 0.30); + --shadow-lg: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 4px 6px -1px hsl(0 0% 0% / 0.30); + --shadow-xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 8px 10px -1px hsl(0 0% 0% / 0.30); + --shadow-2xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.75); } @layer base { @@ -119,5 +188,6 @@ } body { @apply bg-background text-foreground; + letter-spacing: var(--tracking-normal); } -} +} \ No newline at end of file