Add local-mode workspace, ZIP import/export, and richer pin/ADC/serial sims

Boot:
- Editor now picks local vs server mode based on URL flag, sign-in
  state, and a stale local-mode flag. Signed-in users are no longer
  bounced to IndexedDB if they had previously clicked "Use locally".

Local mode:
- New LocalWorkspaceClient (src/static/local-workspace.js) with
  pluggable IndexedDB and File System Access backends. Picked folder
  handles persist across reloads with a Reconnect button when the
  permission lapses.
- Static-only host: scripts/serve_static_editor.py serves src/static/
  with COOP/COEP so SharedArrayBuffer-backed sims keep working.
- Bundled MicroPython stubs ship under src/static/bundled-lib/ for
  static hosting; FastAPI also exposes them at /api/public/lib-bundle.

Workspace import / export:
- Zero-dep ZIP encoder + reader (STORE + DEFLATE via
  DecompressionStream). Export/Import buttons in the workspace badge
  work in both local and server modes; imports are confined to code/.

Pin / ADC / Serial simulation:
- machine.py grows ADC, UART, expanded Pin, and PWM mocks, all driven
  by SharedArrayBuffer when cross-origin isolated and falling back to
  postMessage + [pin-out] stdout markers otherwise — pins, ADC slider,
  and serial input now keep working over plain HTTP / LAN-IP origins.
- NeoPixel pins are claimed via a [pin-claim] marker and dropped from
  the Pins panel so the data line doesn't flicker per write().
- New demos: adc_slider_demo.py, pin_demo.py, serial_demo.py.

Lib layout:
- Single source of truth at repo lib/; workspace/lib/ caching layer
  removed and the directory deleted. Filesystem service reads stubs
  directly from PROJECT_ROOT/lib.

UI:
- Home page slimmed to "Sign in" + "Use locally" with optional editor
  / manage-users links. Admin user/invite UI moved to /users.
- Workspace badge gains storage indicator, Folder…/Reconnect, Export,
  Import, and Exit controls.
- Mobile-friendly tweaks: safer-area padding, larger touch targets,
  iOS-zoom-proof serial input, file-tree highlight fix.

Tests:
- test_auth.py patches PROJECT_ROOT for the lib-shared test so the
  repo-root lib refactor stays green. test_api.py asserts the new
  "LED Editor" branding.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 06:16:02 +12:00
parent 9f28eabd2d
commit ca0ca6fe7e
26 changed files with 5080 additions and 793 deletions

View File

