Commit Graph

481 Commits

Author SHA1 Message Date
David Montero Crespo 149fbee1e9 feat(boards): ESP-EYE ad card + declarative builtInCamera on ProBoardDef
Overlay boards with a camera now declare builtInCamera on their def (bool,
or { maxFrameBytes } for drivers with tight frame buffers) and the canvas
CameraToggle keys off it - the hardcoded esp32-cam / xiao-esp32s3-sense
kinds keep working unchanged.
2026-08-19 02:50:49 +02:00
David Montero Crespo 4eb8060dcb fix(sensors): illumination sliders are logarithmic, like the quantity they set
The lux slider was linear 0-1000, but an LDR's response (and light
perception) is logarithmic: in the night-light example the LED's whole
on/off behaviour lived below ~20 lux — 2% of the slider's travel — and the
remaining 98% did nothing, reading as "the sensor barely works". The
physics were right (the divider model puts the example's 500-count
threshold at ~20 lux, sensible for a night light); the CONTROL was wrong.

The photoresistor and photodiode lux sliders now run in log position
space: value = min + 10^(p·log10(span+1)) - 1, anchored exactly on min
and max (the +1 shift gives the log axis a zero). Mid-travel lands in the
tens of lux, and the night-light toggles near the middle of the track
instead of at its first pixels. Same mapping on the component's own
on-canvas slider. The model, the examples and the reported lux values are
untouched — only where a given lux sits on the track changed.

Slider controls gain an opt-in `scale: 'log'`; everything else stays
linear.
2026-08-18 22:42:25 +02:00
David Montero Crespo d7a4b5e4eb feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:

Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.

Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.

