Skip to content

Commit 45b845a

Browse files
committed
feat: add theme switch, fix error catch only first time is work
1 parent 69f3b21 commit 45b845a

File tree

9 files changed

+139
-33
lines changed

9 files changed

+139
-33
lines changed

src/App.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,36 @@ import RootHeader from "@/layout/root-header";
66
import { PlaygroundProvider } from "@/core/context/PlaygroundProvider";
77
import LazyLoading from "@/components/lazy-loading";
88
import { Toaster } from "./components/ui/sonner";
9+
import { ThemeProvider } from "./core/context/ThemeProvider";
910

1011
const RootEditor = lazy(() => import("@/layout/root-editor"));
1112
const RootPreview = lazy(() => import("@/layout/root-preview"));
1213

1314
function App() {
1415
return (
15-
<div className='flex h-screen flex-col'>
16-
<RootHeader />
17-
<Toaster richColors position='top-right' />
16+
<ThemeProvider defaultTheme='light'>
17+
<div className='flex h-screen flex-col'>
18+
<RootHeader />
19+
<Toaster richColors position='top-right' />
1820

19-
<section className='flex-1'>
20-
<PlaygroundProvider>
21-
<Allotment defaultSizes={[100, 100]}>
22-
<Allotment.Pane minSize={250}>
23-
<Suspense fallback={<LazyLoading text='RootEditor Loading...' />}>
24-
<RootEditor />
25-
</Suspense>
26-
</Allotment.Pane>
27-
<Allotment.Pane minSize={0}>
28-
<Suspense fallback={<LazyLoading text='RootPreview Loading...' />}>
29-
<RootPreview />
30-
</Suspense>
31-
</Allotment.Pane>
32-
</Allotment>
33-
</PlaygroundProvider>
34-
</section>
35-
</div>
21+
<section className='flex-1'>
22+
<PlaygroundProvider>
23+
<Allotment defaultSizes={[100, 100]}>
24+
<Allotment.Pane minSize={250}>
25+
<Suspense fallback={<LazyLoading text='RootEditor Loading...' />}>
26+
<RootEditor />
27+
</Suspense>
28+
</Allotment.Pane>
29+
<Allotment.Pane minSize={0}>
30+
<Suspense fallback={<LazyLoading text='RootPreview Loading...' />}>
31+
<RootPreview />
32+
</Suspense>
33+
</Allotment.Pane>
34+
</Allotment>
35+
</PlaygroundProvider>
36+
</section>
37+
</div>
38+
</ThemeProvider>
3639
);
3740
}
3841

src/components/code-container/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Editor, type EditorProps, type OnMount, loader } from "@monaco-editor/react";
22
import { createATA } from "./ata";
3+
import { useTheme } from "@/hooks/useTheme";
34

