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:
226
editor.html
226
editor.html
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user