@@ -16,7 +16,8 @@ body {
button,
.tab,
.file-item,
.mode-btn {
.menu-item,
.menu-toggle {
touch-action: manipulation;
}
@@ -28,7 +29,7 @@ button,
}
.sidebar-toggle {
display: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
@@ -37,7 +38,8 @@ button,
background: white;
color: #2d3748;
border-radius: 8px;
font-size: 1.2rem;
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
flex-shrink: 0;
}
@@ -46,10 +48,99 @@ button,
background: #f7fafc;
}
.sidebar-toggle .sidebar-toggle-icon {
display: inline-block;
transition: transform 0.15s ease;
}
/* Flip the chevron when the file browser is hidden. */
.sidebar-toggle[aria-expanded="false"] .sidebar-toggle-icon {
transform: rotate(180deg);
}
.sidebar-backdrop {
display: none;
}
/* Editor-header dropdown menu (Home + run options). */
.header-menu {
position: relative;
}
.menu-toggle {
list-style: none;
cursor: pointer;
padding: 0.45rem 0.7rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: white;
color: #4a5568;
font-size: 1.1rem;
line-height: 1;
user-select: none;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
min-height: 36px;
}
.menu-toggle::-webkit-details-marker {
display: none;
}
.menu-toggle::marker {
display: none;
content: '';
}
.menu-toggle:hover {
background: #f7fafc;
border-color: #cbd5e0;
}
.header-menu[open] > .menu-toggle {
background: #edf2f7;
border-color: #cbd5e0;
}
.menu-content {
position: absolute;
right: 0;
top: calc(100% + 6px);
background: white;
border: 1px solid #cbd5e0;
border-radius: 8px;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
z-index: 60;
min-width: 220px;
padding: 0.35rem;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.menu-item {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.55rem 0.7rem;
border-radius: 6px;
text-decoration: none;
color: #2d3748;
font-size: 0.9rem;
cursor: pointer;
white-space: nowrap;
}
.menu-item:hover {
background: #f7fafc;
}
.menu-checkbox input[type="checkbox"] {
margin: 0;
}
/* Sidebar */
.sidebar {
width: 300px;
@@ -108,6 +199,8 @@ button,
align-items: center;
gap: 0.5rem;
transition: background-color 0.2s;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.file-item:hover {
@@ -116,6 +209,7 @@ button,
.file-item.selected {
background-color: #3182ce;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.22);
}
.file-item.drag-target {
@@ -131,11 +225,6 @@ button,
font-weight: 600;
}
.file-item.root-drop {
border: 1px dashed #4a5568;
margin-bottom: 0.5rem;
}
.file-icon {
font-size: 1rem;
}
@@ -179,6 +268,49 @@ button,
border-radius: 6px;
padding: 0.2rem 0.45rem;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.workspace-badge-label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 220px;
}
.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 {
@@ -199,31 +331,6 @@ button,
color: #e53e3e;
}
.mode-toggle {
display: inline-flex;
border: 1px solid #cbd5e0;
border-radius: 8px;
overflow: hidden;
}
.mode-btn {
border: none;
background: #edf2f7;
color: #4a5568;
padding: 0.45rem 0.8rem;
font-size: 0.85rem;
cursor: pointer;
}
.mode-btn + .mode-btn {
border-left: 1px solid #cbd5e0;
}
.mode-btn.active {
background: #3182ce;
color: white;
}
.editor-actions {
display: flex;
gap: 0.5rem;
@@ -240,6 +347,21 @@ button,
transition: all 0.2s;
}
.icon-btn {
font-size: 1rem;
line-height: 1;
padding: 0.5rem 0.75rem;
min-width: 40px;
}
#run-btn.icon-btn {
color: #2f855a;
}
#stop-btn.icon-btn {
color: #c53030;
}
.editor-actions button:hover:not(:disabled) {
background-color: #f7fafc;
border-color: #cbd5e0;
@@ -254,23 +376,6 @@ button,
background-color: #edf2f7;
}
.run-main-toggle {
display: inline-flex;
align-items: center;
gap: 0.45rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-size: 0.85rem;
color: #374151;
background: white;
white-space: nowrap;
}
.run-main-toggle input[type="checkbox"] {
margin: 0;
}
.editor-container {
flex: 1;
position: relative;
@@ -375,7 +480,7 @@ button,
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
padding: 1rem;
padding: 0;
resize: none;
}
@@ -386,6 +491,18 @@ button,
line-height: 1.5;
}
/* Pin line-number gutter hard to the left edge of the editor pane. */
.cm-editor .cm-gutters {
border-right: 1px solid #e2e8f0;
background: #f7fafc;
padding-left: 0;
}
.cm-editor .cm-content {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.cm-focused {
outline: none;
}
@@ -396,6 +513,15 @@ button,
display: flex;
flex-direction: column;
background: #0f172a;
flex-shrink: 0;
}
.console-container.is-collapsed {
height: auto;
}
.console-container.is-collapsed .console-output {
display: none;
}
.led-sim-panel {
@@ -481,11 +607,415 @@ button,
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.45);
}
.pin-panel {
border-top: 1px solid #e2e8f0;
background: #0b1220;
color: #e5e7eb;
padding: 0.55rem 0.75rem 0.7rem;
}
.pin-panel.hidden {
display: none;
}
.pin-panel-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.45rem;
}
.pin-panel-title {
font-size: 0.85rem;
font-weight: 600;
color: #f3f4f6;
}
.pin-panel-hint {
font-size: 0.7rem;
color: #94a3b8;
}
.pin-rows {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 26vh;
overflow-y: auto;
}
.pin-row {
display: grid;
grid-template-columns: 70px 24px 56px 1fr 90px;
align-items: center;
gap: 0.55rem;
font-size: 0.8rem;
}
.pin-row-label {
color: #cbd5e1;
font-variant-numeric: tabular-nums;
}
.pin-led {
width: 14px;
height: 14px;
border-radius: 50%;
background: #1e293b;
border: 1px solid #334155;
box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.6);
justify-self: center;
}
.pin-led.on {
background: #fde047;
border-color: #facc15;
box-shadow: 0 0 10px rgba(253, 224, 71, 0.7), inset 0 0 4px rgba(0, 0, 0, 0.25);
}
.pin-toggle {
appearance: none;
border: 1px solid #334155;
background: #1e293b;
color: #e5e7eb;
border-radius: 6px;
padding: 0.2rem 0;
font-family: 'SFMono-Regular', Menlo, monospace;
font-size: 0.85rem;
cursor: pointer;
text-align: center;
}
.pin-toggle.on {
background: #16a34a;
border-color: #15803d;
color: #f0fdf4;
}
.pin-pwm-bar {
position: relative;
height: 10px;
background: #1e293b;
border-radius: 999px;
border: 1px solid #334155;
overflow: hidden;
}
.pin-pwm-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #38bdf8, #818cf8);
transition: width 80ms linear;
}
.pin-row-detail {
text-align: right;
font-size: 0.72rem;
color: #94a3b8;
font-variant-numeric: tabular-nums;
}
@media (max-width: 600px) {
.pin-row {
grid-template-columns: 60px 22px 50px 1fr 70px;
gap: 0.4rem;
font-size: 0.75rem;
}
.pin-row-detail {
font-size: 0.68rem;
}
}
.adc-panel {
border-top: 1px solid #e2e8f0;
background: #0f172a;
color: #e5e7eb;
padding: 0.6rem 0.75rem 0.75rem;
}
.adc-panel.hidden {
display: none;
}
.adc-panel-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.45rem;
}
.adc-panel-title {
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.02em;
color: #f3f4f6;
}
.adc-panel-hint {
font-size: 0.72rem;
color: #9ca3af;
}
.adc-sliders {
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 32vh;
overflow-y: auto;
}
.adc-row {
display: grid;
grid-template-columns: 90px 1fr 130px;
align-items: center;
gap: 0.6rem;
}
.adc-row-label {
font-size: 0.8rem;
color: #cbd5e1;
}
.adc-slider {
width: 100%;
appearance: none;
-webkit-appearance: none;
height: 6px;
background: #1e293b;
border-radius: 999px;
outline: none;
cursor: pointer;
}
.adc-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #38bdf8;
border: 2px solid #0f172a;
cursor: grab;
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.3);
}
.adc-slider::-webkit-slider-thumb:active {
cursor: grabbing;
}
.adc-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #38bdf8;
border: 2px solid #0f172a;
cursor: grab;
}
.adc-readout {
font-family: 'SFMono-Regular', Menlo, monospace;
font-size: 0.78rem;
color: #94a3b8;
text-align: right;
font-variant-numeric: tabular-nums;
}
@media (max-width: 600px) {
.adc-row {
grid-template-columns: 1fr;
gap: 0.25rem;
padding-bottom: 0.25rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.15);
}
.adc-row-label {
font-size: 0.78rem;
}
.adc-readout {
text-align: left;
}
}
.serial-panel {
border-top: 1px solid #e2e8f0;
background: #0b1220;
color: #e5e7eb;
padding: 0.55rem 0.75rem 0.7rem;
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.serial-panel.hidden {
display: none;
}
.serial-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
}
.serial-title {
font-size: 0.85rem;
font-weight: 600;
color: #f3f4f6;
}
.serial-meta {
font-size: 0.72rem;
color: #94a3b8;
flex: 1 1 auto;
}
.serial-clear {
appearance: none;
border: 1px solid #334155;
background: #1e293b;
color: #e5e7eb;
padding: 0.15rem 0.55rem;
border-radius: 6px;
font-size: 0.72rem;
cursor: pointer;
}
.serial-clear:hover {
background: #243244;
}
.serial-output {
background: #020617;
border: 1px solid #1e293b;
border-radius: 6px;
padding: 0.55rem 0.7rem;
font-family: 'SFMono-Regular', Menlo, Consolas, monospace;
font-size: 0.82rem;
line-height: 1.4;
height: 120px;
max-height: 28vh;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
.serial-rx {
color: #e2e8f0;
}
.serial-tx {
color: #34d399;
}
.serial-form {
display: flex;
gap: 0.5rem;
align-items: center;
}
.serial-input {
flex: 1 1 auto;
min-width: 0;
background: #0f172a;
border: 1px solid #334155;
color: #f8fafc;
padding: 0.4rem 0.6rem;
border-radius: 6px;
font-family: 'SFMono-Regular', Menlo, Consolas, monospace;
font-size: 0.85rem;
}
.serial-input:focus {
outline: none;
border-color: #38bdf8;
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.25);
}
.serial-newline {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: #cbd5e1;
user-select: none;
}
.serial-send {
appearance: none;
border: 1px solid #1d4ed8;
background: #2563eb;
color: #fff;
font-weight: 600;
padding: 0.4rem 0.85rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
}
.serial-send:hover {
background: #1d4ed8;
}
@media (max-width: 600px) {
.serial-output {
height: 110px;
font-size: 0.78rem;
}
.serial-form {
flex-wrap: wrap;
}
.serial-input {
flex: 1 1 100%;
}
.serial-newline {
flex: 0 0 auto;
}
.serial-send {
flex: 0 0 auto;
margin-left: auto;
}
}
.console-header {
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
color: #cbd5e1;
border-bottom: 1px solid #1e293b;
background: transparent;
border-left: 0;
border-right: 0;
border-top: 0;
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
text-align: left;
cursor: pointer;
font-family: inherit;
}
.console-header:hover {
background: rgba(255, 255, 255, 0.04);
}
.console-header .chevron {
display: inline-block;
transition: transform 0.15s ease;
color: #94a3b8;
}
.console-container.is-collapsed .console-header .chevron {
transform: rotate(-90deg);
}
.console-container.is-collapsed .console-header {
border-bottom: 0;
}
.console-output {
@@ -574,6 +1104,13 @@ button,
border-color: #2c5aa0;
}
/* Desktop: hide the file browser when collapsed via the hamburger toggle. */
@media (min-width: 769px) {
.sidebar.is-collapsed {
display: none;
}
}
/* Responsive design */
@media (max-width: 768px) {
.container {
@@ -638,51 +1175,53 @@ button,
}
.editor-header {
padding: 0.55rem 0.65rem;
flex-wrap: wrap;
gap: 0.45rem;
padding: 0.5rem 0.6rem;
flex-wrap: nowrap;
gap: 0.4rem;
align-items: center;
padding-top: max(0.55rem, env(safe-area-inset-top));
padding-top: max(0.5rem, env(safe-area-inset-top));
}
.file-info {
flex: 1 1 auto;
min-width: 0;
gap: 0.5rem;
flex-wrap: wrap;
gap: 0.4rem;
flex-wrap: nowrap;
overflow: hidden;
}
.file-info .runtime-hint {
.save-status {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#workspace-badge {
font-size: 0.7rem;
padding: 0.15rem 0.4rem;
}
.mode-toggle {
order: 3;
}
.mode-btn {
min-height: 36px;
padding: 0.5rem 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 8rem;
}
.editor-actions {
width: 100%;
order: 4;
flex-wrap: wrap;
gap: 0.4rem;
flex: 0 0 auto;
gap: 0.35rem;
}
.editor-actions button {
flex: 1 1 110px;
.editor-actions .icon-btn {
flex: 0 0 auto;
font-size: 1.05rem;
padding: 0.45rem 0.7rem;
min-width: 40px;
min-height: 40px;
font-size: 0.95rem;
}
.run-main-toggle {
flex: 1 1 calc(50% - 0.4rem);
.menu-toggle {
min-height: 40px;
font-size: 0.85rem;
flex: 0 0 auto;
padding: 0.45rem 0.55rem;
}
.tabs {
@@ -756,6 +1295,57 @@ button,
}
}
/* ADC / Pins / Serial: touch-friendly on phones + safe-area padding */
@media (max-width: 768px) {
.adc-panel,
.pin-panel,
.serial-panel {
padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.75rem, env(safe-area-inset-right));
padding-bottom: max(0.65rem, env(safe-area-inset-bottom));
-webkit-tap-highlight-color: transparent;
}
.adc-slider {
min-height: 2.75rem;
padding: 0.75rem 0;
box-sizing: border-box;
}
.adc-slider::-webkit-slider-thumb {
width: 28px;
height: 28px;
}
.adc-slider::-moz-range-thumb {
width: 28px;
height: 28px;
}
.pin-toggle {
min-height: 44px;
min-width: 44px;
padding: 0.35rem 0.45rem;
}
.pin-led {
width: 18px;
height: 18px;
}
.serial-send,
.serial-clear {
min-height: 44px;
padding: 0.4rem 0.85rem;
font-size: 0.9rem;
}
.serial-input {
min-height: 44px;
font-size: 16px;
}
}
/* Hide drawer affordances entirely on desktop, even with [hidden] flipped off. */
@media (min-width: 769px) {
.sidebar-backdrop {