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:
parent
1a8fab21cd
commit
e027e9cbe0
11
index.html
11
index.html
@ -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>
|
||||
|
||||
25
src/components/ThemeToggle.tsx
Normal file
25
src/components/ThemeToggle.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
54
src/providers/ThemeProvider.tsx
Normal file
54
src/providers/ThemeProvider.tsx
Normal 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;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user