45
loader.config({
56
paths: {
@@ -23,6 +24,7 @@ interface CodeContainerProps {
2324

2425
export default function CodeContainer(props: CodeContainerProps) {
2526
const { file = {} as CodeContainerFileInfo, onChange, options } = props;
27+
const { theme } = useTheme();
2628

2729
const handleEditorDidMount: OnMount = (editor, monaco) => {
2830
// 无法识别 jsx 的问题(设置 tsconfig)
@@ -53,6 +55,7 @@ export default function CodeContainer(props: CodeContainerProps) {
5355
onChange={onChange}
5456
loading='Feching monaco source'
5557
options={{
58+
theme: theme === "system" ? undefined : `vs-${theme}`,
5659
scrollBeyondLastLine: false,
5760
minimap: {
5861
enabled: false,

src/components/error-alert.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AlertCircle, X } from "lucide-react";
22
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
3-
import { useState } from "react";
3+
import { useEffect, useState } from "react";
44
import { cn } from "@/lib/utils";
55
import { Button } from "./ui/button";
66

@@ -13,7 +13,11 @@ export default function ErrorAlert(props: ErrorAlertProps) {
1313
const { errMsg, className } = props;
1414
const [visible, setVisible] = useState(true);
1515

16-
if (!visible || !errMsg) return null;
16+
useEffect(() => {
17+
setVisible(true);
18+
}, [errMsg]);
19+
20+
if (!visible) return null;
1721

1822
const closeError = () => {
1923
setVisible(false);

src/core/context/PlaygroundProvider.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { useCallback, useState } from "react";
22
import { type MultipleFiles, PlaygroundContext } from ".";
33
import { fileName2Language } from "../util";
4-
import { defaultFiles } from "../files";
4+
import { defaultFiles, ENTRY_FILE_NAME } from "../files";
55

66
interface PlaygroundProviderProps {
77
children: React.ReactNode;
88
}
99
export const PlaygroundProvider = (props: PlaygroundProviderProps) => {
1010
const { children } = props;
1111
const [files, setFiles] = useState<MultipleFiles>(defaultFiles);
12-
const [selectedFileName, setSelectedFileName] = useState("main.tsx");
12+
const [selectedFileName, setSelectedFileName] = useState(ENTRY_FILE_NAME);
1313

1414
const addFile = useCallback(
1515
(name: string) => {

src/core/context/ThemeProvider.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useEffect, useState } from "react";
2+
import { type Theme, ThemeContext } from ".";
3+
4+
type ThemeProviderProps = {
5+
children: React.ReactNode;
6+
defaultTheme?: Theme;
7+
storageKey?: string;
8+
};
9+
10+
export function ThemeProvider(props: ThemeProviderProps) {
11+
const { children, defaultTheme = "light", storageKey = "play-ui-theme" } = props;
12+
13+
const [theme, _setTheme] = useState<Theme>(
14+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
15+
);
16+
17+
useEffect(() => {
18+
const root = window.document.documentElement;
19+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
20+
21+
const applyTheme = (theme: Theme) => {
22+
root.classList.remove("light", "dark");
23+
const systemTheme = mediaQuery.matches ? "dark" : "light";
24+
const effectiveTheme = theme === "system" ? systemTheme : theme;
25+
root.classList.add(effectiveTheme);
26+
};
27+
28+
const handleChange = () => {
29+
if (theme === "system") applyTheme("system");
30+
};
31+
32+
applyTheme(theme);
33+
34+
mediaQuery.addEventListener("change", handleChange);
35+
return () => mediaQuery.removeEventListener("change", handleChange);
36+
}, [theme]);
37+
38+
const setTheme = (theme: Theme) => {
39+
localStorage.setItem(storageKey, theme);
40+
_setTheme(theme);
41+
};
42+
43+
const value = {
44+
theme,
45+
setTheme,
46+
};
47+
48+
return (
49+
<ThemeContext {...props} value={value}>
50+
{children}
51+
</ThemeContext>
52+
);
53+
}

src/core/context/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import type { CodeContainerFileInfo } from "@/components/code-container";
21
import { createContext } from "react";
2+
import type { CodeContainerFileInfo } from "@/components/code-container";
3+
import { ENTRY_FILE_NAME } from "../files";
34

5+
// CodeFile Context
46
export interface MultipleFiles {
57
[key: string]: CodeContainerFileInfo;
68
}
@@ -16,5 +18,17 @@ export interface PlaygroundContextProps {
1618
}
1719

1820
export const PlaygroundContext = createContext<PlaygroundContextProps>({
19-
selectedFileName: "App.tsx",
21+
selectedFileName: ENTRY_FILE_NAME,
2022
} as PlaygroundContextProps);
23+
24+
// Theme Context
25+
export type Theme = "dark" | "light" | "system";
26+
27+
export type ThemeContextProps = {
28+
theme: Theme;
29+
setTheme: (theme: Theme) => void;
30+
};
31+
32+
export const ThemeContext = createContext<ThemeContextProps>({
33+
theme: "system",
34+
} as ThemeContextProps);

src/hooks/useTheme.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { useContext } from "react";
2+
import { ThemeContext } from "@/core/context";
3+
4+
export const useTheme = () => {
5+
const context = useContext(ThemeContext);
6+
7+
return context;
8+
};

src/layout/root-header/index.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1+
import { useCallback } from "react";
2+
import { MoonIcon, SunIcon } from "lucide-react";
3+
4+
import { Button } from "@/components/ui/button";
15
import ReactLogo from "@/icons/react-logo";
6+
import { useTheme } from "@/hooks/useTheme";
7+
28
export default function RootHeader() {
9+
const { theme, setTheme } = useTheme();
10+
11+
const toggleTheme = useCallback(() => {
12+
setTheme(theme === "dark" ? "light" : "dark");
13+
}, [theme, setTheme]);
14+
315
return (
4-
<header className='flex items-center border-b p-2'>
5-
<ReactLogo className='h-8 w-8' style={{ color: "#61DAFB" }} />
6-
<span className='ml-2'>React Mini Playground</span>
16+
<header className='flex items-center justify-between border-b p-2'>
17+
<div className='flex items-center'>
18+
<ReactLogo className='h-8 w-8' style={{ color: "#61DAFB" }} />
19+
<span className='ml-2'>React Mini Playground</span>
20+
</div>
21+
<div className='flex items-center'>
22+
<Button variant='ghost' className='size-8 cursor-pointer px-0' onClick={toggleTheme}>
23+
<SunIcon className='hidden [html.dark_&]:block' />
24+
<MoonIcon className='hidden [html.light_&]:block' />
25+
<span className='sr-only'>Toggle theme</span>
26+
</Button>
27+
</div>
728
</header>
829
);
930
}

src/layout/root-preview/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ export default function RootPreview() {
5353
const [errMsg, setErrMsg] = useState<string>();
5454

5555
const handleMessage = (event: MessageEvent<MessageData>) => {
56-
console.log(event);
57-
5856
if (event.data.type === "ERROR") {
5957
setErrMsg(event.data.message);
6058
}
@@ -67,7 +65,9 @@ export default function RootPreview() {
6765
};
6866
}, []);
6967

70-
console.log("iframe render-----", iframeUrl);
68+
useEffect(() => {
69+
console.log("iframe render-----", iframeUrl);
70+
}, [iframeUrl]);
7171

7272
return (
7373
<div className='box-border h-full w-full overflow-auto'>
@@ -79,7 +79,7 @@ export default function RootPreview() {
7979
})}
8080
/>
8181

82-
<ErrorAlert className='mx-12 mt-12' errMsg={errMsg} />
82+
{errMsg && <ErrorAlert className='mx-12 mt-12' errMsg={errMsg} />}
8383
</div>
8484
);
8585
}

0 commit comments

Comments
 (0)