Skip to content

Commit 843604f

Browse files
committed
Replace textarea with Monaco editor for better UX and syntax highlighting
1 parent c8b8ccd commit 843604f

File tree

4 files changed

+140
-62
lines changed

4 files changed

+140
-62
lines changed

package-lock.json

Lines changed: 58 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@floating-ui/react": "^0.27.8",
3939
"@giscus/react": "^3.1.0",
4040
"@mdx-js/react": "^3.0.0",
41+
"@monaco-editor/react": "^4.7.0",
4142
"@popperjs/core": "^2.11.8",
4243
"@radix-ui/react-avatar": "^1.1.7",
4344
"@radix-ui/react-collapsible": "^1.1.12",

src/components/InteractivePythonEditor/index.tsx

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, {useEffect, useRef, useState} from 'react';
1+
import React, {useState} from 'react';
2+
import Editor from '@monaco-editor/react';
23
import './styles.css';
34

45
declare global {
@@ -8,11 +9,13 @@ declare global {
89
}
910
}
1011

11-
const DEFAULT_CODE = `print("Hello from Pyodide!")\nfor i in range(3):\n print('Line', i+1)`;
12+
const DEFAULT_CODE = `print("Hello from Pyodide!")
13+
for i in range(3):
14+
print('Line', i+1)`;
1215

