Commit Graph

840 Commits

Author SHA1 Message Date
David Montero aab5e1be08 feat(canvas): resistors default to vertical on add
Every resistor variant ('resistor' + 'resistor-<value>') now lands on
the canvas rotated 90 degrees: reads better, takes less horizontal
space, and drops straight into breadboard columns. Explicit rotations
in metadata defaults are respected. The breadboard auto-vertical drag
check widens from the two-entry set to the same prefix predicate, so
preconfigured variants (resistor-330 etc.) rotate on the board too.
2026-07-18 18:02:41 +02:00
David Montero f98c3268fa feat(breadboard): Wokwi-style parts-on-breadboard — hole snapping + invisible seating wires
Parts now plug INTO the breadboard instead of using it as a junction box:

- Drag magnetism: while dragging, the part's anchor pin snaps to the
  nearest hole center (9 px range, 9.6 px grid) so parts land perfectly
  aligned, like Wokwi.
- Seating: every pin within 4 px of a hole gets an invisible zero-length
  wire (Wire.bb) from pin to hole — the exact model Wokwi persists as
  ["r1:1","bb1:6t.b","",["$bb"]]. Electrically they are ordinary
  wires, so the netlist builder, digital trace and SPICE need zero
  changes; they are simply not rendered and not hit-testable. Seating
  re-computes on every move/rotation (updateComponent), and moving the
  breadboard carries its seated parts along.
- Resistors auto-rotate to vertical when dragged over a breadboard
  (their 58.8 px pin span bridges the center trench rows b-f exactly).
- Seat tolerance 4 px: absorbs the worst element pin-spacing residual
  (~1.6 px) while staying under half the hole pitch, so a pin is never
  ambiguous between holes.

Wokwi interchange fixes that fell out of the diagram.json research:
- import maps the top-level rotate attr onto properties.rotation
  (previously every rotated part imported flat) and export emits it
  back as rotate instead of leaking it into attrs;
- $bb / empty-color connections import as bb seating wires and export
  back as ["$bb"] entries, so parts-on-breadboard projects round-trip;
- wokwi-breadboard-half aliases to the full breadboard (hole names are
  a strict superset, so every connection stays valid).

Breadboard elements now export their pure hole grids and import cleanly
without a DOM (node tests); geometry + store seating covered by
breadboard-snap.test.ts and breadboard-seating.test.ts.
2026-07-18 08:47:25 +02:00
David Montero 8e33088752 fix(editor): sync URL after New workspace + sever project identity on .vlx import
Three stale-project-identity fixes from reviewing the New-workspace flow:

- New workspace (web): handleNewClick cleared the workspace and the
  current project but left the browser on the old /user/slug URL — a
  refresh (or back-button pop) silently reloaded the OLD project over
  the fresh unsaved workspace. Now replaceState's to the localized
  /editor (replace, not push, so no back-entry points at the stale
  project route).
- New workspace (desktop menu): same URL fix for the newProject menu
  action, which cleared identity but never left the project route.
- .vlx import: importVlxFile mutated the stores WITHOUT clearing
  currentProject — with a saved project open, autosave saw the
  imported content as dirty edits on the old projectId and silently
  PUT the .vlx contents over the user's saved project (and pushed the
  clobber to GitHub on linked projects). Now severs identity first,
  same guard loadExample.ts already documents.
2026-07-18 08:24:06 +02:00
David Montero 30882f3930 refactor(verify): extract store-driven pre-flight verification into verifyFromStore
verifyCircuitFromStore() builds the worst-case snapshot (every wired
digital pin driven HIGH) and solves it — extracted verbatim from
EditorToolbar's runVerification so programmatic runners (editor
extensions, agents) can gate their own run paths on the same rules.
No behavior change for the Run button.
2026-07-18 06:44:38 +02:00
David Montero 7ed9c51bd3 feat(wires): first-time auto-routing around components
Creating a wire with a direct pin-to-pin click (no user waypoints) now
routes around other components' bounding boxes instead of crossing
them. Routing happens exactly once, at creation: the routed corners are
stored as ordinary waypoints, so every later manual edit stays where
the user puts it — never re-routed.

Router (utils/wireAutoRoute.ts):
- tries the preview elbow first (clear -> keep existing behavior and
  the WYSIWYG shape), then the opposite elbow, then A* over the
  compressed grid spanned by pin coordinates and obstacle edges
  inflated by an 8 px clearance, with a 40 px per-bend penalty so
  straighter routes win
- obstacles are component boxes only (never boards — pins sit on both
  board edges and detouring around a board produces absurd routes),
  excluding the wire's own endpoint components, measured from the
  rendered DOM; rects containing an endpoint are dropped
- any failure (walled-off target, oversized grid, no DOM) falls back
  to the previous direct-elbow behavior
2026-07-18 05:59:45 +02:00
David Montero abbbbad559 feat(wires): fuse sub-pixel jogs + snap segment drags to the wire's own runs
Hand-aligning a dragged segment could leave two parallel runs a pixel
or two apart, joined by a tiny perpendicular step, because alignment
snapping only ever targeted OTHER wires' geometry.

- Segment and bend-point drags now also snap (6 px threshold) against
  the dragged wire's own points — excluding the ones being dragged —
  so a run clicks into line with its neighbour and the exact
  simplification fuses them into one segment on commit.
- fuseMicroJogs: parallel runs offset by under 2 px joined by a tiny
  step are aligned automatically (the run not anchored to a wire
  endpoint moves; shorter run yields when both are free). Applied at
  render time and in renderedToWaypoints/normalizeWireWaypoints, so
  already-saved crooked wires display straight without touching data.
2026-07-18 05:39:37 +02:00
David Montero 152f9e4ce0 feat(wires): wokwi-style rounded bends + degenerate path cleanup
Three wiring quality fixes:

- Rounded corners: every bend now renders as a quadratic curve
  (radius 7, clamped to half the shorter adjacent segment), with
  round line caps/joins. Segment/waypoint drag previews and the
  in-progress preview use the same path builder so the look is
  consistent everywhere.

- Degenerate geometry cleanup at render time: the expanded polyline
  is simplified (duplicates, collinear runs, U-turns) before the
  path is emitted, so wires saved with junk waypoints no longer
  render on top of themselves. Stored data is untouched until the
  user edits the wire.

- WYSIWYG commit: finishWireCreation materialises the final-leg
  elbow exactly as the live preview drew it (longer axis first) and
  normalises the stored waypoints. Previously the committed wire
  fell back to horizontal-first and visibly changed shape on click.

simplifyOrthogonalPath moved to wireUtils (re-exported from
wireHitDetection for existing imports); the duplicated inline
expansions in SimulatorCanvas now use the shared helper. Waypoint
dots on idle wires removed (visual noise); endpoint dots stay.
2026-07-18 05:02:47 +02:00
David Montero 2e5ac20eba fix(simulator): don't fire key-bound buttons while typing in Monaco
Monaco's focus sink is a plain div (.native-edit-context under the
EditContext API), neither an input tag nor contentEditable, so the
typing guard missed it and a mapped letter typed into the code editor
pressed the button. Treat any keydown originating inside .monaco-editor
as typing.
2026-07-18 04:02:01 +02:00
David Montero 5218f7314b feat(simulator): map pushbuttons to keyboard keys
Any pushbutton (pushbutton / pushbutton-6mm) can now be driven from the
keyboard. Assign a key from the component property dialog — a keycap
control captures the next keypress (Escape cancels, modifiers alone are
rejected) — and a keycap badge next to the component label shows the
mapping on the canvas. Several buttons may share one key on purpose;
the dialog shows a hint when that happens.

At runtime a global bridge translates keydown/keyup into the same
button-press / button-release DOM events the mouse fires on the wokwi
element, so every simulation path (avr8js pin logic, SPICE-driven
inputs, the QEMU GPIO bridge, the pressed visual) behaves identically
to a mouse click. Guards: ignored while typing in inputs or the code
editor, ignored with Ctrl/Alt/Meta held, auto-repeat collapses into one
long press, and window blur releases everything so no button sticks
after Alt-Tab.

The binding is stored as the component's 'key' property, so it
round-trips through project saves and .vlx exports and is undoable like
any other property edit. Strings added to all 9 locales.
2026-07-18 04:02:01 +02:00
David Montero Crespo e72413a13f feat(ui): replace native confirm() dialogs with reusable modal
Convert the remaining window.confirm() call sites to the in-app
MessageDialogHost, extended with a new confirm mode (Cancel + Confirm
buttons, optional danger styling) via showConfirmDialog().

Sites converted:
- New workspace (EditorPage)
- Load project / delete file (FileExplorer)
- Overwrite SPIFFS file (BoardOptionsModal)
- Delete VFS node (VirtualFileSystem)

All dialog strings are internationalized across the 9 supported locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru); the two previously
English-only modals now pull from i18n too.
2026-07-18 01:59:18 +02:00
David Montero Crespo d1d06a44d3 feat(canvas): breadboards always sit behind everything
A breadboard is the physical base of a circuit — boards, components and
wires all plug into it — so it should never cover them. Pin its group at
z-index -1 (below boards z 0, components z 1/2, wires z 35), ignoring
selection. Detected by metadataId prefix 'breadboard' (breadboard,
breadboard-mini). Its own pins stay wireable wherever it's not covered.
2026-07-17 20:50:59 +02:00
David Montero Crespo cbdabb730d feat(canvas): only the pin under the cursor lights up, on every component
The dense-component threshold (>60 pins) meant boards like the Arduino
(31 pins) still painted every pin blue on hover — a wall of squares. Drop
the threshold: every component/board now keeps its squares invisible and
lights up only the ONE under the cursor (matching the breadboard, which
users already liked). Wiring mode still paints them all — all valid targets.
2026-07-17 20:37:57 +02:00
David Montero Crespo f6dec9ace2 fix(canvas): board pins stay clickable — hover handlers on the wrapper
Removing isActive from board showPins (prev commit) exposed a latent bug:
BoardOnCanvas put onMouseEnter/onMouseLeave on the drag overlay, a SIBLING
of PinOverlay. Moving the cursor from the overlay onto a pin square fired
the overlay's mouseleave, cleared hoveredBoardId, and hid every pin right
as you reached one — so a board pin could never be clicked to start a wire
(breadboards were fine: their group wrapper owns both body and pins).

