diff --git a/src/app/globals.css b/src/app/globals.css index 15ac81a4..08784cb6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -74,6 +74,41 @@ --diff-bg-red: #ff9b9533; --diff-fg-green: #2ddf5966; --diff-bg-green: #82ffa433; + + --graph-bg-color: #fff; + --graph-text-color: #222; + --tbl-bg-color: #fff; + --tbl-text-color: #222; + --popover-bg-color: #f8f9fa; + --popover-text-color: #222; + --tooltip-bg-color: #fff; + --tooltip-text-color: #000; + + --btn-text-normal: #222; + + --cmdk-bg: #fff; + --cmdk-foreground: #222; + --cmdk-group-heading: #888; + --cmdk-scrollbar-track: #f1f1f1; + --cmdk-scrollbar-thumb: #c1c1c1; + --cmdk-list-bg: #fff; + --cmdk-list-shadow: 0 4px 24px 0 rgba(0,0,0,0.08); + --cmdk-list-border: #e5e7eb; + + --rf-controls-bg: #fff; + --rf-controls-border: #e5e7eb; + --rf-controls-icon: #222; + --rf-controls-btn-hover: #f3f4f6; + --rf-controls-btn-bg: #fff; + + --tab-inactive: #52525b; + + --logo-version: #222; + --fullscreen-btn-text: #222; + --fullscreen-btn-bg: #fff; + --fullscreen-btn-border: #e5e7eb; + --fullscreen-btn-hover-bg: #f3f4f6; + --fullscreen-btn-hover-text: #222; } [data-theme="dark"] { @@ -104,6 +139,51 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; + + --graph-bg-color: #23272e; + --graph-text-color: #eee; + --tbl-bg-color: #23272e; + --tbl-text-color: #eee; + --popover-bg-color: #2a2d32; + --popover-text-color: #eee; + --tooltip-bg-color: #23272e; + --tooltip-text-color: #eee; + + --hl-key: #9cdcfe; + --hl-string: #ce9178; + --hl-number: #b5cea8; + --hl-null: #d16969; + --hl-empty: #ffffff4d; + --hl-index: #ffffff4d; + + --btn-text-normal: #e6e6e6; + + --cmdk-bg: #23272e; + --cmdk-foreground: #e6e6e6; + --cmdk-group-heading: #b0b0b0; + --cmdk-scrollbar-track: #23272e; + --cmdk-scrollbar-thumb: #444c56; + --cmdk-list-bg: #23272e; + --cmdk-list-shadow: 0 4px 24px 0 rgba(0,0,0,0.32); + --cmdk-list-border: #444c56; + + --kbd-bg-color: #2a2d32; + --kbd-color: #e6e6e6; + + --rf-controls-bg: #23272e; + --rf-controls-border: #444c56; + --rf-controls-icon: #e6e6e6; + --rf-controls-btn-hover: #2a2d32; + --rf-controls-btn-bg: #23272e; + + --tab-inactive: #e0e3ea; + + --logo-version: #e6e6e6; + --fullscreen-btn-text: #e6e6e6; + --fullscreen-btn-bg: #23272e; + --fullscreen-btn-border: #444c56; + --fullscreen-btn-hover-bg: #2a2d32; + --fullscreen-btn-hover-text: #fff; } } @@ -229,7 +309,8 @@ body { .graph-node { min-width: fit-content; border-width: 1px; - background-color: rgb(255 255 255); + background-color: var(--graph-bg-color); + color: var(--graph-text-color); font-size: var(--graph-font-size); font-family: var(--graph-font-family); } @@ -272,7 +353,8 @@ body { .popover-item { width: max-content; - background: #f8f9fa; + background: var(--popover-bg-color); + color: var(--popover-text-color); border: 1px solid #dee2e6; word-wrap: break-word; font-size: 12px; @@ -287,7 +369,8 @@ body { border-collapse: collapse; th { - background-color: var(--tbl-key-bg-color); + background-color: var(--tbl-bg-color); + color: var(--tbl-text-color); font-weight: normal; text-align: left; vertical-align: top; @@ -301,6 +384,8 @@ body { td { position: relative; + background-color: var(--tbl-bg-color); + color: var(--tbl-text-color); font-weight: normal; text-align: left; vertical-align: top; @@ -366,12 +451,10 @@ body { padding: 15px; position: absolute; z-index: 1; - color: #000000; + color: var(--tooltip-text-color); font-family: var(--graph-font-family); - - /* stolen from https://www.radix-ui.com/primitives/docs/components/popover */ border-radius: 4px; - background-color: white; + background-color: var(--tooltip-bg-color); box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px; @@ -416,14 +499,16 @@ body { } .search-cmd-kbd { - background: var(--kbd-bg-color); - color: var(--kbd-color); + background: var(--kbd-bg-color) !important; + color: var(--kbd-color) !important; border: 1px solid hsl(var(--border)); border-radius: 4px; height: 20px; line-height: 20px; padding: 0 6px; font-family: var(--graph-font-family); + font-size: 13px; + display: inline-block; } #cmd-panel { @@ -503,3 +588,113 @@ body { transform: translateX(0); } } + +.text-hl-key { + color: var(--hl-key); +} +.text-hl-string { + color: var(--hl-string); +} +.text-hl-number { + color: var(--hl-number); +} +.text-hl-null { + color: var(--hl-null); +} +.text-hl-empty { + color: var(--hl-empty); +} +.text-hl-index { + color: var(--hl-index); +} + +.text-btn { + color: var(--btn-text-normal) !important; +} + +.cmdk-group, +[cmdk-group] { + background: var(--cmdk-bg) !important; + color: var(--cmdk-foreground) !important; +} +[cmdk-group-heading] { + color: var(--cmdk-group-heading) !important; +} +.cmdk-group .scrollbar-thin, +[cmdk-group] .scrollbar-thin { + scrollbar-color: var(--cmdk-scrollbar-thumb) var(--cmdk-scrollbar-track); +} +.cmdk-group .scrollbar-thin::-webkit-scrollbar, +[cmdk-group] .scrollbar-thin::-webkit-scrollbar { + width: 6px; + background: var(--cmdk-scrollbar-track); +} +.cmdk-group .scrollbar-thin::-webkit-scrollbar-thumb, +[cmdk-group] .scrollbar-thin::-webkit-scrollbar-thumb { + background: var(--cmdk-scrollbar-thumb); + border-radius: 6px; +} + +.cmdk-list, +[cmdk-list] { + background: var(--cmdk-list-bg) !important; + box-shadow: var(--cmdk-list-shadow) !important; + border: 1px solid var(--cmdk-list-border) !important; + color: var(--cmdk-foreground) !important; +} + +.react-flow__controls { + background: var(--rf-controls-bg) !important; + border: 1px solid var(--rf-controls-border) !important; + border-radius: 8px !important; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.08); + padding: 4px; +} +.react-flow__controls-button { + background: var(--rf-controls-btn-bg) !important; + border: none; + color: var(--rf-controls-icon) !important; + fill: var(--rf-controls-icon) !important; + transition: background 0.2s; +} +.react-flow__controls-button svg { + color: var(--rf-controls-icon) !important; + fill: var(--rf-controls-icon) !important; +} +.react-flow__controls-button:hover { + background: var(--rf-controls-btn-hover) !important; +} + +.react-flow__attribution { + background: none !important; + box-shadow: none !important; + color: var(--rf-controls-icon) !important; +} + +[data-orientation="horizontal"], +[role="tablist"] .dark\:text-zinc-200 { +} +[role="tab"][data-state="inactive"], +button[role="tab"][data-state="inactive"] { + color: var(--tab-inactive) !important; +} + +a.flex.items-center.pointer .w-24 { + color: var(--logo-version) !important; +} +button[title~="full"][class*="inline-flex"] { + color: var(--fullscreen-btn-text) !important; + background: var(--fullscreen-btn-bg) !important; + border-color: var(--fullscreen-btn-border) !important; +} +button[title~="full"][class*="inline-flex"]:hover { + background: var(--fullscreen-btn-hover-bg) !important; + color: var(--fullscreen-btn-hover-text) !important; +} + +a.flex.items-center.pointer img[src$="icon.svg"] { + transition: filter 0.2s; +} +[data-theme="dark"] a.flex.items-center.pointer img[src$="icon.svg"] { + filter: invert(1) brightness(2); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 97fe72ed..a913a0ef 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { GoogleAnalytics } from "@next/third-parties/google"; import { NextIntlClientProvider } from "next-intl"; import { getLocale, getMessages, getTranslations } from "next-intl/server"; import { ThemeProvider } from "next-themes"; +import ClientOnly from "@/components/ClientOnly"; export async function generateMetadata() { const locale = await getLocale(); @@ -62,7 +63,9 @@ export default async function MainLayout({ children }: { children: React.ReactNo {/* TODO: support dark theme */} - {children} + + {children} + diff --git a/src/components/ClientOnly.tsx b/src/components/ClientOnly.tsx new file mode 100644 index 00000000..3b86d427 --- /dev/null +++ b/src/components/ClientOnly.tsx @@ -0,0 +1,9 @@ +"use client"; +import React, { useState, useEffect } from "react"; + +export default function ClientOnly({ children }: { children: React.ReactNode }) { + const [mounted, setMounted] = useState(false); + useEffect(() => { setMounted(true); }, []); + if (!mounted) return null; + return <>{children}; +} \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index d218e656..0314052f 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -18,8 +18,8 @@ const buttonVariants = cva( secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "text-btn hover:bg-accent hover:text-accent-foreground", - icon: "text-black hover:bg-accent hover:text-accent-foreground", - "icon-outline": "text-black border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + icon: "text-foreground hover:bg-accent hover:text-accent-foreground", + "icon-outline": "text-foreground border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", input: "border border-input shadow-sm hover:cursor-text bg-input text-btn-input", }, diff --git a/src/containers/editor/RightPanel.tsx b/src/containers/editor/RightPanel.tsx index 9e2b0f2c..8f92f2a1 100644 --- a/src/containers/editor/RightPanel.tsx +++ b/src/containers/editor/RightPanel.tsx @@ -3,7 +3,6 @@ import * as React from "react"; import { useState } from "react"; import { Container, ContainerContent, ContainerHeader } from "@/components/Container"; -import { Button } from "@/components/ui/button"; import ViewSearchInput from "@/components/ui/search/ViewSearchInput"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -15,9 +14,11 @@ import { ViewMode, ViewModeValue } from "@/lib/db/config"; import { useEditorStore } from "@/stores/editorStore"; import { useConfigFromCookies } from "@/stores/hook"; import { useStatusStore } from "@/stores/statusStore"; -import { Expand, Shrink, Table2, Text, Waypoints } from "lucide-react"; +import { Expand, Shrink, Table2, Text, Waypoints, Sun, Moon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useShallow } from "zustand/shallow"; +import { useTheme } from "next-themes"; +import Button from "@/containers/editor/sidenav/Button"; export default function RightPanel() { const cc = useConfigFromCookies(); @@ -65,6 +66,9 @@ function Buttons({ viewMode }: { viewMode: ViewMode }) { setEnableTextCompare: state.setEnableTextCompare, })), ); + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = React.useState(false); + React.useEffect(() => { setMounted(true); }, []); return (
@@ -72,19 +76,33 @@ function Buttons({ viewMode }: { viewMode: ViewMode }) { <>
- +
)} {viewMode === ViewMode.Graph && } +
); } +function ThemeButton() { + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = React.useState(false); + React.useEffect(() => { setMounted(true); }, []); + const isDark = theme === "dark"; + return ( + + /> ); } diff --git a/src/containers/editor/editor/Editor.tsx b/src/containers/editor/editor/Editor.tsx index 011cf469..458d6cc3 100644 --- a/src/containers/editor/editor/Editor.tsx +++ b/src/containers/editor/editor/Editor.tsx @@ -10,6 +10,7 @@ import { getTree } from "@/stores/treeStore"; import { loader, Editor as MonacoEditor } from "@monaco-editor/react"; import { useTranslations } from "next-intl"; import { useShallow } from "zustand/shallow"; +import { useTheme } from "next-themes"; loader.config({ paths: { vs: vsURL } }); @@ -21,6 +22,7 @@ export default function Editor({ kind, ...props }: EditorProps) { const translations = useTranslations(); const setEditor = useEditorStore((state) => state.setEditor); const setTranslations = useEditorStore((state) => state.setTranslations); + const { theme } = useTheme(); useDisplayExample(); useRevealNode(); @@ -29,6 +31,7 @@ export default function Editor({ kind, ...props }: EditorProps) { } + theme={theme === "dark" ? "vs-dark" : "light"} options={{ fontSize: 13, // 设置初始字体大小 scrollBeyondLastLine: false, // 行数超过一屏时才展示滚动条 @@ -58,6 +61,8 @@ export default function Editor({ kind, ...props }: EditorProps) { setEditor(wrapper); setTranslations(translations); console.l(`finished initial editor ${kind}:`, wrapper); + // 同步设置monaco主题 + monaco.editor.setTheme(theme === "dark" ? "vs-dark" : "light"); }} {...props} /> diff --git a/src/containers/editor/sidenav/index.tsx b/src/containers/editor/sidenav/index.tsx index b3ba7706..cbd1e001 100644 --- a/src/containers/editor/sidenav/index.tsx +++ b/src/containers/editor/sidenav/index.tsx @@ -31,6 +31,8 @@ import PopoverBtn, { popoverBtnClass } from "./PopoverButton"; import SharePopover from "./SharePopover"; import StatisticsPopover from "./StatisticsPopover"; import Toggle from "./Toggle"; +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; export default function SideNav() { const cc = useConfigFromCookies(); @@ -63,6 +65,9 @@ export default function SideNav() { }; }), ); + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + useEffect(() => { setMounted(true); }, []); return (
+