diff --git a/src/main.js b/src/main.js
index 53845ee..1e6e8f3 100644
--- a/src/main.js
+++ b/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 { flashFirmware } from './serial/flasher.js';
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
-import { initResizablePanels } from './ui/panels.js';
-import { initProjectsDialog, open as openProjects } from './ui/projectsDialog.js';
+import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js';
+import { initProjectsDialog, refreshAll as refreshProjects, refreshDeviceList } from './ui/projectsDialog.js';
import './style.css';
// ─── Blockly Workspace ───────────────────────────────────
@@ -77,14 +77,22 @@ loadWorkspace();
function onResize() {
const blocklyArea = document.getElementById('blockly-area');
const blocklyDiv = document.getElementById('blockly-div');
- blocklyDiv.style.width = blocklyArea.offsetWidth + 'px';
- blocklyDiv.style.height = blocklyArea.offsetHeight + 'px';
- Blockly.svgResize(workspace);
+ if (blocklyArea && blocklyDiv) {
+ blocklyDiv.style.width = blocklyArea.offsetWidth + 'px';
+ blocklyDiv.style.height = blocklyArea.offsetHeight + 'px';
+ Blockly.svgResize(workspace);
+ }
}
window.addEventListener('resize', onResize);
onResize();
initResizablePanels();
+initPanelToggles();
+initProjectTabs();
+setProjectsPanelCallbacks({
+ onDeviceTab: () => refreshDeviceList(),
+ onExpand: () => refreshProjects(),
+});
// ─── UI State Helpers ────────────────────────────────────
@@ -330,7 +338,14 @@ initProjectsDialog({
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) => {
if (e.key === 'Enter') {
diff --git a/src/style.css b/src/style.css
index 26c183d..b7e5f0a 100644
--- a/src/style.css
+++ b/src/style.css
@@ -21,6 +21,7 @@
--border: #313244;
--radius: 6px;
--toolbar-height: 48px;
+ --panel-header-height: 30px;
}
html, body {
@@ -140,13 +141,21 @@ html, body {
height: calc(100vh - var(--toolbar-height));
}
-#blockly-area {
- flex: 1 1 60%;
- position: relative;
+/* --- Top Area (Blockly + Projects sidebar) --- */
+#top-area {
+ flex: 1 1 65%;
+ display: flex;
min-height: 200px;
overflow: hidden;
}
+#blockly-area {
+ flex: 1;
+ position: relative;
+ min-width: 200px;
+ overflow: hidden;
+}
+
#blockly-div {
position: absolute;
inset: 0;
@@ -155,12 +164,17 @@ html, body {
/* --- Bottom Panels --- */
#bottom-panels {
flex: 0 0 35%;
- min-height: 120px;
+ min-height: 0;
display: flex;
position: relative;
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 {
position: absolute;
top: -3px;
@@ -171,27 +185,79 @@ html, body {
z-index: 10;
}
-#code-panel, #terminal-panel {
+/* --- Generic Panel --- */
+.ide-panel {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.ide-panel .panel-body {
flex: 1;
display: flex;
flex-direction: column;
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 {
border-right: 1px solid var(--border);
}
-.panel-header {
- background: var(--bg-secondary);
- padding: 6px 12px;
- 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-panel.collapsed,
+#terminal-panel.collapsed {
+ flex: 0 0 auto;
}
/* --- Code Preview --- */
@@ -206,10 +272,11 @@ html, body {
color: var(--text-primary);
white-space: pre;
tab-size: 4;
+ margin: 0;
}
/* --- Terminal --- */
-#terminal-panel {
+#terminal-panel .panel-body {
background: var(--bg-secondary);
}
@@ -242,12 +309,181 @@ html, body {
outline: none;
}
-#terminal-input:disabled {
- opacity: 0.5;
+#terminal-input:disabled { 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);
+ 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) --- */
@@ -273,206 +509,15 @@ html, body {
}
/* Scrollbar styling */
-::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
+::-webkit-scrollbar { width: 8px; height: 8px; }
+::-webkit-scrollbar-track { background: var(--bg-secondary); }
+::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
+::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
-::-webkit-scrollbar-track {
- 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 --- */
+/* --- Utility --- */
.hidden { display: none !important; }
+/* --- Flash Progress Overlay --- */
#flash-overlay {
position: fixed;
inset: 0;
diff --git a/src/ui/panels.js b/src/ui/panels.js
index dd99db8..dd04dcb 100644
--- a/src/ui/panels.js
+++ b/src/ui/panels.js
@@ -1,10 +1,10 @@
export function initResizablePanels() {
const handle = document.getElementById('resize-handle-h');
const container = document.getElementById('workspace-container');
- const blocklyArea = document.getElementById('blockly-area');
+ const topArea = document.getElementById('top-area');
const bottomPanels = document.getElementById('bottom-panels');
- if (!handle || !container || !blocklyArea || !bottomPanels) return;
+ if (!handle || !container || !topArea || !bottomPanels) return;
let dragging = false;
@@ -15,18 +15,89 @@ export function initResizablePanels() {
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
- const containerRect = container.getBoundingClientRect();
- const offset = e.clientY - containerRect.top;
- const totalHeight = containerRect.height;
- const topPercent = Math.max(20, Math.min(80, (offset / totalHeight) * 100));
+ const rect = container.getBoundingClientRect();
+ const offset = e.clientY - rect.top;
+ const pct = Math.max(20, Math.min(85, (offset / rect.height) * 100));
- blocklyArea.style.flex = `0 0 ${topPercent}%`;
- bottomPanels.style.flex = `0 0 ${100 - topPercent}%`;
+ topArea.style.flex = `0 0 ${pct}%`;
+ bottomPanels.style.flex = `0 0 ${100 - pct}%`;
window.dispatchEvent(new Event('resize'));
});
- document.addEventListener('mouseup', () => {
- dragging = false;
+ document.addEventListener('mouseup', () => { 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();
+ }
+ });
});
}
diff --git a/src/ui/projectsDialog.js b/src/ui/projectsDialog.js
index ccaf42c..891df1e 100644
--- a/src/ui/projectsDialog.js
+++ b/src/ui/projectsDialog.js
@@ -8,8 +8,7 @@ let execCode = null;
let writeFile = null;
let checkConnected = null;
-// DOM refs (cached on first open)
-let overlay, browserList, deviceList;
+let browserList, deviceList;
let browserNameInput, deviceNameInput;
let browserSaveBtn, browserLoadBtn, browserDeleteBtn;
let deviceSaveBtn, deviceLoadBtn, deviceDeleteBtn;
@@ -25,7 +24,6 @@ export function initProjectsDialog(deps) {
writeFile = deps.writeFileToDevice;
checkConnected = deps.isConnected;
- overlay = document.getElementById('projects-overlay');
browserList = document.getElementById('browser-list');
deviceList = document.getElementById('device-list');
browserNameInput = document.getElementById('browser-save-name');
@@ -38,27 +36,20 @@ export function initProjectsDialog(deps) {
deviceDeleteBtn = document.getElementById('device-delete-btn');
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);
browserLoadBtn.addEventListener('click', loadBrowser);
browserDeleteBtn.addEventListener('click', deleteBrowser);
deviceSaveBtn.addEventListener('click', saveDevice);
deviceLoadBtn.addEventListener('click', loadDevice);
deviceDeleteBtn.addEventListener('click', deleteDevice);
-}
-export function open() {
- browserSelected = null;
- deviceSelected = null;
- overlay.classList.remove('hidden');
refreshBrowserList();
refreshDeviceList();
}
-function close() {
- overlay.classList.add('hidden');
+export function refreshAll() {
+ refreshBrowserList();
+ refreshDeviceList();
}
// ─── Browser column ──────────────────────────────────────
@@ -123,7 +114,6 @@ function loadBrowser() {
const state = projects[browserSelected];
if (!state) return;
Blockly.serialization.workspaces.load(state, workspace);
- close();
}
function deleteBrowser() {
@@ -136,7 +126,7 @@ function deleteBrowser() {
// ─── Device column ───────────────────────────────────────
-async function refreshDeviceList() {
+export async function refreshDeviceList() {
deviceList.innerHTML = '';
deviceSelected = null;
updateDeviceButtons();
@@ -223,7 +213,7 @@ async function loadDevice() {
);
const state = JSON.parse(raw.trim());
Blockly.serialization.workspaces.load(state, workspace);
- close();
+ deviceStatus.textContent = '';
} catch {
deviceStatus.textContent = 'Load failed';
}