Move the hover handlers to the wrapper div that contains the board body,
the drag overlay AND the pin squares, so moving among them never fires
mouseleave. Board dragging (onMouseDown on the overlay) is unaffected.
2026-07-17 20:23:35 +02:00
David Montero Crespo 5b472c7929 feat(canvas): calmer pin overlays + pin presses never pan the canvas
Three UX fixes to the pin squares:

- Pins show on hover or while wiring only. The active board and the
  selected component used to light every pin permanently.
- Dense components (>60 pins — breadboards) don't paint a wall of blue
  on hover: squares stay invisible and light up individually under the
  cursor. While a wire is in progress every square paints again since
  they're all valid targets (new `wiring` prop threaded to PinOverlay).
- mousedown on a pin square stops propagation, so press-and-drag from a
  pin no longer pans the canvas.
2026-07-17 20:04:23 +02:00
David Montero Crespo b6ce131b03 fix(canvas): stack pins with their component + pickers above floating panels
Two stacking bugs:

1. Pin overlays used a global z-index 30 while component bodies sit at
   z 0-5, all in .canvas-world's single stacking context — so a covered
   component's pins painted on top of whatever covered it (arduino pins
   showing through a breadboard). Each component/board group is now a
   zero-size positioned wrapper that forms its own stacking context
   (boards z 0, components z 1, selected z 2): pins stay above their own
   body but are hidden together with it. .components-area becomes
   pointer-events: none so board pins/drag overlays (now trapped at z 0)
   keep receiving clicks through it; component groups re-enable their own.

2. The Add Component / board picker overlays (z 1000/2000) rendered
   behind the pro AI chat panel (z 8000). Both now portal to <body> at
   z 9000.

Verified in-app: board drag, component drag, wire creation from board
pin to LED, covered pins hidden (0/14 leak), covering component's pins
still clickable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:44:58 +02:00
David Montero Crespo 108d330efd feat(editor): preserve in-progress workspace across a login redirect
The Sign-in links navigate with a full page load (they mount in a
separate React root without Router context), which wipes the in-memory
Zustand workspace. New utils/workspaceDraft stashes the whole workspace
(reusing the lossless .vlx serialisation) to sessionStorage before that
navigation and restores it once when the editor remounts after login —
so a user who was building a circuit and signs in lands back on their
work instead of the empty starter board.

