feat: fullscreen VSCode-style code editor for module verify logic

Add a dark-themed fullscreen code editor overlay in the level editor
for editing module verify JS. Features line numbers, cursor position
tracking, tab-to-spaces, Ctrl+Enter to apply, and syncs back to the
property panel on save.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 18:04:23 +01:00
parent f9492bff4c
commit bb72c58a15

View File

@@ -76,6 +76,104 @@ body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', syste
.modal-actions { margin-top: 12px; display: flex; gap: 8px; justify-content: flex-end; }
.modal-actions button { padding: 6px 14px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg); color: var(--text); cursor: pointer; font-size: 12px; }
.modal-actions .primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 600; }
/* ===== Fullscreen Code Editor ===== */
#code-editor-overlay {
position: fixed; inset: 0; z-index: 200;
background: rgba(0,0,0,0.85);
display: none; flex-direction: column;
align-items: center; justify-content: center;
padding: 20px;
}
#code-editor-overlay.show { display: flex; }
#code-editor-panel {
width: min(900px, 95vw); height: min(680px, 90vh);
background: #1e1e2e; border: 1px solid #333;
border-radius: 8px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 16px 48px rgba(0,0,0,0.6);
}
/* Title bar (VSCode-like) */
#code-editor-titlebar {
display: flex; align-items: center; gap: 10px;
padding: 8px 16px; background: #181825;
border-bottom: 1px solid #333; flex-shrink: 0;
}
#code-editor-titlebar .tab {
padding: 4px 12px; background: #1e1e2e;
border-radius: 4px 4px 0 0; font-size: 12px;
color: var(--accent); font-family: 'Cascadia Code', 'Fira Code', monospace;
border: 1px solid #333; border-bottom: 1px solid #1e1e2e;
margin-bottom: -1px;
}
#code-editor-titlebar .title-hint {
color: #666; font-size: 11px; margin-left: auto;
}
#code-editor-titlebar .close-x {
width: 28px; height: 28px; border: none; background: transparent;
color: #888; cursor: pointer; font-size: 18px; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
}
#code-editor-titlebar .close-x:hover { background: #ff555530; color: var(--red); }
/* Editor body */
#code-editor-body {
flex: 1; display: flex; overflow: hidden;
}
/* Line numbers gutter */
#code-editor-gutter {
width: 48px; padding: 12px 8px 12px 0;
background: #181825; text-align: right;
font-family: 'Cascadia Code', 'Fira Code', monospace;
font-size: 13px; line-height: 1.6;
color: #555; user-select: none; overflow: hidden;
flex-shrink: 0;
}
/* Main textarea */
#code-editor-textarea {
flex: 1; resize: none; border: none; outline: none;
background: #1e1e2e; color: #cdd6f4;
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
font-size: 13px; line-height: 1.6;
padding: 12px 16px; tab-size: 2;
white-space: pre; overflow: auto;
}
#code-editor-textarea::selection { background: rgba(0,229,153,0.25); }
/* Status bar */
#code-editor-statusbar {
display: flex; align-items: center; gap: 16px;
padding: 4px 16px; background: #181825;
border-top: 1px solid #333; flex-shrink: 0;
font-size: 11px; color: #666;
}
#code-editor-statusbar .status-accent { color: var(--accent); }
/* Bottom actions */
#code-editor-actions {
display: flex; justify-content: flex-end; gap: 8px;
padding: 10px 16px; background: #181825;
border-top: 1px solid #333; flex-shrink: 0;
}
#code-editor-actions button {
padding: 6px 16px; border-radius: 4px; cursor: pointer;
font-size: 12px; font-weight: 600; border: 1px solid #333;
}
#code-editor-actions .btn-cancel { background: #2a2a3e; color: #aaa; }
#code-editor-actions .btn-cancel:hover { background: #333; }
#code-editor-actions .btn-save { background: var(--accent); color: #000; border-color: var(--accent); }
#code-editor-actions .btn-save:hover { filter: brightness(1.15); }
/* Expand button for property panel */
.btn-expand-code {
padding: 3px 8px; border: 1px solid var(--border); border-radius: 3px;
background: var(--bg); color: var(--text2); cursor: pointer;
font-size: 10px; transition: all 0.15s; flex-shrink: 0;
}
.btn-expand-code:hover { border-color: var(--accent); color: var(--accent); }
</style>
</head>
<body>
@@ -182,6 +280,31 @@ body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', syste
</div>
</div>
<!-- Fullscreen Code Editor -->
<div id="code-editor-overlay">
<div id="code-editor-panel">
<div id="code-editor-titlebar">
<div class="tab" id="code-editor-tab">verify.js</div>
<span class="title-hint">Module Verify Logic</span>
<button class="close-x" id="code-editor-close" title="Close (Esc)"></button>
</div>
<div id="code-editor-body">
<div id="code-editor-gutter"></div>
<textarea id="code-editor-textarea" spellcheck="false"></textarea>
</div>
<div id="code-editor-statusbar">
<span class="status-accent">JavaScript</span>
<span id="code-editor-cursor">Ln 1, Col 1</span>
<span>UTF-8</span>
<span style="margin-left:auto;">Tab Size: 2</span>
</div>
<div id="code-editor-actions">
<button class="btn-cancel" id="code-editor-discard">Discard</button>
<button class="btn-save" id="code-editor-apply">✓ Apply Changes</button>
</div>
</div>
</div>
<script>
// ==================== State ====================
const TILE = 16;
@@ -869,9 +992,9 @@ function updateProps() {
const portsStr = (ent.ports || []).map(p => `${p.name}:${p.dir}`).join(', ');
html += propText('Ports', 'ports', portsStr);
html += `<div class="prop-row" style="font-size:10px;color:#888;">Format: A:out, B:out, C:in</div>`;
// Verify code editor
// Verify code editor with expand button
const verifyCode = ent.verify || `(test) => {\n return test({A:0, B:0}).C === 0\n && test({A:1, B:1}).C === 1;\n}`;
html += `<div class="prop-row"><label>Verify (JS)</label><textarea data-prop="verify" style="font-family:monospace;font-size:11px;min-height:100px;white-space:pre;">${esc(verifyCode)}</textarea></div>`;
html += `<div class="prop-row" style="align-items:flex-start;"><label>Verify (JS)</label><textarea id="verify-textarea" data-prop="verify" style="font-family:monospace;font-size:11px;min-height:80px;white-space:pre;">${esc(verifyCode)}</textarea><button class="btn-expand-code" id="btn-expand-verify" title="Open fullscreen editor (Ctrl+E)">⛶</button></div>`;
}
}
@@ -884,6 +1007,13 @@ function updateProps() {
if (el.type === 'number') applyPropChange(el.dataset.prop, el.value, el.type);
});
});
// Wire expand button for verify code editor
const btnExpand = document.getElementById('btn-expand-verify');
const verifyTA = document.getElementById('verify-textarea');
if (btnExpand && verifyTA) {
btnExpand.addEventListener('click', () => openCodeEditor(verifyTA, 'verify.js'));
}
}
function propNum(label, prop, val) {
@@ -1330,6 +1460,98 @@ function createBiLink() {
init();
// ==================== Fullscreen Code Editor ====================
const codeEditorOverlay = document.getElementById('code-editor-overlay');
const codeEditorTextarea = document.getElementById('code-editor-textarea');
const codeEditorGutter = document.getElementById('code-editor-gutter');
const codeEditorCursor = document.getElementById('code-editor-cursor');
const codeEditorTab = document.getElementById('code-editor-tab');
let codeEditorTarget = null; // the prop textarea we're editing for
function openCodeEditor(propTextarea, tabName) {
codeEditorTarget = propTextarea;
codeEditorTab.textContent = tabName || 'verify.js';
codeEditorTextarea.value = propTextarea.value;
codeEditorOverlay.classList.add('show');
updateGutter();
updateCursorPos();
// Focus after animation frame
requestAnimationFrame(() => codeEditorTextarea.focus());
}
function closeCodeEditor(apply) {
if (apply && codeEditorTarget) {
codeEditorTarget.value = codeEditorTextarea.value;
// Trigger change event so applyPropChange fires
codeEditorTarget.dispatchEvent(new Event('change', { bubbles: true }));
}
codeEditorOverlay.classList.remove('show');
codeEditorTarget = null;
}
function updateGutter() {
const lines = codeEditorTextarea.value.split('\n').length;
let html = '';
for (let i = 1; i <= Math.max(lines, 20); i++) {
html += i + '\n';
}
codeEditorGutter.textContent = html;
}
function updateCursorPos() {
const val = codeEditorTextarea.value;
const pos = codeEditorTextarea.selectionStart;
const before = val.substring(0, pos);
const line = before.split('\n').length;
const col = pos - before.lastIndexOf('\n');
codeEditorCursor.textContent = `Ln ${line}, Col ${col}`;
}
// Sync gutter with scroll
codeEditorTextarea.addEventListener('scroll', () => {
codeEditorGutter.scrollTop = codeEditorTextarea.scrollTop;
});
// Update gutter + cursor on input
codeEditorTextarea.addEventListener('input', () => { updateGutter(); updateCursorPos(); });
codeEditorTextarea.addEventListener('click', updateCursorPos);
codeEditorTextarea.addEventListener('keyup', updateCursorPos);
// Tab key inserts spaces
codeEditorTextarea.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = codeEditorTextarea.selectionStart;
const end = codeEditorTextarea.selectionEnd;
const val = codeEditorTextarea.value;
codeEditorTextarea.value = val.substring(0, start) + ' ' + val.substring(end);
codeEditorTextarea.selectionStart = codeEditorTextarea.selectionEnd = start + 2;
updateGutter();
}
// Escape closes
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
closeCodeEditor(false);
}
// Ctrl+Enter applies
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
closeCodeEditor(true);
}
});
// Buttons
document.getElementById('code-editor-close').addEventListener('click', () => closeCodeEditor(false));
document.getElementById('code-editor-discard').addEventListener('click', () => closeCodeEditor(false));
document.getElementById('code-editor-apply').addEventListener('click', () => closeCodeEditor(true));
// Click outside panel to close
codeEditorOverlay.addEventListener('click', (e) => {
if (e.target === codeEditorOverlay) closeCodeEditor(false);
});
// Auto-load from server on start
loadFromServer().catch(() => {
console.log('[editor] Server load failed, using embedded defaults');