2026-03-04 05:30:25 +07:00
|
|
|
/**
|
|
|
|
|
* Component Picker Modal
|
|
|
|
|
*
|
|
|
|
|
* Modal interface for searching and selecting components from the wokwi-elements library.
|
|
|
|
|
* Features:
|
|
|
|
|
* - Search bar with real-time filtering
|
|
|
|
|
* - Category tabs for filtering
|
|
|
|
|
* - Grid layout with component thumbnails
|
|
|
|
|
* - Click to select and add component
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
2026-07-17 23:44:58 +07:00
|
|
|
import { createPortal } from 'react-dom';
|
2026-05-09 21:29:07 +07:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-03-04 05:30:25 +07:00
|
|
|
import { ComponentRegistry } from '../services/ComponentRegistry';
|
|
|
|
|
import type { ComponentMetadata, ComponentCategory } from '../types/component-metadata';
|
2026-03-16 01:39:26 +07:00
|
|
|
import type { BoardKind } from '../types/board';
|
|
|
|
|
import { BOARD_KIND_LABELS } from '../types/board';
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
import { isProBoardKind } from '../lib/proBoardGate';
|
2026-03-16 01:39:26 +07:00
|
|
|
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
|
2026-07-21 04:47:50 +07:00
|
|
|
import raspberryPi4Png from '../assets/raspberry-pi-4-board.png';
|
|
|
|
|
import raspberryPi5Png from '../assets/raspberry-pi-5-board.png';
|
2026-04-22 02:45:45 +07:00
|
|
|
import { Attiny85 } from './velxio-components/Attiny85';
|
|
|
|
|
import './velxio-components/Esp32Element'; // registers velxio-esp32
|
|
|
|
|
import './velxio-components/PiPicoWElement'; // registers velxio-pi-pico-w
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
import './velxio-components/Stm32BluePillElement'; // registers velxio-stm32-bluepill
|
2026-07-10 04:13:50 +07:00
|
|
|
import './velxio-components/Ssd1306I2cElement'; // registers velxio-ssd1306-i2c-4pin
|
2026-04-22 01:21:03 +07:00
|
|
|
// Register every wokwi tag that the picker might try to instantiate as a
|
|
|
|
|
// thumbnail. The picker calls `document.createElement(tagName)`, so any tag
|
|
|
|
|
// that isn't already a registered custom element renders as an empty
|
|
|
|
|
// HTMLUnknownElement (blank card preview).
|
|
|
|
|
import '@wokwi/elements';
|
2026-04-22 02:45:45 +07:00
|
|
|
import '../velxio-elements';
|
2026-03-04 05:30:25 +07:00
|
|
|
import './ComponentPickerModal.css';
|
|
|
|
|
|
|
|
|
|
interface ComponentPickerModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onSelectComponent: (metadata: ComponentMetadata) => void;
|
2026-03-16 01:39:26 +07:00
|
|
|
onSelectBoard?: (kind: BoardKind) => void;
|
2026-03-04 05:30:25 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-16 01:39:26 +07:00
|
|
|
const BOARD_DESCRIPTIONS: Record<BoardKind, string> = {
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno': '8-bit AVR, 32KB flash, 14 digital I/O',
|
|
|
|
|
'arduino-nano': 'Compact 8-bit AVR, same as Uno',
|
|
|
|
|
'arduino-mega': '8-bit AVR, 256KB flash, 54 digital I/O',
|
|
|
|
|
'raspberry-pi-pico': 'RP2040 dual-core Cortex-M0+',
|
|
|
|
|
'pi-pico-w': 'RP2040 + WiFi/BT, same emulator as Pico',
|
|
|
|
|
'raspberry-pi-3': 'ARM64 Cortex-A53 quad-core, Linux/Python (QEMU)',
|
feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring)
The BoardKind type and the QEMU backend already supported
raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing
the Pi 3 arm64 image set, but the frontend had no way to actually
select either: the board picker, the canvas renderer, the serial
monitor, the oscilloscope channel list, and the editor toolbar all
hard-coded "raspberry-pi-3" as the only Pi entry. ComponentRegistry
even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3
custom-element tag — a placeholder that meant both boards rendered as
a Pi 3 in the picker thumbnail and on the canvas.
Add dedicated boards top-to-bottom:
* `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style
schematic SVG (authored from scratch, not traced). Pi 4 is the
green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI;
Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge,
2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power
button. Both carry a small "velxio" mark in the corner.
* `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that
returns the 40-pin BCM layout. Every Pi from the 1B+ onwards
uses the same physical pin positions and same BCM GPIO
assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this
helper and example wires drawn against one model transfer to
the others without re-routing.
* React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the
custom elements at absolute positions (mirrors how
RaspberryPi3.tsx handles the Pi 3 illustration).
* Wire-up across the editor surface:
- BoardOnCanvas: BOARD_SIZE entry + switch case.
- BoardPickerModal: description, icon, kinds list.
- ComponentPickerModal: thumbnails now instantiate the dedicated
custom element (was velxio-raspberry-pi-3 fallback).
- SerialMonitor / EditorToolbar: pill labels, icons, colours.
- Oscilloscope: GPIO channel list (28 BCM pins).
- SimulatorCanvas: remote-boards filter for run/stop sync.
- SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3.
- boardPinToNumber: accepts physical pin numbers ("1"-"40"),
BCM names ("GPIO14") and power labels for any Pi 3/4/5 id.
- ComponentRegistry: dedicated tagNames + per-board thumbnails
(green for Pi 4, darker green for Pi 5).
* EditorToolbar's Pi 3 special cases (Linux/Python compile path,
Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5
inherit the same behaviour automatically, and any future Pi
family member (Zero / 1 / 2) lands in the right code paths the
moment its backend boots.
QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry
'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes
both boards selectable end-to-end without any backend follow-up.
2026-05-23 09:46:59 +07:00
|
|
|
'raspberry-pi-4': 'ARM64 Cortex-A72 quad-core, Linux/Python (QEMU)',
|
|
|
|
|
'raspberry-pi-5': 'ARM64 Cortex-A76 quad-core + RP1 I/O, Linux/Python (QEMU)',
|
2026-04-22 02:45:45 +07:00
|
|
|
esp32: 'Xtensa LX6 dual-core, WiFi+BT, 38 GPIO (QEMU)',
|
|
|
|
|
'esp32-devkit-c-v4': 'ESP32 DevKit C V4, official Espressif (QEMU)',
|
|
|
|
|
'esp32-cam': 'ESP32 + 2MP camera, microSD (QEMU)',
|
2026-03-16 01:39:26 +07:00
|
|
|
'wemos-lolin32-lite': 'Compact ESP32, LiPo battery support (QEMU)',
|
2026-04-22 02:45:45 +07:00
|
|
|
'esp32-s3': 'Xtensa LX7 dual-core, WiFi+BT, AI accel (QEMU)',
|
|
|
|
|
'xiao-esp32-s3': 'Seeed XIAO tiny form, 8MB flash+PSRAM (QEMU)',
|
2026-03-16 01:39:26 +07:00
|
|
|
'arduino-nano-esp32': 'Nano form-factor, ESP32-S3, RGB LED (QEMU)',
|
2026-04-22 02:45:45 +07:00
|
|
|
'esp32-c3': 'RISC-V single-core, WiFi+BLE, 22 GPIO (QEMU)',
|
|
|
|
|
'xiao-esp32-c3': 'Seeed XIAO ESP32-C3 mini board (QEMU)',
|
2026-03-16 01:39:26 +07:00
|
|
|
'aitewinrobot-esp32c3-supermini': 'ESP32-C3 SuperMini (QEMU)',
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
'stm32-bluepill': 'STM32F103C8 Cortex-M3, 64KB flash, 37 GPIO (QEMU)',
|
|
|
|
|
'stm32-blackpill': 'STM32F411CE Cortex-M4, 512KB flash, 50 GPIO (QEMU)',
|
feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.
tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:59:04 +07:00
|
|
|
'stm32-bluepill-f103cb': 'STM32F103CB Cortex-M3, 128KB flash, 37 GPIO (QEMU)',
|
|
|
|
|
'stm32-blackpill-f401': 'STM32F401CE Cortex-M4, 512KB flash, 50 GPIO (QEMU)',
|
|
|
|
|
'stm32-f4-discovery': 'STM32F407VG Cortex-M4, 1MB flash, 4 onboard LEDs (QEMU)',
|
|
|
|
|
'stm32-olimex-h405': 'Olimex STM32-H405, F405RG Cortex-M4, 1MB flash (QEMU)',
|
|
|
|
|
'stm32-netduino-plus2': 'Netduino Plus 2, STM32F405 Cortex-M4 (QEMU)',
|
|
|
|
|
'stm32-netduino2': 'Netduino 2, STM32F205 Cortex-M3 (QEMU, serial)',
|
2026-04-22 02:45:45 +07:00
|
|
|
attiny85: '8-bit AVR, 8KB flash, 6 GPIO (browser)',
|
2026-03-16 01:39:26 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ALL_BOARDS: BoardKind[] = [
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno',
|
|
|
|
|
'arduino-nano',
|
|
|
|
|
'arduino-mega',
|
|
|
|
|
'raspberry-pi-pico',
|
|
|
|
|
'pi-pico-w',
|
|
|
|
|
'raspberry-pi-3',
|
feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring)
The BoardKind type and the QEMU backend already supported
raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing
the Pi 3 arm64 image set, but the frontend had no way to actually
select either: the board picker, the canvas renderer, the serial
monitor, the oscilloscope channel list, and the editor toolbar all
hard-coded "raspberry-pi-3" as the only Pi entry. ComponentRegistry
even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3
custom-element tag — a placeholder that meant both boards rendered as
a Pi 3 in the picker thumbnail and on the canvas.
Add dedicated boards top-to-bottom:
* `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style
schematic SVG (authored from scratch, not traced). Pi 4 is the
green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI;
Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge,
2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power
button. Both carry a small "velxio" mark in the corner.
* `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that
returns the 40-pin BCM layout. Every Pi from the 1B+ onwards
uses the same physical pin positions and same BCM GPIO
assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this
helper and example wires drawn against one model transfer to
the others without re-routing.
* React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the
custom elements at absolute positions (mirrors how
RaspberryPi3.tsx handles the Pi 3 illustration).
* Wire-up across the editor surface:
- BoardOnCanvas: BOARD_SIZE entry + switch case.
- BoardPickerModal: description, icon, kinds list.
- ComponentPickerModal: thumbnails now instantiate the dedicated
custom element (was velxio-raspberry-pi-3 fallback).
- SerialMonitor / EditorToolbar: pill labels, icons, colours.
- Oscilloscope: GPIO channel list (28 BCM pins).
- SimulatorCanvas: remote-boards filter for run/stop sync.
- SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3.
- boardPinToNumber: accepts physical pin numbers ("1"-"40"),
BCM names ("GPIO14") and power labels for any Pi 3/4/5 id.
- ComponentRegistry: dedicated tagNames + per-board thumbnails
(green for Pi 4, darker green for Pi 5).
* EditorToolbar's Pi 3 special cases (Linux/Python compile path,
Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5
inherit the same behaviour automatically, and any future Pi
family member (Zero / 1 / 2) lands in the right code paths the
moment its backend boots.
QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry
'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes
both boards selectable end-to-end without any backend follow-up.
2026-05-23 09:46:59 +07:00
|
|
|
'raspberry-pi-4',
|
|
|
|
|
'raspberry-pi-5',
|
2026-04-22 02:45:45 +07:00
|
|
|
'esp32',
|
|
|
|
|
'esp32-devkit-c-v4',
|
|
|
|
|
'esp32-cam',
|
|
|
|
|
'wemos-lolin32-lite',
|
|
|
|
|
'esp32-s3',
|
|
|
|
|
'xiao-esp32-s3',
|
|
|
|
|
'arduino-nano-esp32',
|
|
|
|
|
'esp32-c3',
|
|
|
|
|
'xiao-esp32-c3',
|
|
|
|
|
'aitewinrobot-esp32c3-supermini',
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
'stm32-bluepill',
|
|
|
|
|
'stm32-blackpill',
|
feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.
tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:59:04 +07:00
|
|
|
'stm32-bluepill-f103cb',
|
|
|
|
|
'stm32-blackpill-f401',
|
|
|
|
|
'stm32-f4-discovery',
|
|
|
|
|
'stm32-olimex-h405',
|
|
|
|
|
'stm32-netduino-plus2',
|
|
|
|
|
'stm32-netduino2',
|
2026-03-19 09:33:20 +07:00
|
|
|
'attiny85',
|
2026-03-16 01:39:26 +07:00
|
|
|
];
|
|
|
|
|
|
2026-03-04 05:30:25 +07:00
|
|
|
export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
|
|
|
|
isOpen,
|
|
|
|
|
onClose,
|
|
|
|
|
onSelectComponent,
|
2026-03-16 01:39:26 +07:00
|
|
|
onSelectBoard,
|
2026-03-04 05:30:25 +07:00
|
|
|
}) => {
|
2026-05-09 21:29:07 +07:00
|
|
|
const { t } = useTranslation();
|
2026-03-04 05:30:25 +07:00
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
2026-04-22 02:45:45 +07:00
|
|
|
const [selectedCategory, setSelectedCategory] = useState<ComponentCategory | 'all' | 'boards'>(
|
|
|
|
|
'all',
|
|
|
|
|
);
|
2026-03-04 05:30:25 +07:00
|
|
|
const [registry] = useState(() => ComponentRegistry.getInstance());
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
// Wait for registry to load
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadRegistry = async () => {
|
|
|
|
|
await registry.load();
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
};
|
|
|
|
|
loadRegistry();
|
|
|
|
|
}, [registry]);
|
|
|
|
|
|
|
|
|
|
// Filter components based on search and category
|
|
|
|
|
const filteredComponents = useMemo(() => {
|
|
|
|
|
if (isLoading) return [];
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
let components = searchQuery ? registry.search(searchQuery) : registry.getAllComponents();
|
2026-03-04 05:30:25 +07:00
|
|
|
|
|
|
|
|
if (selectedCategory !== 'all') {
|
2026-04-22 02:45:45 +07:00
|
|
|
components = components.filter((c) => c.category === selectedCategory);
|
2026-03-04 05:30:25 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return components;
|
|
|
|
|
}, [searchQuery, selectedCategory, registry, isLoading]);
|
|
|
|
|
|
|
|
|
|
// Get available categories
|
|
|
|
|
const categories = useMemo(() => {
|
|
|
|
|
if (isLoading) return [];
|
|
|
|
|
return registry.getCategories();
|
|
|
|
|
}, [registry, isLoading]);
|
|
|
|
|
|
|
|
|
|
// Handle ESC key to close modal
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleEsc = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') onClose();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
window.addEventListener('keydown', handleEsc);
|
|
|
|
|
return () => window.removeEventListener('keydown', handleEsc);
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen, onClose]);
|
|
|
|
|
|
|
|
|
|
if (!isOpen) return null;
|
|
|
|
|
|
2026-07-17 23:44:58 +07:00
|
|
|
// Portal to <body>: the picker must escape the canvas subtree so no ancestor
|
|
|
|
|
// stacking context can pin it below floating panels (e.g. the AI chat).
|
|
|
|
|
return createPortal(
|
2026-03-04 05:30:25 +07:00
|
|
|
<div className="component-picker-overlay" onClick={onClose}>
|
|
|
|
|
<div className="component-picker-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="modal-header">
|
2026-05-09 21:29:07 +07:00
|
|
|
<h2>{t('editor.componentPicker.title')}</h2>
|
|
|
|
|
<button className="close-btn" onClick={onClose} aria-label={t('editor.componentPicker.close')}>
|
2026-03-04 06:42:17 +07:00
|
|
|
X
|
2026-03-04 05:30:25 +07:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search Bar */}
|
|
|
|
|
<div className="search-section">
|
|
|
|
|
<div className="search-input-wrapper">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
className="search-input"
|
2026-05-09 21:29:07 +07:00
|
|
|
placeholder={t('editor.componentPicker.searchPlaceholder')}
|
2026-03-04 05:30:25 +07:00
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{searchQuery && (
|
|
|
|
|
<button
|
|
|
|
|
className="clear-search-btn"
|
|
|
|
|
onClick={() => setSearchQuery('')}
|
2026-05-09 21:29:07 +07:00
|
|
|
aria-label={t('editor.componentPicker.clearSearch')}
|
2026-03-04 05:30:25 +07:00
|
|
|
>
|
2026-03-04 06:42:17 +07:00
|
|
|
X
|
2026-03-04 05:30:25 +07:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Category Tabs */}
|
|
|
|
|
<div className="category-tabs">
|
|
|
|
|
<button
|
|
|
|
|
className={`category-tab ${selectedCategory === 'all' ? 'active' : ''}`}
|
|
|
|
|
onClick={() => setSelectedCategory('all')}
|
|
|
|
|
>
|
2026-05-09 21:29:07 +07:00
|
|
|
{t('editor.componentPicker.allComponents')}
|
2026-03-04 05:30:25 +07:00
|
|
|
</button>
|
2026-04-22 02:45:45 +07:00
|
|
|
{categories
|
|
|
|
|
.filter((c) => c !== 'boards')
|
|
|
|
|
.map((category) => (
|
|
|
|
|
<button
|
|
|
|
|
key={category}
|
|
|
|
|
className={`category-tab ${selectedCategory === category ? 'active' : ''}`}
|
|
|
|
|
onClick={() => setSelectedCategory(category)}
|
|
|
|
|
>
|
|
|
|
|
{ComponentRegistry.getCategoryDisplayName(category)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
2026-03-16 01:39:26 +07:00
|
|
|
{onSelectBoard && (
|
|
|
|
|
<button
|
|
|
|
|
className={`category-tab ${selectedCategory === 'boards' ? 'active' : ''}`}
|
|
|
|
|
onClick={() => setSelectedCategory('boards')}
|
|
|
|
|
>
|
2026-05-09 21:29:07 +07:00
|
|
|
{t('editor.componentPicker.boards')}
|
2026-03-16 01:39:26 +07:00
|
|
|
</button>
|
|
|
|
|
)}
|
2026-03-04 05:30:25 +07:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-16 01:39:26 +07:00
|
|
|
{/* Boards Panel */}
|
|
|
|
|
{selectedCategory === 'boards' ? (
|
|
|
|
|
<div className="components-grid">
|
|
|
|
|
{ALL_BOARDS.map((kind) => (
|
|
|
|
|
<BoardCard
|
|
|
|
|
key={kind}
|
|
|
|
|
kind={kind}
|
2026-04-22 02:45:45 +07:00
|
|
|
onSelect={() => {
|
|
|
|
|
onSelectBoard?.(kind);
|
|
|
|
|
onClose();
|
|
|
|
|
}}
|
2026-03-16 01:39:26 +07:00
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
2026-04-21 20:56:20 +07:00
|
|
|
{/* Single scrollable area wrapping both the boards row (only in
|
|
|
|
|
"All Components" view) and the components grid, so the modal
|
|
|
|
|
shows ONE scrollbar instead of two stacked ones. */}
|
|
|
|
|
<div className="components-scroll">
|
|
|
|
|
{selectedCategory === 'all' && onSelectBoard && (
|
2026-04-22 02:45:45 +07:00
|
|
|
<div
|
|
|
|
|
className="components-grid components-grid--inline"
|
|
|
|
|
style={{ borderBottom: '1px solid #333', paddingBottom: 8, marginBottom: 4 }}
|
|
|
|
|
>
|
|
|
|
|
{ALL_BOARDS.filter(
|
|
|
|
|
(k) =>
|
|
|
|
|
!searchQuery ||
|
|
|
|
|
BOARD_KIND_LABELS[k].toLowerCase().includes(searchQuery.toLowerCase()),
|
2026-04-21 20:56:20 +07:00
|
|
|
).map((kind) => (
|
|
|
|
|
<BoardCard
|
|
|
|
|
key={kind}
|
|
|
|
|
kind={kind}
|
2026-04-22 02:45:45 +07:00
|
|
|
onSelect={() => {
|
|
|
|
|
onSelectBoard(kind);
|
|
|
|
|
onClose();
|
|
|
|
|
}}
|
2026-04-21 20:56:20 +07:00
|
|
|
/>
|
|
|
|
|
))}
|
2026-03-16 01:39:26 +07:00
|
|
|
</div>
|
2026-03-04 05:30:25 +07:00
|
|
|
)}
|
2026-04-21 20:56:20 +07:00
|
|
|
|
|
|
|
|
<div className="components-grid components-grid--inline">
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="loading-state">
|
|
|
|
|
<div className="spinner"></div>
|
2026-05-09 21:29:07 +07:00
|
|
|
<p>{t('editor.componentPicker.loading')}</p>
|
2026-04-21 20:56:20 +07:00
|
|
|
</div>
|
|
|
|
|
) : filteredComponents.length === 0 ? (
|
|
|
|
|
<div className="no-results">
|
2026-05-09 21:29:07 +07:00
|
|
|
<p>{t('editor.componentPicker.noResults')}</p>
|
2026-04-21 20:56:20 +07:00
|
|
|
{searchQuery && (
|
|
|
|
|
<button
|
|
|
|
|
className="clear-filters-btn"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSearchQuery('');
|
|
|
|
|
setSelectedCategory('all');
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-09 21:29:07 +07:00
|
|
|
{t('editor.componentPicker.clearFilters')}
|
2026-04-21 20:56:20 +07:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
filteredComponents.map((component) => (
|
|
|
|
|
<ComponentCard
|
|
|
|
|
key={component.id}
|
|
|
|
|
component={component}
|
2026-05-05 20:33:53 +07:00
|
|
|
onSelect={() => {
|
|
|
|
|
// Pro overlays can intercept clicks on pro_only
|
|
|
|
|
// components by setting window.__velxio_pro_gate__.
|
|
|
|
|
// Returning true means "handled — do not pass through".
|
|
|
|
|
if (component.pro_only) {
|
|
|
|
|
const gate = (window as unknown as {
|
|
|
|
|
__velxio_pro_gate__?: (c: typeof component) => boolean;
|
|
|
|
|
}).__velxio_pro_gate__;
|
|
|
|
|
if (gate && gate(component)) return;
|
|
|
|
|
}
|
|
|
|
|
onSelectComponent(component);
|
|
|
|
|
}}
|
2026-04-21 20:56:20 +07:00
|
|
|
/>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-03-04 05:30:25 +07:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-16 01:39:26 +07:00
|
|
|
{/* Footer Info */}
|
|
|
|
|
<div className="modal-footer">
|
|
|
|
|
<span className="component-count">
|
2026-04-22 02:45:45 +07:00
|
|
|
{filteredComponents.length} component{filteredComponents.length !== 1 ? 's' : ''}{' '}
|
|
|
|
|
available
|
2026-03-16 01:39:26 +07:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-03-04 05:30:25 +07:00
|
|
|
</div>
|
2026-07-17 23:44:58 +07:00
|
|
|
</div>,
|
|
|
|
|
document.body
|
2026-03-04 05:30:25 +07:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Component Card - Individual component display in the grid
|
|
|
|
|
*/
|
|
|
|
|
interface ComponentCardProps {
|
|
|
|
|
component: ComponentMetadata;
|
|
|
|
|
onSelect: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 01:21:03 +07:00
|
|
|
// Passive components (resistor / capacitor / inductor) come with metadata
|
|
|
|
|
// thumbnails that already encode the preset value (color bands for resistors,
|
|
|
|
|
// value labels for caps/inductors). The live wokwi elements either ignore
|
|
|
|
|
// `value` visually or render it identically across presets, so for these we
|
|
|
|
|
// short-circuit to the SVG. Everything else still uses the live element so
|
|
|
|
|
// LEDs, displays, etc. preview correctly.
|
|
|
|
|
const PASSIVE_TAGS = new Set([
|
|
|
|
|
'wokwi-resistor',
|
|
|
|
|
'wokwi-capacitor',
|
2026-04-22 02:45:45 +07:00
|
|
|
'velxio-capacitor-electrolytic',
|
2026-04-22 01:21:03 +07:00
|
|
|
'wokwi-inductor',
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 05:30:25 +07:00
|
|
|
const ComponentCard: React.FC<ComponentCardProps> = ({ component, onSelect }) => {
|
|
|
|
|
const thumbnailRef = React.useRef<HTMLDivElement>(null);
|
2026-04-22 01:21:03 +07:00
|
|
|
const usePresetSvg =
|
|
|
|
|
PASSIVE_TAGS.has(component.tagName) &&
|
|
|
|
|
typeof component.thumbnail === 'string' &&
|
|
|
|
|
component.thumbnail.trim().startsWith('<svg');
|
2026-03-04 05:30:25 +07:00
|
|
|
|
|
|
|
|
// Render actual web component as thumbnail
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!thumbnailRef.current) return;
|
2026-04-22 01:21:03 +07:00
|
|
|
if (usePresetSvg) return; // SVG is rendered via dangerouslySetInnerHTML below
|
2026-03-04 05:30:25 +07:00
|
|
|
|
|
|
|
|
// Create the actual wokwi element
|
|
|
|
|
const element = document.createElement(component.tagName);
|
|
|
|
|
|
|
|
|
|
// Scale factors for different component types
|
|
|
|
|
let scale = 0.5;
|
|
|
|
|
if (component.tagName.includes('arduino') || component.tagName.includes('esp32')) {
|
|
|
|
|
scale = 0.35; // Boards are larger, scale them down more
|
|
|
|
|
} else if (component.tagName.includes('lcd') || component.tagName.includes('display')) {
|
|
|
|
|
scale = 0.4; // Displays need a bit more space
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(element as HTMLElement).style.transform = `scale(${scale})`;
|
|
|
|
|
(element as HTMLElement).style.transformOrigin = 'center center';
|
|
|
|
|
|
2026-04-22 01:21:03 +07:00
|
|
|
// Pass the preset's default value through so value-sensitive elements
|
|
|
|
|
// (e.g. wokwi-resistor color bands) render the right look in the picker.
|
|
|
|
|
if (component.defaultValues?.value !== undefined) {
|
|
|
|
|
(element as any).value = component.defaultValues.value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 05:30:25 +07:00
|
|
|
// Set default properties for better preview appearance
|
|
|
|
|
if (component.tagName === 'wokwi-led') {
|
|
|
|
|
(element as any).value = true; // Turn on LED
|
|
|
|
|
(element as any).color = component.defaultValues?.color || 'red';
|
|
|
|
|
} else if (component.tagName === 'wokwi-rgb-led') {
|
|
|
|
|
(element as any).red = true;
|
|
|
|
|
(element as any).green = true;
|
|
|
|
|
(element as any).blue = true;
|
|
|
|
|
} else if (component.tagName === 'wokwi-pushbutton') {
|
|
|
|
|
(element as any).color = component.defaultValues?.color || 'red';
|
|
|
|
|
} else if (component.tagName === 'wokwi-lcd1602' || component.tagName === 'wokwi-lcd2004') {
|
|
|
|
|
(element as any).text = 'Hello World!';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
thumbnailRef.current.innerHTML = '';
|
|
|
|
|
thumbnailRef.current.appendChild(element);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (thumbnailRef.current) {
|
|
|
|
|
thumbnailRef.current.innerHTML = '';
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-22 01:21:03 +07:00
|
|
|
}, [component.tagName, component.defaultValues, usePresetSvg]);
|
2026-03-04 05:30:25 +07:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button className="component-card" onClick={onSelect}>
|
|
|
|
|
<div className="card-thumbnail">
|
2026-04-22 01:21:03 +07:00
|
|
|
{usePresetSvg ? (
|
|
|
|
|
<div
|
|
|
|
|
className="component-preview"
|
|
|
|
|
dangerouslySetInnerHTML={{ __html: component.thumbnail }}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div ref={thumbnailRef} className="component-preview" />
|
|
|
|
|
)}
|
2026-03-04 05:30:25 +07:00
|
|
|
</div>
|
|
|
|
|
<div className="card-content">
|
|
|
|
|
<div className="card-name">{component.name}</div>
|
2026-04-22 02:45:45 +07:00
|
|
|
{component.description && <div className="card-description">{component.description}</div>}
|
2026-03-04 05:30:25 +07:00
|
|
|
<div className="card-meta">
|
|
|
|
|
<span className="card-category">{component.category}</span>
|
2026-04-22 02:45:45 +07:00
|
|
|
{component.pinCount > 0 && <span className="card-pins">{component.pinCount} pins</span>}
|
2026-03-04 05:30:25 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
};
|
2026-03-16 01:39:26 +07:00
|
|
|
|
|
|
|
|
// Tag name used to render a thumbnail for each board kind.
|
|
|
|
|
// Boards without a tag will show a generic chip icon.
|
|
|
|
|
const BOARD_TAG: Partial<Record<BoardKind, string>> = {
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno': 'wokwi-arduino-uno',
|
|
|
|
|
'arduino-nano': 'wokwi-arduino-nano',
|
|
|
|
|
'arduino-mega': 'wokwi-arduino-mega',
|
|
|
|
|
'raspberry-pi-pico': 'wokwi-nano-rp2040-connect',
|
|
|
|
|
'pi-pico-w': 'velxio-pi-pico-w',
|
|
|
|
|
esp32: 'velxio-esp32',
|
|
|
|
|
'esp32-devkit-c-v4': 'velxio-esp32',
|
|
|
|
|
'esp32-cam': 'velxio-esp32',
|
|
|
|
|
'wemos-lolin32-lite': 'velxio-esp32',
|
|
|
|
|
'esp32-s3': 'velxio-esp32',
|
|
|
|
|
'xiao-esp32-s3': 'velxio-esp32',
|
|
|
|
|
'arduino-nano-esp32': 'velxio-esp32',
|
|
|
|
|
'esp32-c3': 'velxio-esp32',
|
|
|
|
|
'xiao-esp32-c3': 'velxio-esp32',
|
|
|
|
|
'aitewinrobot-esp32c3-supermini': 'velxio-esp32',
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
'stm32-bluepill': 'velxio-stm32-bluepill',
|
|
|
|
|
'stm32-blackpill': 'velxio-stm32-blackpill',
|
feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.
tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:59:04 +07:00
|
|
|
'stm32-bluepill-f103cb': 'velxio-stm32-bluepill-f103cb',
|
|
|
|
|
'stm32-blackpill-f401': 'velxio-stm32-blackpill-f401',
|
|
|
|
|
'stm32-f4-discovery': 'velxio-stm32-f4-discovery',
|
|
|
|
|
'stm32-olimex-h405': 'velxio-stm32-olimex-h405',
|
|
|
|
|
'stm32-netduino-plus2': 'velxio-stm32-netduino-plus2',
|
|
|
|
|
'stm32-netduino2': 'velxio-stm32-netduino2',
|
2026-03-16 01:39:26 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface BoardCardProps {
|
|
|
|
|
kind: BoardKind;
|
|
|
|
|
onSelect: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BoardCard: React.FC<BoardCardProps> = ({ kind, onSelect }) => {
|
|
|
|
|
const thumbnailRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!thumbnailRef.current) return;
|
2026-07-21 04:47:50 +07:00
|
|
|
// Static-image boards handled below via reactThumbnail: the whole Pi
|
|
|
|
|
// Linux family uses board illustrations (a live custom element at
|
|
|
|
|
// natural size + CSS scale keeps its unscaled layout box, so the
|
|
|
|
|
// 100px thumbnail clips it to a narrow sliver).
|
|
|
|
|
if (
|
|
|
|
|
kind === 'raspberry-pi-3' ||
|
|
|
|
|
kind === 'raspberry-pi-4' ||
|
|
|
|
|
kind === 'raspberry-pi-5' ||
|
|
|
|
|
kind === 'attiny85'
|
|
|
|
|
)
|
|
|
|
|
return;
|
2026-03-16 01:39:26 +07:00
|
|
|
|
|
|
|
|
const tag = BOARD_TAG[kind];
|
|
|
|
|
if (!tag) return;
|
|
|
|
|
|
|
|
|
|
const el = document.createElement(tag) as HTMLElement;
|
|
|
|
|
// Use setAttribute so observedAttributes + connectedCallback read the correct value
|
|
|
|
|
el.setAttribute('board-kind', kind);
|
|
|
|
|
el.style.transform = 'scale(0.28)';
|
|
|
|
|
el.style.transformOrigin = 'center center';
|
|
|
|
|
|
|
|
|
|
thumbnailRef.current.innerHTML = '';
|
|
|
|
|
thumbnailRef.current.appendChild(el);
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
return () => {
|
|
|
|
|
if (thumbnailRef.current) thumbnailRef.current.innerHTML = '';
|
|
|
|
|
};
|
2026-03-16 01:39:26 +07:00
|
|
|
}, [kind]);
|
|
|
|
|
|
|
|
|
|
const reactThumbnail =
|
|
|
|
|
kind === 'raspberry-pi-3' ? (
|
2026-04-22 02:45:45 +07:00
|
|
|
<img
|
|
|
|
|
src={raspberryPi3Svg}
|
|
|
|
|
alt="Raspberry Pi 3"
|
|
|
|
|
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
|
|
|
|
|
/>
|
2026-07-21 04:47:50 +07:00
|
|
|
) : kind === 'raspberry-pi-4' ? (
|
|
|
|
|
<img
|
|
|
|
|
src={raspberryPi4Png}
|
|
|
|
|
alt="Raspberry Pi 4"
|
|
|
|
|
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
|
|
|
|
|
/>
|
|
|
|
|
) : kind === 'raspberry-pi-5' ? (
|
|
|
|
|
<img
|
|
|
|
|
src={raspberryPi5Png}
|
|
|
|
|
alt="Raspberry Pi 5"
|
|
|
|
|
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
|
|
|
|
|
/>
|
2026-03-16 01:39:26 +07:00
|
|
|
) : kind === 'attiny85' ? (
|
|
|
|
|
<div style={{ transform: 'scale(0.55)', transformOrigin: 'center center' }}>
|
|
|
|
|
<Attiny85 />
|
|
|
|
|
</div>
|
|
|
|
|
) : null;
|
|
|
|
|
|
|
|
|
|
return (
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
<button className="component-card" onClick={onSelect} style={{ position: 'relative' }}>
|
|
|
|
|
{isProBoardKind(kind) && (
|
|
|
|
|
<span
|
|
|
|
|
title="Pro feature — paid plan or Velxio Desktop"
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
2026-07-21 04:47:50 +07:00
|
|
|
top: 8,
|
|
|
|
|
right: 8,
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
zIndex: 1,
|
2026-07-21 04:47:50 +07:00
|
|
|
padding: '3px 10px',
|
|
|
|
|
borderRadius: 999,
|
|
|
|
|
fontSize: 11,
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
fontWeight: 700,
|
2026-07-21 04:47:50 +07:00
|
|
|
letterSpacing: 0.6,
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
color: '#1a1205',
|
|
|
|
|
background: 'linear-gradient(180deg,#ffd566,#f5a623)',
|
2026-07-21 04:47:50 +07:00
|
|
|
boxShadow: '0 1px 4px rgba(0,0,0,0.35)',
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
PRO
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-03-16 01:39:26 +07:00
|
|
|
<div className="card-thumbnail">
|
2026-04-22 02:45:45 +07:00
|
|
|
{reactThumbnail ? reactThumbnail : <div ref={thumbnailRef} className="component-preview" />}
|
2026-03-16 01:39:26 +07:00
|
|
|
</div>
|
|
|
|
|
<div className="card-content">
|
|
|
|
|
<div className="card-name">{BOARD_KIND_LABELS[kind]}</div>
|
|
|
|
|
<div className="card-description">{BOARD_DESCRIPTIONS[kind]}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
};
|