From fbbcb87f505149356fb304892ad0a5c8b4b0f997 Mon Sep 17 00:00:00 2001 From: d-kimsuon Date: Sun, 26 Oct 2025 16:11:44 +0900 Subject: [PATCH] restore theme feature --- src/app/components/MarkdownContent.tsx | 3 +- .../AssistantConversationContent.tsx | 3 +- src/components/SettingsControls.tsx | 18 ++++++---- src/components/ThemeProvider.tsx | 14 ++++++++ src/components/ui/sonner.tsx | 3 +- src/hooks/useTheme.ts | 22 ++++++++++++ src/lib/api/QueryClientProviderWrapper.tsx | 34 +++++-------------- src/main.tsx | 5 ++- src/routes/__root.tsx | 6 ++-- .../platform/services/UserConfigService.ts | 1 + .../hono/middleware/config.middleware.ts | 1 + src/server/lib/config/config.ts | 1 + src/testing/layers/testPlatformLayer.ts | 1 + 13 files changed, 72 insertions(+), 40 deletions(-) create mode 100644 src/components/ThemeProvider.tsx create mode 100644 src/hooks/useTheme.ts diff --git a/src/app/components/MarkdownContent.tsx b/src/app/components/MarkdownContent.tsx index 9235319..e5ccbc4 100644 --- a/src/app/components/MarkdownContent.tsx +++ b/src/app/components/MarkdownContent.tsx @@ -6,6 +6,7 @@ import { oneLight, } from "react-syntax-highlighter/dist/esm/styles/prism"; import remarkGfm from "remark-gfm"; +import { useTheme } from "../../hooks/useTheme"; interface MarkdownContentProps { content: string; @@ -16,7 +17,7 @@ export const MarkdownContent: FC = ({ content, className = "", }) => { - const resolvedTheme = "light" as "light" | "dark"; // TODO: 設定から取り出す + const { resolvedTheme } = useTheme(); const syntaxTheme = resolvedTheme === "dark" ? oneDark : oneLight; return ( diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/AssistantConversationContent.tsx b/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/AssistantConversationContent.tsx index cb8bbd6..426aa3e 100644 --- a/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/AssistantConversationContent.tsx +++ b/src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/AssistantConversationContent.tsx @@ -16,6 +16,7 @@ import { import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema"; import type { AssistantMessageContent } from "@/lib/conversation-schema/message/AssistantMessageSchema"; import { Button } from "../../../../../../../components/ui/button"; +import { useTheme } from "../../../../../../../hooks/useTheme"; import type { SidechainConversation } from "../../../../../../../lib/conversation-schema"; import { MarkdownContent } from "../../../../../../components/MarkdownContent"; import { SidechainConversationModal } from "../conversationModal/SidechainConversationModal"; @@ -38,7 +39,7 @@ export const AssistantConversationContent: FC<{ getSidechainConversationByPrompt, getSidechainConversations, }) => { - const resolvedTheme = "light" as "light" | "dark"; // TODO: 設定から取り出す + const { resolvedTheme } = useTheme(); const syntaxTheme = resolvedTheme === "dark" ? oneDark : oneLight; if (content.type === "text") { return ( diff --git a/src/components/SettingsControls.tsx b/src/components/SettingsControls.tsx index df2e27d..a2a590f 100644 --- a/src/components/SettingsControls.tsx +++ b/src/components/SettingsControls.tsx @@ -10,6 +10,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { useTheme } from "@/hooks/useTheme"; import { projectDetailQuery, projectListQuery } from "../lib/api/queries"; interface SettingsControlsProps { @@ -32,7 +33,7 @@ export const SettingsControls: FC = ({ const themeId = useId(); const { config, updateConfig } = useConfig(); const queryClient = useQueryClient(); - const theme = "system"; // TODO: 設定から取り出す + const { theme } = useTheme(); const { i18n } = useLingui(); const handleHideNoUserMessageChange = async () => { @@ -98,6 +99,14 @@ export const SettingsControls: FC = ({ }); }; + const handleThemeChange = async (value: "light" | "dark" | "system") => { + const newConfig = { + ...config, + theme: value, + }; + updateConfig(newConfig); + }; + return (
@@ -298,12 +307,7 @@ export const SettingsControls: FC = ({ )} - diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..7e8e4ed --- /dev/null +++ b/src/components/ThemeProvider.tsx @@ -0,0 +1,14 @@ +import { type FC, type PropsWithChildren, useEffect } from "react"; +import { useTheme } from "../hooks/useTheme"; + +export const ThemeProvider: FC = ({ children }) => { + const { resolvedTheme } = useTheme(); + + useEffect(() => { + const root = window.document.documentElement; + root.classList.remove("light", "dark"); + root.classList.add(resolvedTheme); + }, [resolvedTheme]); + + return <>{children}; +}; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 403bc73..82fa38c 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,7 +1,8 @@ import { Toaster as Sonner, type ToasterProps } from "sonner"; +import { useTheme } from "../../hooks/useTheme"; const Toaster = ({ ...props }: ToasterProps) => { - const theme = "system"; // TODO: 設定から取り出す + const { theme } = useTheme(); return ( { + const { config } = useConfig(); + const resolvedTheme = useMemo(() => { + if (config?.theme === "light" || config?.theme === "dark") { + return config?.theme; + } + + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + }, [config?.theme]); + + return { + theme: config?.theme ?? "system", + resolvedTheme, + }; +}; diff --git a/src/lib/api/QueryClientProviderWrapper.tsx b/src/lib/api/QueryClientProviderWrapper.tsx index c41d0e9..4d48b20 100644 --- a/src/lib/api/QueryClientProviderWrapper.tsx +++ b/src/lib/api/QueryClientProviderWrapper.tsx @@ -1,36 +1,18 @@ -import { - isServer, - QueryClient, - QueryClientProvider, -} from "@tanstack/react-query"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { FC, PropsWithChildren } from "react"; -let browserQueryClient: QueryClient | undefined; - -export const getQueryClient = () => { - if (isServer) { - return makeQueryClient(); - } else { - browserQueryClient ??= makeQueryClient(); - return browserQueryClient; - } -}; - -export const makeQueryClient = () => - new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: true, - retry: false, - }, +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: true, + retry: false, }, - }); + }, +}); export const QueryClientProviderWrapper: FC = ({ children, }) => { - const queryClient = getQueryClient(); - return ( {children} ); diff --git a/src/main.tsx b/src/main.tsx index 31fdac5..4ea2668 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,7 @@ import ReactDOM from "react-dom/client"; import { routeTree } from "./routeTree.gen"; import "./styles.css"; +import { QueryClientProviderWrapper } from "./lib/api/QueryClientProviderWrapper"; const router = createRouter({ routeTree, @@ -26,7 +27,9 @@ if (rootElement && !rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( - + + + , ); } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 665999f..08f23cc 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -3,15 +3,15 @@ import { createRootRoute, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { RootErrorBoundary } from "../app/components/RootErrorBoundary"; import { SSEEventListeners } from "../app/components/SSEEventListeners"; +import { ThemeProvider } from "../components/ThemeProvider"; import { Toaster } from "../components/ui/sonner"; -import { QueryClientProviderWrapper } from "../lib/api/QueryClientProviderWrapper"; import { LinguiClientProvider } from "../lib/i18n/LinguiProvider"; import { SSEProvider } from "../lib/sse/components/SSEProvider"; export const Route = createRootRoute({ component: () => ( - + @@ -30,7 +30,7 @@ export const Route = createRootRoute({ - + ), diff --git a/src/server/core/platform/services/UserConfigService.ts b/src/server/core/platform/services/UserConfigService.ts index 9187e9a..6f6734e 100644 --- a/src/server/core/platform/services/UserConfigService.ts +++ b/src/server/core/platform/services/UserConfigService.ts @@ -9,6 +9,7 @@ const LayerImpl = Effect.gen(function* () { enterKeyBehavior: "shift-enter-send", permissionMode: "default", locale: "ja", + theme: "system", }); const setUserConfig = (newConfig: UserConfig) => diff --git a/src/server/hono/middleware/config.middleware.ts b/src/server/hono/middleware/config.middleware.ts index 10e5992..b57a5fe 100644 --- a/src/server/hono/middleware/config.middleware.ts +++ b/src/server/hono/middleware/config.middleware.ts @@ -19,6 +19,7 @@ export const configMiddleware = createMiddleware( enterKeyBehavior: "shift-enter-send", permissionMode: "default", locale: "ja", + theme: "system", } satisfies UserConfig), ); } diff --git a/src/server/lib/config/config.ts b/src/server/lib/config/config.ts index 7719fe1..3a6996e 100644 --- a/src/server/lib/config/config.ts +++ b/src/server/lib/config/config.ts @@ -13,6 +13,7 @@ export const userConfigSchema = z.object({ .optional() .default("default"), locale: localeSchema.optional().default("en"), + theme: z.enum(["light", "dark", "system"]).optional().default("system"), }); export type UserConfig = z.infer; diff --git a/src/testing/layers/testPlatformLayer.ts b/src/testing/layers/testPlatformLayer.ts index ed1ab80..8376149 100644 --- a/src/testing/layers/testPlatformLayer.ts +++ b/src/testing/layers/testPlatformLayer.ts @@ -62,6 +62,7 @@ export const testPlatformLayer = (overrides?: { overrides?.userConfig?.enterKeyBehavior ?? "shift-enter-send", permissionMode: overrides?.userConfig?.permissionMode ?? "default", locale: overrides?.userConfig?.locale ?? "ja", + theme: overrides?.userConfig?.theme ?? "system", }), });