Strictly scoped to the login round-trip by a one-shot restore flag (not
a general autosave), and skipped when a named project is already loaded
so it never clobbers one. EditorPage calls restoreStashedWorkspace() on
mount; the pro overlay's auth links call stashWorkspaceForAuth() before
navigating.
2026-07-17 06:27:28 +02:00
David Montero Crespo 9826737831 feat(ui): global message dialog to replace window.alert()
useMessageDialogStore + <MessageDialogHost /> (mounted once in App.tsx)
give a themed in-app dialog callable from anywhere — React components
and plain .ts modules alike via showMessageDialog(msg, {kind}). Swaps
the native alert() calls in FileExplorer (import errors) and the
desktop menu (.vlx open errors, updater status) for it; the pro overlay
can reuse the same store.
2026-07-17 06:06:28 +02:00
David Montero Crespo 17e9e5a8c2 refactor(custom-chips): move the AI entry point out of OSS into an overlay slot
The 'Create with AI' button referenced the Pro AI agent (hardcoded
prompt, agent event) inside the anonymous OSS dialog — chat/agent logic
must live in the velxio-prod overlay, not here. CustomChipDialog now
just exposes a generic `data-velxio-slot="custom-chip-actions"`
extension point (empty in OSS) and hangs its close handler on the slot
element so the overlay can dismiss the dialog after acting. The button
itself, its prompt and the Pro entitlement gate move to the overlay.
2026-07-17 04:36:14 +02:00
David Montero Crespo cd22335838 feat(custom-chips): AI entry point + friendly auth error
- 'Create with AI' button in CustomChipDialog dispatches the generic
  velxio:agent-prompt window event (no-op without a listener — the pro
  overlay's chat panel picks it up and prefills the composer).
- chipCompileService maps the hosted deployment's 401 gate to a human
  'sign in to compile custom chips' message instead of a raw status
  line. Self-hosted OSS keeps the route open and never sees either.
2026-07-16 20:34:42 +02:00
David Montero Crespo d85e4ac14a feat(elements): side-effect registrar for all custom elements
Registers upstream @wokwi/elements, velxio-elements/ and the element
classes living next to their React wrappers in velxio-components/ —
without pulling any React component graph. Used by pin-metadata
introspection in tests/generators (the pro agent's metadata export).
2026-07-16 18:31:20 +02:00
David Montero Crespo 4cdbf9c89e seo: add SiteNavigationElement structured data (Editor first)
Google's auto-generated sitelinks for "velxio" surfaced docs/blog pages
but not the Editor (the primary app) or Home. Sitelinks can't be forced,
but an explicit SiteNavigationElement ItemList naming the primary nav —
Editor, Examples, Documentation, Pricing, About, in that order — is the
recognized structured-data hint for what the site's main sections are.
2026-07-16 06:21:50 +02:00
David Montero Crespo fcfe47eab5 redesign(examples): compact toolbar + denser grid + fix card black bars
The examples page showed only ~3 cards per row (the grid was capped at
max-width 1200px with minmax(300px) columns), a tall header, and three
stacked filter rows (16 board tabs + category + difficulty) that pushed
the actual examples far down the page. Card thumbnails also letterboxed
with black bars on the sides.

- Grid: widen to 1680px + minmax(232px) columns → ~6 cols on a 1600px
  screen (was 3). Cards smaller/denser (radius, info padding, title).
- Thumbnails: fixed 5:3 aspect-ratio container + object-fit:cover so the
  preview fills edge to edge — no more black side bars.
- Filters: replace the search row + 16 board tabs + two button rows with
  ONE compact toolbar (search + Board/Category/Difficulty dropdowns +
  live count) and removable filter chips (badges with ×) + Clear all.
- Header trimmed (smaller title, less margin) so cards start high.
2026-07-16 06:09:02 +02:00
David Montero Crespo 5fc57c264d fix(minimap): live-track pan/zoom + real board footprints + bigger map
The red viewport rectangle read the React `pan` prop, but the canvas
pans by mutating panRef + the .canvas-world transform directly (no
setState until pointer-up, for zero-lag dragging). So while you dragged
the canvas the rectangle sat frozen and only jumped at the end. The
minimap now also receives panRef/zoomRef and mirrors them via a
requestAnimationFrame loop that setStates only on change, so the rect
follows the canvas every frame while keeping the canvas render-free
during the gesture.

Also: boards were all drawn as one fixed 120x90 world rectangle
regardless of the actual board, misrepresenting the layout. Use the real
per-board BOARD_SIZE (now exported from BoardOnCanvas) so each footprint
is proportional. And enlarge the map 100x75 -> 160x120 (same 4:3 as the
4000x3000 world) so it's usable.
2026-07-16 05:51:29 +02:00
David Montero Crespo 0d7dad16dd feat(projects): duplicateProject accepts name/description/visibility overrides
The profile Duplicate dialog passes the chosen name, description and
visibility to POST /projects/{id}/duplicate. Options are optional — an
empty call still clones with the source name + " (copy)".
2026-07-16 04:51:24 +02:00
David Montero Crespo 6eba0d4850 feat(projects): duplicateProject client for POST /projects/{id}/duplicate
Pairs with the pro-overlay endpoint that clones a project (row + file
groups) into the caller's account. Lives next to deleteProject — the
OSS client already fronts the pro-only projects API when overlaid.
2026-07-16 04:27:32 +02:00
David Montero Crespo 57015212ab feat(picker): featured components sort first — breadboards lead the list
New optional `featured` metadata flag: ComponentRegistry stable-sorts
featured components to the front after loading (and indexes categories
from the sorted list, so per-category views keep the same order). The
two breadboards are marked featured in component-overrides.json — they
are everyday parts and now open the component grid instead of sitting
at the bottom below every diode.
2026-07-16 01:04:29 +02:00
David Montero Crespo 56686da95f feat(breadboard): use the Fritzing parts-library artwork (CC-BY-SA 3.0)
Swap the programmatic SVGs for the real Fritzing breadboard art
(breadboard2.svg / miniBreadboard.svg from fritzing/fritzing-parts),
served from /component-svgs/fritzing/ and scaled x4/3 so the hole pitch
is the wokwi-standard 9.6 CSS px. pinInfo is computed from the measured
Fritzing hole grid (terminal col 1 at x=10.92, rails at x=25.33 in
5-hole groups; wokwi rows a-e map onto fritzing J..F on the full board,
1:1 on the mini; the red stripe marks the + row of each rail pair), so
wire endpoints land exactly on the drawn holes. The element reserves its
final size immediately and falls back to a light programmatic SVG with
identical geometry if the asset can't load. ATTRIBUTION.md records the
CC-BY-SA 3.0 license of the two SVG files.

Verified in the app: fritzing art renders for both boards, the LED
circuit through full-board column + mini column + ground rail still
lights, SPICE overlay shows the merged nets (5.00V on the pin-8 net).
2026-07-15 22:12:00 +02:00
David Montero Crespo 4d0c30e12c style(breadboard): whiter body tone, closer to the familiar breadboard look 2026-07-15 22:01:28 +02:00
David Montero Crespo 6307aed007 feat: breadboard (full 830-point) + mini breadboard (170-point) parts
Two velxio-native passive parts, rendered as web components with
programmatic SVG + precomputed pinInfo (velxio-breadboard 830 holes,
velxio-breadboard-mini 170). Pin names follow the Wokwi convention
(holes `18t.d` / `17b.i`, rails `tp/tn/bp/bn.N`) and the metadata ids
are `breadboard` / `breadboard-mini`, so wokwi diagram.json zips
import/export with no aliasing.

Internal connectivity (5-hole column strips, full-length power rails)
is centralized in utils/breadboardNets.ts and wired into every net
consumer:
- NetlistBuilder: unionBreadboardGroups joins wired holes per group at
  the union-find level in buildNetlist, buildWireNetMap and
  buildBoardPinNetMap — SPICE, the circuit verifier and the voltage
  overlay all see one net per strip/rail with no extra cards.
- DynamicComponent.traceDetailed: the digital trace hops through every
  other wired hole of the entered group, so parts wired through a
  breadboard still resolve their board pin (2-terminal
  PASSIVE_PIN_PAIRS could not express N-hole groups).

Verified end-to-end in the app: Uno pin 8 -> full-board column ->
resistor -> mini-board column -> LED -> ground rail -> GND lights the
LED, and the HUD shows the 3 collapsed SPICE nets. 8 new unit tests
(breadboard-nets.test.ts); netlist-builder + circuit-verifier suites
stay green.
2026-07-15 21:50:36 +02:00
velxio-deploy 814755b249 chore(examples): refresh 1 thumb file(s) [auto] 2026-07-15 21:07:04 +02:00
David Montero 305e37561e feat(examples): add ESP32-S3 ILI9341 TFT example
Hardware-SPI (FSPI/GPSPI2) ILI9341 draw from an ESP32-S3: fill + rounded
rect + text via Adafruit_GFX/ILI9341. Exercises the S3 GPSPI2 controller
end to end (worker SPI stream + DC/CS/RST -> the canvas TFT decoder).
2026-07-11 04:18:07 +02:00
David Montero e5f43c07c6 fix(store): sync simulator with activeBoardId in addBoard
When addBoard promotes a board to active (first board, or the previously
active one was removed) it set activeBoardId without syncing s.simulator,
unlike setActiveBoardId which sets both. Parts that read s.simulator - SPI
displays (ILI9341) attach spi.onByte to the active simulator - then wired
onto the previous board's bus and never received data, so a boards[] ESP32
example with a TFT rendered black. Sync simulator to the promoted board
(no-op when the active board is unchanged).
2026-07-11 04:18:07 +02:00
velxio-deploy 8958fe7c88 chore(examples): refresh 4 thumb file(s) [auto] 2026-07-09 23:23:09 +02:00
David Montero d7f4e8e966 feat(ssd1306): add the 4-pin I2C OLED module + examples on Uno/ESP32/Pico/STM32
Adds `velxio-ssd1306-i2c-4pin`, a native 4-pin SSD1306 OLED module
(GND/VCC/SCL/SDA) — the cheap 0.96" I2C board most beginners actually have,
matching Wokwi's board-ssd1306. The 8-pin `wokwi-ssd1306` breakout stays; this
is the distinct 4-pin part (issue #215). Same SSD1306Core render pipeline
(imageData/redraw) so the display paints identically; I2C-only, address via the
i2cAddress property (default 0x3C). Styled after the existing 8-pin element
(blue PCB, dark screen, corner holes, star).

Ships four "SSD1306 OLED (4-pin I2C)" gallery examples wiring it over I2C on
Arduino Uno (A4/A5), ESP32 (21/22), Raspberry Pi Pico (GP4/GP5) and STM32 Blue
Pill (PB7/PB6).
2026-07-09 23:13:50 +02:00
David Montero 95e9fe9716 fix(routing): redirect /en/* to the prefix-free path instead of a blank page
English is the default locale and is served at the root with no prefix, so
/en/project/x (a natural guess by analogy with /es/, /zh-cn/, ...) matched no
route and rendered blank. Redirect /en/* -> /* (and /en -> /), preserving query
and hash, so those URLs land on the right page while the canonical prefix-free
English URLs stay put for SEO. The other 8 locales already work under their
/<locale>/ prefixes.
2026-07-09 22:33:56 +02:00
David Montero b51bbcf06d refactor(ssd1306): drop the i2c/spi aliases; CS-only auto-detect + protocol pin
Follow-up to the SSD1306 picker consolidation. All 68 saved projects that used
the retired ssd1306-i2c / ssd1306-spi ids have been migrated to the single
`ssd1306` (metadataId rewritten, protocol pinned), so the simulation aliases
are no longer needed and are removed.

- Auto-detect refined to CS-only: chip-select is the SPI-exclusive signal;
  DC does NOT imply SPI (on the 8-pin module DC doubles as the I2C address /
  SA0 line, so many I2C circuits wire it). Fixes false-SPI on those circuits.
- The `ssd1306` part honors an explicit `protocol` property when present
  (migrated legacy projects carry it) and auto-detects otherwise.
- loadProjectState normalizes any lingering ssd1306-i2c/spi ids (old .vlx
  files, pre-migration snapshots) to `ssd1306` + the matching protocol, so
  removing the aliases can never blank an old import.
2026-07-09 22:13:31 +02:00
David Montero b6dd2f5201 feat(ssd1306): one auto-detecting OLED part (merge the I2C/SPI picker entries)
The SSD1306 was three picker entries — a generic `ssd1306` with a protocol
selector plus `ssd1306-i2c` / `ssd1306-spi` shortcuts (issue #101) — all the
same 8-pin wokwi-ssd1306 element. That is confusing for one physical module
(issue #215). Wokwi ships a single I2C-only part; this goes one better: a
single part that auto-detects the protocol from the wiring, like a real
breadboard — CS or DC wired to a GPIO means SPI, otherwise I2C. No protocol
switch to set, just wire it up.

Works on every board with an I2C/SPI bus (AVR, RP2040, ESP32 Xtensa, STM32).
The ssd1306-i2c / ssd1306-spi ids stay as backward-compat simulation aliases
for projects saved before the merge, but are removed from the picker. Adds an
i2cAddress property (0x3c/0x3d) matching the real module and Wokwi.

Note: ESP32-C3, Raspberry Pi 3 and the bare RISC-V board do not emulate I2C/SPI
peripherals, so no I2C/SPI device (this or any other) attaches there yet.
2026-07-09 21:46:40 +02:00
David Montero 061bb91da1 fix(esp32-c3): drive digital inputs from the SPICE solve (spiceDrivenInputs)
Esp32C3Simulator already had the GPIO_IN plumbing (setPinState -> gpioIn ->
GPIO_IN_REG read) but never opted into connectDigitalInputsToMcu, so a pin
wired to a switch/button was never fed the solved circuit voltage and
digitalRead() ignored the real wiring. Enabling the flag (as AVRSimulator and
RP2040Simulator already do) completes the issue #247 fix: the ESP32-C3 now
reads GPIO2 from the SPICE solve, so toggling the slide switch flips the LED.
BasicParts' button/slide-switch seed already yields to spiceDriven(), so there
is no double-drive.
2026-07-09 19:16:37 +02:00
David Montero f5c1887869 fix(sim): model slide-switch as a real SPDT + tie ESP32 dual 3V3/5V pins to the rail
The slide-switch SPICE model only wired pin 1 <-> pin 2 (an SPST), ignoring
pin 3. The part is really an SPDT whose common wiper (pin 2) selects pin 1 at
value=0 or pin 3 at value=1, so a switch wired GND-1 / signal-2 / VCC-3 (the
natural Wokwi hookup) could never pull its signal high. Fixes the reported
ESP32-C3 case (issue #247) where only the green LED lit and the switch never
toggled the red one.

Second cause on that board: the ESP32-C3-DevKitM-1 exposes its supply as
3V3.1/3V3.2 and 5V.1/5V.2 (there is no bare 3V3/5V pin). VCC_PIN_RE has no
numeric-suffix branch on purpose (a dual-supply pin such as L293D VCC2 must
not collapse onto the shared logic rail), so those numbered pins floated at
0 V and the switch's HIGH side was dead. List them in boardPinGroups for
esp32-c3 / esp32-s3 / esp32-cam.

- componentToSpice: SPDT emission (both throws, complementary 0.01/1e9 R).
- digitalGateEngine: both driveSwitch paths (all-digital + mixed) made SPDT to
  match, so the pure-digital paint and the ngspice solve agree.
- examples-digital / examples-circuits: rewire every slide-switch so the rail
  feeds pin 3 and pin 1 is the value=0 throw, preserving value=ON=HIGH.
- spice-slide-switch-spdt-repro test reproduces issue #247 at the netlist level.
2026-07-09 18:58:12 +02:00
David Montero 3934cbc0f8 fix(editor): stop unified toolbar controls overlapping on a narrow bar
The top bar packs three zones onto one row: the view-mode toggle, the editor
actions (Compile/Run/Stop/...), and the canvas controls (board selector,
Serial, Scope, zoom, Add) portaled in from SimulatorCanvas. The editor zone
was flex:1 min-width:0 while the canvas zone was fixed-width, so when the bar
narrowed - mainly when the right-docked AI chat opens - the editor zone shrank
below its own buttons and painted them over the board selector / Serial /
Scope. The existing collapse logic was keyed to the viewport (@media 768px),
so it never fired on a wide screen with the chat open.

Make the shared bar a container-query context and collapse every zone by the
bar's own width instead of the viewport: view-mode labels drop to icons first,
then Serial/Scope/Add labels and the board selector ellipsizes, then the
component count and finally the zoom buttons (wheel-zoom still works). Floor
the editor zone at its collapsed content width so it can never underflow and
overlap; past that the lower-priority canvas controls yield toward the right
edge instead. Verified across bar widths 660-1140px with the AI chat open:
overlap eliminated, dropdown menus still render un-clipped.
2026-07-03 07:32:14 +02:00
David Montero 7f65cd65bf refactor(editor): remove redundant file-tabs bar from the toolbar
The file-tabs strip in the toolbar center duplicated affordances that
already exist elsewhere: the file it showed is selected in the left file
explorer, and its board-owner label duplicated the board selector combo.
It also ate horizontal space and crowded the action row on narrow panes.
Remove the FileTabs component entirely; the left explorer is now the single
place to switch files. The toolbar center slot stays as an empty flex
spacer so the right action group remains pinned to the far right.
2026-07-03 06:50:21 +02:00
David Montero 1ad669fae9 feat(editor): multi-board primary Run runs all boards (split-menu for active-only)
In a multi-board project the wired boards are one system, so running just
the active board almost never matches intent (a cross-wired UART pair only
comes alive when both run). The primary Run button now runs ALL boards when
there is more than one, with a split caret-menu to still run only the active
board. Single-board and board-less behaviour is unchanged, and the separate
Run-All double-triangle button is kept only for board+chip / chips-only
projects where the primary Run is not already a run-all.
2026-07-03 01:22:17 +02:00
David Montero 1fa43b380d fix(examples): add series resistor to ESP32 Blink LED example
The esp32-blink-led example wired the external red LED straight from GPIO4
to the anode with no current-limiting resistor. Add a 220 Ohm resistor in
series (GPIO4 -> R -> LED anode -> GND) so the example models correct
practice and matches the other LED examples.
2026-07-02 22:38:17 +02:00
David Montero a1137f1929 fix(sim): re-solve SPICE on ESP32/STM32/Pi output pin edges
WebSocket-backed boards (ESP32, STM32, Raspberry Pi) reach the electrical
simulation only through PinManager.triggerPinChange, which updated the pin
state + notified listeners but never requested an electrical re-solve. AVR
and RP2040 already resolve at their own toggle sites. As a result an analog
part on an MCU-driven net (e.g. a resistor-less LED whose brightness comes
from the SPICE solve) stayed at its first solved value until unrelated
activity (such as serial output) forced a solve — so an ESP32 blink with no
Serial in loop() left the LED stuck on.

Request an electrical re-solve after an 'mcu'-sourced pin edge, in one place
(triggerPinChange), covering all WS boards. Gated to source==='mcu' so the
solver's own input feedback (triggerPinChange with the default 'external'
source) can't loop; requestElectricalResolve coalesces overlapping ticks so
a per-edge call is cheap.
2026-07-02 22:33:46 +02:00
David Montero 6ccc090f0c fix(editor): sync active file group when the active board changes
After deleting the default board and adding a different one via the canvas
picker, the editor kept editing the removed board's (now deleted) file group
while compile read the NEW board's default group — so code typed into the
editor was silently dropped and the board ran its default sketch ("compiles
fine but runs the old code"). addBoard now points the editor at the new
board's group when it becomes active, and removeBoard re-points it at whatever
board is active afterwards. setActiveBoardId already did this; the canvas
picker calls addBoard directly. Adds a regression test.
2026-06-26 23:03:50 +02:00
David Montero ed132afb91 sim: drive RP2040 + STM32 digital inputs from the real circuit
Extend the spice-driven input path (already live for AVR/ESP32) to RP2040 and
STM32 so digitalRead() of an INPUT pin reflects the actual wiring: a pin tied
to a rail reads that rail, and an INPUT_PULLUP button-to-GND reads idle-HIGH /
pressed-LOW instead of floating or inverted.

RP2040 (rp2040js, frontend-only): the GPIO listener now splits input vs output
mode. Input pins report their pad pull (InputPullUp/Down) via setPinPull and
seed the pull's idle level (rp2040js does not auto-apply the pad pull to the
readable input register); the SPICE solve then overrides via connectDigital-
InputsToMcu when the net is actually sourced. Output pins drive as before.
spiceDrivenInputs = true.

STM32 (backend QEMU): the worker now forwards a new gpio_pull event (from the
libqemu-arm picsimlab_pull_pin callback) so the netlist stamps the matching
weak resistor; Stm32Bridge surfaces it, Stm32BridgeShim opts into
spiceDrivenInputs, and collectPinStates maps PA0/PC13 names to the linear pin
so the pull is read. STM32 outputs stay on the part layer (unchanged).

Event-driven parts with no SPICE model (rotary encoder, keypad) remain
protected by the existing sourcedNets gate in the connector.
2026-06-26 22:05:24 +02:00
David Montero 48099c0bda feat(avr): drive inputs from real circuit, modeling the internal pull-up
Re-do the AVR spice-driven digital inputs (reverted in c11c195) the right way so
INPUT_PULLUP buttons keep working. PinManager.updatePort now detects the AVR
internal pull-up (input DDR bit + PORT bit high) and sets the pin pull, so the
netlist stamps the 45k pull-up and an INPUT_PULLUP input reads HIGH at idle.
connectDigitalInputsToMcu drives a pin from the solve only when its net is
source-backed by a RAIL or a COMPONENT card (button switch, divider, cross-board
output) — NOT by the internal pull alone — so INPUT_PULLUP pins wired to
event-driven parts with no SPICE model (rotary encoder, keypad) are left to the
part layer and never clobbered. AVR only; RP2040/STM32 stay on the part-seed
until their pulls are modeled.
2026-06-26 20:18:41 +02:00
David Montero c11c1954e1 revert(boards): undo spice-driven digital inputs for AVR/RP2040/STM32
The spiceDrivenInputs change (e81450e + f4401cc) fixed plain-INPUT-wired-to-rail
reads but BROKE the far more common INPUT_PULLUP + button-to-GND pattern: the
internal pull-up is not modeled in the netlist, so the input floated LOW and read
as permanently pressed (verified live on the stm32-bluepill-button example).
Revert all the spice-driven-input changes to the pre-fix part-seed behaviour,
which handles INPUT_PULLUP correctly. Proper fix (model the internal pull-up per
board so BOTH patterns work) is a follow-up. Keeps the Pi LED fix.
2026-06-26 18:51:06 +02:00
David Montero f4401cc2dd fix(rp2040,stm32): drive digital inputs from the real circuit, like AVR
Extend the source-backed SPICE-driven input fix to the Pico (RP2040) and STM32:
a GP/PA pin wired to a rail or button now reads the right level from the solve,
while floating event-part nets (encoder/keypad/dialer/dip/stepper) stay on the
part layer. RP2040 just opts in (spiceDrivenInputs); STM32 opts in via the
Stm32BridgeShim and connectDigitalInputsToMcu maps PA0/PC13 names to the linear
pin setPinState expects (stm32PinNameToLinear).
2026-06-26 18:34:54 +02:00
David Montero e81450e348 fix(avr): drive digital inputs from the real circuit (SPICE), not a fake seed
An Arduino input wired to a power rail read the wrong level: a pin tied to 5V
read LOW, and a button-to-5V read idle-HIGH / pressed-LOW. AVR inputs were never
fed the solved circuit voltage (only the ESP32 had spiceDrivenInputs), so a
bare-rail input had no driver and buttons fell back to a hardcoded active-low
pull-up seed that ignored the wiring.

Enable spiceDrivenInputs on AVRSimulator, and gate connectDigitalInputsToMcu on
a new NetlistBuilder sourcedNets set (rails, GPIO V-sources, pulls, and any net
a component card touches). Only source-backed input pins are driven from the
solve; floating nets are left to the part layer, so event-driven parts with no
SPICE model (rotary encoder, keypad, dialer, dip-switch, stepper) keep driving
their own pins instead of being forced LOW.
2026-06-26 18:00:03 +02:00
David Montero d0a51e6239 feat(seo): prerender /about so the release cards + meta are in raw HTML
/about had its own seoMeta but was never in the entry-server prerender map,
so it was served as the SPA shell (homepage title/canonical) — a soft-404
risk for a page that is in the sitemap. Add it to ROUTE_COMPONENTS so
prerender-seo.mjs emits dist/about/index.html with the real About content
(now featuring the Velxio 3.0 release card) and a self-referencing canonical.
2026-06-26 08:06:20 +02:00
David Montero 7fb2ee3de9 feat(esp32): drive digital inputs from the solved circuit (real-wiring fidelity)
ESP32 digitalRead now reflects the actual circuit instead of a part-level
seed, so a button behaves like hardware — including breaking when it's
mis-wired.

- connectDigitalInputsToMcu: after each SPICE solve, threshold every ESP32
  input pin's net voltage (3.3 V LVCMOS, hysteresis) and push the level into
  QEMU. Only pins the MCU isn't driving as outputs are injected.
- Esp32BridgeShim advertises spiceDrivenInputs; the pushbutton / 6mm-button /
  slide-switch parts skip their direct setPinState seed for such boards and
  only flip the component property (pressed/value), which re-solves the
  circuit. The connector then decides the level from the real wiring.
- makePinPullHandler no longer seeds the pin; it only records the pull
  (netlist resistor) + requests a re-solve, so the read stays circuit-driven.
- GROUND_PIN_RE now matches bare numbered grounds (GND2, GND3) — the ESP32
  DevKit element labels its second pad 'GND2', which previously floated.

Net effect: a correctly-wired INPUT_PULLUP button idles HIGH and reads LOW
pressed; a button mis-wired with GND on the wrong terminal reads stuck-LOW,
matching real silicon. AVR / RP2040 keep the legacy part-seed path.
2026-06-24 22:52:52 +02:00
David Montero d38ed04f85 fix(pi): drive canvas LEDs from guest GPIO (PinManager -> SPICE)
The Pi bridge onPinChange was a no-op, so guest GPIO writes never reached the
PinManager / SPICE solver and wired LEDs stayed dark even though user scripts
printed 'LED on'. Mirror the ESP32 branch: forward to pm.triggerPinChange so
GPIO drives the canvas. Interconnect still preserves and calls this before its
own cross-board routing.
2026-06-24 04:48:29 +02:00
David Montero 4992f808ce fix(esp32): seed the pull via getEsp32Bridge, not getBoardBridge
makePinPullHandler drove the post-boot INPUT_PULLUP seed through
getBoardBridge(), which only indexes the Pi bridge map (bridgeMap) — for
an ESP32 it returned undefined and the sendPinEvent seed silently no-op'd,
so the digital input stayed LOW even though the pull config was read and
the SPICE net showed the pulled voltage. ESP32 bridges live in
esp32BridgeMap; use getEsp32Bridge().
2026-06-24 04:40:08 +02:00
David Montero f9fee8ad7c feat(esp32): emulate INPUT_PULLUP on RTC pins + drive the digital read
Completes the internal-pull emulation for the common case (a button on an
RTC-capable GPIO like 4/15/25/... with INPUT_PULLUP):

- Backend reads the RTC_IO pad RUE/RDE bits via the new
  get_internals(QEMU_INTERNAL_RTCIO) and emits gpio_pull for RTC pins, so
  pull-up/down on those pads is finally visible (it lives in RTC_IO, not
  IO_MUX). IO_MUX path still covers non-RTC pins.
- The digitalRead path is driven by seeding the GPIO input level, not by
  SPICE. The part-level INPUT_PULLUP seed (BasicParts) is sent at attach,
  before the multi-second QEMU boot finishes, so it is lost and the pin
  reads LOW. makePinPullHandler now drives the pin to the pull's idle level
  via sendPinEvent when the guest programs the pull (post-boot), so it
  sticks. A real button press/release still overrides it.
2026-06-24 04:20:38 +02:00
David Montero df9e06c99a feat(esp32): emulate internal pull-up/pull-down for GPIO inputs
INPUT_PULLUP / INPUT_PULLDOWN had no effect in simulation: the ESP32's
internal pull resistors live inside QEMU and were invisible to the SPICE
solver, so an input wired to a button-to-GND floated to 0 V and read LOW
even at idle. The canonical active-low button never worked.

Read the pull config straight out of the running guest: the IO_MUX
register (FUN_PU bit 8 / FUN_PD bit 7) is already exposed read-only via
qemu_picsimlab_get_internals(3), so no QEMU rebuild is needed. The worker
scans it on the 100 ms poll thread and emits gpio_pull; the bridge feeds
it to PinManager; the netlist stamps a weak 45k resistor to the rail so
idle inputs read the correct level. 45k matches the real internal pull
and is weak enough that any external driver/pull dominates.

Verified with ngspice: idle ~3.3 V (HIGH), pressed ~0 V (LOW).
2026-06-24 03:32:30 +02:00
David Montero d8b6c77335 fix(spice): model pushbutton as a real 4-pin tactile switch
The pushbutton was modelled as a switch between only 1.l and 2.l; the
other two legs (1.r, 2.r) connected to nothing. Wiring GND/GPIO to those
legs silently produced a dead button, and the failure was invisible.

Model it like hardware: 1.l is internally shorted to 1.r and 2.l to 2.r,
and pressing bridges terminal 1 to terminal 2. Wiring to any leg now
works, and putting GPIO and GND on the same terminal is a dead short,
exactly as on a real tactile switch. Back-compat A/B variant preserved.
2026-06-24 03:32:17 +02:00
David Montero 03339a132a feat(pi): gpiozero core + boot feedback, terminal and upload UX
- boot_images manifest: bump arm64 rootfs (gpiozero/colorzero baked in,
  hostname applied at boot, reworded MOTD)
- RaspberryPi3Bridge: onBooted shell-ready detector + sendAndWaitForPrompt
  flow control (resets on disconnect)
- RaspberryPiWorkspace: distinct Booting overlay + piBooted-driven status,
  inline SVG icons replacing emoji glyphs
- SerialMonitor: strip CSI/DSR escapes so the dumb console no longer shows
  a literal [6n next to the prompt
- VirtualFileSystem: upload auto-starts the Pi and waits for the shell, then
  flow-controls each command (no more dropped lines on large files)
- i18n: bootingTitle/bootingNote + reworded offlineNote2 across 9 locales
2026-06-22 20:21:06 +02:00
David Montero eb6a59adf6 style(ui): unify every interactive surface on the single #0071e3 accent
Sweep all off-brand blues (#007acc logo-blue, #0e639c, #3b82f6) and the neon
cyan family (#4fc3f7, #00b8d4, #00e5ff, rgba 0,184,212 / 0,122,204) across the
editor, simulator, examples gallery and pages to var(--color-action-primary).
The Code/Both/Circuit toggle, board chips, library manager, modals etc. now
match the Libraries / +Add buttons. Also keep the editor Libraries label
visible in the default split (lower the container-query threshold).
2026-06-21 05:34:25 +02:00
David Montero d03fed3293 style(ui): solid palette rebrand — drop neon outlines for tokenized solid fills
Unify the editor toolbar and landing page on the single #0071e3 accent
(token --color-action-primary). Replace the fluorescent cyan/green outline
treatment (Libraries pill, overflow icons, compile/run/stop, feature tiles,
pricing/licensing badges, classroom banner) with solid fills following the
+Add button. Green/red kept solid and semantic-only (run/success, stop/error).
2026-06-21 05:07:43 +02:00
David Montero e1697dc6f3 fix(simulator): recordUpdateWire was never bound in SimulatorCanvas (ReferenceError)
recordUpdateWire was used by the wire colour palette and the right-click menu but
never destructured from the store, so every colour-swatch click threw
'recordUpdateWire is not defined' and did nothing. tsc would have caught this, but
build:docker skips type-checking, and the only prior caller (the touch-only palette)
was never exercised. Bind it like the other record* actions. Together with the
applyNow fix this makes wire colour changes from the UI actually work.
2026-06-19 20:28:54 +02:00
David Montero 4a46cd6c95 fix(simulator): wire colour change from UI was a no-op (recordUpdateWire applyNow)
recordUpdateWire pushed its command with { applyNow: false }, so it recorded the
change for undo but never executed it. Its only callers (the wire colour palette
and the new right-click menu) pass the new colour and expect it applied — neither
pre-applies via the raw updateWire mutator. Net result: changing a wire colour
from the UI did nothing (only the 0-9/c/l/m/p/y keyboard shortcut, which calls
updateWire directly, worked). Drop applyNow:false so it applies like every other
record* command (recordRemoveWire etc.). Adds an undo/redo regression test.
2026-06-19 19:49:12 +02:00
David Montero 0c935c5b29 feat(simulator): discoverable wire-color UI on desktop
Changing a wire's color on desktop was keyboard-only (select wire + 0-9/c/l/m/p/y)
with no visible control — the floating color palette (SelectionActionBar) only
rendered on touch devices, so desktop users had no way to discover it. Now:
- the wire SelectionActionBar (top-center, with the color palette) also shows on
  desktop when a wire is selected and the sim is stopped (it is pinned top-center
  so it never covers pins); component/board bars stay touch-only.
- right-clicking a wire opens a context menu with the color swatches + delete.
Keyboard shortcuts still work. Mirrors the existing board context-menu pattern.
2026-06-19 19:35:46 +02:00
David Montero 8c32b16589 fix(sim): SSD1306 page-addressing mode (Tiny4kOLED/U8g2 OLED garbled)
SSD1306Core only handled horizontal/vertical addressing (0x20/0x21/0x22) and
defaulted memMode to horizontal. Page-mode drivers (Tiny4kOLED on ATtiny85,
U8g2 page buffer, classic SSD1306 libs) position the cursor with the single-byte
commands 0xB0-0xB7 (page) and 0x00-0x0F / 0x10-0x1F (column nibbles) and rely on
the SSD1306 power-on default of PAGE addressing — they never send 0x20. velxio
ignored those cursor commands and advanced in horizontal mode, so every setCursor
was a no-op and the hatching/border/text piled onto wrong rows -> garbled display.
Fix: default memMode=2 (datasheet power-on) and handle the page/column-set
commands. Adafruit_SSD1306 still works (it sends 0x20,0x00 + 0x21/0x22 explicitly).
Verified: decoded the real ATTinyCore Tiny4kOLED I2C stream renders a clean
border + '128x64'. Adds a page-addressing render test.
2026-06-19 18:08:01 +02:00
David Montero 37b3fac978 feat(sim): ATtiny85 USI I2C — drive I2C devices (SSD1306 OLED) via TinyWireM
The ATtiny85 has no hardware TWI; TinyWireM/Tiny4kOLED drive I2C through the USI
peripheral (SDA=PB0, SCL=PB2). avr8js ships AVRUSI but velxio never instantiated
it, so the I2C bus had no master on the ATtiny85 and devices (e.g. SSD1306 OLED)
got no data — the display stayed blank (and wokwi-ssd1306 threw putImageData with
an empty framebuffer). New UsiI2cBridge instantiates AVRUSI and sniffs the SDA/SCL
lines, replaying START/STOP + 8-bit bytes onto the shared I2C bus as
start/connectToSlave/writeByte/stop (the same calls AVRTWI makes for the Uno).
Validated against real ATTinyCore Tiny4kOLED firmware: decodes addr 0x3C + SSD1306
init/data stream. Firmware tolerates NACK so the sniffer is passive.
2026-06-19 07:44:18 +02:00
David Montero 06f672f47f fix(sim): wire ATtiny85 PBx pin edges into the SPICE mixed-mode loop
arduinoPinToName() had no ATtiny85 case, so pin 1 reverse-mapped to "1"
instead of "PB1" (the wire/netlist name). The MCU-edge listener was never
attached (name not in pinsInCircuit) and the SPICE V-source was never altered
on digitalWrite, so a blink LED's branch current stayed at its HIGH value —
the LED latched ON and never turned off (and analogWrite duty changes never
re-solved). Map attiny85 pin N -> "PBN", mirroring pinNameToArduinoPin.
2026-06-19 05:15:26 +02:00
David Montero c0b1e577b4 fix(sim): correct ATtiny85 Timer0 PWM register addresses
AVRSimulator used wrong ATtiny85 Timer0 data-space addresses: OCR0A 0x56
(=PINB), OCR0B 0x5c (=EECR), TCCR0A 0x4f (=TCNT1). analogWrite() writes
OCR0B at data 0x48, so pollPwmRegisters() read the wrong register and PWM
duty was never seen — attiny85-pwm-fade showed no fade. Corrected both
PWM_PINS_TINY85 and attiny85Timer0Config to TCCR0A=0x4A/OCR0A=0x49/OCR0B=0x48
(verified against the ATTinyCore analogWrite disassembly). delay()/millis
(overflow-based) was unaffected. Tests updated off the old 0x5c/0x56.
2026-06-19 04:23:41 +02:00
David Montero 2dc0e61801 chore(sitemap): canonical trailing-slash URLs + lastmod bump 2026-06-18 22:12:36 +02:00
David Montero 1a7d4fabf4 fix(sim): re-solve when a part burns out so the open actually applies
The live solver only re-solved on component/wire/board changes, so a runtime
burnout (which changes burntComponents, not components) wouldn't rebuild the
netlist — the burnt part stayed in the circuit until something else changed.
Trigger a re-solve on burntComponents change too. The monitor skips already-
burnt parts, so this converges (one extra solve).
2026-06-18 05:10:00 +02:00
velxio-deploy 25d5e539ab chore(examples): refresh 1 thumb file(s) [auto] 2026-06-18 05:06:37 +02:00
David Montero e4aae82ca5 feat(sim): P4 slice 2 — burnt parts go open + LED joins the charred visual
- The live solver now excludes runtime-destroyed components from the netlist,
  so a burnt part actually goes OPEN: its current stops and anything it fed
  loses power (cascading failure), the way real hardware behaves once a part
  burns out. Filter is in CircuitSimulationService.runSolve (no-op when nothing
  is burnt).
- The LED's burnout now also marks it in the shared burntComponents set, so a
  burnt LED gets the same charred + smoke-badge visual (and is opened in the
  solve) as a resistor / capacitor, on top of going dark.
2026-06-18 04:57:00 +02:00
David Montero cbcf6ba81e feat(sim): P4 runtime burnout for resistors + electrolytic capacitors
Generalizes the LED's burnout to passive parts via a centralized monitor that
watches the live electrical solve. When a part is stressed past its rating for
a sustained moment it's marked "destroyed": the canvas renders it charred with a
smoke badge and a fault is logged to the output console. Clears on Reset.

Follows the Fritzing-simulator precedent (smoke-on-component) wrapped in a
first-order thermal delay so a brief inrush spike doesn't destroy a part — only
sustained overload (or a catastrophic >=3x overload, instant) does.

- runtimeBurnout.ts: pure stress (resistor power, cap voltage / reverse) + a
  thermal-delay burn decision, plus a monitor subscribed to the electrical +
  simulator stores. Resistor burns past 2x rated (the verifier already warns at
  1x for intentional teaching over-power); a cap bursts over its voltage rating
  or on reverse polarity.
- useSimulatorStore: burntComponents set + mark/clear actions; cleared on
  Reset / restartParts.
- DynamicComponent + SimulatorCanvas.css: charred filter + smoke badge.

Tests: thermal-delay decision (instant / sustained / spike / cooldown) + stress
computation (resistor power, cap over-voltage, reverse, unwired -> null).
2026-06-18 04:35:56 +02:00
David Montero 6f3603d88b feat(sim): P2 wiring ERC slice 2 — VCC-to-GND short + shorted-out parts
- Power short (blocking error): a wire joining a VCC-type pin directly to a
  GND-type pin shorts the supply to ground. The current-based short-circuit
  rule only inspects battery/signal-generator/power-supply sources, so it
  misses a board-rail-to-GND short with no such source -> name it structurally.
- Shorted-out part (warning): a 2-terminal part with both terminals on the same
  node has no effect on the circuit.

Both graph-based, run before the solve. Zero false positives across the 69
gallery examples; gallery pre-flight tests still pass (no spurious blocking).
2026-06-18 03:32:17 +02:00
David Montero 8683c1ecf0 feat(sim): P2 wiring ERC — missing power + dangling 2-terminal parts
First slice of the connection ("malas conexiones") checks, graph-based and run
before the solve so they report even on circuits too incomplete to solve:

- Missing power: a rated peripheral (sensor/display) wired into the circuit but
  missing its VCC or GND connection -> warning. Boards are excluded (they live
  in input.boards and self-power).
- Dangling 2-terminal part: a resistor / LED / capacitor / diode / inductor
  connected on only one side (the other terminal floating) -> warning.

Both non-blocking. Verified zero false positives across all 69 gallery
examples. Tests: dangling resistor warns, fully-wired doesn't, module missing
GND warns.
2026-06-18 02:05:42 +02:00
David Montero 4e20d03f4c feat(sim): P1 over-voltage for boards + electrolytic capacitors
Extends the over-voltage rule to the two cases the previous slice deferred:

- Boards (ESP32 / Pico / Arduino / ...): a board's supply pins all collapse to
  the self-driven vcc_rail net, so an external source on them makes the .op
  singular rather than readable. Added a graph-based check (runs before the
  solve): if a power source is wired to a board supply pin and its nominal
  voltage exceeds that pin's rating, warn. Threaded boardKind into
  BoardForSpice so the verifier can look up the board rating.
- Electrolytic capacitors: new `voltage` rating property (select, default 25V,
  on capacitor-electrolytic + cap-elec-* presets, via component-overrides +
  regenerated metadata). The verifier reads the DC voltage across the +/- pins
  and warns on over-voltage (vent/burst) and on reverse polarity (a polarized
  cap wired backwards). Defaults to 25V when the property is unset.

Tests: 9V battery -> ESP32 VIN warns, 1.5V doesn't; 24V across a 16V cap warns,
5V across a 25V cap doesn't; reverse-biased cap warns. All real-ngspice.
2026-06-18 01:19:13 +02:00
David Montero 12ed306efb fix(editor): keep Circuit check findings when a Run auto-compiles
handleCompile() wiped all logs at the start, including the 'Circuit check'
group the pre-flight verifier had just logged (e.g. an over-voltage warning).
A Run auto-compiles right after verification, so the warning vanished. Preserve
circuit-check entries on compile, same as the boardless path already did.
2026-06-17 22:52:40 +02:00
David Montero 5a9bb3a70e feat(sim): P1 over-voltage warnings for parts with a rated input voltage
Adds a non-blocking circuit-verifier rule: a component whose supply pin sees
more than its datasheet absolute-maximum voltage warns ("X V on the VIN pin --
above its Y V maximum; not emulated accurately"). This is the "fed too much
voltage" mistake the operator asked for (a 3.3-5V module wired to a 9V battery).

- New componentRatings.ts: per-PIN abs-max table (SSD1306/ILI9341 displays,
  DHT/BMP280/HC-SR04/MPU6050 sensors, NeoPixel, servo). Per-pin thresholds so a
  3V3 pin (3.6V) and a VIN pin (6V) are judged separately. Unknown parts are
  simply not checked; an unwired or floating supply pin is skipped.
- circuitVerifier reads each rated part's supply-vs-ground voltage from the
  solved nets (via pinNetMap) and warns when it exceeds the rating.
- VCC/VDD/3V3/5V pins ride the shared vcc_rail net (NetlistBuilder convention);
  VIN is a normal net. Both handled.
- Tests: 9V on a module VIN warns; 5V on VIN does not; a 3.3V pin on a 5V rail
  warns.

Boards (esp32/pico/arduino) carry ratings in the table but aren't checked yet
-- BoardForSpice doesn't thread its boardKind; follow-up.
2026-06-17 22:40:03 +02:00
David Montero b7d1e6469d fix(editor): show spinner + block re-clicks during circuit verification
Clicking Run runs a pre-flight circuit-verification SPICE solve before
compiling. On a cold ngspice worker that solve takes a second or two, but the
Run button kept showing the play icon and stayed enabled, so it looked dead
and got clicked repeatedly -- each click stacking another verification (the
reported 6x [handleRun] click).

- Add a `verifying` state: the Run button now shows the same spinner as the
  Compile button and is disabled while the pre-flight solve runs.
- Synchronous re-entrancy guard (runInFlightRef) ignores re-clicks while a
  verification is already in flight.
2026-06-17 22:06:15 +02:00
David Montero 9ec48d5021 polish(sim): route circuit faults to the output console instead of a toolbar toast
The runtime burnout / pre-flight messages used an inline `setMessage` toast
that rendered as a bar near the Run/Stop buttons and overlapped them. Route
all circuit findings into the compile output console instead — one unified,
red/orange diagnostics log next to the compiler output (Proteus-style):

- New "Circuit check" console group (CIRCUIT_CHECK_TARGET). checkOrBlock logs
  every error (red) / warning (orange) there and opens the console; the
  blocking modal is kept for the explicit Run-anyway / Cancel decision.
- Runtime `velxio-circuit-fault` events (LED burnout) log to the same group
  instead of the toast; no auto-open (the continuous solver can fault on load).
- Warnings-only no longer pop a toast — the console entry is the record.
- The run-path clears preserve circuit-check entries so findings survive a
  "Run anyway" auto-compile.
2026-06-17 21:46:04 +02:00
David Montero f7f0eb4ba8 fix(editor): Run button bypassed circuit pre-flight verification
The Run (and Run All) buttons were wired `onClick={handleRun}`, so React
passed the click event as the first argument. handleRun(skipVerify=false)
then treated the truthy event as skipVerify=true and skipped checkOrBlock
entirely -- the pre-flight circuit verifier never ran on a button click.
This is the real reason a 9V battery wired straight to an LED ran with no
warning even though the verifier exists and is correct (project 2840fd12).

- onClick={() => handleRun()} and onClick={() => handleRunAll()} so
  skipVerify stays at its false default.
- Give handleRunAll the same checkOrBlock pre-flight gate handleRun has.
2026-06-17 21:02:57 +02:00
David Montero f523dfb554 chore(sim): log circuit pre-flight verification outcome (observability)
Verification failing silently in production is otherwise hard to spot — the
rules read 0 A when branch currents are missing. Log the errors/warnings,
whether a solve landed, and which branch/node vectors came back.
2026-06-17 20:52:10 +02:00
David Montero 3372151405 fix(sim): circuit verifier was silently blind to current faults in prod
The pre-flight circuit verifier reads branch currents via runNetlist ->
readAllCurrentVectors() (ngSpice_AllVecs enumeration). The production
Web-Worker ngspice WASM build does not surface voltage-source #branch
vectors through that enumeration for an .op plot, so branchCurrents came
back empty and every current rule (short-circuit, LED over-current) read
?? 0 -> no fault. The live solver avoided this by requesting each current
explicitly by name; the Node test build enumerates them, so the gap was
invisible to the suite. Net effect: a 9V battery wired straight to an LED
ran with no warning (reported on project 2840fd12).

- runNetlist: request every V_* source branch current explicitly by name
  and merge with the enumeration, so source/LED currents are always present
  regardless of the worker WASM's AllVecs behaviour.
- circuitVerifier: non-finite source/LED current -> blocking unstable-solve
  fault ("could not solve a stable current - likely a short or a part with
  no current limit, e.g. an LED with no series resistor").
- LED runtime (BasicParts): burn out on a non-finite current instead of
  falling through to the digital fallback and glowing; raise burnout
  threshold 20mA -> 100mA so high-power/RGB channels are not falsely
  destroyed; clear the burnt latch on Reset (resetBoard bumps hexEpoch).

Tests: real-data repro, mocked non-finite verifier test, runtime
non-finite / high-power / latch-recovery tests.
2026-06-17 20:37:32 +02:00
David Montero 2d23b878e7 fix(canvas): rotated-component pin positions in 3 paths (fixes #230, #231, #232)
All three bugs are rotated components whose pin geometry is computed in a
path that ignores the rotation, so pins/wire-starts land tens of pixels off
the visual pin tips. The live rotate action already recalculates correctly;
these are the paths that didn't.

#231 (context-menu 'Tap a pin to wire'): both onPinSelect handlers in
SimulatorCanvas computed the wire start as getBoundingClientRect().left +
pin.x — adding the UNROTATED pin offset to the ROTATED bounding-box corner.
On a 90-deg HC-SR04 that put the start ~70-100px off (measured). Replaced
with calculatePinPosition(id, x+6, y+6, rotation), the same rotation-aware
helper wires and the pin overlay use.

#232 (rotate -> delete -> undo): recordRemoveComponent's undo restored the
component + wires but never recalculated wire endpoints, so a rotated part's
wires kept the unrotated coords captured at delete time. Added a
requestAnimationFrame updateWirePositions(id) after restore.

#230 + #232 (pin boxes wrong after import / undo / load, 'fixes if rotated
again'): PinOverlay captured the wrapper's layout box (the rotation pivot)
once at mount. On import/undo/load the component mounts already-rotated and
its wokwi-element may not be sized on the mount tick, baking a wrong pivot
that only refreshed when rotation changed. PinOverlay now re-measures after
layout (rAF) and whenever it is about to become visible (showPins dep).
2026-06-16 05:25:31 +02:00
David Montero 3f23a7950c fix(sim): emulate AVR EEPROM (fixes #203)
AVRSimulator never instantiated avr8js's AVREEPROM peripheral, so any
EEPROM.read/write/update hung the sketch: the Arduino EEPROM library spins
on `while (EECR & (1<<EEPE))` waiting for the write-complete bit to clear,
and with no peripheral driving EECR that bit never cleared (issue #203 —
EEPROM.update(0,123) + EEPROM.read(0) hangs instead of printing 123).

Wire AVREEPROM to the CPU in both loadHex() and reset() via a new
attachEeprom() helper. The EEPROMMemoryBackend is created once per
simulator instance and reused across firmware reloads and resets, so a
value written in one run is still readable on the next boot — matching
real hardware, where re-flashing leaves EEPROM intact. Sizes per variant
(Uno 1024 B, Mega2560 4096 B, ATtiny85 512 B); ATtiny85 gets its own
register map (EECR 0x3C / EEDR 0x3D / EEARL 0x3E / EEARH 0x3F) since
avr8js's default eepromConfig targets the ATmega328P.

Adds eeprom.test.ts: drives the EEPROM register protocol against the
production AVRSimulator (loadHex + step), asserting a byte round-trips,
the EEPE poll terminates (no hang), and contents survive a reset.
2026-06-16 04:18:51 +02:00
David Montero 608c2538c9 fix(sim): correct NTC temperature sensor divider + reset to default on restart
The NTC breakout's SPICE topology was inverted relative to the example
sketch's decode formula (rNtc = R_PULL * v / (5 - v)), which assumes a 10k
pull-up from VCC to OUT and the NTC from OUT to GND. The mapper had the NTC
on top (VCC->OUT) and the pull-down on the bottom, so the recovered
temperature ran backwards: dragging the slider to 100C made the sketch
print -25C. Swap the two resistors so V_OUT = 5 * Rntc / (Rntc + Rpull),
matching the sketch and the hand-built reference netlist in
spice-avr-mixed.test.ts (T=0 -> ADC 789, T=25 -> 511, T=50 -> 270).

Also replace the SensorParts linear approximation (2.5 - (t-25)*0.02) with
the same beta-model divider so the non-SPICE ADC injection decodes back to
the slider value, and drop the dead onInput path that treated the element's
value as a raw ADC count.

Reset now restores interactive sensors (temperature/lux/gas sliders) to
their configured defaults: resetBoard re-dispatches each sensor's default
into the running sim and bumps sensorResetNonce so the open
SensorControlPanel remounts and the slider snaps back. Previously a restart
left the NTC frozen at the last dragged temperature.

Updated the examples netlist snapshot for the swapped NTC cards.
2026-06-15 23:21:36 +02:00
David Montero e51d26ce33 feat(editor): follow-up GitHub star prompt for past dismissers
The star banner used a single localStorage flag (velxio_star_prompted)
set identically whether the user clicked through to the repo or just
closed it, so once dismissed it never showed again and clickers and
closers were indistinguishable.

Now track three flags:
  velxio_star_prompted     - dismissed the first ask
  velxio_star_prompted_v2  - dismissed the follow-up (stop forever)
  velxio_star_clicked      - clicked through to the repo (stop forever)

Anyone who dismissed the first ask without clicking through gets ONE
follow-up (round 2) with a stronger message; clicking the repo link at
any time opts them out permanently. Capped at two asks total.

Adds starBanner.title2/body2 copy in all 9 locales.
2026-06-15 22:15:14 +02:00
David Montero c81e5b839d fix(sim): finish dropping the proWifiGate wiring (9360f95 was incomplete)
Commit 9360f95 deleted lib/proWifiGate.ts but left useSimulatorStore importing
it (the store edits weren't staged), so a clean checkout of master failed to
build (import of a deleted module). velxio.dev was unaffected — deploy.sh builds
from the working tree, which had the removal applied. Commit the removal so HEAD
is consistent.
2026-06-15 22:09:17 +02:00
David Montero 9360f9521b revert(sim): drop the WiFi run-gate seam (WiFi becomes freemium)
The proWifiGate seam blocked a free user from running ANY Pico W WiFi sketch.
The product model changed: LOCAL WiFi (the chip associating to the simulator's
virtual AP) is now FREE — only REAL internet (the backend bridge) and the IoT
gateway are paid, gated inside the pro overlay's Cyw43PioPeripheral / backend.
So a free user must be allowed to run WiFi sketches. Remove the gate seam and
its two store call sites. The board-kind firmware variant + the run-time
peripheral re-attach (which fixed the plain-firmware import-network crash) stay.
2026-06-15 20:04:42 +02:00
David Montero d1088aefc2 fix(rp2040): Pico W always boots the W firmware (no `import network` crash)
Two robustness fixes for the paid-WiFi open-core split:

1. A pi-pico-w board now boots the RPI_PICO_W firmware variant (which has the
   `network` module) based on its BOARD KIND, not on whether the WiFi
   peripheral happens to be attached. Previously the variant was
   `pioPeripheral ? 'pico-w' : 'pico'`, so any moment the peripheral was
   absent (see #2) booted the plain Pico firmware and a Pico W sketch crashed
   with "ImportError: no module named 'network'". Store boardKind in
   attachPioPeripheral and pick the variant from it. (OSS: 'pico-w' isn't
   registered, so firmwareConfig falls back to 'pico' — a self-hosted Pico W
   has no WiFi engine anyway.)

2. Re-attach the PIO peripheral in loadMicroPythonProgram before loading
   firmware. An example deep-link adds the board during render, which races the
   pro overlay's async mountPro that installs the CYW43 factory — so the
   board-add attach returned null and a PAID user's Pico W booted plain
   firmware too. attachPioPeripheral is idempotent; by run time the factory is
   installed, so a paid user gets the W peripheral and real WiFi.
2026-06-15 18:55:13 +02:00
David Montero 90c2d1da51 feat(sim): WiFi run-gate seam so free users get an upgrade prompt, not a crash
Pico W WiFi is a paid overlay feature. A free/web user running a Pico W sketch
that uses WiFi had no peripheral attached -> the simulator picked the plain Pico
firmware (no `network` module) -> the run crashed on `import network` with a
raw Python traceback (on a public example page, no less).

Add lib/proWifiGate.ts (mirrors proBoardGate): a stable doorbell the overlay
fills in. useSimulatorStore gates both loadMicroPythonProgram (before loading
firmware) and startBoard (run backstop for example/loaded boards): if the gate
blocks, fire the upgrade prompt and skip the run. Non-WiFi Pico W sketches still
run for free. No-op in OSS (default 'allow' -> a Pico W runs as a plain Pico).
2026-06-15 18:16:50 +02:00
David Montero Crespo fae9208fae Merge branch 'master' of https://github.com/davidmonterocrespo24/velxio 2026-06-15 11:57:59 -03:00
David Montero Crespo cf6a609671 Raspberri pi 4 y 5 2026-06-15 11:57:10 -03:00
David Montero 375b952d47 refactor(examples): move Pico W WiFi examples to the pro overlay seam
The Pico W WiFi showcase examples are a paid-overlay feature now (the WiFi
engine moved to the overlay). Read them through a build-time `@pro` seam so the
SSR prerender + gallery + sitemap include them when built with the overlay, and
OSS gets an empty stub.

- data/examples.ts: import { proExamples } from '@pro/data/proExamples' (static,
  build-time) instead of the local examples-picow-wifi.ts; delete that file.
- src/__pro_stub__/data/proExamples.ts: OSS no-op (empty list) for the @pro alias.
- vitest.config.ts: mirror the @pro alias (stub by default / overlay when
  VITE_PRO_BUILD) so tests loading examples.ts resolve it.
- scripts/generate-sitemap.mjs: also parse <PRO_OVERLAY_PATH>/data/proExamples.ts
  when building with the overlay (the script reads example IDs from source text,
  so it can't follow the alias).
- Tests: drop the picow-wifi import/usage from the 5 OSS example tests (they
  validate the OSS set now); prune the 4 obsolete picow netlist snapshots. The
  overlay's proExamples get their own coverage in pro/.../__tests__/.
2026-06-15 15:54:44 +02:00
David Montero b820c372cc feat(seo): add /v3 release landing page + refresh About for 3.0
- New Velxio3Page (/v3): retro CPUs (Z80/8080/4004/4040/8086), MicroSD,
  ePaper, multi-board interconnect, ngspice WASM migration, undo/redo,
  -88% bundle, 100+ examples. JSON-LD (SoftwareApplication 3.0.0, FAQ,
  breadcrumb). Mirrors the v2.5 page; reuses SEOPage.css/Velxio2Page.css.
- Wire-up: App.tsx route, entry-server.tsx prerender map, seoRoutes.ts
  entry (priority 0.95, prerendered to dist/v3/index.html). v2-5 demoted
  to 0.9.
- AboutPage: Velxio 3.0 is now the 'latest' release card (v2.5 + v2 kept);
  stats refreshed (boards 17 -> 19+, CPU architectures 6 -> 10+ for the
  new retro ISAs).
- i18n: new v3 block in releases.json + about.releases.v3* in common2.json,
  auto-translated to all 9 locales via translate-i18n.mjs.
2026-06-15 08:52:09 +02:00
David Montero b166dfd3e2 test: refresh stale picow-wifi-relay-web-server netlist snapshot
The stored snapshot predated the relay-LED netlist emission (current-sense
V-source + LED diode model), so examples-netlist-snapshot failed on a clean
checkout regardless of any source change. Regenerate it to match the current
NetlistBuilder output. Unblocks the deploy test gate.
2026-06-15 08:45:27 +02:00
David Montero fb813ffde0 feat(opencore): extract Pico W WiFi to a pluggable PIO peripheral seam
Move the CYW43439 (Pico W) WiFi emulation out of the open-source tree so it
can ship as a paid feature in a private overlay. OSS keeps a plain Pico W
(no WiFi); the overlay registers the cyw43 protocol + backend network stack
at runtime via generic seams.

Frontend:
- Add simulation/PioPeripheral.ts: a generic "PIO bus peripheral" seam
  (feedWord / inDiscardableWriteData / resetFraming / hostWakeLevel /
  onHostWake / onSimulationStart). No factory is installed in OSS, so
  createPioPeripheral() returns null and a Pico W simulates as a plain Pico.
- RP2040Simulator: keep the fragile PIO-FIFO plumbing (it must re-run after
  loadMicroPython swaps the chip) but drive it through PioPeripheral instead
  of an inlined cyw43 import (attachCyw43 -> attachPioPeripheral, etc.).
- useSimulatorStore: generic attach/detach + setBoardWifiStatus; drop the
  cyw43 bridge map.
- MicroPythonLoader: add registerFirmwareVariant() so an overlay can add the
  RPI_PICO_W build; remove the OSS pico-w config + bundled .uf2.
- Delete simulation/cyw43/ (moved to the overlay).

Backend:
- core/hooks.py: add generic register_ws_sim_handler / dispatch_ws_sim_message
  and register_gateway_proxy / dispatch_gateway_proxy seams.
- simulation.py: route start_picow / stop_picow / picow_packet_out through the
  ws_sim_handler hook (the overlay handles + gates them).
- iot_gateway.py: resolve the Pico W gateway through the gateway_proxy hook.
- Delete services/picow_net/ + picow_net_bridge.py (moved to the overlay).

Tests: move the cyw43/picow suites to the overlay; update RP2040Simulator
mock stubs to attachPioPeripheral.
2026-06-15 08:33:28 +02:00
David Montero 07a0bfef6e seo: emit trailing-slash canonical + sitemap URLs
Static/docs/example routes are served as <route>/index.html and nginx
301-redirects the slash-less form to add the trailing slash. The sitemap
generator, the prerender canonical/og:url, and the client useSEO canonical
all emitted the slash-LESS form, so every sitemap URL was fetched as a
redirect (filed under 'Page with redirect' in Search Console) and each
canonical pointed at a redirecting URL.

Align all three to the trailing-slash form so sitemap URL == canonical ==
served URL == 200, with no redirect hop.

- generate-sitemap.mjs: append '/' to every <loc> (root stays '/')
- prerender-seo.mjs: withSlash() on canonical + og:url (routes + examples)
- useSEO.ts: withTrailingSlash() on canonical + og:url (covers dynamic
  project pages too)
2026-06-15 06:10:32 +02:00
David Montero Crespo 09eaf00c55 fix(canvas): don't treat the active board as a delete target on Delete/Backspace
The canvas had two independent window keydown listeners. The wire handler
removed the selected wire and returned, but that return cannot stop the
separate component/board handler, which fell into 'else if (activeBoardId)'
and popped the board-removal confirmation. activeBoardId is not a visual
selection -- it is just the board whose code is open in the editor, so it is
effectively always set. Pressing Delete to remove a wire (or after deleting a
component) therefore always asked to remove the board.

Remove the keyboard board-delete branch entirely: Delete/Backspace now only
removes the selected component. Board removal stays on its deliberate paths
(right-click Remove board, touch pin-picker delete). Also add the text-field
guard (input/textarea/select/contenteditable) to the wire handler so Backspace
while typing in the AI chat no longer deletes a selected wire.
2026-06-14 23:28:49 -03:00
David Montero Crespo f9ebdc240e fix(i18n): load + resolve non-English locales on direct navigation
Every non-English page fell back to English when loaded directly (deep link,
refresh, SEO-indexed URL). Two causes:

1. index.ts seeded lng with the URL locale at init, but only the English
   bundle is inlined. LocaleSync only fetches a locale bundle when
   i18n.language !== target, which was already false on a direct non-default
   load, so the bundle was never loaded. Init at DEFAULT_LOCALE and let
   LocaleSync drive the locale from the URL (a real changeLanguage that
   re-renders).

2. The region-coded locales (zh-cn, pt-br) never resolved their own bundle:
   i18next's default code formatting rewrote them to zh-CN / pt-BR, which
   failed the lowercase supportedLngs check and were dropped from the resolve
   hierarchy. Add lowerCaseLng: true.

Verified in a production build: /zh-cn/* and /pt-br/* render translated;
toResolveHierarchy now returns ['zh-cn','en'] / ['pt-br','en'].
2026-06-14 08:17:05 +02:00
David Montero 54e265b108 fix(picow): drop the broken 'Open in tab' link from the device panel
Opening the gateway in a new tab backgrounds the emulation tab and pauses
its rAF, freezing the chip — the page then 502s. The in-tab iframe is the
only way the Pico W device page stays reachable, so the panel no longer
offers an open-in-tab affordance (Reload + Close only).
2026-06-14 07:42:44 +02:00