feat: Add Dark Theme Support

This commit is contained in:
Rajarshee Chatterjee
2025-12-08 23:55:44 +05:30
parent 36b06d1de8
commit af182913e0
10 changed files with 66 additions and 92 deletions
+3
View File
@@ -2,8 +2,11 @@ import React from 'react';
import { Toaster } from '@/components/ui/sonner';
import Home from './pages/home';
import { TooltipProvider } from './components/ui/tooltip';
import { useTheme } from './hooks/useTheme';
function App() {
useTheme();
return (
<div className="min-h-screen bg-background">
<TooltipProvider>
+2 -2
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Copy, Zap } from 'lucide-react';
import { Copy, ArrowRight } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
@@ -60,7 +60,7 @@ export function NovelCard({ novel, onParse }: NovelCardProps) {
className="flex-1 h-8 px-2"
onClick={() => onParse(novel.path)}
>
<Zap className="w-3.5 h-3.5" />
<ArrowRight className="w-3.5 h-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
+3 -3
View File
@@ -4,7 +4,7 @@ import {
ChevronLeft,
ChevronRight,
Copy,
Zap,
ArrowRight,
Download,
} from 'lucide-react';
import { toast } from 'sonner';
@@ -506,11 +506,11 @@ export default function ParseNovelSection({
handleParseChapter(chapter.path)
}
>
<Zap className="w-3.5 h-3.5" />
<ArrowRight className="w-3.5 h-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Parse chapter</p>
<p>Open in Parse Chapter tab</p>
</TooltipContent>
</Tooltip>
</div>
+22 -1
View File
@@ -1,14 +1,25 @@
import React from 'react';
import { Moon, Sun } from 'lucide-react';
import { Plugin } from '@/types/plugin';
import { Button } from '@/components/ui/button';
import { useTheme } from '@/hooks/useTheme';
type PluginHeaderProps = {
selectedPlugin?: Plugin.PluginBase;
};
export default function PluginHeader({ selectedPlugin }: PluginHeaderProps) {
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
const isDark = theme === 'dark';
return (
<header className="border-b">
<header className="border-b border-border">
<div className="px-8 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-sm bg-[var(--color-accent-soft)] text-xl font-semibold text-[var(--color-accent-strong)] transition-colors">
@@ -23,6 +34,16 @@ export default function PluginHeader({ selectedPlugin }: PluginHeaderProps) {
</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="h-9 w-9"
aria-label="Toggle theme"
title={`Theme: ${theme}`}
>
{isDark ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
</Button>
</div>
</header>
);
+2 -2
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Filter, Zap } from 'lucide-react';
import { Filter, BookOpen } from 'lucide-react';
import { FiltersSheet } from '@/components/filters/filters-sheet';
import { NovelCard } from '@/components/novel-card';
@@ -182,7 +182,7 @@ export default function PopularNovelsSection({
) : novels.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
<div className="rounded-full bg-muted p-4 mb-4">
<Zap className="w-8 h-8 text-muted-foreground" />
<BookOpen className="w-8 h-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">
{plugin ? 'No novels to display' : 'No plugin selected'}
+18
View File
@@ -0,0 +1,18 @@
import { useEffect } from 'react';
import { useAppStore } from '@/store';
export function useTheme() {
const theme = useAppStore(state => state.theme);
const setTheme = useAppStore(state => state.setTheme);
useEffect(() => {
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [theme]);
return { theme, setTheme };
}
+1 -1
View File
@@ -32,7 +32,7 @@ function Home() {
<div className="min-h-screen bg-background">
<PluginHeader selectedPlugin={plugin} />
<div className="flex h-[calc(100vh-64px)]">
<aside className="w-64 border-r bg-background flex flex-col">
<aside className="w-64 border-r border-border bg-background flex flex-col">
<div className="p-6 flex-shrink-0 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
-39
View File
@@ -1,39 +0,0 @@
import { StoreCreator } from '.';
export enum EDialog {}
export type DialogStore = {
activeDialogs: EDialog[];
showDialog(dialog: EDialog): void;
hideDialog(dialog: EDialog): void;
hideAllDialogs(): void;
};
/**
* @param set Use this inside actions to change the state
* @param get Use this inside actions to get the current state
*/
export const DialogStore: StoreCreator<DialogStore> = set => ({
// this is initial state
activeDialogs: [],
// those are actions
showDialog(dialog: EDialog) {
set(state => ({
...state,
activeDialogs: [...state.activeDialogs, dialog],
}));
},
hideDialog(dialog) {
set(state => ({
...state,
activeDialogs: state.activeDialogs.filter(d => d !== dialog),
}));
},
hideAllDialogs() {
set(state => ({
...state,
activeDialogs: [],
}));
},
});
+15 -14
View File
@@ -1,15 +1,11 @@
import { create, StateCreator } from 'zustand';
import { PluginStore } from './pluginStore';
import { DialogStore } from './dialogStore';
import { OverlayStore } from './overlayStore';
import { NavigationStore } from './navigationStore';
export type AppStore = DialogStore &
OverlayStore &
PluginStore &
export type AppStore = PluginStore &
NavigationStore & {
useSwitches: boolean;
setUseSwitches(value: boolean): void;
theme: 'light' | 'dark';
setTheme(value: 'light' | 'dark'): void;
};
// Helper types to use "slicing" like in Redux... We could just not use slicing, but eh
@@ -17,18 +13,23 @@ export type SetStore = Parameters<StateCreator<AppStore>>[0];
export type GetStore = Parameters<StateCreator<AppStore>>[1];
export type StoreCreator<T> = (s: SetStore, g: GetStore) => T;
const getInitialTheme = (): 'light' | 'dark' => {
const stored = localStorage.getItem('theme');
if (stored === 'light' || stored === 'dark') {
return stored;
}
return 'light';
};
export const useAppStore = create<AppStore>((set: SetStore, get: GetStore) => ({
...DialogStore(set, get),
...OverlayStore(set, get),
...PluginStore(set, get),
...NavigationStore(set, get),
useSwitches: !!localStorage.getItem('useSwitches'),
setUseSwitches(uS: boolean) {
theme: getInitialTheme(),
setTheme(theme: 'light' | 'dark') {
set(state => ({
...state,
useSwitches: uS,
theme,
}));
if (uS) localStorage.setItem('useSwitches', 'true');
else localStorage.removeItem('useSwitches');
localStorage.setItem('theme', theme);
},
}));
-30
View File
@@ -1,30 +0,0 @@
import { StoreCreator } from '.';
export type OverlayStore = {
loading: boolean;
showLoading(): void;
hideLoading(): void;
};
/**
* @param set Use this inside actions to change the state
* @param get Use this inside actions to get the current state
*/
export const OverlayStore: StoreCreator<OverlayStore> = set => ({
// this is initial state
loading: false,
// those are actions
showLoading() {
set(state => ({
...state,
loading: true,
}));
},
hideLoading() {
set(state => ({
...state,
loading: false,
}));
},
});