diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx index ac3bbfec..a501cd6a 100644 --- a/apps/web/src/components/EditorPane.tsx +++ b/apps/web/src/components/EditorPane.tsx @@ -50,6 +50,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import { EditorToolbar } from "./EditorToolbar"; +import { ThemeToggle } from "./ThemeToggle"; import { RevisionHistoryDialog } from "./dialogs/RevisionHistoryDialog"; import { api } from "@/lib/api"; import { consumeStandaloneMobileEditorReturn, openStandaloneMobileEditor } from "@/lib/mobile-editor"; @@ -2330,13 +2331,14 @@ const RichEditorPane = ({ )} - - - + + {!readOnly && ( + ); +}; diff --git a/apps/web/src/components/settings/PreferenceCard.tsx b/apps/web/src/components/settings/PreferenceCard.tsx index 96314728..498141c3 100644 --- a/apps/web/src/components/settings/PreferenceCard.tsx +++ b/apps/web/src/components/settings/PreferenceCard.tsx @@ -13,6 +13,7 @@ import { type AppLocalePreference, } from "@/i18n"; import { ShortcutSettingsItem } from "./ShortcutSettingsItem"; +import { useTheme, type ThemePreference } from "../ThemeProvider"; interface PreferenceCardProps { imageCompressionEnabled: boolean; @@ -28,6 +29,7 @@ export const PreferenceCard = ({ onShortcutSettingsChange, }: PreferenceCardProps) => { const { t } = useTranslation(); + const { preference: themePreference, setPreference: setThemePreference } = useTheme(); const [activeLocalePreference, setActiveLocalePreference] = useState(() => getAppLocalePreference()); const handleLocalePreferenceChange = (preference: AppLocalePreference) => { @@ -82,6 +84,28 @@ export const PreferenceCard = ({ /> + +
+
+
{t("settings.themeTitle")}
+
{t("settings.themeDescription")}
+
+
+ +
+
- - - + + + + + ); diff --git a/apps/web/src/mobile-edit.tsx b/apps/web/src/mobile-edit.tsx index a37b0696..b28e2b06 100644 --- a/apps/web/src/mobile-edit.tsx +++ b/apps/web/src/mobile-edit.tsx @@ -1,6 +1,7 @@ import React from "react"; import { createRoot } from "react-dom/client"; import { MobileStandaloneTiptapEditor } from "@/components/MobileStandaloneTiptapEditor"; +import { initializeTheme } from "@/components/ThemeProvider"; import "./styles/mobile-markdown-editor.css"; const root = document.getElementById("mobile-editor-root"); @@ -9,6 +10,8 @@ if (!root) { throw new Error("Mobile editor root not found"); } +initializeTheme(); + createRoot(root).render( diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index 1f857bc6..3944ba16 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -25,6 +25,17 @@ --brand-green-800-rgb: 0 92 25; --brand-green-900-rgb: 0 67 18; --brand-green-950-rgb: 0 43 11; + --slate-50-rgb: 248 250 252; + --slate-100-rgb: 241 245 249; + --slate-200-rgb: 226 232 240; + --slate-300-rgb: 203 213 225; + --slate-400-rgb: 148 163 184; + --slate-500-rgb: 100 116 139; + --slate-600-rgb: 71 85 105; + --slate-700-rgb: 51 65 85; + --slate-800-rgb: 30 41 59; + --slate-900-rgb: 15 23 42; + --dark-surface-rgb: 255 255 255; --editor-body-font-size: 0.9375rem; --editor-body-line-height: 1.6; --editor-paragraph-spacing: 0.45rem; @@ -66,6 +77,59 @@ -moz-osx-font-smoothing: grayscale; } +:root.dark { + color-scheme: dark; + --background: 222 47% 11%; + --foreground: 210 40% 98%; + --card: 217 33% 17%; + --card-foreground: 210 40% 98%; + --popover: 217 33% 17%; + --popover-foreground: 210 40% 98%; + --primary: 142 71% 45%; + --primary-foreground: 222 47% 11%; + --secondary: 217 33% 24%; + --secondary-foreground: 142 71% 75%; + --muted: 217 33% 24%; + --muted-foreground: 215 20% 65%; + --accent: 217 33% 24%; + --accent-foreground: 142 71% 75%; + --destructive: 0 72% 51%; + --destructive-foreground: 210 40% 98%; + --border: 215 28% 28%; + --input: 215 28% 28%; + --ring: 142 71% 45%; + --slate-50-rgb: 15 23 42; + --slate-100-rgb: 30 41 59; + --slate-200-rgb: 51 65 85; + --slate-300-rgb: 71 85 105; + --slate-400-rgb: 100 116 139; + --slate-500-rgb: 148 163 184; + --slate-600-rgb: 203 213 225; + --slate-700-rgb: 226 232 240; + --slate-800-rgb: 241 245 249; + --slate-900-rgb: 248 250 252; + --dark-surface-rgb: 30 41 59; + --brand-green: #4ade80; + --brand-green-hover: #22c55e; + --brand-green-muted: #14532d; + --brand-green-soft: #052e16; + --brand-green-border: #166534; + --brand-green-text: #86efac; + --brand-green-rgb: 74 222 128; + --brand-green-hover-rgb: 34 197 94; + --brand-green-50-rgb: 20 83 45; + --brand-green-100-rgb: 22 101 52; + --brand-green-200-rgb: 34 133 67; + --brand-green-300-rgb: 74 222 128; + --brand-green-400-rgb: 134 239 172; + --brand-green-500-rgb: 74 222 128; + --brand-green-600-rgb: 34 197 94; + --brand-green-700-rgb: 134 239 172; + --brand-green-800-rgb: 187 247 208; + --brand-green-900-rgb: 220 252 231; + --brand-green-950-rgb: 240 253 244; +} + html, body, #root { @@ -90,6 +154,11 @@ body { overscroll-behavior-y: auto; } +:root.dark body { + background: #0f172a; + color: #f8fafc; +} + button, input, textarea { @@ -457,3 +526,153 @@ textarea { .markdown-content a:hover { color: var(--brand-green); } + +/* Theme-aware surfaces for legacy white utilities and fixed semantic colors. */ +:root.dark [class~="bg-white"] { + background-color: rgb(var(--dark-surface-rgb)) !important; +} + +:root.dark [class~="bg-white/70"] { + background-color: rgb(var(--dark-surface-rgb) / 0.7) !important; +} + +:root.dark [class~="bg-white/75"] { + background-color: rgb(var(--dark-surface-rgb) / 0.75) !important; +} + +:root.dark [class~="bg-white/80"] { + background-color: rgb(var(--dark-surface-rgb) / 0.8) !important; +} + +:root.dark [class~="bg-white/85"] { + background-color: rgb(var(--dark-surface-rgb) / 0.85) !important; +} + +:root.dark [class~="bg-white/90"] { + background-color: rgb(var(--dark-surface-rgb) / 0.9) !important; +} + +:root.dark [class~="bg-white/95"] { + background-color: rgb(var(--dark-surface-rgb) / 0.95) !important; +} + +:root.dark [class~="border-white"] { + border-color: rgb(var(--slate-200-rgb)) !important; +} + +:root.dark [class~="text-slate-950"] { + color: hsl(var(--foreground)) !important; +} + +:root.dark [class~="bg-slate-50"], +:root.dark [class*="bg-slate-50/"] { background-color: rgb(var(--slate-50-rgb)) !important; } +:root.dark [class~="bg-slate-100"], +:root.dark [class*="bg-slate-100/"] { background-color: rgb(var(--slate-100-rgb)) !important; } +:root.dark [class~="bg-slate-200"], +:root.dark [class*="bg-slate-200/"] { background-color: rgb(var(--slate-200-rgb)) !important; } +:root.dark [class~="bg-slate-300"], +:root.dark [class*="bg-slate-300/"] { background-color: rgb(var(--slate-300-rgb)) !important; } +:root.dark [class~="bg-slate-400"], +:root.dark [class*="bg-slate-400/"] { background-color: rgb(var(--slate-400-rgb)) !important; } +:root.dark [class~="bg-slate-500"], +:root.dark [class*="bg-slate-500/"] { background-color: rgb(var(--slate-500-rgb)) !important; } +:root.dark [class~="bg-slate-600"], +:root.dark [class*="bg-slate-600/"] { background-color: rgb(var(--slate-600-rgb)) !important; } +:root.dark [class~="bg-slate-700"], +:root.dark [class*="bg-slate-700/"] { background-color: rgb(var(--slate-700-rgb)) !important; } +:root.dark [class~="bg-slate-800"], +:root.dark [class*="bg-slate-800/"] { background-color: rgb(var(--slate-800-rgb)) !important; } +:root.dark [class~="bg-slate-900"], +:root.dark [class*="bg-slate-900/"] { background-color: rgb(var(--slate-900-rgb)) !important; } + +:root.dark [class~="text-slate-50"] { color: rgb(var(--slate-50-rgb)) !important; } +:root.dark [class~="text-slate-100"] { color: rgb(var(--slate-100-rgb)) !important; } +:root.dark [class~="text-slate-200"] { color: rgb(var(--slate-200-rgb)) !important; } +:root.dark [class~="text-slate-300"] { color: rgb(var(--slate-300-rgb)) !important; } +:root.dark [class~="text-slate-400"] { color: rgb(var(--slate-400-rgb)) !important; } +:root.dark [class~="text-slate-500"] { color: rgb(var(--slate-500-rgb)) !important; } +:root.dark [class~="text-slate-600"] { color: rgb(var(--slate-600-rgb)) !important; } +:root.dark [class~="text-slate-700"] { color: rgb(var(--slate-700-rgb)) !important; } +:root.dark [class~="text-slate-800"] { color: rgb(var(--slate-800-rgb)) !important; } +:root.dark [class~="text-slate-900"] { color: rgb(var(--slate-900-rgb)) !important; } + +:root.dark [class~="border-slate-50"], +:root.dark [class*="border-slate-50/"] { border-color: rgb(var(--slate-50-rgb)) !important; } +:root.dark [class~="border-slate-100"], +:root.dark [class*="border-slate-100/"] { border-color: rgb(var(--slate-100-rgb)) !important; } +:root.dark [class~="border-slate-200"], +:root.dark [class*="border-slate-200/"] { border-color: rgb(var(--slate-200-rgb)) !important; } +:root.dark [class~="border-slate-300"], +:root.dark [class*="border-slate-300/"] { border-color: rgb(var(--slate-300-rgb)) !important; } +:root.dark [class~="border-slate-400"], +:root.dark [class*="border-slate-400/"] { border-color: rgb(var(--slate-400-rgb)) !important; } + +:root.dark [class*="hover:bg-slate-50"]:hover, +:root.dark [class*="hover:bg-slate-100"]:hover { background-color: rgb(var(--slate-100-rgb)) !important; } + +:root.dark [class*="hover:text-slate-900"]:hover, +:root.dark [class*="focus:text-slate-900"]:focus { color: rgb(var(--slate-900-rgb)) !important; } + +:root.dark [class~="bg-amber-50"], +:root.dark [class~="bg-amber-50/95"] { + background-color: #451a03 !important; +} + +:root.dark [class~="text-amber-700"], +:root.dark [class~="text-amber-800"] { + color: #fcd34d !important; +} + +:root.dark [class~="bg-rose-50"], +:root.dark [class~="bg-rose-50/30"] { + background-color: #4c0519 !important; +} + +:root.dark [class~="text-rose-600"], +:root.dark [class~="text-rose-700"] { + color: #fda4af !important; +} + +:root.dark [class~="border-rose-100"], +:root.dark [class~="border-rose-200"] { + border-color: #881337 !important; +} + +:root.dark .ProseMirror blockquote { + color: #cbd5e1; + background: var(--brand-green-soft); +} + +:root.dark .ProseMirror code, +:root.dark .ProseMirror pre { + border-color: #166534; + background: #13261a; + color: #cbd5e1; +} + +:root.dark .markdown-content { + color: #cbd5e1; +} + +:root.dark .markdown-content h1, +:root.dark .markdown-content h2, +:root.dark .markdown-content h3 { + color: #f8fafc; +} + +:root.dark .markdown-content h1, +:root.dark .markdown-content hr { + border-color: #334155; +} + +:root.dark .markdown-content pre { + background-color: #13261a; + border-color: #166534; + color: #cbd5e1; +} + +:root.dark .markdown-content code { + background-color: #13261a; + border-color: #166534; + color: #cbd5e1; +} diff --git a/apps/web/src/styles/mobile-markdown-editor.css b/apps/web/src/styles/mobile-markdown-editor.css index eac9bdc6..26149757 100644 --- a/apps/web/src/styles/mobile-markdown-editor.css +++ b/apps/web/src/styles/mobile-markdown-editor.css @@ -256,6 +256,64 @@ input { padding: 8px 0 max(10px, env(safe-area-inset-bottom)); } +:root.dark { + color-scheme: dark; +} + +:root.dark body, +:root.dark .mobile-editor-shell { + background: #0f172a; + color: #f8fafc; +} + +:root.dark .mobile-editor-header, +:root.dark .mobile-editor-title, +:root.dark .mobile-editor-tags, +:root.dark .mobile-editor-notebook-sheet { + background: #1e293b; + color: #f8fafc; +} + +:root.dark .mobile-editor-header, +:root.dark .mobile-editor-tool-row { + border-color: #334155; +} + +:root.dark .mobile-editor-back, +:root.dark .mobile-editor-tool-button { + color: #cbd5e1; +} + +:root.dark .mobile-editor-status { + background: #334155; + color: #cbd5e1; +} + +:root.dark .mobile-editor-status.active, +:root.dark .mobile-editor-tool-button:active, +:root.dark .mobile-editor-tool-button[aria-pressed="true"] { + background: #14532d; + border-color: #166534; + color: #86efac; +} + +:root.dark .mobile-editor-status.error, +:root.dark .mobile-editor-error { + background: #4c0519; + border-color: #881337; + color: #fda4af; +} + +:root.dark .mobile-editor-notebook-button, +:root.dark .mobile-editor-tags::placeholder, +:root.dark .mobile-editor-title::placeholder { + color: #94a3b8; +} + +:root.dark .mobile-editor-sheet-backdrop { + background: rgb(2 6 23 / 0.62); +} + .mobile-editor-notebook-sheet-handle { width: 38px; height: 4px; diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index 133468de..f3682257 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -2,8 +2,10 @@ import type { Config } from "tailwindcss"; import tailwindAnimate from "tailwindcss-animate"; const brandGreen = (shade: number) => `rgb(var(--brand-green-${shade}-rgb) / )`; +const slate = (shade: number) => `rgb(var(--slate-${shade}-rgb) / )`; export default { + darkMode: "class", content: { relative: true, files: ["./index.html", "./src/**/*.{ts,tsx}"], @@ -30,6 +32,19 @@ export default { sm: "calc(var(--radius) - 4px)", }, colors: { + slate: { + 50: slate(50), + 100: slate(100), + 200: slate(200), + 300: slate(300), + 400: slate(400), + 500: slate(500), + 600: slate(600), + 700: slate(700), + 800: slate(800), + 900: slate(900), + 950: "#020617", + }, border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 193a4c26..0c82e207 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -47,8 +47,8 @@ export default defineConfig({ start_url: "/", scope: "/", display: "standalone", - background_color: "#f8fafc", - theme_color: "#f8fafc", + background_color: "#0f172a", + theme_color: "#0f172a", lang: "zh-CN", categories: ["productivity", "utilities"], icons: [