-
Notifications
You must be signed in to change notification settings - Fork 27
feat: implement dark mode support with theme toggle and CSS adjustments #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,10 +4,25 @@ | |
| <meta charset="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <script> | ||
| (function () { | ||
| const storageKey = "jobs-theme-preference"; | ||
| const root = document.documentElement; | ||
| const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; | ||
| const storedPreference = localStorage.getItem(storageKey); | ||
| const themePreference = storedPreference === "light" || storedPreference === "dark" || storedPreference === "system" | ||
| ? storedPreference | ||
| : "system"; | ||
| const resolvedTheme = themePreference === "system" ? (prefersDark ? "dark" : "light") : themePreference; | ||
|
Comment on lines
+7
to
+16
|
||
|
|
||
| root.classList.toggle("dark", resolvedTheme === "dark"); | ||
| root.setAttribute("data-theme", resolvedTheme); | ||
| })(); | ||
| </script> | ||
| <title>frontend</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.jsx"></script> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { Button } from "@/components/ui/button"; | ||
| import type { Theme } from "@/hooks/useTheme"; | ||
| import { Moon, Sun } from "lucide-react"; | ||
|
|
||
| interface ThemeToggleProps { | ||
| theme: Theme; | ||
| onToggle: () => void; | ||
| } | ||
|
|
||
| export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) { | ||
| const isDark = theme === "dark"; | ||
|
|
||
| return ( | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={onToggle} | ||
| aria-label={isDark ? "Ativar tema claro" : "Ativar tema escuro"} | ||
| title={isDark ? "Ativar tema claro" : "Ativar tema escuro"} | ||
| className="gap-2" | ||
| > | ||
| {isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />} | ||
| <span className="hidden sm:inline">{isDark ? "Tema claro" : "Tema escuro"}</span> | ||
| </Button> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,69 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useMemo, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type Theme = "light" | "dark"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type ThemePreference = Theme | "system"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const THEME_STORAGE_KEY = "jobs-theme-preference"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DARK_CLASS = "dark"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isThemePreference(value: string | null): value is ThemePreference { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value === "light" || value === "dark" || value === "system"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getSystemTheme(): Theme { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function resolveTheme(themePreference: ThemePreference): Theme { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return themePreference === "system" ? getSystemTheme() : themePreference; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function applyThemeToRoot(theme: Theme): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const root = document.documentElement; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| root.classList.toggle(DARK_CLASS, theme === "dark"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| root.setAttribute("data-theme", theme); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function useTheme() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [themePreference, setThemePreference] = useState<ThemePreference>(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const storedPreference = localStorage.getItem(THEME_STORAGE_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return isThemePreference(storedPreference) ? storedPreference : "system"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resolvedTheme = useMemo(() => resolveTheme(themePreference), [themePreference]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| applyThemeToRoot(resolvedTheme); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem(THEME_STORAGE_KEY, themePreference); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+38
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function useTheme() { | |
| const [themePreference, setThemePreference] = useState<ThemePreference>(() => { | |
| const storedPreference = localStorage.getItem(THEME_STORAGE_KEY); | |
| return isThemePreference(storedPreference) ? storedPreference : "system"; | |
| }); | |
| const resolvedTheme = useMemo(() => resolveTheme(themePreference), [themePreference]); | |
| useEffect(() => { | |
| applyThemeToRoot(resolvedTheme); | |
| localStorage.setItem(THEME_STORAGE_KEY, themePreference); | |
| function getStoredThemePreference(): ThemePreference { | |
| try { | |
| const storedPreference = localStorage.getItem(THEME_STORAGE_KEY); | |
| return isThemePreference(storedPreference) ? storedPreference : "system"; | |
| } catch { | |
| return "system"; | |
| } | |
| } | |
| export function useTheme() { | |
| const [themePreference, setThemePreference] = useState<ThemePreference>(() => { | |
| return getStoredThemePreference(); | |
| }); | |
| const resolvedTheme = useMemo(() => resolveTheme(themePreference), [themePreference]); | |
| useEffect(() => { | |
| applyThemeToRoot(resolvedTheme); | |
| try { | |
| localStorage.setItem(THEME_STORAGE_KEY, themePreference); | |
| } catch { | |
| // Ignore storage write errors; theme is still applied to the DOM. | |
| } |
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolvedTheme is memoized only on themePreference, so when themePreference === "system" and the OS theme changes, the hook applies the new theme to the DOM but does not trigger a re-render and resolvedTheme stays stale. This can leave UI (toggle icon/labels) out of sync with the actual applied theme. Consider storing the resolved theme in state (or using useSyncExternalStore/a media-query subscription) and updating it in the media query handler so consumers re-render.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,10 +22,30 @@ | |
| --input: 30 28% 82%; | ||
| --ring: 198 63% 36%; | ||
| --radius: 0.8rem; | ||
| color-scheme: light; | ||
| } | ||
|
|
||
| .dark { | ||
| --background: 215 30% 10%; | ||
| --foreground: 210 30% 95%; | ||
| --card: 216 26% 14%; | ||
| --card-foreground: 210 30% 95%; | ||
| --primary: 198 78% 63%; | ||
| --primary-foreground: 218 35% 12%; | ||
| --secondary: 216 20% 23%; | ||
| --secondary-foreground: 210 20% 94%; | ||
| --muted: 216 20% 19%; | ||
| --muted-foreground: 215 20% 72%; | ||
| --accent: 25 75% 58%; | ||
| --accent-foreground: 0 0% 100%; | ||
| --border: 216 16% 30%; | ||
| --input: 216 16% 30%; | ||
| --ring: 198 78% 63%; | ||
| color-scheme: dark; | ||
| } | ||
|
|
||
| * { | ||
| @apply border-border; | ||
| @apply border-border transition-colors duration-300; | ||
| } | ||
|
Comment on lines
47
to
49
|
||
|
|
||
| body { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docker compose upuses-d/--detach(lowercase).-Dis not a valid flag and will fail for readers following the README.