panels are all hideable/resizeable
parent
1e6fe41ddd
commit
3a040552bb
74
index.html
74
index.html
|
|
@ -34,47 +34,35 @@
|
||||||
<button id="btn-save" title="Save code to device as main.py" disabled>
|
<button id="btn-save" title="Save code to device as main.py" disabled>
|
||||||
<span class="icon">💾</span> Save
|
<span class="icon">💾</span> Save
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-projects" title="Open/save projects (browser or device)">
|
<button id="btn-projects" title="Toggle projects panel">
|
||||||
<span class="icon">📂</span> Projects
|
<span class="icon">📂</span> Projects
|
||||||
</button>
|
</button>
|
||||||
<span id="connection-status" class="status-disconnected">Disconnected</span>
|
<span id="connection-status" class="status-disconnected">Disconnected</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main workspace area -->
|
|
||||||
<main id="workspace-container">
|
<main id="workspace-container">
|
||||||
<!-- Blockly editor fills the top area -->
|
<!-- Top row: Blockly + Projects sidebar -->
|
||||||
|
<div id="top-area">
|
||||||
<div id="blockly-area">
|
<div id="blockly-area">
|
||||||
<div id="blockly-div"></div>
|
<div id="blockly-div"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom panels: code preview + serial terminal -->
|
<!-- Projects right sidebar -->
|
||||||
<div id="bottom-panels">
|
<div id="projects-panel" class="ide-panel">
|
||||||
<div id="resize-handle-h" class="resize-handle horizontal"></div>
|
<div class="panel-header">
|
||||||
<div id="code-panel">
|
<span class="panel-title">Projects</span>
|
||||||
<div class="panel-header">Generated Code</div>
|
<button class="panel-toggle" data-panel="projects-panel" title="Toggle panel">◂</button>
|
||||||
<pre id="code-preview"><code id="code-output"></code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="terminal-panel">
|
<div class="panel-body">
|
||||||
<div class="panel-header">Serial Terminal</div>
|
<!-- Tabs: Browser / Device -->
|
||||||
<div id="terminal-output"></div>
|
<div class="proj-tabs">
|
||||||
<div id="terminal-input-row">
|
<button class="proj-tab active" data-tab="browser">Browser</button>
|
||||||
<input type="text" id="terminal-input" placeholder="Type command…" disabled />
|
<button class="proj-tab" data-tab="device">Device</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Projects dialog -->
|
<!-- Browser tab -->
|
||||||
<div id="projects-overlay" class="hidden">
|
<div class="proj-tab-content" id="proj-tab-browser">
|
||||||
<div id="projects-modal">
|
|
||||||
<div class="projects-header">
|
|
||||||
<h3>Projects</h3>
|
|
||||||
<button id="projects-close">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="projects-columns">
|
|
||||||
<div class="projects-col">
|
|
||||||
<h4>Browser</h4>
|
|
||||||
<ul class="projects-list" id="browser-list"></ul>
|
<ul class="projects-list" id="browser-list"></ul>
|
||||||
<div class="projects-actions">
|
<div class="projects-actions">
|
||||||
<div class="projects-name-row">
|
<div class="projects-name-row">
|
||||||
|
|
@ -87,8 +75,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-col">
|
|
||||||
<h4>Device</h4>
|
<!-- Device tab -->
|
||||||
|
<div class="proj-tab-content hidden" id="proj-tab-device">
|
||||||
<ul class="projects-list" id="device-list"></ul>
|
<ul class="projects-list" id="device-list"></ul>
|
||||||
<div class="projects-actions">
|
<div class="projects-actions">
|
||||||
<div class="projects-name-row">
|
<div class="projects-name-row">
|
||||||
|
|
@ -106,6 +95,33 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom panels: code preview + serial terminal -->
|
||||||
|
<div id="bottom-panels">
|
||||||
|
<div id="resize-handle-h" class="resize-handle horizontal"></div>
|
||||||
|
<div id="code-panel" class="ide-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">Generated Code</span>
|
||||||
|
<button class="panel-toggle" data-panel="code-panel" title="Toggle panel">▾</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<pre id="code-preview"><code id="code-output"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="terminal-panel" class="ide-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">Serial Terminal</span>
|
||||||
|
<button class="panel-toggle" data-panel="terminal-panel" title="Toggle panel">▾</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div id="terminal-output"></div>
|
||||||
|
<div id="terminal-input-row">
|
||||||
|
<input type="text" id="terminal-input" placeholder="Type command..." disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<!-- Flash progress overlay -->
|
<!-- Flash progress overlay -->
|
||||||
<div id="flash-overlay" class="hidden">
|
<div id="flash-overlay" class="hidden">
|
||||||
<div id="flash-modal">
|
<div id="flash-modal">
|
||||||
|
|
|
||||||
21
src/main.js
21
src/main.js
|
|
@ -13,8 +13,8 @@ import { connect, disconnect, isConnected, onData, writeString } from './serial/
|
||||||
import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js';
|
import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js';
|
||||||
import { flashFirmware } from './serial/flasher.js';
|
import { flashFirmware } from './serial/flasher.js';
|
||||||
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
||||||
import { initResizablePanels } from './ui/panels.js';
|
import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js';
|
||||||
import { initProjectsDialog, open as openProjects } from './ui/projectsDialog.js';
|
import { initProjectsDialog, refreshAll as refreshProjects, refreshDeviceList } from './ui/projectsDialog.js';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
// ─── Blockly Workspace ───────────────────────────────────
|
// ─── Blockly Workspace ───────────────────────────────────
|
||||||
|
|
@ -77,14 +77,22 @@ loadWorkspace();
|
||||||
function onResize() {
|
function onResize() {
|
||||||
const blocklyArea = document.getElementById('blockly-area');
|
const blocklyArea = document.getElementById('blockly-area');
|
||||||
const blocklyDiv = document.getElementById('blockly-div');
|
const blocklyDiv = document.getElementById('blockly-div');
|
||||||
|
if (blocklyArea && blocklyDiv) {
|
||||||
blocklyDiv.style.width = blocklyArea.offsetWidth + 'px';
|
blocklyDiv.style.width = blocklyArea.offsetWidth + 'px';
|
||||||
blocklyDiv.style.height = blocklyArea.offsetHeight + 'px';
|
blocklyDiv.style.height = blocklyArea.offsetHeight + 'px';
|
||||||
Blockly.svgResize(workspace);
|
Blockly.svgResize(workspace);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', onResize);
|
window.addEventListener('resize', onResize);
|
||||||
onResize();
|
onResize();
|
||||||
initResizablePanels();
|
initResizablePanels();
|
||||||
|
initPanelToggles();
|
||||||
|
initProjectTabs();
|
||||||
|
setProjectsPanelCallbacks({
|
||||||
|
onDeviceTab: () => refreshDeviceList(),
|
||||||
|
onExpand: () => refreshProjects(),
|
||||||
|
});
|
||||||
|
|
||||||
// ─── UI State Helpers ────────────────────────────────────
|
// ─── UI State Helpers ────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -330,7 +338,14 @@ initProjectsDialog({
|
||||||
isConnected,
|
isConnected,
|
||||||
});
|
});
|
||||||
|
|
||||||
btnProjects.addEventListener('click', () => openProjects());
|
btnProjects.addEventListener('click', () => {
|
||||||
|
const panel = document.getElementById('projects-panel');
|
||||||
|
panel.classList.toggle('collapsed');
|
||||||
|
const btn = panel.querySelector('.panel-toggle');
|
||||||
|
if (btn) btn.innerHTML = panel.classList.contains('collapsed') ? '◂' : '▸';
|
||||||
|
refreshProjects();
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
});
|
||||||
|
|
||||||
terminalInput.addEventListener('keydown', async (e) => {
|
terminalInput.addEventListener('keydown', async (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
|
|
|
||||||
477
src/style.css
477
src/style.css
|
|
@ -21,6 +21,7 @@
|
||||||
--border: #313244;
|
--border: #313244;
|
||||||
--radius: 6px;
|
--radius: 6px;
|
||||||
--toolbar-height: 48px;
|
--toolbar-height: 48px;
|
||||||
|
--panel-header-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
|
|
@ -140,13 +141,21 @@ html, body {
|
||||||
height: calc(100vh - var(--toolbar-height));
|
height: calc(100vh - var(--toolbar-height));
|
||||||
}
|
}
|
||||||
|
|
||||||
#blockly-area {
|
/* --- Top Area (Blockly + Projects sidebar) --- */
|
||||||
flex: 1 1 60%;
|
#top-area {
|
||||||
position: relative;
|
flex: 1 1 65%;
|
||||||
|
display: flex;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#blockly-area {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
min-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#blockly-div {
|
#blockly-div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
@ -155,12 +164,17 @@ html, body {
|
||||||
/* --- Bottom Panels --- */
|
/* --- Bottom Panels --- */
|
||||||
#bottom-panels {
|
#bottom-panels {
|
||||||
flex: 0 0 35%;
|
flex: 0 0 35%;
|
||||||
min-height: 120px;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bottom-panels.all-collapsed {
|
||||||
|
flex: 0 0 var(--panel-header-height);
|
||||||
|
min-height: var(--panel-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
.resize-handle.horizontal {
|
.resize-handle.horizontal {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -3px;
|
top: -3px;
|
||||||
|
|
@ -171,27 +185,79 @@ html, body {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code-panel, #terminal-panel {
|
/* --- Generic Panel --- */
|
||||||
|
.ide-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-panel .panel-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ide-panel.collapsed .panel-body {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 0 12px;
|
||||||
|
height: var(--panel-header-height);
|
||||||
|
min-height: var(--panel-header-height);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header .panel-title {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-toggle:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Bottom panel specifics --- */
|
||||||
|
#code-panel, #terminal-panel {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#code-panel {
|
#code-panel {
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
#code-panel.collapsed,
|
||||||
background: var(--bg-secondary);
|
#terminal-panel.collapsed {
|
||||||
padding: 6px 12px;
|
flex: 0 0 auto;
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.8px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Code Preview --- */
|
/* --- Code Preview --- */
|
||||||
|
|
@ -206,10 +272,11 @@ html, body {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Terminal --- */
|
/* --- Terminal --- */
|
||||||
#terminal-panel {
|
#terminal-panel .panel-body {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,12 +309,181 @@ html, body {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#terminal-input:disabled {
|
#terminal-input:disabled { opacity: 0.5; }
|
||||||
opacity: 0.5;
|
#terminal-input::placeholder { color: var(--text-muted); }
|
||||||
|
|
||||||
|
/* --- Projects Sidebar Panel --- */
|
||||||
|
#projects-panel {
|
||||||
|
width: 280px;
|
||||||
|
min-width: 0;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
transition: width 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#terminal-input::placeholder {
|
#projects-panel.collapsed {
|
||||||
|
width: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects-panel.collapsed .panel-header {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
text-orientation: mixed;
|
||||||
|
height: 100%;
|
||||||
|
width: 30px;
|
||||||
|
padding: 10px 0;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
border-bottom: none;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects-panel.collapsed .panel-toggle {
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Projects tabs */
|
||||||
|
.proj-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proj-tab {
|
||||||
|
flex: 1;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proj-tab:hover { color: var(--text-primary); }
|
||||||
|
|
||||||
|
.proj-tab.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proj-tab-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proj-tab-content.hidden { display: none; }
|
||||||
|
|
||||||
|
/* Projects list */
|
||||||
|
.projects-list {
|
||||||
|
list-style: none;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 40vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-list li {
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-list li:hover { background: var(--bg-surface); }
|
||||||
|
|
||||||
|
.projects-list li.selected {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-list .empty-msg {
|
||||||
|
padding: 6px 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Projects actions */
|
||||||
|
.projects-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-name-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-name-row input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-name-row input:focus { border-color: var(--accent); }
|
||||||
|
|
||||||
|
.projects-btn-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-actions button {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-actions button:hover:not(:disabled) {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-toolbar);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-actions button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-actions button.btn-danger:hover:not(:disabled) {
|
||||||
|
background: var(--red);
|
||||||
|
border-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-note {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
min-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Blockly Overrides (dark theme) --- */
|
/* --- Blockly Overrides (dark theme) --- */
|
||||||
|
|
@ -273,206 +509,15 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||||
width: 8px;
|
::-webkit-scrollbar-track { background: var(--bg-secondary); }
|
||||||
height: 8px;
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||||
}
|
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
/* --- Utility --- */
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Projects Dialog --- */
|
|
||||||
|
|
||||||
#projects-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#projects-modal {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px 28px;
|
|
||||||
width: 680px;
|
|
||||||
max-width: 95vw;
|
|
||||||
max-height: 85vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#projects-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 4px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#projects-close:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-columns {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-col {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-col h4 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-list {
|
|
||||||
list-style: none;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: 180px;
|
|
||||||
max-height: 260px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-list li {
|
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-list li:hover {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-list li.selected {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--bg-toolbar);
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-list .empty-msg {
|
|
||||||
padding: 8px 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
font-style: italic;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-name-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-name-row input {
|
|
||||||
flex: 1;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
outline: none;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-name-row input:focus {
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-btn-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-actions button {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 6px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s, border-color 0.15s;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-actions button:hover:not(:disabled) {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--bg-toolbar);
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-actions button:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-actions button.btn-danger:hover:not(:disabled) {
|
|
||||||
background: var(--red);
|
|
||||||
border-color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-note {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-style: italic;
|
|
||||||
min-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Flash Progress Overlay --- */
|
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
/* --- Flash Progress Overlay --- */
|
||||||
#flash-overlay {
|
#flash-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
export function initResizablePanels() {
|
export function initResizablePanels() {
|
||||||
const handle = document.getElementById('resize-handle-h');
|
const handle = document.getElementById('resize-handle-h');
|
||||||
const container = document.getElementById('workspace-container');
|
const container = document.getElementById('workspace-container');
|
||||||
const blocklyArea = document.getElementById('blockly-area');
|
const topArea = document.getElementById('top-area');
|
||||||
const bottomPanels = document.getElementById('bottom-panels');
|
const bottomPanels = document.getElementById('bottom-panels');
|
||||||
|
|
||||||
if (!handle || !container || !blocklyArea || !bottomPanels) return;
|
if (!handle || !container || !topArea || !bottomPanels) return;
|
||||||
|
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
|
|
||||||
|
|
@ -15,18 +15,89 @@ export function initResizablePanels() {
|
||||||
|
|
||||||
document.addEventListener('mousemove', (e) => {
|
document.addEventListener('mousemove', (e) => {
|
||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
const containerRect = container.getBoundingClientRect();
|
const rect = container.getBoundingClientRect();
|
||||||
const offset = e.clientY - containerRect.top;
|
const offset = e.clientY - rect.top;
|
||||||
const totalHeight = containerRect.height;
|
const pct = Math.max(20, Math.min(85, (offset / rect.height) * 100));
|
||||||
const topPercent = Math.max(20, Math.min(80, (offset / totalHeight) * 100));
|
|
||||||
|
|
||||||
blocklyArea.style.flex = `0 0 ${topPercent}%`;
|
topArea.style.flex = `0 0 ${pct}%`;
|
||||||
bottomPanels.style.flex = `0 0 ${100 - topPercent}%`;
|
bottomPanels.style.flex = `0 0 ${100 - pct}%`;
|
||||||
|
|
||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => { dragging = false; });
|
||||||
dragging = false;
|
}
|
||||||
|
|
||||||
|
export function initPanelToggles() {
|
||||||
|
const bottomPanels = document.getElementById('bottom-panels');
|
||||||
|
|
||||||
|
document.querySelectorAll('.panel-toggle').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const panel = document.getElementById(btn.dataset.panel);
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
panel.classList.toggle('collapsed');
|
||||||
|
updateToggleIcon(btn, panel);
|
||||||
|
|
||||||
|
if (bottomPanels) {
|
||||||
|
const code = document.getElementById('code-panel');
|
||||||
|
const term = document.getElementById('terminal-panel');
|
||||||
|
const allCollapsed = code?.classList.contains('collapsed') && term?.classList.contains('collapsed');
|
||||||
|
bottomPanels.classList.toggle('all-collapsed', allCollapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btn.dataset.panel === 'projects-panel' && !panel.classList.contains('collapsed') && onProjectsPanelExpanded) {
|
||||||
|
onProjectsPanelExpanded();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.panel-toggle').forEach(btn => {
|
||||||
|
const panel = document.getElementById(btn.dataset.panel);
|
||||||
|
if (panel) updateToggleIcon(btn, panel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateToggleIcon(btn, panel) {
|
||||||
|
const collapsed = panel.classList.contains('collapsed');
|
||||||
|
const id = panel.id;
|
||||||
|
|
||||||
|
if (id === 'projects-panel') {
|
||||||
|
btn.innerHTML = collapsed ? '◂' : '▸';
|
||||||
|
} else {
|
||||||
|
btn.innerHTML = collapsed ? '▸' : '▾';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let onDeviceTabActivated = null;
|
||||||
|
let onProjectsPanelExpanded = null;
|
||||||
|
|
||||||
|
export function setProjectsPanelCallbacks({ onDeviceTab, onExpand }) {
|
||||||
|
onDeviceTabActivated = onDeviceTab;
|
||||||
|
onProjectsPanelExpanded = onExpand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initProjectTabs() {
|
||||||
|
const tabs = document.querySelectorAll('.proj-tab');
|
||||||
|
const contents = {
|
||||||
|
browser: document.getElementById('proj-tab-browser'),
|
||||||
|
device: document.getElementById('proj-tab-device'),
|
||||||
|
};
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
|
||||||
|
Object.values(contents).forEach(c => c?.classList.add('hidden'));
|
||||||
|
const target = contents[tab.dataset.tab];
|
||||||
|
if (target) target.classList.remove('hidden');
|
||||||
|
|
||||||
|
if (tab.dataset.tab === 'device' && onDeviceTabActivated) {
|
||||||
|
onDeviceTabActivated();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ let execCode = null;
|
||||||
let writeFile = null;
|
let writeFile = null;
|
||||||
let checkConnected = null;
|
let checkConnected = null;
|
||||||
|
|
||||||
// DOM refs (cached on first open)
|
let browserList, deviceList;
|
||||||
let overlay, browserList, deviceList;
|
|
||||||
let browserNameInput, deviceNameInput;
|
let browserNameInput, deviceNameInput;
|
||||||
let browserSaveBtn, browserLoadBtn, browserDeleteBtn;
|
let browserSaveBtn, browserLoadBtn, browserDeleteBtn;
|
||||||
let deviceSaveBtn, deviceLoadBtn, deviceDeleteBtn;
|
let deviceSaveBtn, deviceLoadBtn, deviceDeleteBtn;
|
||||||
|
|
@ -25,7 +24,6 @@ export function initProjectsDialog(deps) {
|
||||||
writeFile = deps.writeFileToDevice;
|
writeFile = deps.writeFileToDevice;
|
||||||
checkConnected = deps.isConnected;
|
checkConnected = deps.isConnected;
|
||||||
|
|
||||||
overlay = document.getElementById('projects-overlay');
|
|
||||||
browserList = document.getElementById('browser-list');
|
browserList = document.getElementById('browser-list');
|
||||||
deviceList = document.getElementById('device-list');
|
deviceList = document.getElementById('device-list');
|
||||||
browserNameInput = document.getElementById('browser-save-name');
|
browserNameInput = document.getElementById('browser-save-name');
|
||||||
|
|
@ -38,27 +36,20 @@ export function initProjectsDialog(deps) {
|
||||||
deviceDeleteBtn = document.getElementById('device-delete-btn');
|
deviceDeleteBtn = document.getElementById('device-delete-btn');
|
||||||
deviceStatus = document.getElementById('device-status');
|
deviceStatus = document.getElementById('device-status');
|
||||||
|
|
||||||
document.getElementById('projects-close').addEventListener('click', close);
|
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
|
||||||
|
|
||||||
browserSaveBtn.addEventListener('click', saveBrowser);
|
browserSaveBtn.addEventListener('click', saveBrowser);
|
||||||
browserLoadBtn.addEventListener('click', loadBrowser);
|
browserLoadBtn.addEventListener('click', loadBrowser);
|
||||||
browserDeleteBtn.addEventListener('click', deleteBrowser);
|
browserDeleteBtn.addEventListener('click', deleteBrowser);
|
||||||
deviceSaveBtn.addEventListener('click', saveDevice);
|
deviceSaveBtn.addEventListener('click', saveDevice);
|
||||||
deviceLoadBtn.addEventListener('click', loadDevice);
|
deviceLoadBtn.addEventListener('click', loadDevice);
|
||||||
deviceDeleteBtn.addEventListener('click', deleteDevice);
|
deviceDeleteBtn.addEventListener('click', deleteDevice);
|
||||||
}
|
|
||||||
|
|
||||||
export function open() {
|
|
||||||
browserSelected = null;
|
|
||||||
deviceSelected = null;
|
|
||||||
overlay.classList.remove('hidden');
|
|
||||||
refreshBrowserList();
|
refreshBrowserList();
|
||||||
refreshDeviceList();
|
refreshDeviceList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
export function refreshAll() {
|
||||||
overlay.classList.add('hidden');
|
refreshBrowserList();
|
||||||
|
refreshDeviceList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Browser column ──────────────────────────────────────
|
// ─── Browser column ──────────────────────────────────────
|
||||||
|
|
@ -123,7 +114,6 @@ function loadBrowser() {
|
||||||
const state = projects[browserSelected];
|
const state = projects[browserSelected];
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
Blockly.serialization.workspaces.load(state, workspace);
|
Blockly.serialization.workspaces.load(state, workspace);
|
||||||
close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteBrowser() {
|
function deleteBrowser() {
|
||||||
|
|
@ -136,7 +126,7 @@ function deleteBrowser() {
|
||||||
|
|
||||||
// ─── Device column ───────────────────────────────────────
|
// ─── Device column ───────────────────────────────────────
|
||||||
|
|
||||||
async function refreshDeviceList() {
|
export async function refreshDeviceList() {
|
||||||
deviceList.innerHTML = '';
|
deviceList.innerHTML = '';
|
||||||
deviceSelected = null;
|
deviceSelected = null;
|
||||||
updateDeviceButtons();
|
updateDeviceButtons();
|
||||||
|
|
@ -223,7 +213,7 @@ async function loadDevice() {
|
||||||
);
|
);
|
||||||
const state = JSON.parse(raw.trim());
|
const state = JSON.parse(raw.trim());
|
||||||
Blockly.serialization.workspaces.load(state, workspace);
|
Blockly.serialization.workspaces.load(state, workspace);
|
||||||
close();
|
deviceStatus.textContent = '';
|
||||||
} catch {
|
} catch {
|
||||||
deviceStatus.textContent = 'Load failed';
|
deviceStatus.textContent = 'Load failed';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue