Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/components/CodeEmbed/ConsolePanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useRef, useEffect } from "preact/hooks";
import { Icon } from "../Icon";

export const ConsolePanel = ({ logs, onClear, onClose }) => {
const listRef = useRef(null);

// Auto-scroll to bottom on new logs
useEffect(() => {
if (listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
}, [logs]);

return (
<div className="flex flex-col h-40 w-full border-t-2 border-accent-color bg-white font-mono text-sm">
<div className="flex items-center justify-between bg-bg-gray-10 px-2 py-1 border-b border-gray-200">
<span className="font-bold text-gray-600">Console</span>
<div className="flex gap-2">
<button
onClick={onClear}
className="text-xs hover:text-accent-color px-2 py-1 rounded"
aria-label="Clear console"
>
Clear
</button>
<button
onClick={onClose}
className="hover:text-accent-color"
aria-label="Close console"
>
<Icon kind="close" />
</button>
</div>
</div>
<ul
ref={listRef}
className="flex-1 overflow-y-auto p-2 m-0 list-none space-y-1"
>
{logs.length === 0 && (
<li className="text-gray-400 italic text-xs">No output yet...</li>
)}
{logs.map((log, i) => (
<li
key={i}
className={`
border-b border-gray-100 last:border-0 pb-1 break-words
${log.level === 'error' ? 'text-red-600 bg-red-50' : ''}
${log.level === 'warn' ? 'text-yellow-600 bg-yellow-50' : ''}
${log.level === 'info' ? 'text-blue-600' : ''}
${log.level === 'log' ? 'text-gray-800' : ''}
`}
>
{log.args && log.args.map((arg, j) => (
<span key={j} className="mr-2">
{typeof arg === 'object' ? JSON.stringify(arg) : String(arg)}
</span>
))}
</li>
))}
</ul>
</div>
);
};
32 changes: 32 additions & 0 deletions src/components/CodeEmbed/frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,38 @@ ${(code.scripts?.length ?? 0) > 0 ? '' : `
document.head.appendChild(p5ScriptElement);
}
})

// Console Shim
var _console = window.console;
window.console = {
..._console,
log: function(...args) {
window.parent.postMessage({ type: 'P5_CONSOLE', level: 'log', args: args }, '*');
_console.log(...args);
},
info: function(...args) {
window.parent.postMessage({ type: 'P5_CONSOLE', level: 'info', args: args }, '*');
_console.info(...args);
},
warn: function(...args) {
window.parent.postMessage({ type: 'P5_CONSOLE', level: 'warn', args: args }, '*');
_console.warn(...args);
},
error: function(...args) {
window.parent.postMessage({ type: 'P5_CONSOLE', level: 'error', args: args }, '*');
_console.error(...args);
}
};

// Error Shim
window.onerror = function(msg, url, lineNo, columnNo, error) {
window.parent.postMessage({ type: 'P5_CONSOLE', level: 'error', args: [msg] }, '*');
return false;
};

window.onunhandledrejection = function(event) {
window.parent.postMessage({ type: 'P5_CONSOLE', level: 'error', args: [event.reason] }, '*');
};
</script>
`}
`.replace(/\u00A0/g, " ");
Expand Down
114 changes: 76 additions & 38 deletions src/components/CodeEmbed/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { javascript } from "@codemirror/lang-javascript";
import { cdnLibraryUrl, cdnSoundUrl } from "@/src/globals/globals";

import { CodeFrame } from "./frame";
import { ConsolePanel } from "./ConsolePanel";
import { CopyCodeButton } from "../CopyCodeButton";
import CircleButton from "../CircleButton";
import { Icon } from "../Icon";
Expand Down Expand Up @@ -36,6 +37,24 @@ export const CodeEmbed = (props) => {
const [codeString, setCodeString] = useState(
initialCode.replace(/\u00A0/g, " "),
);

const [showConsole, setShowConsole] = useState(false);
const [logs, setLogs] = useState([]);

useEffect(() => {
const handleMessage = (event) => {
if (event.data && event.data.type === 'P5_CONSOLE') {
setLogs(prevLogs => [...prevLogs, event.data]);
// Auto-open console on error if not already open? Optional UX choice.
if (event.data.level === 'error') {
setShowConsole(true);
}
}
};

window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);

let { previewWidth, previewHeight } = props;
const canvasMatch = /createCanvas\(\s*(\d+),\s*(\d+)\s*(?:,\s*(?:P2D|WEBGL)\s*)?\)/m.exec(initialCode);
Expand Down Expand Up @@ -117,50 +136,69 @@ export const CodeEmbed = (props) => {
>
<Icon kind="stop" />
</CircleButton>
<CircleButton
className={`bg-bg-gray-40 ${showConsole ? "bg-accent-color text-white" : ""}`} // Highlight when active
onClick={() => setShowConsole(!showConsole)}
ariaLabel="Toggle Console"
>
<Icon kind="code-brackets" />
</CircleButton>
</div>
</div>
) : null}
<div className="code-editor-container relative w-full">
<CodeMirror
value={codeString}
theme="light"
width="100%"
minimalSetup={{
highlightSpecialChars: false,
history: false,
drawSelection: true,
syntaxHighlighting: true,
defaultKeymap: true,
historyKeymap: true,
}}
basicSetup={{
lineNumbers: false,
foldGutter: false,
autocompletion: false,
}}
indentWithTab={false}
extensions={[javascript(), EditorView.lineWrapping]}
onChange={(val) => setCodeString(val)}
editable={props.editable}
onCreateEditor={(editorView) =>
(editorView.contentDOM.ariaLabel = "Code Editor")
}
/>
<div className="absolute right-0 top-0 flex flex-col gap-xs p-xs md:flex-row">
<CopyCodeButton textToCopy={codeString || initialCode} />
<CircleButton
onClick={() => {
setCodeString(initialCode);
setPreviewCodeString(initialCode);
announce("Code reset to initial value.");
<div className="flex flex-col w-full"> {/* Container for Editor + Console */}
<div className="code-editor-container relative w-full">
<CodeMirror
value={codeString}
theme="light"
width="100%"
minimalSetup={{
highlightSpecialChars: false,
history: false,
drawSelection: true,
syntaxHighlighting: true,
defaultKeymap: true,
historyKeymap: true,
}}
basicSetup={{
lineNumbers: false,
foldGutter: false,
autocompletion: false,
}}
ariaLabel="Reset code to initial value"
className="bg-white text-black"
>
<Icon kind="refresh" />
</CircleButton>
indentWithTab={false}
extensions={[javascript(), EditorView.lineWrapping]}
onChange={(val) => setCodeString(val)}
editable={props.editable}
onCreateEditor={(editorView) =>
(editorView.contentDOM.ariaLabel = "Code Editor")
}
/>
<div className="absolute right-0 top-0 flex flex-col gap-xs p-xs md:flex-row">
<CopyCodeButton textToCopy={codeString || initialCode} />
<CircleButton
onClick={() => {
setCodeString(initialCode);
setPreviewCodeString(initialCode);
announce("Code reset to initial value.");
setLogs([]); // Clear logs on reset
}}
ariaLabel="Reset code to initial value"
className="bg-white text-black"
>
<Icon kind="refresh" />
</CircleButton>
</div>
</div>

{showConsole && (
<ConsolePanel
logs={logs}
onClear={() => setLogs([])}
onClose={() => setShowConsole(false)}
/>
)}
</div>

<span ref={liveRegionRef} aria-live="polite" class="sr-only" />
</div>
);
Expand Down
Loading