1316
export default function InteractivePythonEditor({
1417
initialCode = DEFAULT_CODE,
15-
height = 260,
18+
height = 400,
1619
}: {
1720
initialCode?: string;
1821
height?: number | string;
@@ -21,36 +24,6 @@ export default function InteractivePythonEditor({
2124
const [output, setOutput] = useState('');
2225
const [loading, setLoading] = useState(false);
2326
const [pyodideReady, setPyodideReady] = useState(false);
24-
const preRef = useRef<HTMLPreElement | null>(null);
25-
26-
useEffect(() => {
27-
// Load Prism for highlighting (CSS + script)
28-
if (!document.querySelector('link[data-prism]')) {
29-
const link = document.createElement('link');
30-
link.setAttribute('data-prism', '');
31-
link.rel = 'stylesheet';
32-
link.href = 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.css';
33-
document.head.appendChild(link);
34-
}
35-
if (!(window as any).Prism) {
36-
const s = document.createElement('script');
37-
s.src = 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js';
38-
s.async = true;
39-
document.head.appendChild(s);
40-
}
41-
}, []);
42-
43-
useEffect(() => {
44-
// Highlight when code or Prism loads
45-
const highlight = () => {
46-
if ((window as any).Prism && preRef.current) {
47-
(window as any).Prism.highlightElement(preRef.current);
48-
}
49-
};
50-
highlight();
51-
const id = setTimeout(highlight, 50);
52-
return () => clearTimeout(id);
53-
}, [code]);
5427

5528
async function ensurePyodide() {
5629
if (pyodideReady) return window.pyodide;
@@ -118,18 +91,22 @@ export default function InteractivePythonEditor({
11891
<div className="ipe-status">{pyodideReady ? 'Pyodide ready' : (loading ? 'Loading...' : 'Pyodide not loaded')}</div>
11992
</div>
12093

121-
<div className="ipe-main">
122-
<textarea
123-
className="ipe-textarea"
94+
<div className="ipe-editor">
95+
<Editor
96+
height={height}
97+
language="python"
12498
value={code}
125-
onChange={(e) => setCode(e.target.value)}
126-
style={{height}}
127-
spellCheck={false}
99+
onChange={(value) => setCode(value || '')}
100+
theme="light"
101+
options={{
102+
minimap: { enabled: false },
103+
fontSize: 14,
104+
lineNumbers: 'on',
105+
roundedSelection: false,
106+
scrollBeyondLastLine: false,
107+
automaticLayout: true,
108+
}}
128109
/>
129-
130-
<div className="ipe-preview" style={{height}}>
131-
<pre ref={preRef} className="language-python"><code>{code}</code></pre>
132-
</div>
133110
</div>
134111

135112
<div className="ipe-output">

src/components/InteractivePythonEditor/styles.css

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,74 @@
1-
.interactive-py-editor {
1+
.interactive-py-editor {
22
border: 1px solid var(--ifm-color-emphasis-200, #e6e6e6);
3-
border-radius: 6px;
3+
border-radius: 8px;
44
overflow: hidden;
5+
background: var(--ifm-background-color, #fff);
6+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
57
font-family: var(--ifm-font-family-base, Inter, Roboto, system-ui, -apple-system, 'Segoe UI');
68
}
9+
710
.ipe-toolbar {
811
display: flex;
9-
gap: 8px;
12+
gap: 10px;
1013
align-items: center;
11-
padding: 8px;
14+
padding: 12px 16px;
1215
background: var(--ifm-color-emphasis-0, #fafafa);
1316
border-bottom: 1px solid var(--ifm-color-emphasis-200, #e6e6e6);
1417
}
18+
1519
.ipe-btn {
16-
padding: 6px 10px;
17-
border-radius: 4px;
18-
border: 1px solid rgba(0,0,0,0.08);
19-
background: white;
20+
padding: 8px 12px;
21+
border-radius: 6px;
22+
border: 1px solid var(--ifm-color-primary, #007acc);
23+
background: var(--ifm-color-primary, #007acc);
24+
color: white;
2025
cursor: pointer;
26+
font-size: 14px;
27+
font-weight: 500;
28+
transition: background 0.2s;
29+
}
30+
31+
.ipe-btn:hover:not(:disabled) {
32+
background: var(--ifm-color-primary-dark, #005a9e);
33+
}
34+
35+
.ipe-btn:disabled {
36+
opacity: 0.6;
37+
cursor: not-allowed;
38+
}
39+
40+
.ipe-status {
41+
margin-left: auto;
42+
color: var(--ifm-color-emphasis-600, #666);
43+
font-size: 13px;
44+
font-style: italic;
45+
}
46+
47+
.ipe-editor {
48+
border-bottom: 1px solid var(--ifm-color-emphasis-200, #e6e6e6);
49+
}
50+
51+
.ipe-output {
52+
background: #1e1e1e;
53+
color: #d4d4d4;
54+
padding: 16px;
55+
}
56+
57+
.ipe-output-header {
58+
font-weight: 600;
59+
margin-bottom: 8px;
60+
color: #fff;
61+
}
62+
63+
.ipe-output-body {
64+
background: #1e1e1e;
65+
color: #d4d4d4;
66+
padding: 12px;
67+
border-radius: 4px;
68+
min-height: 100px;
69+
overflow: auto;
70+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
71+
font-size: 13px;
72+
white-space: pre-wrap;
73+
word-break: break-word;
2174
}
22-
.ipe-btn:disabled { opacity: 0.6; cursor: default }
23-
.ipe-status { margin-left: auto; color: #666; font-size: 13px }
24-
.ipe-main { display: flex; gap: 8px; padding: 12px }
25-
.ipe-textarea { flex: 1 1 50%; font-family: monospace; font-size: 13px; padding: 10px; border: 1px solid var(--ifm-color-emphasis-200, #e6e6e6); border-radius: 4px; resize: vertical; }
26-
.ipe-preview { flex: 1 1 50%; overflow: auto; background: #f7f7f7; border-radius: 4px; padding: 10px; border: 1px solid var(--ifm-color-emphasis-200, #e6e6e6) }
27-
.ipe-output { border-top: 1px solid var(--ifm-color-emphasis-200, #e6e6e6); background: #fff; padding: 10px }
28-
.ipe-output-header { font-weight: 600; margin-bottom: 6px }
29-
.ipe-output-body { background: #111; color: #fff; padding: 10px; border-radius: 4px; min-height: 80px; overflow: auto }
30-
31-
/* Ensure Prism code block wraps nicely */
32-
.language-python { white-space: pre-wrap; word-break: break-word; }

0 commit comments

Comments
 (0)