Mobile UX polish: scrollable column, menu workspace section, pin tap fixes
The phone layout had three problems that compounded when running ADC / Pin / Serial demos: the editor refused to shrink past 42vh so panels spilled over a clipped console, the workspace badge was crammed full of buttons, and the IN-pin toggle button was unreliable to tap. - styles.css: on `<= 768px`, make `.main-content` scroll vertically and give the editor a fixed 50vh height (drops to 32vh via `:has()` when a simulator panel is open). All panels + console pin to `flex: 0 0 auto` so flexbox stops squashing them. Inner panel scroll caps tightened to 22vh so two stacked panels don't push the console below the fold. `.pin-toggle` gets `touch-action: manipulation` + `user-select: none` to fix iOS taps inside scrollable parents. - script.js: pin button now has a `pointerup` backup with a 300 ms debounce alongside `click` (Safari sometimes drops `click` on small buttons inside scroll containers). The `⋮` workspace menu auto-closes on outside `pointerdown` as well as `click`, so the open dropdown can't sit on top of the Pin panel and absorb taps. - script.js / index.html / styles.css: move every Workspace action (Export, Import, Reset demos, plus the local-mode-only Folder…, Reconnect, IndexedDB swap, Exit) out of the badge and into a new "Workspace" section in the `⋮` menu. Badge keeps just the storage label. Adds `.menu-separator`, `.menu-section-label`, `.menu-note`, and `.menu-action` styles; removes the now-unused `.workspace-badge-action` / `-exit` / `-note` rules. - bundled-demos/pin_demo.py: pin 4 is now driven exclusively by the IRQ handler, so it stays steady until the IN button is pressed — previously it auto-flashed via on/off in the loop, which made the IRQ effect indistinguishable from the existing animation. The IRQ handler also no longer prints on every press (the panel indicator is the feedback). Cache busters: styles.css 32 -> 36, script.js 57 -> 59. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
A "Pins" panel appears below the editor while this script runs:
|
A "Pins" panel appears below the editor while this script runs:
|
||||||
|
|
||||||
* Pin 2 (OUT) — blinks every 200 ms; the indicator follows along.
|
* Pin 2 (OUT) — blinks automatically every ~200 ms via ``.value(...)``.
|
||||||
* Pin 4 (OUT) — chases through .on() / .off() / .toggle().
|
* Pin 4 (OUT) — stays put until you press the button, then flips
|
||||||
* Pin 0 (IN) — click the toggle button in the panel to flip its value.
|
(driven from the IRQ handler with ``.toggle()``).
|
||||||
When it goes 0 -> 1 we register an IRQ that toggles pin 2.
|
* Pin 0 (IN) — click the toggle button in the panel to drive a 0 → 1
|
||||||
* Pin 13 (PWM) — duty sweeps up and down; the bar shows the live duty cycle.
|
rising edge; the IRQ fires and flips pin 4.
|
||||||
|
* Pin 13 (PWM) — duty sweeps up and down; the bar shows the live duty.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@@ -21,8 +22,9 @@ fader = PWM(Pin(13), freq=1000, duty_u16=0)
|
|||||||
|
|
||||||
|
|
||||||
def on_button(pin):
|
def on_button(pin):
|
||||||
print("[irq] button rising edge -> toggling pin 2")
|
# Pin 4 is IRQ-driven on purpose — its only source of change is the
|
||||||
led_a.toggle()
|
# button press, so when you see it flip you know the IRQ fired.
|
||||||
|
led_b.toggle()
|
||||||
|
|
||||||
|
|
||||||
button.irq(handler=on_button, trigger=Pin.IRQ_RISING)
|
button.irq(handler=on_button, trigger=Pin.IRQ_RISING)
|
||||||
@@ -33,12 +35,10 @@ duty = 0
|
|||||||
direction = 1024
|
direction = 1024
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# Pin 2: fast on/off via direct .value(...) writes (no IRQ involvement).
|
||||||
led_a.value(tick % 2)
|
led_a.value(tick % 2)
|
||||||
if tick % 4 == 0:
|
|
||||||
led_b.on()
|
|
||||||
elif tick % 4 == 2:
|
|
||||||
led_b.off()
|
|
||||||
|
|
||||||
|
# Pin 13: triangular duty sweep so the PWM bar visibly fills and drains.
|
||||||
duty += direction
|
duty += direction
|
||||||
if duty >= 65535:
|
if duty >= 65535:
|
||||||
duty = 65535
|
duty = 65535
|
||||||
@@ -48,6 +48,8 @@ while True:
|
|||||||
direction = 1024
|
direction = 1024
|
||||||
fader.duty_u16(duty)
|
fader.duty_u16(duty)
|
||||||
|
|
||||||
|
# Poll the IN pin so its IRQ actually fires when the panel button changes.
|
||||||
|
# (`.value()` reads the current state and dispatches any pending edge.)
|
||||||
button.value()
|
button.value()
|
||||||
|
|
||||||
tick += 1
|
tick += 1
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta name="theme-color" content="#2d3748">
|
<meta name="theme-color" content="#2d3748">
|
||||||
<title>LED Editor</title>
|
<title>LED Editor</title>
|
||||||
<link rel="icon" href="data:,">
|
<link rel="icon" href="data:,">
|
||||||
<link rel="stylesheet" href="/static/styles.css?v=32">
|
<link rel="stylesheet" href="/static/styles.css?v=36">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -48,6 +48,9 @@
|
|||||||
<input type="checkbox" id="panel-16x16-checkbox" />
|
<input type="checkbox" id="panel-16x16-checkbox" />
|
||||||
16×16 panel
|
16×16 panel
|
||||||
</label>
|
</label>
|
||||||
|
<div class="menu-separator" role="separator"></div>
|
||||||
|
<div class="menu-section-label" id="workspace-menu-label">Workspace</div>
|
||||||
|
<div id="workspace-menu-actions" role="group"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,6 +121,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/static/script.js?v=57"></script>
|
<script type="module" src="/static/script.js?v=59"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -147,93 +147,123 @@ class TextEditor {
|
|||||||
|
|
||||||
async updateWorkspaceBanner() {
|
async updateWorkspaceBanner() {
|
||||||
const badge = document.getElementById('workspace-badge');
|
const badge = document.getElementById('workspace-badge');
|
||||||
|
const menuActions = document.getElementById('workspace-menu-actions');
|
||||||
|
const menuLabel = document.getElementById('workspace-menu-label');
|
||||||
if (!badge) return;
|
if (!badge) return;
|
||||||
if (this.localMode) {
|
|
||||||
|
// Helper: a button styled as a menu item.
|
||||||
|
const makeMenuButton = (text, title, onClick, opts = {}) => {
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.type = 'button';
|
||||||
|
b.className = 'menu-item menu-action' + (opts.danger ? ' menu-action-danger' : '');
|
||||||
|
b.setAttribute('role', 'menuitem');
|
||||||
|
b.textContent = text;
|
||||||
|
if (title) b.title = title;
|
||||||
|
b.addEventListener('click', (event) => {
|
||||||
|
// Close the dropdown after a click — feels more like a normal menu.
|
||||||
|
const details = document.getElementById('header-menu');
|
||||||
|
if (details) details.open = false;
|
||||||
|
onClick(event);
|
||||||
|
});
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: a passive note line inside the menu (e.g. for "no folder picker").
|
||||||
|
const makeMenuNote = (text, title) => {
|
||||||
|
const n = document.createElement('div');
|
||||||
|
n.className = 'menu-note';
|
||||||
|
n.textContent = text;
|
||||||
|
if (title) n.title = title;
|
||||||
|
return n;
|
||||||
|
};
|
||||||
|
|
||||||
badge.innerHTML = '';
|
badge.innerHTML = '';
|
||||||
|
if (menuActions) menuActions.innerHTML = '';
|
||||||
|
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.className = 'workspace-badge-label';
|
label.className = 'workspace-badge-label';
|
||||||
label.textContent = 'Local · IndexedDB';
|
|
||||||
badge.appendChild(label);
|
badge.appendChild(label);
|
||||||
|
badge.classList.remove('hidden');
|
||||||
|
|
||||||
|
if (this.localMode) {
|
||||||
|
label.textContent = 'Local · IndexedDB';
|
||||||
|
if (menuLabel) menuLabel.textContent = 'Workspace · Local';
|
||||||
|
|
||||||
|
if (menuActions) {
|
||||||
if (supportsFolderPicker()) {
|
if (supportsFolderPicker()) {
|
||||||
const pick = document.createElement('button');
|
menuActions.appendChild(
|
||||||
pick.type = 'button';
|
makeMenuButton('📁 Save to folder…', 'Save files to a folder on this device', () =>
|
||||||
pick.className = 'workspace-badge-action';
|
this.pickLocalFolder(),
|
||||||
pick.textContent = 'Folder…';
|
),
|
||||||
pick.title = 'Save files to a folder on this device';
|
);
|
||||||
pick.addEventListener('click', () => this.pickLocalFolder());
|
|
||||||
badge.appendChild(pick);
|
|
||||||
} else {
|
} else {
|
||||||
const why = document.createElement('span');
|
menuActions.appendChild(
|
||||||
why.className = 'workspace-badge-note';
|
makeMenuNote(
|
||||||
why.textContent = '(no folder picker)';
|
'(folder picker unavailable)',
|
||||||
why.title =
|
|
||||||
'window.showDirectoryPicker is not exposed in this browser context.\n' +
|
'window.showDirectoryPicker is not exposed in this browser context.\n' +
|
||||||
'• Firefox / Safari: not implemented (use Chromium-based browser).\n' +
|
'• Firefox / Safari: not implemented (use a Chromium-based browser).\n' +
|
||||||
'• Brave: enable brave://flags/#file-system-access-api or relax Shields for this site.\n' +
|
'• Brave: enable brave://flags/#file-system-access-api or relax Shields for this site.\n' +
|
||||||
'• HTTPS-only requirement: must be served from localhost or https://.\n' +
|
'• HTTPS-only requirement: must be served from localhost or https://.\n' +
|
||||||
'Files keep saving to IndexedDB; use Export to download a ZIP.';
|
'Files keep saving to IndexedDB; use Export to download a ZIP.',
|
||||||
badge.appendChild(why);
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportBtn = document.createElement('button');
|
menuActions.appendChild(
|
||||||
exportBtn.type = 'button';
|
makeMenuButton('⬇️ Export workspace…', 'Download every workspace file as a .zip', () =>
|
||||||
exportBtn.className = 'workspace-badge-action';
|
this.exportWorkspaceZip(),
|
||||||
exportBtn.textContent = 'Export';
|
),
|
||||||
exportBtn.title = 'Download every workspace file as a .zip';
|
);
|
||||||
exportBtn.addEventListener('click', () => this.exportWorkspaceZip());
|
menuActions.appendChild(
|
||||||
badge.appendChild(exportBtn);
|
makeMenuButton('⬆️ Import .zip…', 'Upload a .zip — its files land in code/ (overwrites on conflict)', () =>
|
||||||
|
this.importWorkspaceZip(),
|
||||||
const importBtn = document.createElement('button');
|
),
|
||||||
importBtn.type = 'button';
|
);
|
||||||
importBtn.className = 'workspace-badge-action';
|
menuActions.appendChild(
|
||||||
importBtn.textContent = 'Import';
|
makeMenuButton(
|
||||||
importBtn.title = 'Upload a .zip — its files land in code/ (overwrites on conflict)';
|
'↻ Reset demos',
|
||||||
importBtn.addEventListener('click', () => this.importWorkspaceZip());
|
'Re-copy the bundled demos into code/ (overwrites your edits to those files)',
|
||||||
badge.appendChild(importBtn);
|
() => this.resetDemoFiles(),
|
||||||
|
),
|
||||||
const resetBtn = document.createElement('button');
|
);
|
||||||
resetBtn.type = 'button';
|
menuActions.appendChild(
|
||||||
resetBtn.className = 'workspace-badge-action';
|
makeMenuButton(
|
||||||
resetBtn.textContent = 'Reset demos';
|
'🚪 Exit local mode',
|
||||||
resetBtn.title = 'Re-copy the bundled demos into code/ (overwrites your edits to those files)';
|
'Leave local mode and return to the home page',
|
||||||
resetBtn.addEventListener('click', () => this.resetDemoFiles());
|
() => {
|
||||||
badge.appendChild(resetBtn);
|
|
||||||
|
|
||||||
const exit = document.createElement('button');
|
|
||||||
exit.type = 'button';
|
|
||||||
exit.className = 'workspace-badge-exit';
|
|
||||||
exit.textContent = 'Exit';
|
|
||||||
exit.title = 'Leave local mode and return to the home page';
|
|
||||||
exit.addEventListener('click', () => {
|
|
||||||
if (!confirm('Leave local mode? Your files stay in this browser; you can come back later.')) return;
|
if (!confirm('Leave local mode? Your files stay in this browser; you can come back later.')) return;
|
||||||
exitLocalMode();
|
exitLocalMode();
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
});
|
},
|
||||||
badge.appendChild(exit);
|
{ danger: true },
|
||||||
badge.classList.remove('hidden');
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const info = await this.localWorkspace.describeStorage();
|
const info = await this.localWorkspace.describeStorage();
|
||||||
if (info) {
|
if (info) {
|
||||||
if (info.mode === 'filesystem') {
|
if (info.mode === 'filesystem') {
|
||||||
label.textContent = `Local · ${info.label}`;
|
label.textContent = `Local · ${info.label}`;
|
||||||
const swap = document.createElement('button');
|
if (menuActions) {
|
||||||
swap.type = 'button';
|
const swap = makeMenuButton(
|
||||||
swap.className = 'workspace-badge-action';
|
'↺ Switch to IndexedDB',
|
||||||
swap.textContent = 'IndexedDB';
|
'Switch storage back to in-browser IndexedDB',
|
||||||
swap.title = 'Switch storage back to in-browser IndexedDB';
|
() => this.useIndexedDbStorage(),
|
||||||
swap.addEventListener('click', () => this.useIndexedDbStorage());
|
);
|
||||||
badge.insertBefore(swap, exit);
|
// Insert near the top, just under the picker button (or first child).
|
||||||
|
menuActions.insertBefore(swap, menuActions.firstChild);
|
||||||
|
}
|
||||||
} else if (info.pendingReconnect) {
|
} else if (info.pendingReconnect) {
|
||||||
label.textContent = `Local · IndexedDB (folder “${info.pendingFolderName}” needs reconnect)`;
|
label.textContent = `Local · folder “${info.pendingFolderName}” (reconnect)`;
|
||||||
const reconnect = document.createElement('button');
|
if (menuActions) {
|
||||||
reconnect.type = 'button';
|
const reconnect = makeMenuButton(
|
||||||
reconnect.className = 'workspace-badge-action';
|
`🔌 Reconnect “${info.pendingFolderName}”`,
|
||||||
reconnect.textContent = 'Reconnect';
|
'Re-grant read/write access to the previously picked folder',
|
||||||
reconnect.title = 'Re-grant read/write access to the previously picked folder';
|
() => this.reconnectLocalFolder(),
|
||||||
reconnect.addEventListener('click', () => this.reconnectLocalFolder());
|
);
|
||||||
badge.insertBefore(reconnect, exit);
|
menuActions.insertBefore(reconnect, menuActions.firstChild);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
label.textContent = 'Local · IndexedDB';
|
label.textContent = 'Local · IndexedDB';
|
||||||
}
|
}
|
||||||
@@ -243,39 +273,31 @@ class TextEditor {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
badge.innerHTML = '';
|
|
||||||
const label = document.createElement('span');
|
|
||||||
label.className = 'workspace-badge-label';
|
|
||||||
label.textContent = this.workspaceUserId
|
label.textContent = this.workspaceUserId
|
||||||
? `Workspace: user ${this.workspaceUserId}`
|
? `Workspace: user ${this.workspaceUserId}`
|
||||||
: 'Server workspace';
|
: 'Server workspace';
|
||||||
badge.appendChild(label);
|
if (menuLabel) menuLabel.textContent = 'Workspace';
|
||||||
|
|
||||||
const exportBtn = document.createElement('button');
|
if (menuActions) {
|
||||||
exportBtn.type = 'button';
|
menuActions.appendChild(
|
||||||
exportBtn.className = 'workspace-badge-action';
|
makeMenuButton('⬇️ Export workspace…', 'Download every workspace file as a .zip', () =>
|
||||||
exportBtn.textContent = 'Export';
|
this.exportWorkspaceZip(),
|
||||||
exportBtn.title = 'Download every workspace file as a .zip';
|
),
|
||||||
exportBtn.addEventListener('click', () => this.exportWorkspaceZip());
|
);
|
||||||
badge.appendChild(exportBtn);
|
menuActions.appendChild(
|
||||||
|
makeMenuButton('⬆️ Import .zip…', 'Upload a .zip — its files land in code/ (overwrites on conflict)', () =>
|
||||||
const importBtn = document.createElement('button');
|
this.importWorkspaceZip(),
|
||||||
importBtn.type = 'button';
|
),
|
||||||
importBtn.className = 'workspace-badge-action';
|
);
|
||||||
importBtn.textContent = 'Import';
|
menuActions.appendChild(
|
||||||
importBtn.title = 'Upload a .zip — its files land in code/ (overwrites on conflict)';
|
makeMenuButton(
|
||||||
importBtn.addEventListener('click', () => this.importWorkspaceZip());
|
'↻ Reset demos',
|
||||||
badge.appendChild(importBtn);
|
'Re-copy the bundled demos into code/ (overwrites your edits to those files)',
|
||||||
|
() => this.resetDemoFiles(),
|
||||||
const resetBtn = document.createElement('button');
|
),
|
||||||
resetBtn.type = 'button';
|
);
|
||||||
resetBtn.className = 'workspace-badge-action';
|
}
|
||||||
resetBtn.textContent = 'Reset demos';
|
|
||||||
resetBtn.title = 'Re-copy the bundled demos into code/ (overwrites your edits to those files)';
|
|
||||||
resetBtn.addEventListener('click', () => this.resetDemoFiles());
|
|
||||||
badge.appendChild(resetBtn);
|
|
||||||
|
|
||||||
badge.classList.remove('hidden');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetDemoFiles() {
|
async resetDemoFiles() {
|
||||||
@@ -1361,12 +1383,18 @@ class TextEditor {
|
|||||||
setupHeaderMenu() {
|
setupHeaderMenu() {
|
||||||
const menu = document.getElementById('header-menu');
|
const menu = document.getElementById('header-menu');
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
document.addEventListener('click', (event) => {
|
/* Close the dropdown when the user taps anywhere outside it. We listen
|
||||||
|
* on both `pointerdown` (fires reliably for touch) and `click` (mouse
|
||||||
|
* users + keyboard activations) so the menu can't end up floating over
|
||||||
|
* the Pin / ADC / Serial panels on mobile. */
|
||||||
|
const closeIfOutside = (event) => {
|
||||||
if (!menu.open) return;
|
if (!menu.open) return;
|
||||||
if (!menu.contains(event.target)) {
|
const target = event.target;
|
||||||
|
if (target instanceof Node && menu.contains(target)) return;
|
||||||
menu.open = false;
|
menu.open = false;
|
||||||
}
|
};
|
||||||
});
|
document.addEventListener('pointerdown', closeIfOutside);
|
||||||
|
document.addEventListener('click', closeIfOutside);
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Escape' && menu.open) {
|
if (event.key === 'Escape' && menu.open) {
|
||||||
menu.open = false;
|
menu.open = false;
|
||||||
@@ -2433,7 +2461,8 @@ class TextEditor {
|
|||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
button.className = 'pin-toggle';
|
button.className = 'pin-toggle';
|
||||||
button.textContent = '0';
|
button.textContent = '0';
|
||||||
button.addEventListener('click', () => {
|
const togglePin = (event) => {
|
||||||
|
if (event && typeof event.preventDefault === 'function') event.preventDefault();
|
||||||
const cur = this.pinInView ? (this.pinInView[pin] | 0) : (this._pinInLocal[pin] || 0);
|
const cur = this.pinInView ? (this.pinInView[pin] | 0) : (this._pinInLocal[pin] || 0);
|
||||||
const next = cur ? 0 : 1;
|
const next = cur ? 0 : 1;
|
||||||
this._pinInLocal[pin] = next;
|
this._pinInLocal[pin] = next;
|
||||||
@@ -2460,7 +2489,26 @@ class TextEditor {
|
|||||||
}
|
}
|
||||||
button.textContent = next ? '1' : '0';
|
button.textContent = next ? '1' : '0';
|
||||||
button.classList.toggle('on', Boolean(next));
|
button.classList.toggle('on', Boolean(next));
|
||||||
|
};
|
||||||
|
/* On iOS Safari, `click` on a small button inside a scrollable parent
|
||||||
|
* sometimes fails to fire (the tap gets reclassified as a scroll
|
||||||
|
* gesture). `pointerup` fires reliably for finger taps; we guard with
|
||||||
|
* a 300ms suppression flag so we don't double-toggle on browsers that
|
||||||
|
* deliver both events. */
|
||||||
|
let lastTriggerAt = 0;
|
||||||
|
const trigger = (event) => {
|
||||||
|
const now = (typeof performance !== 'undefined' && performance.now)
|
||||||
|
? performance.now()
|
||||||
|
: Date.now();
|
||||||
|
if (now - lastTriggerAt < 300) return;
|
||||||
|
lastTriggerAt = now;
|
||||||
|
togglePin(event);
|
||||||
|
};
|
||||||
|
button.addEventListener('pointerup', (event) => {
|
||||||
|
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||||||
|
trigger(event);
|
||||||
});
|
});
|
||||||
|
button.addEventListener('click', trigger);
|
||||||
|
|
||||||
const bar = document.createElement('div');
|
const bar = document.createElement('div');
|
||||||
bar.className = 'pin-pwm-bar';
|
bar.className = 'pin-pwm-bar';
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ button,
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
|
||||||
z-index: 60;
|
z-index: 60;
|
||||||
min-width: 220px;
|
min-width: 240px;
|
||||||
|
max-width: min(85vw, 320px);
|
||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -141,6 +142,54 @@ button,
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-separator {
|
||||||
|
height: 1px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
margin: 0.35rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-section-label {
|
||||||
|
padding: 0.35rem 0.7rem 0.2rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-note {
|
||||||
|
padding: 0.4rem 0.7rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons styled as menu items (Workspace actions, etc.). */
|
||||||
|
.menu-action {
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-action:hover,
|
||||||
|
.menu-action:focus-visible {
|
||||||
|
background: #f1f5f9;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-action-danger {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-action-danger:hover,
|
||||||
|
.menu-action-danger:focus-visible {
|
||||||
|
background: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
/* Sidebar */
|
/* Sidebar */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
@@ -277,40 +326,7 @@ button,
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: 220px;
|
max-width: 240px;
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-badge-exit,
|
|
||||||
.workspace-badge-action {
|
|
||||||
appearance: none;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
background: transparent;
|
|
||||||
color: #475569;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.05rem 0.4rem;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
cursor: pointer;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-badge-exit:hover,
|
|
||||||
.workspace-badge-action:hover {
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-badge-action {
|
|
||||||
border-color: #93c5fd;
|
|
||||||
color: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-badge-action:hover {
|
|
||||||
background: #dbeafe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-badge-note {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#current-file {
|
#current-file {
|
||||||
@@ -685,6 +701,12 @@ button,
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
/* iOS Safari swallows taps on small buttons inside scrollable parents
|
||||||
|
* unless `touch-action: manipulation` is set (kills the 300ms double-tap
|
||||||
|
* zoom delay and the "is this a scroll start?" hesitation). */
|
||||||
|
touch-action: manipulation;
|
||||||
|
-webkit-tap-highlight-color: rgba(56, 189, 248, 0.25);
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin-toggle.on {
|
.pin-toggle.on {
|
||||||
@@ -1172,6 +1194,12 @@ button,
|
|||||||
.main-content {
|
.main-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
/* Let the right column scroll as a whole on phones. With ADC + Pins +
|
||||||
|
* Serial all open at once, their combined intrinsic heights exceed the
|
||||||
|
* viewport; without this the console (last child) gets clipped. */
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-header {
|
.editor-header {
|
||||||
@@ -1244,9 +1272,31 @@ button,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-container {
|
.editor-container {
|
||||||
|
/* Pin the editor at a usable height; the rest of the column scrolls. */
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 50vh;
|
||||||
min-height: 42vh;
|
min-height: 42vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* When any of the live simulator panels (Pins / ADC / Serial / LED grid)
|
||||||
|
* is visible, give the editor less screen real estate so the panels and
|
||||||
|
* console don't get pushed below the fold. The whole column is still
|
||||||
|
* scrollable, this just biases space toward the panels. */
|
||||||
|
.main-content:has(.pin-panel:not(.hidden), .adc-panel:not(.hidden), .serial-panel:not(.hidden), .led-sim-panel:not(.hidden)) .editor-container {
|
||||||
|
height: 32vh;
|
||||||
|
min-height: 26vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stop the editor / panels / console from being squashed by flexbox —
|
||||||
|
* each keeps its natural (or capped) height and the column scrolls. */
|
||||||
|
.led-sim-panel,
|
||||||
|
.pin-panel,
|
||||||
|
.adc-panel,
|
||||||
|
.serial-panel,
|
||||||
|
.console-container {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
font-size: 14px; /* >=16px would prevent iOS zoom but feels too large here; CM is contenteditable so no zoom anyway. */
|
font-size: 14px; /* >=16px would prevent iOS zoom but feels too large here; CM is contenteditable so no zoom anyway. */
|
||||||
}
|
}
|
||||||
@@ -1306,6 +1356,21 @@ button,
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cap each panel's scroll region tighter on mobile so two or three of them
|
||||||
|
* stacked don't push the console far below the fold. The whole column is
|
||||||
|
* already scrollable; this just keeps individual panels compact. */
|
||||||
|
.pin-rows {
|
||||||
|
max-height: 22vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adc-sliders {
|
||||||
|
max-height: 22vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-output {
|
||||||
|
max-height: 22vh;
|
||||||
|
}
|
||||||
|
|
||||||
.adc-slider {
|
.adc-slider {
|
||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
padding: 0.75rem 0;
|
padding: 0.75rem 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user