-
Notifications
You must be signed in to change notification settings - Fork 1
テーマチェンジ機能の追加 #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
テーマチェンジ機能の追加 #30
Changes from all commits
e1c7034
f72a1f9
48ad6bb
b73dabe
f9a2e5f
72b8260
89e2271
4924cb7
db9f312
2c05b76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. twilightなかった? まあ別にatomOneDarkでも良いんだけど
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prism系の方ならありました。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 }) => <Heading level={1}>{children}</Heading>, | ||
|
|
@@ -33,7 +35,7 @@ const components: Components = { | |
| li: ({ node, ...props }) => <li className="my-1" {...props} />, | ||
| a: ({ node, ...props }) => <a className="link link-info" {...props} />, | ||
| strong: ({ node, ...props }) => ( | ||
| <strong className="text-primary" {...props} /> | ||
| <strong className="text-primary dark:text-secondary" {...props} /> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここに書くとaiの吹き出し以外でも色が変わってしまうのでは… |
||
| ), | ||
| table: ({ node, ...props }) => ( | ||
| <div className="w-max max-w-full overflow-x-auto mx-auto my-2 rounded-lg border border-base-content/5 shadow-sm"> | ||
|
|
@@ -42,130 +44,133 @@ const components: Components = { | |
| ), | ||
| hr: ({ node, ...props }) => <hr className="border-primary my-4" {...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 }) => <CodeComponent {...{ node, className, ref, style, ...props }} />, | ||
| }; | ||
| function CodeComponent({ node, className, ref, style, ...props }: { node: unknown; className?: string; ref?: unknown; style?: unknown; [key: string]: unknown }) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 型指定が汚い 🤔 (別issueにする) |
||
| 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 ( | ||
| <div className="border border-primary border-2 shadow-md m-2 rounded-lg"> | ||
| <ExecFile | ||
| language={execLang} | ||
| filenames={match[3].split(",")} | ||
| content={String(props.children || "").replace(/\n$/, "")} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| } 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 ( | ||
| <div className="border border-primary border-2 shadow-md m-2 rounded-lg"> | ||
| <EditorComponent | ||
| language={aceLang} | ||
| tabSize={4} | ||
| filename={match[3]} | ||
| readonly={match[2] === "-readonly"} | ||
| initContent={String(props.children || "").replace(/\n$/, "")} | ||
| <ExecFile | ||
| language={execLang} | ||
| filenames={match[3].split(",")} | ||
| content={String(props.children || "").replace(/\n$/, "")} | ||
| /> | ||
| </div> | ||
| ); | ||
| } else if (match[2] === "-repl") { | ||
| // repl付きの言語指定 | ||
| // 現状はPythonのみ対応 | ||
| switch (match[1]) { | ||
| case "python": | ||
| return ( | ||
| <div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg"> | ||
| <PythonEmbeddedTerminal | ||
| content={String(props.children || "").replace(/\n$/, "")} | ||
| /> | ||
| </div> | ||
| ); | ||
| 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 ( | ||
| <SyntaxHighlighter | ||
| language={match[1]} | ||
| PreTag="div" | ||
| className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!" | ||
| style={tomorrow} // todo dark theme (editor.tsx で指定したのと同じテーマを選ぶようにすること) | ||
| {...props} | ||
| > | ||
| {String(props.children || "").replace(/\n$/, "")} | ||
| </SyntaxHighlighter> | ||
| ); | ||
| } else if (String(props.children).includes("\n")) { | ||
| // 言語指定なしコードブロック | ||
| return ( | ||
| <SyntaxHighlighter | ||
| PreTag="div" | ||
| className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!" | ||
| style={tomorrow} // todo dark theme | ||
| {...props} | ||
| > | ||
| {String(props.children || "").replace(/\n$/, "")} | ||
| </SyntaxHighlighter> | ||
| ); | ||
| } else { | ||
| // inline | ||
| return ( | ||
| <code | ||
| className="bg-base-200/60 border border-base-300 px-1 py-0.5 rounded text-sm " | ||
| {...props} | ||
| /> | ||
| <div className="border border-primary border-2 shadow-md m-2 rounded-lg"> | ||
| <EditorComponent | ||
| language={aceLang} | ||
| tabSize={4} | ||
| filename={match[3]} | ||
| readonly={match[2] === "-readonly"} | ||
| initContent={String(props.children || "").replace(/\n$/, "")} | ||
| /> | ||
| </div> | ||
| ); | ||
| } else if (match[2] === "-repl") { | ||
| // repl付きの言語指定 | ||
| // 現状はPythonのみ対応 | ||
| switch (match[1]) { | ||
| case "python": | ||
| return ( | ||
| <div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg"> | ||
| <PythonEmbeddedTerminal | ||
| content={String(props.children || "").replace(/\n$/, "")} | ||
| /> | ||
| </div> | ||
| ); | ||
| default: | ||
| console.warn(`Unsupported language for repl: ${match[1]}`); | ||
| break; | ||
| } | ||
| } | ||
| }, | ||
| }; | ||
| return ( | ||
| <SyntaxHighlighter | ||
| language={match[1]} | ||
| PreTag="div" | ||
| className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!" | ||
| style={codetheme} | ||
| {...props} | ||
| > | ||
| {String(props.children || "").replace(/\n$/, "")} | ||
| </SyntaxHighlighter> | ||
| ); | ||
| } else if (String(props.children).includes("\n")) { | ||
| // 言語指定なしコードブロック | ||
| return ( | ||
| <SyntaxHighlighter | ||
| PreTag="div" | ||
| className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!" | ||
| style={codetheme} | ||
| {...props} | ||
| > | ||
| {String(props.children || "").replace(/\n$/, "")} | ||
| </SyntaxHighlighter> | ||
| ); | ||
| } else { | ||
| // inline | ||
| return ( | ||
| <code | ||
| className="bg-base-200/60 border border-base-300 px-1 py-0.5 rounded text-sm " | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <label className="flex cursor-pointer gap-2" style={{ marginLeft: "1em" }}> | ||
| <svg | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| width="20" | ||
| height="20" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round"> | ||
| <circle cx="12" cy="12" r="5" /> | ||
| <path | ||
| d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" /> | ||
| </svg> | ||
| <input | ||
| type="checkbox" | ||
| checked={isChecked} | ||
| className="toggle theme-controller" | ||
| onChange={(e) => { | ||
| const isdark = e.target.checked; | ||
| const theme = isdark ? "dark" : "light"; | ||
| document.documentElement.setAttribute("data-theme", theme); | ||
| }} | ||
| /> | ||
| <svg | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| width="20" | ||
| height="20" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round"> | ||
| <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> | ||
| </svg> | ||
| </label> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
text-neutral-content か text-base-content (daisyuiの色) にしておけばlightモードでは黒、darkモードでは白になるんじゃないかな
(daisyuiのドキュメントの右上のボタンからテーマを変更するとテーマごとの色を確認できる)
ダークモードで文字を完全な白にするのはコントラストが強すぎるので普通は少しグレーにするのがいいらしいです(ダークモードユーザーじゃないのでしらんけど)