restore theme feature

This commit is contained in:
d-kimsuon
2025-10-26 16:11:44 +09:00
parent aa7616a5c7
commit fbbcb87f50
13 changed files with 72 additions and 40 deletions

View File

@@ -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<MarkdownContentProps> = ({
content,
className = "",
}) => {
const resolvedTheme = "light" as "light" | "dark"; // TODO: 設定から取り出す
const { resolvedTheme } = useTheme();
const syntaxTheme = resolvedTheme === "dark" ? oneDark : oneLight;
return (

View File

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

View File

@@ -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<SettingsControlsProps> = ({
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<SettingsControlsProps> = ({
});
};
const handleThemeChange = async (value: "light" | "dark" | "system") => {
const newConfig = {
...config,
theme: value,
};
updateConfig(newConfig);
};
return (
<div className={`space-y-4 ${className}`}>
<div className="flex items-center space-x-2">
@@ -298,12 +307,7 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
<Trans id="settings.theme" message="Theme" />
</label>
)}
<Select
value={theme || "system"}
onValueChange={() => {
// TODO: 設定を更新する
}}
>
<Select value={theme ?? "system"} onValueChange={handleThemeChange}>
<SelectTrigger id={themeId} className="w-full">
<SelectValue placeholder={i18n._("Select theme")} />
</SelectTrigger>

View File

@@ -0,0 +1,14 @@
import { type FC, type PropsWithChildren, useEffect } from "react";
import { useTheme } from "../hooks/useTheme";
export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
const { resolvedTheme } = useTheme();
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(resolvedTheme);
}, [resolvedTheme]);
return <>{children}</>;
};

View File

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

22
src/hooks/useTheme.ts Normal file
View File

@@ -0,0 +1,22 @@
import { useMemo } from "react";
import { useConfig } from "../app/hooks/useConfig";
type ResolvedTheme = "light" | "dark";
export const useTheme = () => {
const { config } = useConfig();
const resolvedTheme = useMemo<ResolvedTheme>(() => {
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,
};
};

View File

@@ -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<PropsWithChildren> = ({
children,
}) => {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

View File

@@ -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(
<StrictMode>
<RouterProvider router={router} />
<QueryClientProviderWrapper>
<RouterProvider router={router} />
</QueryClientProviderWrapper>
</StrictMode>,
);
}

View File

@@ -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: () => (
<RootErrorBoundary>
<QueryClientProviderWrapper>
<ThemeProvider>
<LinguiClientProvider>
<SSEProvider>
<SSEEventListeners>
@@ -30,7 +30,7 @@ export const Route = createRootRoute({
</SSEEventListeners>
</SSEProvider>
</LinguiClientProvider>
</QueryClientProviderWrapper>
</ThemeProvider>
<Toaster position="top-right" />
</RootErrorBoundary>
),

View File

@@ -9,6 +9,7 @@ const LayerImpl = Effect.gen(function* () {
enterKeyBehavior: "shift-enter-send",
permissionMode: "default",
locale: "ja",
theme: "system",
});
const setUserConfig = (newConfig: UserConfig) =>

View File

@@ -19,6 +19,7 @@ export const configMiddleware = createMiddleware<HonoContext>(
enterKeyBehavior: "shift-enter-send",
permissionMode: "default",
locale: "ja",
theme: "system",
} satisfies UserConfig),
);
}

View File

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

View File

@@ -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",
}),
});