dark mode

This commit is contained in:
M1ngdaXie
2026-02-08 16:23:06 -08:00
parent 10110e26b3
commit 10fd9cdecb
14 changed files with 207 additions and 6 deletions

2
frontend/.gitignore vendored
View File

@@ -24,3 +24,5 @@ dist-ssr
*.sw?
.env
.env.local
/src/assets

View File

@@ -2,7 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/docnest-icon-32.png" />
<link rel="icon" type="image/png" sizes="64x64" href="/docnest-icon-64.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Realtime Collab</title>
<!-- Google Fonts -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -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}

View 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;

View File

@@ -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
View 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
}
};

View File

@@ -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 />

View File

@@ -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;

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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">
<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">