feat(theme): add light theme with theme toggle and fix workspace dialog labels

- Add ThemeProvider with localStorage persistence and default dark theme
- Add ThemeToggle (sun/moon) to the header's right corner
- Restructure index.css: light palette in :root, dark palette in .dark
- Apply stored theme in index.html to avoid flash on load
- Fix mislabeled "Create/Edit Home" text in WorkspaceDialog to "Workspace"
This commit is contained in:
Rodrigo Verdiani 2026-07-04 14:22:40 -03:00
parent 1a8fab21cd
commit e027e9cbe0
7 changed files with 157 additions and 12 deletions

View File

@ -5,6 +5,17 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Decompile.AI</title>
<script>
(function () {
try {
var stored = localStorage.getItem("decompile-ai-theme");
var theme = stored === "light" || stored === "dark" ? stored : "dark";
document.documentElement.classList.add(theme);
} catch (e) {
document.documentElement.classList.add("dark");
}
})();
</script>
</head>
<body>
<div id="root"></div>

View File

@ -0,0 +1,25 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button.tsx";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx";
import { useTheme } from "@/providers/ThemeProvider.tsx";
export const ThemeToggle = () => {
const { theme, toggleTheme } = useTheme();
const isDark = theme === "dark";
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label={isDark ? "Switch to light theme" : "Switch to dark theme"}
>
{isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</Button>
</TooltipTrigger>
<TooltipContent>{isDark ? "Light mode" : "Dark mode"}</TooltipContent>
</Tooltip>
);
};

View File

@ -1,6 +1,7 @@
import { Activity, Binary, FolderOpen, BookOpen } from "lucide-react";
import { Link, NavLink } from "react-router";
import { cn } from "@/lib/utils.ts";
import { ThemeToggle } from "@/components/ThemeToggle.tsx";
const navItems = [
{ href: "/", label: "Workspaces", icon: FolderOpen },
@ -45,6 +46,11 @@ export const Header = () => {
);
})}
</nav>
{/* Right actions */}
<div className="ml-auto flex items-center gap-1">
<ThemeToggle />
</div>
</header>
);
};

View File

@ -43,11 +43,11 @@ export const WorkspaceDialog = ({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{isEditing ? "Edit Home" : "Create Home"}</DialogTitle>
<DialogTitle>{isEditing ? "Edit Workspace" : "Create Workspace"}</DialogTitle>
<DialogDescription>
{isEditing
? "Update your home details."
: "Create a new home to organize your reverse engineering projects."}
? "Update your workspace details."
: "Create a new workspace to organize your reverse engineering projects."}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
@ -76,7 +76,7 @@ export const WorkspaceDialog = ({
Cancel
</Button>
<Button onClick={handleSave} disabled={!name.trim()}>
{isEditing ? "Save Changes" : "Create Home"}
{isEditing ? "Save Changes" : "Create Workspace"}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -53,7 +53,54 @@
}
:root {
/* Dark theme as default - IDE style for RE platform */
/* Light theme - clean IDE style for RE platform */
--background: oklch(0.98 0.005 260);
--foreground: oklch(0.2 0.02 260);
--card: oklch(1 0 0);
--card-foreground: oklch(0.2 0.02 260);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.02 260);
--primary: oklch(0.58 0.14 180);
--primary-foreground: oklch(0.98 0.005 260);
--secondary: oklch(0.95 0.01 260);
--secondary-foreground: oklch(0.2 0.02 260);
--muted: oklch(0.96 0.01 260);
--muted-foreground: oklch(0.5 0.02 260);
--accent: oklch(0.58 0.14 180);
--accent-foreground: oklch(0.98 0.005 260);
--destructive: oklch(0.58 0.22 25);
--border: oklch(0.9 0.01 260);
--input: oklch(0.9 0.01 260);
--ring: oklch(0.58 0.14 180);
--chart-1: oklch(0.58 0.14 180);
--chart-2: oklch(0.6 0.15 140);
--chart-3: oklch(0.55 0.15 280);
--chart-4: oklch(0.65 0.13 60);
--chart-5: oklch(0.5 0.18 330);
--radius: 0.5rem;
--sidebar: oklch(0.97 0.005 260);
--sidebar-foreground: oklch(0.2 0.02 260);
--sidebar-primary: oklch(0.58 0.14 180);
--sidebar-primary-foreground: oklch(0.98 0.005 260);
--sidebar-accent: oklch(0.95 0.01 260);
--sidebar-accent-foreground: oklch(0.2 0.02 260);
--sidebar-border: oklch(0.9 0.01 260);
--sidebar-ring: oklch(0.58 0.14 180);
/* Custom tokens for RE platform */
--code-bg: oklch(0.97 0.005 260);
--code-line-number: oklch(0.6 0.01 260);
--code-keyword: oklch(0.5 0.16 280);
--code-string: oklch(0.5 0.13 140);
--code-comment: oklch(0.6 0.05 140);
--code-function: oklch(0.55 0.15 60);
--code-address: oklch(0.5 0.12 180);
--success: oklch(0.55 0.16 145);
--warning: oklch(0.62 0.15 75);
}
.dark {
/* Dark theme - IDE style for RE platform */
--background: oklch(0.12 0.01 260);
--foreground: oklch(0.93 0.01 260);
--card: oklch(0.16 0.01 260);
@ -77,7 +124,6 @@
--chart-3: oklch(0.6 0.15 280);
--chart-4: oklch(0.75 0.12 60);
--chart-5: oklch(0.55 0.18 330);
--radius: 0.5rem;
--sidebar: oklch(0.14 0.01 260);
--sidebar-foreground: oklch(0.93 0.01 260);
--sidebar-primary: oklch(0.65 0.18 180);

View File

@ -2,6 +2,7 @@ import type { ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeProvider } from "@/providers/ThemeProvider.tsx";
interface ProvidersProps {
children: ReactNode;
@ -9,11 +10,13 @@ interface ProvidersProps {
export const Providers = ({ children }: ProvidersProps) => {
return (
<ThemeProvider>
<QueryClientProvider client={new QueryClient()}>
<TooltipProvider delayDuration={1000}>
{children}
<Toaster position="bottom-right" />
</TooltipProvider>
</QueryClientProvider>
</ThemeProvider>
);
};

View File

@ -0,0 +1,54 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
export type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const STORAGE_KEY = "decompile-ai-theme";
const DEFAULT_THEME: Theme = "dark";
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
const getInitialTheme = (): Theme => {
if (typeof window === "undefined") return DEFAULT_THEME;
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark") return stored;
return DEFAULT_THEME;
};
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider = ({ children }: ThemeProviderProps) => {
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
useEffect(() => {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(theme);
window.localStorage.setItem(STORAGE_KEY, theme);
}, [theme]);
const setTheme = (next: Theme) => setThemeState(next);
const toggleTheme = () => setThemeState((prev) => (prev === "dark" ? "light" : "dark"));
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};