dark mode
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import PixelIcon from '@/components/PixelIcon/PixelIcon';
|
||||
import { LogOut } from 'lucide-react';
|
||||
import DocNestLogo from '@/assets/docnest/docnest-icon-128.png';
|
||||
import ThemeToggle from '@/components/ThemeToggle';
|
||||
|
||||
function Navbar() {
|
||||
const { user, logout } = useAuth();
|
||||
@@ -35,12 +36,18 @@ function Navbar() {
|
||||
gap-2
|
||||
"
|
||||
>
|
||||
<PixelIcon name="gem" size={18} color="hsl(var(--brand-teal))" />
|
||||
<img
|
||||
src={DocNestLogo}
|
||||
alt="DocNest"
|
||||
className="w-6 h-6"
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
DocNest
|
||||
</a>
|
||||
|
||||
{/* User Section */}
|
||||
<div className="flex items-center gap-4">
|
||||
<ThemeToggle className="shadow-soft hover:shadow-card transition-all duration-150" />
|
||||
{user.avatar_url && (
|
||||
<img
|
||||
src={user.avatar_url}
|
||||
|
||||
35
frontend/src/components/ThemeToggle.tsx
Normal file
35
frontend/src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { applyTheme, getPreferredTheme, type ThemeMode } from "@/lib/theme";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type ThemeToggleProps = {
|
||||
className?: string;
|
||||
size?: "sm" | "default" | "icon";
|
||||
};
|
||||
|
||||
function ThemeToggle({ className, size = "icon" }: ThemeToggleProps) {
|
||||
const [theme, setTheme] = useState<ThemeMode>(() => getPreferredTheme());
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
const nextTheme: ThemeMode = theme === "dark" ? "light" : "dark";
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size={size}
|
||||
onClick={() => setTheme(nextTheme)}
|
||||
className={className}
|
||||
aria-label={`Switch to ${nextTheme} mode`}
|
||||
title={`Switch to ${nextTheme} mode`}
|
||||
>
|
||||
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemeToggle;
|
||||
@@ -27,6 +27,28 @@
|
||||
--ring: 214 89% 52%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 215 26% 7%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 215 21% 11%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 215 21% 11%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 213 93% 60%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 173 70% 42%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
--muted: 215 15% 15%;
|
||||
--muted-foreground: 215 10% 58%;
|
||||
--accent: 197 100% 68%;
|
||||
--accent-foreground: 215 26% 7%;
|
||||
--destructive: 0 70% 52%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 215 12% 21%;
|
||||
--input: 215 12% 21%;
|
||||
--ring: 213 93% 60%;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -81,6 +103,47 @@
|
||||
--pixel-text-muted: #64748B;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--surface: 215 21% 11%;
|
||||
--surface-muted: 215 15% 15%;
|
||||
--text-primary: 0 0% 98%;
|
||||
--text-secondary: 215 15% 82%;
|
||||
--text-muted: 215 10% 70%;
|
||||
--brand: 213 93% 60%;
|
||||
--brand-dark: 213 90% 52%;
|
||||
--brand-teal: 173 70% 42%;
|
||||
--brand-teal-dark: 173 68% 34%;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45);
|
||||
--shadow-md: 0 12px 30px rgba(0, 0, 0, 0.55);
|
||||
--shadow-lg: 0 20px 50px rgba(0, 0, 0, 0.65);
|
||||
--focus-ring: 0 0 0 3px rgba(88, 166, 255, 0.35);
|
||||
--gradient-hero: linear-gradient(120deg, #0d1117 0%, #111827 55%, #161b22 100%);
|
||||
--gradient-accent: linear-gradient(120deg, #2f81f7 0%, #14b8a6 100%);
|
||||
|
||||
--pixel-purple-deep: #0b1f4b;
|
||||
--pixel-purple-bright: #2f81f7;
|
||||
--pixel-pink-vibrant: #58a6ff;
|
||||
--pixel-cyan-bright: #14b8a6;
|
||||
--pixel-orange-warm: #f59e0b;
|
||||
--pixel-yellow-gold: #fbbf24;
|
||||
--pixel-green-lime: #22c55e;
|
||||
--pixel-green-forest: #16a34a;
|
||||
|
||||
--pixel-bg-dark: #0d1117;
|
||||
--pixel-bg-medium: #161b22;
|
||||
--pixel-bg-light: #1f2937;
|
||||
--pixel-panel: #0f172a;
|
||||
--pixel-white: #e5e7eb;
|
||||
|
||||
--pixel-shadow-dark: rgba(0, 0, 0, 0.5);
|
||||
--pixel-outline: #30363d;
|
||||
|
||||
--pixel-text-primary: #e5e7eb;
|
||||
--pixel-text-secondary: #c9d1d9;
|
||||
--pixel-text-muted: #8b949e;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
|
||||
37
frontend/src/lib/theme.ts
Normal file
37
frontend/src/lib/theme.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export type ThemeMode = "light" | "dark";
|
||||
|
||||
export const getStoredTheme = (): ThemeMode | null => {
|
||||
try {
|
||||
const value = localStorage.getItem("theme");
|
||||
if (value === "light" || value === "dark") {
|
||||
return value;
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage access errors
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getPreferredTheme = (): ThemeMode => {
|
||||
const stored = getStoredTheme();
|
||||
if (stored) return stored;
|
||||
|
||||
if (typeof window !== "undefined" && window.matchMedia) {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
return "light";
|
||||
};
|
||||
|
||||
export const applyTheme = (theme: ThemeMode) => {
|
||||
if (typeof document === "undefined") return;
|
||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||
|
||||
try {
|
||||
localStorage.setItem("theme", theme);
|
||||
} catch {
|
||||
// Ignore storage access errors
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import { applyTheme, getPreferredTheme } from "./lib/theme";
|
||||
import "./index.css";
|
||||
|
||||
applyTheme(getPreferredTheme());
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
.landing-theme-toggle {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Hero Section
|
||||
======================================== */
|
||||
@@ -50,6 +57,12 @@
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-logo-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.hero-brand {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import FloatingGem from '../components/PixelSprites/FloatingGem';
|
||||
import PixelIcon from '../components/PixelIcon/PixelIcon';
|
||||
import DocNestLogo from '../assets/docnest/docnest-icon-128.png';
|
||||
import ThemeToggle from '../components/ThemeToggle';
|
||||
import { API_BASE_URL } from '../config';
|
||||
import './LandingPage.css';
|
||||
|
||||
@@ -14,6 +16,9 @@ function LandingPage() {
|
||||
|
||||
return (
|
||||
<div className="landing-page">
|
||||
<div className="landing-theme-toggle">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
{/* Hero Section */}
|
||||
<section className="landing-hero">
|
||||
<div className="hero-gem hero-gem-one">
|
||||
@@ -26,7 +31,11 @@ function LandingPage() {
|
||||
<div className="hero-grid">
|
||||
<div className="hero-content">
|
||||
<div className="hero-logo">
|
||||
<PixelIcon name="gem" size={28} color="hsl(var(--brand-teal))" />
|
||||
<img
|
||||
src={DocNestLogo}
|
||||
alt="DocNest"
|
||||
className="hero-logo-icon"
|
||||
/>
|
||||
<span className="hero-brand">DocNest</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
justify-content: center;
|
||||
background: var(--gradient-hero);
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
@@ -18,11 +19,32 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-theme-toggle {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--text-primary));
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { API_BASE_URL } from '../config';
|
||||
import DocNestLogo from '../assets/docnest/docnest-icon-128.png';
|
||||
import ThemeToggle from '../components/ThemeToggle';
|
||||
import './LoginPage.css';
|
||||
|
||||
function LoginPage() {
|
||||
@@ -34,8 +36,14 @@ function LoginPage() {
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-theme-toggle">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<div className="login-container">
|
||||
<h1 className="login-title">DocNest</h1>
|
||||
<div className="login-brand">
|
||||
<img src={DocNestLogo} alt="DocNest" className="login-logo" />
|
||||
<h1 className="login-title">DocNest</h1>
|
||||
</div>
|
||||
<p className="login-subtitle">Collaborate in real time with your team</p>
|
||||
|
||||
<div className="login-buttons">
|
||||
|
||||
Reference in New Issue
Block a user