diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx
index f8939c2..5a4fa19 100644
--- a/app/[docs_id]/chatForm.tsx
+++ b/app/[docs_id]/chatForm.tsx
@@ -151,7 +151,7 @@ export function ChatForm({ documentContent, sectionId }: ChatFormProps) {
className={clsx(
"chat-bubble",
{ "bg-primary text-primary-content": msg.sender === 'user' },
- { "bg-secondary-content text-black": msg.sender === 'ai' && !msg.isError },
+ { "bg-secondary-content dark:bg-neutral text-black dark:text-white": msg.sender === 'ai' && !msg.isError },
{ "chat-bubble-error": msg.isError }
)}
style={{maxWidth: "100%", wordBreak: "break-word"}}
diff --git a/app/[docs_id]/markdown.tsx b/app/[docs_id]/markdown.tsx
index 80d99f3..7d31b97 100644
--- a/app/[docs_id]/markdown.tsx
+++ b/app/[docs_id]/markdown.tsx
@@ -5,7 +5,8 @@ import { PythonEmbeddedTerminal } from "../terminal/python/embedded";
import { Heading } from "./section";
import { type AceLang, EditorComponent } from "../terminal/editor";
import { ExecFile, ExecLang } from "../terminal/exec";
-import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/hljs";
+import { useChangeTheme } from "./themeToggle";
+import { tomorrow, atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
export function StyledMarkdown({ content }: { content: string }) {
return (
@@ -15,6 +16,7 @@ export function StyledMarkdown({ content }: { content: string }) {
);
}
+
// TailwindCSSがh1などのタグのスタイルを消してしまうので、手動でスタイルを指定する必要がある
const components: Components = {
h1: ({ children }) => {children},
@@ -33,7 +35,7 @@ const components: Components = {
li: ({ node, ...props }) =>
@@ -42,130 +44,133 @@ const components: Components = {
),
hr: ({ node, ...props }) =>
,
pre: ({ node, ...props }) => props.children,
- code: ({ node, className, ref, style, ...props }) => {
- const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
- className || ""
- );
- if (match) {
- if (match[2] === "-exec" && match[3]) {
- /*
- ```python-exec:main.py
+ code: ({ node, className, ref, style, ...props }) =>
,
+};
+function CodeComponent({ node, className, ref, style, ...props }: { node: unknown; className?: string; ref?: unknown; style?: unknown; [key: string]: unknown }) {
+ const theme = useChangeTheme();
+ const codetheme= theme === "tomorrow" ? tomorrow : atomOneDark;
+ const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
+ className || ""
+ );
+ if (match) {
+ if (match[2] === "-exec" && match[3]) {
+ /*
+ ```python-exec:main.py
+ hello, world!
+ ```
+ ↓
+ ---------------------------
+ [▶ 実行] `python main.py`
hello, world!
- ```
- ↓
- ---------------------------
- [▶ 実行] `python main.py`
- hello, world!
- ---------------------------
- */
- let execLang: ExecLang | undefined = undefined;
- switch (match[1]) {
- case "python":
- execLang = "python";
- break;
- case "cpp":
- case "c++":
- execLang = "cpp";
- break;
- default:
- console.warn(`Unsupported language for exec: ${match[1]}`);
- break;
- }
- if (execLang) {
- return (
-
-
-
- );
- }
- } else if (match[3]) {
- // ファイル名指定がある場合、ファイルエディター
- let aceLang: AceLang | undefined = undefined;
- switch (match[1]) {
- case "python":
- aceLang = "python";
- break;
- case "cpp":
- case "c++":
- aceLang = "c_cpp";
- break;
- case "json":
- aceLang = "json";
- break;
- case "csv":
- aceLang = "csv";
- break;
- case "text":
- case "txt":
- aceLang = "text";
- break;
- default:
- console.warn(`Unsupported language for editor: ${match[1]}`);
- break;
- }
+ ---------------------------
+ */
+ let execLang: ExecLang | undefined = undefined;
+ switch (match[1]) {
+ case "python":
+ execLang = "python";
+ break;
+ case "cpp":
+ case "c++":
+ execLang = "cpp";
+ break;
+ default:
+ console.warn(`Unsupported language for exec: ${match[1]}`);
+ break;
+ }
+ if (execLang) {
return (
-
);
- } else if (match[2] === "-repl") {
- // repl付きの言語指定
- // 現状はPythonのみ対応
- switch (match[1]) {
- case "python":
- return (
-
- );
- default:
- console.warn(`Unsupported language for repl: ${match[1]}`);
- break;
- }
+ }
+ } else if (match[3]) {
+ // ファイル名指定がある場合、ファイルエディター
+ let aceLang: AceLang | undefined = undefined;
+ switch (match[1]) {
+ case "python":
+ aceLang = "python";
+ break;
+ case "cpp":
+ case "c++":
+ aceLang = "c_cpp";
+ break;
+ case "json":
+ aceLang = "json";
+ break;
+ case "csv":
+ aceLang = "csv";
+ break;
+ case "text":
+ case "txt":
+ aceLang = "text";
+ break;
+ default:
+ console.warn(`Unsupported language for editor: ${match[1]}`);
+ break;
}
return (
-
- {String(props.children || "").replace(/\n$/, "")}
-
- );
- } else if (String(props.children).includes("\n")) {
- // 言語指定なしコードブロック
- return (
-
- {String(props.children || "").replace(/\n$/, "")}
-
- );
- } else {
- // inline
- return (
-
+
+
+
);
+ } else if (match[2] === "-repl") {
+ // repl付きの言語指定
+ // 現状はPythonのみ対応
+ switch (match[1]) {
+ case "python":
+ return (
+
+ );
+ default:
+ console.warn(`Unsupported language for repl: ${match[1]}`);
+ break;
+ }
}
- },
-};
+ return (
+
+ {String(props.children || "").replace(/\n$/, "")}
+
+ );
+ } else if (String(props.children).includes("\n")) {
+ // 言語指定なしコードブロック
+ return (
+
+ {String(props.children || "").replace(/\n$/, "")}
+
+ );
+ } else {
+ // inline
+ return (
+
+ );
+ }
+}
diff --git a/app/[docs_id]/themeToggle.tsx b/app/[docs_id]/themeToggle.tsx
new file mode 100644
index 0000000..91b5404
--- /dev/null
+++ b/app/[docs_id]/themeToggle.tsx
@@ -0,0 +1,75 @@
+"use client";
+import { useState, useEffect} from "react";
+
+export function useChangeTheme(){
+ const [theme, setTheme] = useState("tomorrow");
+ useEffect(() => {
+
+ const updateTheme = () => {
+ const theme = document.documentElement.getAttribute("data-theme");
+ setTheme(theme === "dark" ? "twilight" : "tomorrow");
+ };
+
+ const observer = new MutationObserver(updateTheme);
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+
+
+ return () => observer.disconnect();
+ }, []);
+ return theme;
+
+};
+export function ThemeToggle() {
+ const theme = useChangeTheme();
+ const isChecked = theme === "twilight";
+ useEffect(() => {
+ const checkIsDarkSchemePreferred = () =>
+ window?.matchMedia?.('(prefers-color-scheme:dark)')?.matches ?? false;
+ const initialTheme = checkIsDarkSchemePreferred() ? "dark" : "light";
+ document.documentElement.setAttribute("data-theme", initialTheme);
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/app/globals.css b/app/globals.css
index 82b1549..19a48e4 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,5 +1,11 @@
@import "tailwindcss";
-@plugin "daisyui";
+@plugin "daisyui"
+{
+ themes: light --default, dark --prefersdark;
+};
+
+@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
+
/* CDNからダウンロードするURLを指定したらなんかエラー出るので、npmでインストールしてlayout.tsxでimportすることにした */
@theme {
diff --git a/app/navbar.tsx b/app/navbar.tsx
index 5ec3f99..393a88c 100644
--- a/app/navbar.tsx
+++ b/app/navbar.tsx
@@ -1,3 +1,4 @@
+import { ThemeToggle } from "./[docs_id]/themeToggle";
export function Navbar() {
return (
@@ -23,9 +24,10 @@ export function Navbar() {
-
- {/* タイトル(サイドバー非表示の場合のみ) */}
- Navbar Title
+
+ {/* サイドバーが常時表示されている場合のみ */}
+ Navbar Title
+
);
diff --git a/app/sidebar.tsx b/app/sidebar.tsx
index 3d2d81d..5c9d77b 100644
--- a/app/sidebar.tsx
+++ b/app/sidebar.tsx
@@ -4,6 +4,7 @@ import { usePathname } from "next/navigation";
import useSWR, { Fetcher } from "swr";
import { splitMarkdown } from "./[docs_id]/splitMarkdown";
import { pagesList } from "./pagesList";
+import { ThemeToggle } from "./[docs_id]/themeToggle";
const fetcher: Fetcher
= (url) =>
fetch(url).then((r) => r.text());
@@ -12,18 +13,21 @@ export function Sidebar() {
const pathname = usePathname();
const docs_id = pathname.replace(/^\//, "");
const { data, error, isLoading } = useSWR(`/docs/${docs_id}.md`, fetcher);
-
+
if (error) console.error("Sidebar fetch error:", error);
const splitmdcontent = splitMarkdown(data ?? "");
return (
{/* todo: 背景色ほんとにこれでいい? */}
-
+
{/* サイドバーが常時表示されている場合のみ */}
- Navbar Title
+ Navbar Title
+
+
+
{pagesList.map((group) => (
-
diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx
index e6e68a8..93afa9f 100644
--- a/app/terminal/editor.tsx
+++ b/app/terminal/editor.tsx
@@ -21,6 +21,7 @@ import { useFile } from "./file";
import { useEffect } from "react";
import { useSectionCode } from "../[docs_id]/section";
import clsx from "clsx";
+import { useChangeTheme } from "../[docs_id]/themeToggle";
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する
@@ -34,6 +35,7 @@ interface EditorProps {
readonly?: boolean;
}
export function EditorComponent(props: EditorProps) {
+ const theme= useChangeTheme();
const { files, writeFile } = useFile();
const code = files[props.filename] || props.initContent;
const sectionContext = useSectionCode();
@@ -86,7 +88,7 @@ export function EditorComponent(props: EditorProps) {
(null);
const fitAddonRef = useRef(null);
const [termReady, setTermReady] = useState(false);
-
+ const theme = useChangeTheme();
const getRowsRef = useRef<(cols: number) => number>(undefined);
getRowsRef.current = props.getRows;
const onReadyRef = useRef<() => void>(undefined);
onReadyRef.current = props.onReady;
-
+
// ターミナルの初期化処理
useEffect(() => {
const abortController = new AbortController();
-
// globals.cssでフォントを指定し読み込んでいるが、
// それが読み込まれる前にterminalを初期化してしまうとバグる。
document.fonts.load("0.875rem Inconsolata Variable").then(() => {
@@ -166,5 +166,21 @@ export function useTerminal(props: TerminalProps) {
};
}, []);
+ // テーマが変わったときにterminalのテーマを更新する
+ useEffect(() => {
+ if (terminalInstanceRef.current) {
+ const fromCSS = (varName: string) =>
+ window.getComputedStyle(document.body).getPropertyValue(varName);
+
+ terminalInstanceRef.current.options = ({
+ theme: {
+ background: fromCSS(theme === "tomorrow" ? "--color-base-300" : "--color-neutral-900"),
+ foreground: fromCSS("--color-base-content")
+ }
+ });
+ }
+}, [theme]);
+
+
return { terminalRef, terminalInstanceRef, termReady };
}
diff --git a/package-lock.json b/package-lock.json
index 4babc11..5f7ce77 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"name": "my-code",
"version": "0.1.0",
"dependencies": {
- "@fontsource-variable/inconsolata": "^5.2.6",
+ "@fontsource-variable/inconsolata": "^5.2.7",
"@fontsource-variable/noto-sans-jp": "^5.2.6",
"@google/genai": "^1.21.0",
"@opennextjs/cloudflare": "^1.7.1",
@@ -25,7 +25,7 @@
"react-ace": "^14.0.1",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
- "react-syntax-highlighter": "^15.6.1",
+ "react-syntax-highlighter": "^15.6.6",
"remark-gfm": "^4.0.1",
"swr": "^2.3.6",
"zod": "^4.0.17"
@@ -8394,9 +8394,9 @@
}
},
"node_modules/@fontsource-variable/inconsolata": {
- "version": "5.2.6",
- "resolved": "https://registry.npmjs.org/@fontsource-variable/inconsolata/-/inconsolata-5.2.6.tgz",
- "integrity": "sha512-lODgGNXKm7/PkAwnqDwiMrRbct/Lg4NqlcBwSXpaqFHmrVycLSS7oPZ/AshbDkLNJXv8yjGkqAr5oOGsgUE1QA==",
+ "version": "5.2.7",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/inconsolata/-/inconsolata-5.2.7.tgz",
+ "integrity": "sha512-vRCuyz9l7oHeU77Ed0+slKKf0oIx2Tq8/VkyH48tkclO6AySMLFmRZznm9SUyx1QqlMsqCDMF2KK0ysY5vJvWg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
@@ -18491,16 +18491,16 @@
}
},
"node_modules/react-syntax-highlighter": {
- "version": "15.6.1",
- "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
- "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
+ "version": "15.6.6",
+ "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
+ "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"highlight.js": "^10.4.1",
"highlightjs-vue": "^1.0.0",
"lowlight": "^1.17.0",
- "prismjs": "^1.27.0",
+ "prismjs": "^1.30.0",
"refractor": "^3.6.0"
},
"peerDependencies": {
diff --git a/package.json b/package.json
index 2d6b574..1bd23f1 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
},
"dependencies": {
- "@fontsource-variable/inconsolata": "^5.2.6",
+ "@fontsource-variable/inconsolata": "^5.2.7",
"@fontsource-variable/noto-sans-jp": "^5.2.6",
"@google/genai": "^1.21.0",
"@opennextjs/cloudflare": "^1.7.1",
@@ -31,7 +31,7 @@
"react-ace": "^14.0.1",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
- "react-syntax-highlighter": "^15.6.1",
+ "react-syntax-highlighter": "^15.6.6",
"remark-gfm": "^4.0.1",
"swr": "^2.3.6",
"zod": "^4.0.17"