committed by
GitHub
55 changed files with 369 additions and 438 deletions
@ -1,6 +1,5 @@ |
|||
module.exports = { |
|||
plugins: { |
|||
tailwindcss: {}, |
|||
autoprefixer: {}, |
|||
'@tailwindcss/postcss': {}, |
|||
}, |
|||
}; |
|||
|
|||
@ -0,0 +1,39 @@ |
|||
import { useTheme } from "@app/core/hooks/useTheme"; |
|||
import { Moon, Sun } from "lucide-react"; |
|||
import React from "react"; |
|||
|
|||
type Theme = "light" | "dark"; |
|||
|
|||
export default function ThemeSwitcher({ |
|||
className = "", |
|||
}: { className?: string }) { |
|||
const currentTheme = useTheme(); // Get current theme from DOM
|
|||
const [theme, setTheme] = React.useState<Theme>(currentTheme); |
|||
|
|||
React.useEffect(() => { |
|||
document.documentElement.setAttribute("data-theme", theme); |
|||
localStorage.setItem("theme", theme); |
|||
}, [theme]); |
|||
|
|||
const themeIcons = { |
|||
light: ( |
|||
<Sun className="size-5 transition-transform duration-300 scale-100" /> |
|||
), |
|||
dark: ( |
|||
<Moon className="size-5 transition-transform duration-300 scale-100" /> |
|||
), |
|||
}; |
|||
|
|||
const toggleTheme = () => setTheme(theme === "light" ? "dark" : "light"); |
|||
|
|||
return ( |
|||
<button |
|||
type="button" |
|||
className={`transition-all duration-300 hover:text-accent ${className}`} |
|||
onClick={toggleTheme} |
|||
aria-label={`Current theme: ${theme}. Click to change theme.`} |
|||
> |
|||
{themeIcons[theme]} |
|||
</button> |
|||
); |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
import { useAppStore } from "@core/stores/appStore.ts"; |
|||
import type { ReactNode } from "react"; |
|||
|
|||
export interface ThemeControllerProps { |
|||
children: ReactNode; |
|||
} |
|||
|
|||
export const ThemeController = ({ |
|||
children, |
|||
}: ThemeControllerProps): JSX.Element => { |
|||
const { darkMode, accent } = useAppStore(); |
|||
|
|||
return ( |
|||
<div data-theme={darkMode ? "dark" : "light"} data-accent={accent}> |
|||
{children} |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,67 @@ |
|||
import type React from "react"; |
|||
import { createContext, useContext, useEffect, useState } from "react"; |
|||
|
|||
type Theme = "light" | "dark" | "system"; |
|||
|
|||
interface ThemeContextType { |
|||
theme: Theme; |
|||
setTheme: (theme: Theme) => void; |
|||
} |
|||
|
|||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined); |
|||
|
|||
export function ThemeProvider({ children }: { children: React.ReactNode }) { |
|||
const [theme, setTheme] = useState<Theme>(() => { |
|||
if (typeof window !== "undefined") { |
|||
const savedTheme = localStorage.getItem("theme") as Theme; |
|||
return savedTheme || "system"; |
|||
} |
|||
return "system"; |
|||
}); |
|||
|
|||
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); |
|||
} else { |
|||
root.classList.add(theme); |
|||
} |
|||
|
|||
localStorage.setItem("theme", theme); |
|||
}, [theme]); |
|||
|
|||
useEffect(() => { |
|||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); |
|||
|
|||
const handleChange = () => { |
|||
if (theme === "system") { |
|||
const root = window.document.documentElement; |
|||
root.classList.remove("light", "dark"); |
|||
root.classList.add(mediaQuery.matches ? "dark" : "light"); |
|||
} |
|||
}; |
|||
|
|||
mediaQuery.addEventListener("change", handleChange); |
|||
return () => mediaQuery.removeEventListener("change", handleChange); |
|||
}, [theme]); |
|||
|
|||
return ( |
|||
<ThemeContext.Provider value={{ theme, setTheme }}> |
|||
{children} |
|||
</ThemeContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export function useTheme() { |
|||
const context = useContext(ThemeContext); |
|||
if (context === undefined) { |
|||
throw new Error("useTheme must be used within a ThemeProvider"); |
|||
} |
|||
return context; |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
import { useEffect, useState } from "react"; |
|||
|
|||
type Theme = "light" | "dark"; |
|||
|
|||
export function useTheme() { |
|||
const [theme, setTheme] = useState<Theme>(() => { |
|||
if (typeof window === "undefined") return "light"; |
|||
return ( |
|||
(document.documentElement.getAttribute("data-theme") as Theme) || "light" |
|||
); |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
const observer = new MutationObserver((mutations) => { |
|||
for (const mutation of mutations) { |
|||
if ( |
|||
mutation.type === "attributes" && |
|||
mutation.attributeName === "data-theme" |
|||
) { |
|||
const newTheme = document.documentElement.getAttribute( |
|||
"data-theme", |
|||
) as Theme; |
|||
setTheme(newTheme); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
observer.observe(document.documentElement, { |
|||
attributes: true, |
|||
attributeFilter: ["data-theme"], |
|||
}); |
|||
|
|||
return () => observer.disconnect(); |
|||
}, []); |
|||
|
|||
return theme; |
|||
} |
|||
@ -1,44 +0,0 @@ |
|||
const { fontFamily } = require("tailwindcss/defaultTheme"); |
|||
|
|||
/** @type {import('tailwindcss').Config} */ |
|||
module.exports = { |
|||
darkMode: ["class", '[data-theme="dark"]'], |
|||
content: ["./index.html", "./src/**/*.{ts,tsx}"], |
|||
theme: { |
|||
extend: { |
|||
fontFamily: { |
|||
mono: ["Cascadia Code", ...fontFamily.mono], |
|||
sans: ["Inter var", ...fontFamily.sans], |
|||
}, |
|||
keyframes: { |
|||
"accordion-down": { |
|||
from: { height: 0 }, |
|||
to: { height: "var(--radix-accordion-content-height)" }, |
|||
}, |
|||
"accordion-up": { |
|||
from: { height: "var(--radix-accordion-content-height)" }, |
|||
to: { height: 0 }, |
|||
}, |
|||
}, |
|||
animation: { |
|||
"accordion-down": "accordion-down 0.2s ease-out", |
|||
"accordion-up": "accordion-up 0.2s ease-out", |
|||
}, |
|||
colors: { |
|||
backgroundPrimary: "var(--backgroundPrimary)", |
|||
backgroundSecondary: "var(--backgroundSecondary)", |
|||
accent: "var(--accent)", |
|||
accentMuted: "var(--accentMuted)", |
|||
textPrimary: "var(--textPrimary)", |
|||
textSecondary: "var(--textSecondary)", |
|||
link: "var(--link)", |
|||
}, |
|||
brightness: { |
|||
hover: "var(--brighnessHover)", |
|||
press: "var(--brightnessPress)", |
|||
disabled: "var(--brightnessDisabled)", |
|||
}, |
|||
}, |
|||
}, |
|||
plugins: [require("tailwindcss-animate")], |
|||
}; |
|||
Loading…
Reference in new issue