WIP: Theme switcher

- Light/Dark Toggle works
- Theme Preview works
- TODO: Theme switching
This commit is contained in:
grimsi
2024-04-08 11:33:47 +02:00
parent 5a2b26ee0c
commit 47f8febbd2
11 changed files with 662 additions and 683 deletions
+12 -2
View File
@@ -1,6 +1,16 @@
import * as React from "react"
import {Provider as JotaiProvider} from "jotai"
import {ThemeProvider as NextThemesProvider} from "next-themes" import {ThemeProvider as NextThemesProvider} from "next-themes"
import {type ThemeProviderProps} from "next-themes/dist/types" import {ThemeProviderProps} from "next-themes/dist/types"
import {TooltipProvider} from "Frontend/@/components/ui/tooltip";
export function ThemeProvider({children, ...props}: ThemeProviderProps) { export function ThemeProvider({children, ...props}: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider> return (
<JotaiProvider>
<NextThemesProvider {...props}>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</NextThemesProvider>
</JotaiProvider>
)
} }
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import {cn} from "Frontend/@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({className, ...props}, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export {Switch}
+9 -7
View File
@@ -1,18 +1,20 @@
import {useAtom} from "jotai" import {useAtom} from "jotai"
import {atomWithStorage} from "jotai/utils" import {atomWithStorage} from "jotai/utils"
import {Theme} from "Frontend/@/registry/themes" import {Theme} from "@/registry/themes"
type Config = { type Config = {
theme: Theme["name"] theme: {
mode: "light" | "dark", name: Theme["name"],
radius: number mode: "light" | "dark" | "system"
}
} }
const configAtom = atomWithStorage<Config>("config", { const configAtom = atomWithStorage<Config>("config", {
theme: "zinc", theme: {
mode: "light", name: "zinc",
radius: 0.5, mode: "system"
}
}) })
export function useConfig() { export function useConfig() {
+14
View File
@@ -4,3 +4,17 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export function cssVar(variable: string) {
return getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`);
}
export function hsl(hsl: string) {
return `hsl(${hsl}`;
}
export function rand(min: number, max: number) {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled);
}
-158
View File
@@ -105,164 +105,6 @@ export const themes = [
}, },
}, },
}, },
{
name: "stone",
label: "Stone",
activeColor: {
light: "25 5.3% 44.7%",
dark: "33.3 5.5% 32.4%",
},
cssVars: {
light: {
background: "0 0% 100%",
foreground: "20 14.3% 4.1%",
card: "0 0% 100%",
"card-foreground": "20 14.3% 4.1%",
popover: "0 0% 100%",
"popover-foreground": "20 14.3% 4.1%",
primary: "24 9.8% 10%",
"primary-foreground": "60 9.1% 97.8%",
secondary: "60 4.8% 95.9%",
"secondary-foreground": "24 9.8% 10%",
muted: "60 4.8% 95.9%",
"muted-foreground": "25 5.3% 44.7%",
accent: "60 4.8% 95.9%",
"accent-foreground": "24 9.8% 10%",
destructive: "0 84.2% 60.2%",
"destructive-foreground": "60 9.1% 97.8%",
border: "20 5.9% 90%",
input: "20 5.9% 90%",
ring: "20 14.3% 4.1%",
radius: "0.95rem",
},
dark: {
background: "20 14.3% 4.1%",
foreground: "60 9.1% 97.8%",
card: "20 14.3% 4.1%",
"card-foreground": "60 9.1% 97.8%",
popover: "20 14.3% 4.1%",
"popover-foreground": "60 9.1% 97.8%",
primary: "60 9.1% 97.8%",
"primary-foreground": "24 9.8% 10%",
secondary: "12 6.5% 15.1%",
"secondary-foreground": "60 9.1% 97.8%",
muted: "12 6.5% 15.1%",
"muted-foreground": "24 5.4% 63.9%",
accent: "12 6.5% 15.1%",
"accent-foreground": "60 9.1% 97.8%",
destructive: "0 62.8% 30.6%",
"destructive-foreground": "60 9.1% 97.8%",
border: "12 6.5% 15.1%",
input: "12 6.5% 15.1%",
ring: "24 5.7% 82.9%",
},
},
},
{
name: "gray",
label: "Gray",
activeColor: {
light: "220 8.9% 46.1%",
dark: "215 13.8% 34.1%",
},
cssVars: {
light: {
background: "0 0% 100%",
foreground: "224 71.4% 4.1%",
card: "0 0% 100%",
"card-foreground": "224 71.4% 4.1%",
popover: "0 0% 100%",
"popover-foreground": "224 71.4% 4.1%",
primary: "220.9 39.3% 11%",
"primary-foreground": "210 20% 98%",
secondary: "220 14.3% 95.9%",
"secondary-foreground": "220.9 39.3% 11%",
muted: "220 14.3% 95.9%",
"muted-foreground": "220 8.9% 46.1%",
accent: "220 14.3% 95.9%",
"accent-foreground": "220.9 39.3% 11%",
destructive: "0 84.2% 60.2%",
"destructive-foreground": "210 20% 98%",
border: "220 13% 91%",
input: "220 13% 91%",
ring: "224 71.4% 4.1%",
radius: "0.35rem",
},
dark: {
background: "224 71.4% 4.1%",
foreground: "210 20% 98%",
card: "224 71.4% 4.1%",
"card-foreground": "210 20% 98%",
popover: "224 71.4% 4.1%",
"popover-foreground": "210 20% 98%",
primary: "210 20% 98%",
"primary-foreground": "220.9 39.3% 11%",
secondary: "215 27.9% 16.9%",
"secondary-foreground": "210 20% 98%",
muted: "215 27.9% 16.9%",
"muted-foreground": "217.9 10.6% 64.9%",
accent: "215 27.9% 16.9%",
"accent-foreground": "210 20% 98%",
destructive: "0 62.8% 30.6%",
"destructive-foreground": "210 20% 98%",
border: "215 27.9% 16.9%",
input: "215 27.9% 16.9%",
ring: "216 12.2% 83.9%",
},
},
},
{
name: "neutral",
label: "Neutral",
activeColor: {
light: "0 0% 45.1%",
dark: "0 0% 32.2%",
},
cssVars: {
light: {
background: "0 0% 100%",
foreground: "0 0% 3.9%",
card: "0 0% 100%",
"card-foreground": "0 0% 3.9%",
popover: "0 0% 100%",
"popover-foreground": "0 0% 3.9%",
primary: "0 0% 9%",
"primary-foreground": "0 0% 98%",
secondary: "0 0% 96.1%",
"secondary-foreground": "0 0% 9%",
muted: "0 0% 96.1%",
"muted-foreground": "0 0% 45.1%",
accent: "0 0% 96.1%",
"accent-foreground": "0 0% 9%",
destructive: "0 84.2% 60.2%",
"destructive-foreground": "0 0% 98%",
border: "0 0% 89.8%",
input: "0 0% 89.8%",
ring: "0 0% 3.9%",
},
dark: {
background: "0 0% 3.9%",
foreground: "0 0% 98%",
card: "0 0% 3.9%",
"card-foreground": "0 0% 98%",
popover: "0 0% 3.9%",
"popover-foreground": "0 0% 98%",
primary: "0 0% 98%",
"primary-foreground": "0 0% 9%",
secondary: "0 0% 14.9%",
"secondary-foreground": "0 0% 98%",
muted: "0 0% 14.9%",
"muted-foreground": "0 0% 63.9%",
accent: "0 0% 14.9%",
"accent-foreground": "0 0% 98%",
destructive: "0 62.8% 30.6%",
"destructive-foreground": "0 0% 98%",
border: "0 0% 14.9%",
input: "0 0% 14.9%",
ring: "0 0% 83.1%",
},
},
},
{ {
name: "red", name: "red",
label: "Red", label: "Red",
-1
View File
@@ -14,7 +14,6 @@ export default function App() {
attribute="class" attribute="class"
defaultTheme="system" defaultTheme="system"
enableSystem enableSystem
disableTransitionOnChange
> >
<IconContext.Provider value={{size: 20}}> <IconContext.Provider value={{size: 20}}>
<RouterProvider router={router}/> <RouterProvider router={router}/>
@@ -0,0 +1,26 @@
import {hsl} from "Frontend/@/lib/utils";
export default function GameyfinLogo({primary, secondary, className}: {
primary: string,
secondary: string,
className?: string
}) {
const primaryColor = hsl(primary)
const secondaryColor = (secondary === null || secondary === undefined) ? primaryColor : hsl(secondary);
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 365.58 336.34" className={className}>
<polygon points="190.1 49.13 190.1 69.24 207.98 44.13 190.1 49.13" fill={secondaryColor}/>
<polygon points="365.58 0 263.22 28.66 205.64 95.97 365.58 51.18 365.58 0" fill={secondaryColor}/>
<polygon
points="190.1 283.11 248.6 266.73 248.6 149.74 365.58 116.99 365.58 73.12 190.1 122.25 190.1 283.11"
fill={secondaryColor}/>
<polygon
points="58.49 144.48 155.98 117.18 175.48 89.79 175.48 53.23 0 102.36 0 336.34 58.49 254.15 58.49 144.48"
fill={primaryColor}/>
<polygon
points="116.99 199.59 116.99 245.09 65.81 259.42 0 336.34 175.48 287.2 175.48 170.22 131.61 182.5 116.99 199.59"
fill={primaryColor}/>
</svg>
);
}
+23 -25
View File
@@ -1,46 +1,44 @@
import {Theme} from "Frontend/@/registry/themes"; import {Theme} from "Frontend/@/registry/themes";
import {Card} from "Frontend/@/components/ui/card"; import {Card} from "Frontend/@/components/ui/card";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "Frontend/@/components/ui/tooltip"; import {Tooltip, TooltipContent, TooltipTrigger} from "Frontend/@/components/ui/tooltip";
import {useTheme} from "next-themes"; import {useTheme} from "next-themes";
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
import {hsl} from "Frontend/@/lib/utils";
export default function ThemePreview({theme}: { theme: Theme }) { export default function ThemePreview({theme}: { theme: Theme }) {
const {resolvedTheme} = useTheme(); //@ts-ignore
let resolvedTheme: "light" | "dark" = useTheme().resolvedTheme ?? "light";
const {setTheme} = useTheme();
function toggleMode() {
resolvedTheme = resolvedTheme === "light" ? "dark" : "light";
setTheme(resolvedTheme);
}
return ( return (
<TooltipProvider> <Tooltip>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Card className="overflow-hidden flex place-self-center"> <Card
<svg width="228" height="120" viewBox="0 0 228 120" fill="none" className="overflow-hidden flex place-self-center justify-center p-6"
xmlns="http://www.w3.org/2000/svg"> style={{background: hsl(theme.cssVars[resolvedTheme].background)}}>
{/*@ts-ignore*/} <GameyfinLogo primary={theme.cssVars[resolvedTheme].primary}
<path id="background" d="M0 0H228V120H0V0Z" fill={theme.cssVars[resolvedTheme].background}/> secondary={theme.cssVars[resolvedTheme].secondary}
<rect id="background-secondary" x="29" y="54" width="144" height="53" rx="2" className="w-1/2"
fill="#30363D"/> />
<rect x="184" y="54" width="32" height="36" rx="2" fill="#30363D"/>
<rect opacity="0.3" x="29" y="59" width="144" height="12" fill="#2EA043"/>
<path opacity="0.6" d="M0 0H228V23H0V0Z" fill="#484F58"/>
<rect x="13" y="9" width="32" height="6" rx="3" fill="#8B949E"/>
<rect x="29" y="36" width="48" height="6" rx="3" fill="#6E7681"/>
<rect x="34" y="62" width="64" height="6" rx="3" fill="#3FB950"/>
<rect x="210" y="36" width="6" height="6" rx="1" fill="#DA3633"/>
<rect x="202" y="36" width="6" height="6" rx="1" fill="#3FB950"/>
<rect x="53" y="9" width="32" height="6" rx="3" fill="#8B949E"/>
<rect x="93" y="9" width="32" height="6" rx="3" fill="#8B949E"/>
</svg>
</Card> </Card>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
<p className="capitalize">{theme.name}</p> <p className="capitalize">{theme.name}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider>
); );
} }
/* /*
<svg width="228" height="120" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="228" height="120" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0H228V120H0V0Z" fill="#161B22"/> <path id="background" d="M0 0H228V120H0V0Z" fill={theme.cssVars[resolvedTheme].background}/>
<rect x="29" y="54" width="144" height="53" rx="2" fill="#30363D"/> <rect id="background-secondary" x="29" y="54" width="144" height="53" rx="2" fill="#30363D"/>
<rect x="184" y="54" width="32" height="36" rx="2" fill="#30363D"/> <rect x="184" y="54" width="32" height="36" rx="2" fill="#30363D"/>
<rect opacity="0.3" x="29" y="59" width="144" height="12" fill="#2EA043"/> <rect opacity="0.3" x="29" y="59" width="144" height="12" fill="#2EA043"/>
<path opacity="0.6" d="M0 0H228V23H0V0Z" fill="#484F58"/> <path opacity="0.6" d="M0 0H228V23H0V0Z" fill="#484F58"/>
+14 -1
View File
@@ -3,10 +3,12 @@ import * as Yup from 'yup';
import Wizard from "Frontend/components/wizard/Wizard"; import Wizard from "Frontend/components/wizard/Wizard";
import WizardStep from "Frontend/components/wizard/WizardStep"; import WizardStep from "Frontend/components/wizard/WizardStep";
import Input from "Frontend/components/Input"; import Input from "Frontend/components/Input";
import {GearFine, HandWaving, Palette, User} from "@phosphor-icons/react"; import {GearFine, HandWaving, Moon, Palette, SunDim, User} from "@phosphor-icons/react";
import ThemePreview from "Frontend/components/theming/ThemePreview"; import ThemePreview from "Frontend/components/theming/ThemePreview";
import {Theme, themes} from "Frontend/@/registry/themes"; import {Theme, themes} from "Frontend/@/registry/themes";
import {Card} from "Frontend/@/components/ui/card"; import {Card} from "Frontend/@/components/ui/card";
import {Switch} from "Frontend/@/components/ui/switch";
import {useTheme} from "next-themes";
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@@ -35,8 +37,19 @@ function WelcomeStep() {
} }
function ThemeStep() { function ThemeStep() {
const {setTheme, theme} = useTheme();
function toggleMode() {
setTheme(theme === "light" ? "dark" : "light");
}
return ( return (
<div className="flex flex-col size-full items-center"> <div className="flex flex-col size-full items-center">
<div className="w-full flex flex-row items-center justify-center gap-4 mb-16">
<SunDim size={32}/>
<Switch checked={theme === "dark"} onCheckedChange={() => toggleMode()}></Switch>
<Moon size={32}/>
</div>
<div className="grid grid-cols-3 w-1/2 min-w-[468px] gap-12"> <div className="grid grid-cols-3 w-1/2 min-w-[468px] gap-12">
{themes.map(((theme: Theme) => ( {themes.map(((theme: Theme) => (
<ThemePreview key={theme.name} theme={theme}/> <ThemePreview key={theme.name} theme={theme}/>
+47
View File
@@ -30,6 +30,7 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@vaadin/bundles": "24.3.0", "@vaadin/bundles": "24.3.0",
"@vaadin/common-frontend": "0.0.19", "@vaadin/common-frontend": "0.0.19",
@@ -3328,6 +3329,35 @@
} }
} }
}, },
"node_modules/@radix-ui/react-switch": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz",
"integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-previous": "1.0.1",
"@radix-ui/react-use-size": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
@@ -3432,6 +3462,23 @@
} }
} }
}, },
"node_modules/@radix-ui/react-use-previous": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz",
"integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-rect": { "node_modules/@radix-ui/react-use-rect": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz",
+1
View File
@@ -25,6 +25,7 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@vaadin/bundles": "24.3.0", "@vaadin/bundles": "24.3.0",
"@vaadin/common-frontend": "0.0.19", "@vaadin/common-frontend": "0.0.19",