Skip to content

Commit 0acc349

Browse files
authored
Merge pull request #30 from ut-code/changetheme
テーマチェンジ機能の追加
2 parents 67fb2f8 + 2c05b76 commit 0acc349

File tree

10 files changed

+252
-142
lines changed

10 files changed

+252
-142
lines changed

app/[docs_id]/chatForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export function ChatForm({ documentContent, sectionId, replOutputs, fileContents
161161
className={clsx(
162162
"chat-bubble",
163163
{ "bg-primary text-primary-content": msg.sender === 'user' },
164-
{ "bg-secondary-content text-black": msg.sender === 'ai' && !msg.isError },
164+
{ "bg-secondary-content dark:bg-neutral text-black dark:text-white": msg.sender === 'ai' && !msg.isError },
165165
{ "chat-bubble-error": msg.isError }
166166
)}
167167
style={{maxWidth: "100%", wordBreak: "break-word"}}

app/[docs_id]/markdown.tsx

Lines changed: 124 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { PythonEmbeddedTerminal } from "../terminal/python/embedded";
55
import { Heading } from "./section";
66
import { type AceLang, EditorComponent } from "../terminal/editor";
77
import { ExecFile, ExecLang } from "../terminal/exec";
8-
import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/hljs";
8+
import { useChangeTheme } from "./themeToggle";
9+
import { tomorrow, atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
910

1011
export function StyledMarkdown({ content }: { content: string }) {
1112
return (
@@ -15,6 +16,7 @@ export function StyledMarkdown({ content }: { content: string }) {
1516
);
1617
}
1718

19+
1820
// TailwindCSSがh1などのタグのスタイルを消してしまうので、手動でスタイルを指定する必要がある
1921
const components: Components = {
2022
h1: ({ children }) => <Heading level={1}>{children}</Heading>,
@@ -33,7 +35,7 @@ const components: Components = {
3335
li: ({ node, ...props }) => <li className="my-1" {...props} />,
3436
a: ({ node, ...props }) => <a className="link link-info" {...props} />,
3537
strong: ({ node, ...props }) => (
36-
<strong className="text-primary" {...props} />
38+
<strong className="text-primary dark:text-secondary" {...props} />
3739
),
3840
table: ({ node, ...props }) => (
3941
<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 = {
4244
),
4345
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
4446
pre: ({ node, ...props }) => props.children,
45-
code: ({ node, className, ref, style, ...props }) => {
46-
const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
47-
className || ""
48-
);
49-
if (match) {
50-
if (match[2] === "-exec" && match[3]) {
51-
/*
52-
```python-exec:main.py
47+
code: ({ node, className, ref, style, ...props }) => <CodeComponent {...{ node, className, ref, style, ...props }} />,
48+
};
49+
function CodeComponent({ node, className, ref, style, ...props }: { node: unknown; className?: string; ref?: unknown; style?: unknown; [key: string]: unknown }) {
50+
const theme = useChangeTheme();
51+
const codetheme= theme === "tomorrow" ? tomorrow : atomOneDark;
52+
const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
53+
className || ""
54+
);
55+
if (match) {
56+
if (match[2] === "-exec" && match[3]) {
57+
/*
58+
```python-exec:main.py
59+
hello, world!
60+
```
61+
62+
---------------------------
63+
[▶ 実行] `python main.py`
5364
hello, world!
54-
```
55-
56-
---------------------------
57-
[▶ 実行] `python main.py`
58-
hello, world!
59-
---------------------------
60-
*/
61-
let execLang: ExecLang | undefined = undefined;
62-
switch (match[1]) {
63-
case "python":
64-
execLang = "python";
65-
break;
66-
case "cpp":
67-
case "c++":
68-
execLang = "cpp";
69-
break;
70-
default:
71-
console.warn(`Unsupported language for exec: ${match[1]}`);
72-
break;
73-
}
74-
if (execLang) {
75-
return (
76-
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
77-
<ExecFile
78-
language={execLang}
79-
filenames={match[3].split(",")}
80-
content={String(props.children || "").replace(/\n$/, "")}
81-
/>
82-
</div>
83-
);
84-
}
85-
} else if (match[3]) {
86-
// ファイル名指定がある場合、ファイルエディター
87-
let aceLang: AceLang | undefined = undefined;
88-
switch (match[1]) {
89-
case "python":
90-
aceLang = "python";
91-
break;
92-
case "cpp":
93-
case "c++":
94-
aceLang = "c_cpp";
95-
break;
96-
case "json":
97-
aceLang = "json";
98-
break;
99-
case "csv":
100-
aceLang = "csv";
101-
break;
102-
case "text":
103-
case "txt":
104-
aceLang = "text";
105-
break;
106-
default:
107-
console.warn(`Unsupported language for editor: ${match[1]}`);
108-
break;
109-
}
65+
---------------------------
66+
*/
67+
let execLang: ExecLang | undefined = undefined;
68+
switch (match[1]) {
69+
case "python":
70+
execLang = "python";
71+
break;
72+
case "cpp":
73+
case "c++":
74+
execLang = "cpp";
75+
break;
76+
default:
77+
console.warn(`Unsupported language for exec: ${match[1]}`);
78+
break;
79+
}
80+
if (execLang) {
11081
return (
11182
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
112-
<EditorComponent
113-
language={aceLang}
114-
tabSize={4}
115-
filename={match[3]}
116-
readonly={match[2] === "-readonly"}
117-
initContent={String(props.children || "").replace(/\n$/, "")}
83+
<ExecFile
84+
language={execLang}
85+
filenames={match[3].split(",")}
86+
content={String(props.children || "").replace(/\n$/, "")}
11887
/>
11988
</div>
12089
);
121-
} else if (match[2] === "-repl") {
122-
// repl付きの言語指定
123-
// 現状はPythonのみ対応
124-
switch (match[1]) {
125-
case "python":
126-
return (
127-
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
128-
<PythonEmbeddedTerminal
129-
content={String(props.children || "").replace(/\n$/, "")}
130-
/>
131-
</div>
132-
);
133-
default:
134-
console.warn(`Unsupported language for repl: ${match[1]}`);
135-
break;
136-
}
90+
}
91+
} else if (match[3]) {
92+
// ファイル名指定がある場合、ファイルエディター
93+
let aceLang: AceLang | undefined = undefined;
94+
switch (match[1]) {
95+
case "python":
96+
aceLang = "python";
97+
break;
98+
case "cpp":
99+
case "c++":
100+
aceLang = "c_cpp";
101+
break;
102+
case "json":
103+
aceLang = "json";
104+
break;
105+
case "csv":
106+
aceLang = "csv";
107+
break;
108+
case "text":
109+
case "txt":
110+
aceLang = "text";
111+
break;
112+
default:
113+
console.warn(`Unsupported language for editor: ${match[1]}`);
114+
break;
137115
}
138116
return (
139-
<SyntaxHighlighter
140-
language={match[1]}
141-
PreTag="div"
142-
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
143-
style={tomorrow} // todo dark theme (editor.tsx で指定したのと同じテーマを選ぶようにすること)
144-
{...props}
145-
>
146-
{String(props.children || "").replace(/\n$/, "")}
147-
</SyntaxHighlighter>
148-
);
149-
} else if (String(props.children).includes("\n")) {
150-
// 言語指定なしコードブロック
151-
return (
152-
<SyntaxHighlighter
153-
PreTag="div"
154-
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
155-
style={tomorrow} // todo dark theme
156-
{...props}
157-
>
158-
{String(props.children || "").replace(/\n$/, "")}
159-
</SyntaxHighlighter>
160-
);
161-
} else {
162-
// inline
163-
return (
164-
<code
165-
className="bg-base-200/60 border border-base-300 px-1 py-0.5 rounded text-sm "
166-
{...props}
167-
/>
117+
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
118+
<EditorComponent
119+
language={aceLang}
120+
tabSize={4}
121+
filename={match[3]}
122+
readonly={match[2] === "-readonly"}
123+
initContent={String(props.children || "").replace(/\n$/, "")}
124+
/>
125+
</div>
168126
);
127+
} else if (match[2] === "-repl") {
128+
// repl付きの言語指定
129+
// 現状はPythonのみ対応
130+
switch (match[1]) {
131+
case "python":
132+
return (
133+
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
134+
<PythonEmbeddedTerminal
135+
content={String(props.children || "").replace(/\n$/, "")}
136+
/>
137+
</div>
138+
);
139+
default:
140+
console.warn(`Unsupported language for repl: ${match[1]}`);
141+
break;
142+
}
169143
}
170-
},
171-
};
144+
return (
145+
<SyntaxHighlighter
146+
language={match[1]}
147+
PreTag="div"
148+
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
149+
style={codetheme}
150+
{...props}
151+
>
152+
{String(props.children || "").replace(/\n$/, "")}
153+
</SyntaxHighlighter>
154+
);
155+
} else if (String(props.children).includes("\n")) {
156+
// 言語指定なしコードブロック
157+
return (
158+
<SyntaxHighlighter
159+
PreTag="div"
160+
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
161+
style={codetheme}
162+
{...props}
163+
>
164+
{String(props.children || "").replace(/\n$/, "")}
165+
</SyntaxHighlighter>
166+
);
167+
} else {
168+
// inline
169+
return (
170+
<code
171+
className="bg-base-200/60 border border-base-300 px-1 py-0.5 rounded text-sm "
172+
{...props}
173+
/>
174+
);
175+
}
176+
}

app/[docs_id]/themeToggle.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use client";
2+
import { useState, useEffect} from "react";
3+
4+
export function useChangeTheme(){
5+
const [theme, setTheme] = useState("tomorrow");
6+
useEffect(() => {
7+
8+
const updateTheme = () => {
9+
const theme = document.documentElement.getAttribute("data-theme");
10+
setTheme(theme === "dark" ? "twilight" : "tomorrow");
11+
};
12+
13+
const observer = new MutationObserver(updateTheme);
14+
observer.observe(document.documentElement, {
15+
attributes: true,
16+
attributeFilter: ["data-theme"],
17+
});
18+
19+
20+
return () => observer.disconnect();
21+
}, []);
22+
return theme;
23+
24+
};
25+
export function ThemeToggle() {
26+
const theme = useChangeTheme();
27+
const isChecked = theme === "twilight";
28+
useEffect(() => {
29+
const checkIsDarkSchemePreferred = () =>
30+
window?.matchMedia?.('(prefers-color-scheme:dark)')?.matches ?? false;
31+
const initialTheme = checkIsDarkSchemePreferred() ? "dark" : "light";
32+
document.documentElement.setAttribute("data-theme", initialTheme);
33+
}, []);
34+
35+
return (
36+
<label className="flex cursor-pointer gap-2" style={{ marginLeft: "1em" }}>
37+
<svg
38+
xmlns="http://www.w3.org/2000/svg"
39+
width="20"
40+
height="20"
41+
viewBox="0 0 24 24"
42+
fill="none"
43+
stroke="currentColor"
44+
strokeWidth="2"
45+
strokeLinecap="round"
46+
strokeLinejoin="round">
47+
<circle cx="12" cy="12" r="5" />
48+
<path
49+
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" />
50+
</svg>
51+
<input
52+
type="checkbox"
53+
checked={isChecked}
54+
className="toggle theme-controller"
55+
onChange={(e) => {
56+
const isdark = e.target.checked;
57+
const theme = isdark ? "dark" : "light";
58+
document.documentElement.setAttribute("data-theme", theme);
59+
}}
60+
/>
61+
<svg
62+
xmlns="http://www.w3.org/2000/svg"
63+
width="20"
64+
height="20"
65+
viewBox="0 0 24 24"
66+
fill="none"
67+
stroke="currentColor"
68+
strokeWidth="2"
69+
strokeLinecap="round"
70+
strokeLinejoin="round">
71+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
72+
</svg>
73+
</label>
74+
);
75+
}

app/globals.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
@import "tailwindcss";
2-
@plugin "daisyui";
2+
@plugin "daisyui"
3+
{
4+
themes: light --default, dark --prefersdark;
5+
};
6+
7+
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
8+
39

410
/* CDNからダウンロードするURLを指定したらなんかエラー出るので、npmでインストールしてlayout.tsxでimportすることにした */
511
@theme {

app/navbar.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ThemeToggle } from "./[docs_id]/themeToggle";
12
export function Navbar() {
23
return (
34
<div className="navbar bg-base-200 w-full">
@@ -23,9 +24,10 @@ export function Navbar() {
2324
</svg>
2425
</label>
2526
</div>
26-
<div className="mx-2 flex-1 px-2 font-bold text-xl lg:hidden">
27-
{/* タイトル(サイドバー非表示の場合のみ) */}
28-
Navbar Title
27+
<div className="mx-2 flex flex-row items-center px-2 font-bold text-xl lg:hidden">
28+
{/* サイドバーが常時表示されている場合のみ */}
29+
<span className="flex-1">Navbar Title</span>
30+
<ThemeToggle />
2931
</div>
3032
</div>
3133
);

0 commit comments

Comments
 (0)