Speedcube Master adopte un design dark-first. Le theme sombre est le theme par defaut, et le mode clair est une alternative activable.
Le theming est gere par next-themes qui ajoute une classe sur l'element <html>. Le theme par defaut est dark.
// app/layout.tsx
import { ThemeProvider } from "next-themes";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr" suppressHydrationMismatch>
<body>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}Les couleurs sont definies comme variables CSS dans le fichier de tokens. Le theme dark est defini sur :root (par defaut), et le theme light est applique via la classe .light.
/* tokens.css - Dark-first approach */
/* Theme sombre = defaut (pas besoin de .dark) */
:root {
--background: oklch(0.141 0.024 259.1); /* #0B0F1A */
--foreground: oklch(0.906 0.011 252.9); /* #E5E7EB */
--card: oklch(0.168 0.026 261.7); /* #0F1524 */
--card-foreground: oklch(0.906 0.011 252.9);
--primary: oklch(0.546 0.215 264.1); /* #2563EB */
--primary-foreground: oklch(1 0 0); /* #FFFFFF */
--border: oklch(0.269 0.015 252.9); /* #1F2937 */
--muted: oklch(0.21 0.017 255.9); /* #111827 */
--muted-foreground: oklch(0.656 0.014 253.8); /* #9CA3AF */
--accent: oklch(0.696 0.17 149.6); /* #22C55E */
--destructive: oklch(0.577 0.215 27.3); /* #EF4444 */
--warning: oklch(0.728 0.161 72.7); /* #F59E0B */
--ring: oklch(0.546 0.215 264.1);
}
/* Theme clair = override via .light */
.light {
--background: oklch(0.967 0.006 264.5); /* #F4F6FA */
--foreground: oklch(0.145 0 0); /* #171717 */
--card: oklch(0.978 0.004 264.4); /* #F8FAFC */
--card-foreground: oklch(0.145 0 0);
--border: oklch(0.906 0.013 255.5); /* #E2E8F0 */
--muted: oklch(0.962 0.009 255.5); /* #F1F5F9 */
--muted-foreground: oklch(0.554 0.023 255.7); /* #64748B */
/* primary, accent, destructive, warning restent identiques */
}Contrairement a la plupart des applications qui definissent le theme clair par defaut et ajoutent une classe .dark, Speedcube Master fait l'inverse. Le theme sombre est sur :root et le theme clair est active via la classe .light.
:root.light.dark comme selecteurLes composants ne doivent jamais utiliser de couleurs hardcodees. Toujours utiliser les classes Tailwind basees sur les tokens semantiques. Cela garantit que le theme fonctionne automatiquement.
// Correct - utilise les tokens semantiques
<div className="bg-background text-foreground">
<div className="bg-card border border-border rounded-xl p-6">
<h2 className="text-foreground">Titre</h2>
<p className="text-muted-foreground">Description</p>
<button className="bg-primary text-primary-foreground">
Action
</button>
</div>
</div>
// Incorrect - couleurs hardcodees
<div className="bg-[#0B0F1A] text-[#E5E7EB]">
<div className="bg-[#0F1524] border border-[#1F2937]">
<h2 className="text-white">Titre</h2>
<p className="text-gray-400">Description</p>
</div>
</div>Le composant de basculement de theme utilise le hook useTheme() de next-themes pour changer entre les modes sombre et clair.
"use client";
import { useTheme } from "next-themes";
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
aria-label="Basculer le theme"
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
);
}Voici comment les tokens sont utilises dans les composants courants pour garantir la compatibilite avec les deux themes.
// Card avec support dark/light automatique
<Card className="bg-card/95 backdrop-blur-sm border-border">
<CardHeader>
<CardTitle className="text-foreground">Session</CardTitle>
<CardDescription className="text-muted-foreground">
12 solves effectuees
</CardDescription>
</CardHeader>
</Card>
// Badge avec variantes
<Badge variant="default"> {/* bg-primary text-primary-foreground */}
<Badge variant="secondary"> {/* bg-secondary text-secondary-foreground */}
<Badge variant="outline"> {/* border-border text-foreground */}
<Badge variant="destructive"> {/* bg-destructive text-destructive-foreground */}
// Input avec bordure adaptative
<Input className="bg-background border-input text-foreground
placeholder:text-muted-foreground
focus:ring-ring focus:border-ring" />
// Etats hover et focus
<button className="bg-muted hover:bg-muted/80
text-foreground
focus-visible:ring-2 focus-visible:ring-ring
focus-visible:ring-offset-2 focus-visible:ring-offset-background" />