The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.

Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-18 21:50:10 +02:00
David Montero 9936d1956c flash: compile before flashing, stale-build warning, i18n dialog, entitlement seam (#207)
"Flash to real board" no longer needs a prior Compile. The menu item is
enabled for every Arduino/ESP-IDF board (MicroPython keeps its own
gating) and the Flash dialog compiles first when the board has no program
or its code changed since the last build — compiler output streams into
the dialog's console, a red build ends in an error banner there with a
"Recompile & flash" retry, and only a green build reaches the flasher.
Web Serial's port picker needs a user gesture, so the web flow asks the
overlay to grant the port at the click (new WebFlashImpl.preparePort)
before the compile that may take minutes.

Stale detection: compileBoardProgram() now records a fingerprint of the
sources + language + build options (BoardInstance.compiledSourceHash,
utils/sourceFingerprint.ts); the dialog warns and rebuilds when the live
sources differ. utils/boardCompile.ts holds the compile request builder
shared with the toolbar (same files, same options).

The whole dialog and the menu hints are translated (editor.flash.* and
editor.canvas.flashHint.* in the 9 locales); before, every string was
hard-coded English.

Entitlement seam for #207 (desktop "upload to board"): lib/proWebFlash.ts
gains installHardwareFlashGate(); OSS default allows, and when a gate
blocks, the dialog shows a paid-license panel with a "See plans" link
instead of the port picker.
2026-08-18 20:21:57 +02:00
David Montero Crespo c21bfca220 fix(editor): the workspace panel stacks its actions, and the toolbar stops growing a hole
Two places where the editor bar spent width on nothing.

The file explorer put its label and its buttons on one line: WORKSPACE next
to new/open/save, and every board section's name next to rename / new file /
new folder plus the status dot. Between them those set a floor under the
panel's width, and the board name — the one thing a user reads there — got
whatever was left ("M5 Cardput..." in a 150px pane). Both headers now stack:
the label owns its row, the actions sit above (board) or below (workspace),
and the status dot rides in the actions row, where it costs the name
nothing. The actions row keeps its height reserved whether or not the
buttons are showing, so hovering a section never shoves the files below it
down. Default pane width 150 -> 124px; the freed px go to the code editor.

The toolbar's center slot is a flexible spacer that pins the right icons to
the far right, and nothing renders into it (no caller passes centerSlot). So
whenever the bar had width to spare — closing the docked AI chat is exactly
that — the spacer ate it: 191px of black between Reset and Libraries on a
1600px window. Empty, it no longer grows, so the editor controls stay packed
and the surplus collects at the one boundary that already carries a divider,
in front of the canvas controls (which belong over the canvas pane).
2026-08-18 18:42:38 +02:00
David Montero 2c43c638a1 prerender: SSR bodies were never injected; examples gallery renders on the server
replaceRootSeo() swaps the static #root-seo fallback for the page's SSR
body by string search. The old regex required a <script> right after the
block, but Vite hoists the module script into <head> in the built
index.html, so it never matched: every prerendered page (landings, /v3,
/about, all 171 example pages) shipped the HOMEPAGE's fallback body —
same <h1>, same nav — under its own <head>. Two hundred near-duplicate
bodies, and none of the landings' actual content (h1, sections, FAQ)
ever reached the crawler.

ExamplesGallery passes a server snapshot to useSyncExternalStore so
/examples SSRs instead of falling back to meta-only.
2026-08-18 18:10:36 +02:00
David Montero 18b133311e editor: tolerate the trailing slash now that /editor is prerendered; nav links to /editor/
A fresh load of /editor is 301'd to /editor/ by nginx (prerendered dir),
while client-side navigation still lands on /editor. The three exact
pathname comparisons (header active state, header share button, starter
dialog gate) now ignore the trailing slash, and the header nav links to
/editor/ so crawled internal links match the canonical with no redirect
hop.
2026-08-18 17:22:47 +02:00
David Montero b9f663f02e fix(editor): the toolbar strip's label collapse is measured too, and each layout is judged by its own widths
Follow-up to 534b2ff. Dropping the strip to its own bar was now measured,
but two things still produced a wrapped bar on the XIAO example at 1440
with the chat docked:

- The first collapse tier (Serial / Camera / Mic / Scope / Add / Libraries
  labels -> icons) was still a fixed 1010px container query, so on the
  1060px own-bar the labels stayed and the bar wrapped in two. That tier is
  now the unified-toolbar--compact class, set by the same measurement:
  labels on the brand row if they fit, icons on the brand row if those fit,
  else the own bar — labelled if the full bar holds it, icons otherwise.
  The finer tiers (board pill 905, count/zoom 835, view toggle 760/1140)
  stay as container queries.
- Each candidate layout is measured IN that layout: the zones carry their
  own container queries (the view-mode toggle hides on a narrow row and
  reappears on the wide bar), so a width read on the brand row said the
  bar would fit when it would not.
2026-08-18 05:49:45 +02:00
David Montero 534b2ff21b fix(editor): the header measures whether the toolbar strip fits instead of guessing by breakpoint
With the AI chat docked at 1440px, a board with many canvas controls (XIAO:
display + camera + mic + scope) wrapped the strip INSIDE the header — the
canvas controls orphaned on a second line next to a black hole — because
the drop-to-own-bar fallback fired on media-query thresholds (1040 / 1060 /
1400px by chat state) that were only right for one board's strip width.

AppHeader now measures: natural single-row width of the strip (each zone at
its content width) against the room left next to the brand + menus, and
sets app-header--strip-below when it does not fit. Always measured in the
inline layout (class removed, measured, restored before paint) so the
below-state's margins and uncollapsed labels never bias the decision, and
there is nothing to oscillate. Re-measured via ResizeObserver on the strip
host, the brand block and the zones (plus a MutationObserver for the
portaled canvas controls), deferred a frame so a toggle never re-enters
delivery. The three media-query blocks collapse into one class rule; a
1000px window without the chat now keeps one row when the collapsed strip
fits, instead of being forced to two.
2026-08-18 05:21:21 +02:00
David Montero 7fc131d4fb feat(editor): compact file explorer — less indent, smaller icons and rows, 150px default
The explorer rows carried a 26px indent plus 22px file icons, 5px vertical
padding and 12px names in a 165px pane: a lot of margin for a sidebar
whose only job is listing a handful of files, and every px of it was taken
from the code editor. Now: root files indent 16px (12px per nested level),
file icons render at 16px like the folder icon, rows are 3px/6px with an
11px name, the board header actions are 18px, and the header buttons 20px
with a title that truncates before the actions do. The pane defaults to
150px (min 100) and the account footer keeps the first name visible at that
width.
2026-08-18 04:54:49 +02:00
David Montero e71c78f734 fix(canvas): boards added from the picker land in the visible corner like components
Boards were placed 420 world-px right of the ACTIVE board plus 60/30 px per
board already on the canvas: a fixed world offset that ignored pan/zoom (the
new board could land off-screen) and kept marching further out with every
board added, even after the earlier ones had been moved away or removed.
Components had already been fixed to start at the visible top-left and take
the first free slot (utils/dropSlot); boards now go through the same rule,
and both count each other as parked items so they cascade over the same
slots.
2026-08-18 04:39:24 +02:00
David Montero 990cced209 feat(editor): the Save button is the auto-save indicator; drop the header pill
The header rendered a "Saved" / "Unsaved changes" text pill next to the
menus whenever a project was loaded. It ate width in the one row that has
to hold the whole editor toolbar, and it duplicated a signal the Save
button in the workspace panel can carry by colour alone.

The pill is gone. FileExplorer takes the AutoSaveState and colours its Save
button: green saved, orange unsaved changes, pulsing while saving, red on
failure; grey (unchanged) with no project loaded. The tooltip carries what
the pill used to say plus the last-save time. Nine locales get the four new
strings under editor.fileExplorer with English fallbacks.

AppHeader no longer takes autoSave; EditorPage passes it to FileExplorer
instead. No pro overlay code referenced the pill or the prop.
2026-08-18 02:26:35 +02:00
David Montero f09489f498 fix(editor): toolbar never overlaps on a narrow bar; layout switches live in the View menu
Two things went wrong when the window was small and the AI chat docked:

1. Libraries / Output painted over the board selector. The editor zone
   has min-width: max-content, but its child .editor-toolbar-wrapper is
   an inline-size query container, and size containment makes it
   contribute zero intrinsic width — so the floor resolved to nothing,
   flexbox squeezed the zone under its buttons and the bar never got to
   wrap. Containment is now off for the in-bar mount (the wrapper keeps
   it for its standalone mobile mount); the two label collapses that
   depended on it are re-keyed to the unified-toolbar container at the
   same thresholds the canvas controls use (1010 / 905px), so the row
   degrades in step and, when it truly cannot fit, wraps instead.

2. The explorer / Code / Both / Circuit segmented toggle now hides once
   the shared bar is under 760px (its inline display moved to CSS so a
   container query can do that). Every switch it had is a checkmarked
   row in the View menu: File Explorer, and Code / Both / Circuit as a
   radio group under a View mode label. explorerOpen moves from
   EditorPage state into useEditorStore so the menu can read it.
2026-08-18 00:17:38 +02:00
David Montero 0e107fafe6 feat(editor): M5Stack section (Cardputer ADV, Core) ahead of STM32 in the starter dialog
The New Project dialog now lists an M5Stack section — M5 Cardputer ADV first,
then M5Stack Core — between ESP32 and STM32. Both kinds are overlay-registered,
so the section only renders when the overlay mounted (an OSS build shows no
empty block). Selecting a card loads the board's on-LCD hello example
(cardputer-adv-hello / m5stack-core-m5-helloworld), which also provides the
card thumbnail.

The sections builder is extracted as buildStarterSections(defs) and covered
by a unit test that pins the display order.
2026-08-17 18:50:37 +02:00
David Montero Crespo 1b04e910ce fix(editor): quick suggestions stay alive inside snippet placeholders 2026-08-15 21:31:01 +02:00
David Montero aed1796a8f fix(wires): stop board-corner wire anchors — alias fallbacks for pin-name spellings
A wire endpoint naming a pin the board element does not expose silently
resolved to nothing and rendered anchored at the board corner. A full
audit (all gallery examples + every saved project on velxio.dev) found 15
examples and 28 user projects doing it — the same electrical pin spelled
differently per surface: GND.1 on the classic ESP32 (element exposes
GND/GND2), 3.3V vs 3V3, GND2 vs GND.2, D13 vs 13 vs GP13, A0-A2 vs
GP26-28 on the Pico, 36/39 vs VP/VN, bare 5V vs VBUS/VIN.

calculatePinPosition now tries those equivalences AFTER the exact match
and the two existing fallbacks miss (so no current resolution changes),
healing old saves, .vlx imports and agent-authored wires without a data
migration. The Pico element gains A0/A1/A2 alias pads on the ADC pins
(RX2/16 dual-entry precedent) and a GP25 pad at the onboard LED silk so
pico-blink-style wires have an anchor. The two examples that were
outright wrong (microsd-card-esp32 GND.1, pico-serial-echo TX) now use
the element's real pin names.

Netlist snapshots (166) untouched; verifier + builder suites green.
2026-08-15 20:08:47 +02:00
David Montero Crespo fe8ac7e4e3 fix(editor): modals above the agent side panel (z 8000 -> 9000)
With the agent panel open, the library/install/verification modals were
partially covered on the right, hiding the close button and actions.
2026-08-15 08:48:23 +02:00
David Montero Crespo cbd116ce19 fix(editor): fixedOverflowWidgets so marker hovers render above the canvas
A hover wider than the editor pane slid under the simulator canvas
(sibling stacking context) and could not be read.
2026-08-15 08:32:33 +02:00
David Montero 941df93cb3 fix(editor): reconcile active file-group pointers; stop persisting compiledProgram
deleteFileGroup left editorStore.activeGroupId dangling at the removed
group: Monaco (and agent writes) kept editing a group compile never reads,
so a stale same-named sketch.ino shipped instead of the code on screen
(the 'leftover template Blink built for 35 minutes' failure). The pointer
now re-targets a surviving group, and compile re-points the editor when
the two pointers diverge (with a visible warning log).

compiledProgram is no longer serialised into boards_json: the firmware
blob was 99.99% of a typical ESP32 save (a bare board already ~1.04 MB),
pushing every real project against the request-size ceiling, and a
restored binary is stale the moment the code differs. Loaders still
accept it from old rows.
2026-08-15 08:29:39 +02:00
David Montero d2648f5a66 fix(sim): programmatic property changes reach the running simulation
Three stacked defects made ANY non-knob property change invisible to a
running sim (values restored from saved projects, the agent's
set_component_property, the property dialog):
1. DynamicComponent assigned element properties without firing the events
   part simulators bind to — dispatch input/change + dispatchSensorUpdate
   on real value changes (applied-value guard breaks the echo loop).
2. Parts received the shared legacy AVR simulator regardless of board —
   route attachEvents to the simulator of the board the part is wired to
   (getBoardSimulator), so direct analog injection works on non-AVR boards.
3. The SPICE->ADC push stomped un-modeled parts: a net with no SPICE
   element gets an auto pull-down, solves to 0 V, and overwrote the
   part's own injected voltage every solve (the dead analog-joystick).
   Only sourced nets are pushed now.
Verified end-to-end: pot curve exact to the LSB (value 100/300/600 ->
ADC 400/1201/2401 on GPIO35), joystick idle at centre 2047, full-scale
4095 after a store write.
2026-08-15 08:29:39 +02:00
David Montero 44782a419a fix(spice): per-voltage supply rails — VIN/5V no longer clamped to the 3.3V logic rail
Board supply pins split into the main logic rail plus aux rails by real
voltage (ESP32/XIAO VIN,5V -> aux_rail_5 at 5 V; 5V-Arduino 3.3V pin ->
aux_rail_3v3; Pico VBUS/VSYS -> 5 V). An external 5 V supply wired to VIN
used to read 3.3 V (ideal vcc_rail source clamped the whole net — customer
isolated it with a voltmeter). Aux rails join positiveSourceNets and the
relay nominal-voltage check; the voltmeter and hover overlay read them.
Also: ratings entry for the 4-pin ssd1306-i2c-4pin (supply pin VCC) and a
VCC alias on the 8-pin entry, so its power check works at all.
2026-08-15 08:29:39 +02:00
David Montero Crespo c550b29f9e fix(editor): portal toolbar modals to body — container queries broke their overlays
The unified-toolbar responsiveness work gave .unified-toolbar
container-type: inline-size, which makes it the containing block for
position:fixed descendants. The three modals rendered inside the toolbar
(library manager, install-libraries, circuit verification) shrank to the
toolbar's 89px box: pinned to the top, header and close button clipped
offscreen. createPortal(document.body) makes them immune to ancestor
containment. The library manager also gets a fixed tall height
(min(86vh, 1000px)) instead of a bouncy 80vh cap.
2026-08-15 08:19:30 +02:00
David Montero Crespo 7c3b1c6e24 feat(editor): compile-diagnostics seam — publish raw build output for editor markers 2026-08-15 06:44:35 +02:00
David Montero Crespo 55bc03d281 feat(editor): intellisense registry seam for the pro completion engine 2026-08-15 06:13:25 +02:00
David Montero Crespo 8391cc7fa8 fix(gateway): keep in-browser WiFi boards reachable through the IoT gateway
The ESP32 examples now default to the in-browser JS engines, whose lwIP
serves the sketch's HTTP server from the simulation tab itself. Opening
the IoT gateway in a new tab backgrounds that tab, the browser throttles
its timers, and the in-chip server can't answer before the proxy's
timeout — the exact failure the Pico W iframe path already solved.

- WifiStatus grows an inBrowser flag (set by the overlay's JS bridges);
  the serial-monitor link and the WiFi badge now open the in-app iframe
  for any board that carries it, not just pi-pico-w.
- /api/gateway polls ~1s before answering 404: the net bridge dials on
  'got_ip', which races the sketch printing its URL, so a fast click
  used to land before the bridge registered.
2026-08-14 19:45:39 +02:00
David Montero Crespo df19778a3c chore(docs): retire the in-app /docs pages from routes and sitemap
The documentation moved to the Starlight portal served statically at
/docs/ (nginx shadows the old SPA routes and 301s the legacy slugs).
The header Documentation link becomes a full-page <a> so it reaches
nginx instead of client-routing into the removed pages.
2026-08-13 21:20:55 +02:00
David Montero Crespo aa887a1efe feat(components): runtime purchase-link override seam (registerBuyLink)
A private build can now register a per-component buy URL at boot
(fetched from its backend), superseding the datasheet front-matter.
loadDoc merges the override, so both Buy buttons, the UTM decoration
and the click tracking pick it up unchanged — and a component with no
authored datasheet gets a Buy button too (empty body, buy set).
2026-08-12 06:39:29 +02:00
David Montero Crespo e3a71e0958 feat(flash): MicroPython web-flash seam (firmware install + raw-REPL upload)
The proWebFlash seam gains an optional flashMicroPython(files) method
plus an 'uploading' progress phase. MicroPython boards enable the
Flash menu when the overlay implements it — no compile step needed,
the modal collects the workspace group's files (same path as the Run
flow) and explains the two-step install+upload. toFirmwareVariant is
exported so the flasher shares the emulator's per-chip offset table.
2026-08-11 22:55:47 +02:00
David Montero Crespo 820a0fa024 fix(flash): block web flashing for MicroPython boards
compiledProgram holds the 'micropython-loaded' sentinel in MicroPython
mode, not a flash image — atob() blew up with an encoding DOMException
before the port chooser even opened. The menu item now disables with an
explanatory tooltip and the modal shows the limitation instead of the
Connect button.
2026-08-11 22:37:15 +02:00
David Montero Crespo 8694a53d44 docs(flash): drop stale mount-time wording in the web-mode comment 2026-08-11 21:23:56 +02:00
David Montero Crespo def450f4f1 feat(flash): seam for browser Web Serial flashing (pro overlay hook)
The board context menu's Flash item and FlashModal gain a second
backend: a Web Serial flasher the pro overlay can install via
lib/proWebFlash.ts (installWebFlashImpl). Without an overlay the seam
is inert — web builds keep the desktop-only behavior. esptool-js is
declared here because the overlay has no package.json of its own; OSS
code never imports it (verified absent from the vite bundle).
2026-08-11 21:23:56 +02:00
David Montero 75efd68b85 feat(news): announcement opens as a modal before the starter dialog
The corner toast went unread. The announcement is a modal again — what
makes it viable this time is sequencing, not stacking: a tiny gate
(lib/newsGate.ts) lets EditorPage hold the pristine-visit starter
dialog until the news flow is done (nothing to show, or modal closed),
so only one startup overlay is ever on screen and news always comes
first. The pending decision is bounded at 2.5s so a dead feed cannot
hold the starter dialog hostage; reading time is unbounded.

The modal is deliberately a touch wider than the starter dialog
(880px vs 840px) and the news fetch delay drops 4s -> 250ms since the
starter now waits on it. Toast markup and styles removed; Help >
What's new keeps reopening the last delivered post.
2026-08-11 20:12:37 +02:00
David Montero Crespo fb8086c205 fix(canvas): collapse the Camera/Mic buttons to icon-only when the toolbar is cramped
With the AI chat docked, the canvas header ran out of room and the Camera
and Microphone buttons overlapped the icons before them (visible as
"…Camera" colliding on a XIAO-S3-Sense board). Serial and Scope already
collapse to icon-only via the `@container unified-toolbar` query
(font-size: 0 hides the label, the fixed-size SVG stays), but Camera/Mic
carried their layout in INLINE styles, and an inline font-size beats the
class rule so the query could never reach them.

Move the two toggles' static layout into a shared `.canvas-cam-mic-btn`
class (only state-driven styles — background, border, colour, the pulse
animation — stay inline) and add that class to the same collapse rules as
Serial, in both the container query and the mobile media query. Also drop
the header button gap from 8px to 4px once the toolbar is narrow so all
four (Serial/Camera/Mic/Scope) pack in without overlapping.

Side effect: the toggles now share Serial's 28px height, so the toolbar row
lines up (they were a touch shorter before).
2026-08-11 20:01:56 +02:00
David Montero 6e64027921 feat(news): interaction seam for What's New (impression/open/link_click/video_play)
New lib/newsEvents.ts registry (same pattern as newsSource.ts): OSS
default is a no-op sink; an extension overlay can register one to
forward the signals to its own analytics. NewsAnnouncer reports the
toast impression and modal opens (toast vs Help menu); NewsMarkdown
grows an optional onInteract callback fired on body link clicks and
YouTube preview plays — the admin authoring preview passes nothing, so
authoring never counts as engagement.
2026-08-11 18:37:23 +02:00
David Montero 8f7b878149 fix(canvas): guard the mount-time property loop against the persisted imageData string too
c201f5d9 guarded the property-sync effect, but the mount effect has its
own raw Object.entries(properties) loop that runs first and assigned the
saved imageData: '' onto the fresh element — so the firstUpdated
putImageData crash survived. Same guard, second call site.
2026-08-11 05:45:33 +02:00
David Montero 7ecc791187 fix(canvas): don't clobber a display's live ImageData with the persisted string
Saved projects carry imageData: '' in the SSD1306's properties (the
generated metadata lists it as a text prop). The property-sync effect
assigned that string onto the web component, replacing the element's
live ImageData and crashing wokwi-elements' firstUpdated with
'putImageData: parameter 1 is not of type ImageData' on every project
load. Only a real ImageData may pass through now.
2026-08-11 05:19:31 +02:00
David Montero Crespo ea1cdffb56 fix(serial): linkify bare emulated-subnet IPs, not only http:// URLs
The serial monitor turns printed device URLs into IoT-gateway links —
but only when the sketch printed a full http:// URL. That matched the
Arduino idiom and missed MicroPython's: `print("Open:",
wlan.ifconfig()[0])` emits a bare "192.168.4.15", which rendered as
plain text. The address is unreachable from the host by design (it only
exists inside the emulated network), so exactly the users who just
gained real WiFi were handed an IP with no way to open it.

The scheme is now optional; a negative lookahead keeps longer
dotted strings (192.168.4.15.99) from half-matching. ifconfig() tuples
linkify every same-subnet address they contain — all of them route to
the same gateway root, so the extra links are harmless.
2026-08-10 22:11:04 +02:00
David Montero 0653aad3d4 fix(picker): hide the Nano RP2040 Connect shell — art without a simulator
The metadata auto-scan sweeps every wokwi-elements board into the
registry, and boards Velxio does not emulate surface as component cards
that promise support we don't have. Filtered in the picker, not deleted
from the metadata: four saved projects already contain the element and
must keep rendering.
2026-08-10 21:45:05 +02:00
David Montero 2908726007 feat(editor): the starter picker offers the Nano family and the ESP32-CAM
The Arduino section listed only Uno and Mega — Nano and Nano ESP32
existed as first-class boards (kinds, FQBNs, examples) but could only be
reached through the gallery or the board dropdown. Same for the
ESP32-CAM, which now sits in the ESP32 section next to its DevKit
sibling. The camera board's starter is the webcam demo rather than a
blink: the board exists to show the sensor, and a bare LED sketch would
be a misleading first run.
2026-08-10 21:33:55 +02:00
David Montero Crespo 99471d0657 feat(news): announce with a toast, open the modal only on request
Entering the editor could stack two overlays: the starter-template
dialog opened on mount, then four seconds later the "What's new" modal
landed on top of it. Two dimmed backdrops over each other, and the
smaller, *less* important surface in front — the z-indexes said it
outright, news 10000 against the starter's 9000.

The delay made it worse rather than gentler. Arriving after the user has
started reading the templates is the most disruptive moment available;
appearing instantly would at least have let them clear it before
engaging.

The rule this restores: a modal is for what BLOCKS, and an announcement
never blocks. News now slides into the corner as a toast that dims
nothing, steals no focus (role="status", polite), and retires by itself.
The full post is one click behind "Read more" — a modal the user asked
for is a categorically different thing from one thrown at them — and
stays reachable from Help ▸ What's new once the toast is gone.

Worth noting the audience argument too: the starter dialog only fires on
a PRISTINE visit, i.e. for someone brand new, and "what's new" is
meaningless without a baseline to compare against. Returning users, the
ones it is actually written for, arrive with a project loaded and never
saw the collision.

Renamed NewsModal → NewsAnnouncer: it is not a modal by default any more,
and leaving the old name would have mislead the next reader.

Strings are new keys under `news.*`, filled in for all nine locales — the
previous modal had "What's new" and "Got it" hardcoded in English.

Toast sits at z-index 8000, deliberately BELOW the starter dialog, so if
the two ever coincide again the blocking task keeps the foreground.
2026-08-10 19:52:55 +02:00
David Montero Crespo 28eca48168 feat(editor): session entry points in the File and Account menus
The editor menubar's Account menu hosted only the shared `user-menu`
slot, so it had no way in or out of a session: the bottom-left dropdown
renders My projects / Sign in from its own markup, which this menubar
never sees. Signed-in users had no route to their saved work from the
editor, and anonymous ones had no way to sign in without leaving it.

Declares two account-scoped command ids and adds an `optional` item flag
that HIDES a row whose command is unregistered instead of the default
"render disabled". Absence here is not "temporarily unavailable" but
"does not apply": OSS has no accounts, and in pro exactly one of the two
is meaningful at a time — a greyed-out My projects would read as broken.

My projects sits third in File, at the head of the project group rather
than tacked onto the two "new …" rows: it opens saved work, which is
what Open/Save beneath it are for.

Labels reuse header.auth.{myProjects,signIn}, already translated in all
nine locales — no new strings.
2026-08-10 19:47:20 +02:00
David Montero Crespo 0b875d4440 Merge remote-tracking branch 'origin/master' into v3.2-worktree
# Conflicts:
#	backend/app/services/espidf_compiler.py
#	frontend/src/components/editor/EditorToolbar.tsx
#	frontend/src/utils/vlxFile.ts
2026-08-10 17:00:24 +02:00
David Montero Crespo 7a7c9f93f7 feat(simulator): tilt and battery controls for boards that carry an IMU or a gauge
The emulated BMI270 and battery gauge were faithful and inert: with nothing
driving them the IMU reported the board lying flat for the whole run and the
gauge reported the single voltage it was seeded with, so a sketch that reacts
to tilt or warns on low charge could not be exercised at all.

A Sensors popover on the canvas header supplies the missing half. Tilt is
entered as roll/pitch on a drag pad rather than three raw axes — what a user
means is "lean it left" — and gravity in the board's frame is that rotation
applied to (0, 0, 1) g, which is also why the vector stays at 1 g whatever the
attitude. Turning the board spins the gyro for as long as the drag lasts and
parks it back at zero shortly after, matching a real part: a held tilt reads no
rotation. Battery is a slider over the LiPo working range.

Gated per board by builtInImu / builtInBattery on the pro board def, the same
way the Mic toggle is gated, and each section renders only if its flag is set.

The microphone stream also learns to resample. The engine now tells the source
which rate the guest programmed, so a sketch recording at 8 kHz consumes two
captured samples per read instead of playing back at half speed.
2026-08-09 23:48:56 +02:00
David Montero Crespo d446031674 feat(news): bare image URLs render inline in post markdown
Authors paste screenshot URLs without knowing the ![](...) syntax; GFM
autolinks them into plain <a> tags, so videos previewed but images did
not. An autolinked href ending in an image extension now renders as the
image itself; explicit [label](...png) links stay links. All images get
loading=lazy.
2026-08-09 23:12:56 +02:00
David Montero Crespo c0d3543573 feat(metrics): compiles and runs remember which gallery example they came from
useProjectStore.currentExampleId — stamped by loadExample, dropped the
moment a real project (or a blank workspace) takes over. The compile
request and the run event carry it as example_id, analytics only, so
'which examples get compiled and run most' becomes answerable.
2026-08-09 22:34:26 +02:00
David Montero Crespo 8b2715affd feat(metrics): compiles and runs carry the editor BoardKind; ads learn to stay quiet
Several boards share one FQBN (the Pimoroni RP2350 trio all compile as
rpipico2), so FQBN-keyed metrics can't tell them apart. The compile
request and the run event now carry board_kind — analytics only, rides
into the metric hook's extra.

suppressOnlineOnlyAds(): overlay seam so the hosted build can silence
'available in the online editor' ad cards for items it manages — an
embargoed item must vanish, not advertise the site the user is on.
Pure OSS builds never call it; ads behave exactly as before.
2026-08-09 20:48:11 +02:00
David Montero Crespo f791536007 fix(wires): close both halves of the load-time endpoint race
The whenDefined recalc was necessary but not sufficient — measured on
staging, the example route loses BOTH ways:

- the element side: every settle timer can fire before the part is
  findable in the DOM. The pinInfo-ready poll in DynamicComponent now
  also re-derives wire endpoints the first time the part's pinInfo is
  actually readable, and waits up to 10s (the 2s cap was the corner-
  wire bug again on slow connections).
- the wire side: loadExample stores wires with zeroed endpoints BEHIND
  awaits (library installs), potentially after every recalc has come
  and gone. A canvas effect now spots endpoints still at (0,0) and
  runs one deferred recalc; real coordinates land, the condition
  clears, no loop.
2026-08-09 08:32:20 +02:00
David Montero Crespo 4b9fa3f0d0 feat(datasheet): Product-page links carry UTM attribution and report the click
Both Buy renderers (picker hover panel + Part Inspector) now append
utm_source=velxio&utm_medium=simulator&utm_campaign=datasheet with the
component id in utm_content, so vendors can attribute the referral.
trackProductPageClick fires the GA4 event and dispatches a
'velxio:product-page-click' CustomEvent — an overlay build can persist
the click server-side without this tree knowing about it.
2026-08-09 08:16:47 +02:00
David Montero Crespo 75842e3668 fix(wires): recalculate endpoints when a late-defined element upgrades
The canvas re-derives wire endpoints on load-settle timers (100/300/
500ms), but an overlay-defined custom element can upgrade after the
last one — its chunk is large — and calculatePinPosition then finds no
pinInfo, so every wire to the part falls back to its corner and floats
there until something moves it (field report: the Grove gesture module
with all four Grove wires dangling on bare PCB). A customElements
.whenDefined hook now recalculates when the tag actually defines,
however late that is.
2026-08-09 08:13:48 +02:00
David Montero Crespo 6d6d298130 fix(console): the final flush speaks in whole lines, from the real buffer
tailAfterStream sliced result.stdout at the live-buffer offset, but the
buffer interleaves stdout+stderr in arrival order while result.stdout is
stdout-only plus a joining newline — offsets never line up, so the flush
could start mid-line ('f/esptool_py && ...'). Instead, the poll loop now
emits one final onProgress(state:'done') with the completed buffer and
the existing delta logic flushes the tail perfectly aligned. tailAfterStream
is gone.
2026-08-09 08:05:43 +02:00