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 }) =>
  • , a: ({ node, ...props }) => , strong: ({ node, ...props }) => ( - + ), table: ({ 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"