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" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Decompile.AI</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 { Activity, Binary, FolderOpen, BookOpen } from "lucide-react";
|
||||||
import { Link, NavLink } from "react-router";
|
import { Link, NavLink } from "react-router";
|
||||||
import { cn } from "@/lib/utils.ts";
|
import { cn } from "@/lib/utils.ts";
|
||||||
|
import { ThemeToggle } from "@/components/ThemeToggle.tsx";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: "/", label: "Workspaces", icon: FolderOpen },
|
{ href: "/", label: "Workspaces", icon: FolderOpen },
|
||||||
@ -45,6 +46,11 @@ export const Header = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Right actions */}
|
||||||
|
<div className="ml-auto flex items-center gap-1">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,11 +43,11 @@ export const WorkspaceDialog = ({
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{isEditing ? "Edit Home" : "Create Home"}</DialogTitle>
|
<DialogTitle>{isEditing ? "Edit Workspace" : "Create Workspace"}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{isEditing
|
{isEditing
|
||||||
? "Update your home details."
|
? "Update your workspace details."
|
||||||
: "Create a new home to organize your reverse engineering projects."}
|
: "Create a new workspace to organize your reverse engineering projects."}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
@ -76,7 +76,7 @@ export const WorkspaceDialog = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave} disabled={!name.trim()}>
|
<Button onClick={handleSave} disabled={!name.trim()}>
|
||||||
{isEditing ? "Save Changes" : "Create Home"}
|
{isEditing ? "Save Changes" : "Create Workspace"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@ -53,7 +53,54 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
: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);
|
--background: oklch(0.12 0.01 260);
|
||||||
--foreground: oklch(0.93 0.01 260);
|
--foreground: oklch(0.93 0.01 260);
|
||||||
--card: oklch(0.16 0.01 260);
|
--card: oklch(0.16 0.01 260);
|
||||||
@ -77,7 +124,6 @@
|
|||||||
--chart-3: oklch(0.6 0.15 280);
|
--chart-3: oklch(0.6 0.15 280);
|
||||||
--chart-4: oklch(0.75 0.12 60);
|
--chart-4: oklch(0.75 0.12 60);
|
||||||
--chart-5: oklch(0.55 0.18 330);
|
--chart-5: oklch(0.55 0.18 330);
|
||||||
--radius: 0.5rem;
|
|
||||||
--sidebar: oklch(0.14 0.01 260);
|
--sidebar: oklch(0.14 0.01 260);
|
||||||
--sidebar-foreground: oklch(0.93 0.01 260);
|
--sidebar-foreground: oklch(0.93 0.01 260);
|
||||||
--sidebar-primary: oklch(0.65 0.18 180);
|
--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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { ThemeProvider } from "@/providers/ThemeProvider.tsx";
|
||||||
|
|
||||||
interface ProvidersProps {
|
interface ProvidersProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -9,11 +10,13 @@ interface ProvidersProps {
|
|||||||
|
|
||||||
export const Providers = ({ children }: ProvidersProps) => {
|
export const Providers = ({ children }: ProvidersProps) => {
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
<QueryClientProvider client={new QueryClient()}>
|
||||||
<TooltipProvider delayDuration={1000}>
|
<TooltipProvider delayDuration={1000}>
|
||||||
{children}
|
{children}
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</QueryClientProvider>
|
</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