From 0e107fafe6dbb3bd3deaa5cbafc9285b4ef05dee Mon Sep 17 00:00:00 2001 From: David Montero Date: Mon, 17 Aug 2026 18:50:37 +0200 Subject: [PATCH] feat(editor): M5Stack section (Cardputer ADV, Core) ahead of STM32 in the starter dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../new-project-starter-sections.test.ts | 54 ++++++++ .../components/editor/NewProjectDialog.tsx | 115 ++++++++++-------- 2 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 frontend/src/__tests__/new-project-starter-sections.test.ts diff --git a/frontend/src/__tests__/new-project-starter-sections.test.ts b/frontend/src/__tests__/new-project-starter-sections.test.ts new file mode 100644 index 00000000..ae2345fb --- /dev/null +++ b/frontend/src/__tests__/new-project-starter-sections.test.ts @@ -0,0 +1,54 @@ +/** + * NewProjectDialog starter sections — display order and overlay cards. + * + * The dialog hardcodes its sections; overlay-registered kinds (M5Stack, + * XIAO C6, XIAO RP2040) surface only when the private overlay registered + * them. Guards the operator's requested order: M5Stack (Cardputer ADV, then + * Core) sits ahead of STM32, and an OSS build shows no empty M5Stack block. + */ +import { describe, it, expect } from 'vitest'; +import { buildStarterSections } from '../components/editor/NewProjectDialog'; +import type { ProBoardDef } from '../lib/proBoardRegistry'; + +const def = (kind: string, label: string, description: string): ProBoardDef => ({ + kind, + label, + description, + fqbn: null, + tag: `velxio-${kind}`, + size: { w: 100, h: 100 }, +}); + +const titles = (defs: ProBoardDef[]) => + buildStarterSections(defs) + .filter((s) => s.entries.length > 0) + .map((s) => s.title); + +describe('NewProjectDialog starter sections', () => { + it('OSS build (no overlay boards) shows no M5Stack section', () => { + expect(titles([])).toEqual(['Arduino', 'ESP32', 'STM32', 'Raspberry Pi']); + }); + + it('with the M5Stack overlay boards registered, M5Stack comes before STM32', () => { + const defs = [ + def('m5stack-core', 'M5Stack Core', 'ESP32 all-in-one'), + def('cardputer-adv', 'M5 Cardputer ADV', 'ESP32-S3 card computer'), + ]; + const order = titles(defs); + expect(order).toEqual(['Arduino', 'ESP32', 'M5Stack', 'STM32', 'Raspberry Pi']); + expect(order.indexOf('M5Stack')).toBeLessThan(order.indexOf('STM32')); + + const m5 = buildStarterSections(defs).find((s) => s.title === 'M5Stack')!; + // Cardputer ADV first, then the Core — regardless of registration order. + expect(m5.entries.map((e) => e.kind)).toEqual(['cardputer-adv', 'm5stack-core']); + // Card blurb comes from the registered definition, not a hardcoded string. + expect(m5.entries[0].blurb).toBe('ESP32-S3 card computer'); + }); + + it('a partial overlay (only one M5 kind) still renders the section with that card', () => { + const m5 = buildStarterSections([def('m5stack-core', 'M5Stack Core', 'x')]).find( + (s) => s.title === 'M5Stack', + )!; + expect(m5.entries.map((e) => e.kind)).toEqual(['m5stack-core']); + }); +}); diff --git a/frontend/src/components/editor/NewProjectDialog.tsx b/frontend/src/components/editor/NewProjectDialog.tsx index 385a55e9..17df9405 100644 --- a/frontend/src/components/editor/NewProjectDialog.tsx +++ b/frontend/src/components/editor/NewProjectDialog.tsx @@ -4,9 +4,10 @@ * Shown (a) on a pristine `/editor` visit (over an emptied canvas), and * (b) from the "New workspace" button / File menu entry. Offers a blank * workspace plus a ready-to-run Blink starter per board family — Arduino, - * ESP32 (one card per chip generation, XIAO variant preferred), STM32, - * Raspberry Pi — each card carrying the same circuit thumbnail the examples - * gallery uses (/examples-thumbs/.webp, CircuitPreview fallback). + * ESP32 (one card per chip generation, XIAO variant preferred), M5Stack + * (overlay-registered all-in-ones, listed ahead of STM32), STM32, Raspberry + * Pi — each card carrying the same circuit thumbnail the examples gallery + * uses (/examples-thumbs/.webp, CircuitPreview fallback). * * Selecting a board loads its gallery Blink example when one exists (full * wiring: 220Ω resistor + LED); boards without one get a fresh board whose @@ -29,6 +30,7 @@ import { listProBoards, subscribeProBoards, getProBoardsVersion, + type ProBoardDef, } from '../../lib/proBoardRegistry'; import { useSimulatorStore, DEFAULT_BOARD_POSITION } from '../../store/useSimulatorStore'; import { useProjectStore } from '../../store/useProjectStore'; @@ -150,6 +152,10 @@ const PREFERRED_BLINK_EXAMPLE: Record = { 'stm32-f4-discovery': 'stm32-f4-discovery-blink', 'stm32-olimex-h405': 'stm32-olimex-h405-blink', 'stm32-netduino-plus2': 'stm32-netduino-plus2-blink', + // M5Stack all-in-ones (overlay kinds): no LED to blink — the on-LCD hello + // is the first run. Both ids carry a captured gallery thumb. + 'cardputer-adv': 'cardputer-adv-hello', + 'm5stack-core': 'm5stack-core-m5-helloworld', }; /** Dynamic import keeps the (large) gallery data out of the editor bundle @@ -207,6 +213,62 @@ async function applyStarter(kind: string | 'blank'): Promise { } } +export interface StarterSection { + title: string; + entries: Array<{ kind: string; blurb: string }>; +} + +/** + * The card sections, in display order. Overlay-registered kinds only exist + * when the private overlay mounted — an OSS build simply doesn't show those + * cards, and a section left with no entries is not rendered at all. + */ +export function buildStarterSections(defs: ProBoardDef[]): StarterSection[] { + const oss = (k: BoardKind) => ({ kind: k as string, blurb: BOARD_BLURBS[k] ?? '' }); + const pro = (k: string) => { + const d = defs.find((x) => x.kind === k); + return d ? [{ kind: d.kind, blurb: d.description }] : []; + }; + return [ + { + title: 'Arduino', + entries: [ + oss('arduino-uno'), + oss('arduino-mega'), + oss('arduino-nano'), + oss('arduino-nano-esp32'), + ], + }, + // One card per ESP32 chip generation, XIAO variant preferred where + // Seeed makes one: classic → DevKit V1, S3/C3 → XIAO, C6 → XIAO (overlay). + { + title: 'ESP32', + entries: [ + oss('esp32'), + oss('esp32-cam'), + oss('xiao-esp32-s3'), + oss('xiao-esp32-c3'), + ...pro('xiao-esp32c6'), + ], + }, + // M5Stack all-in-ones (overlay): ahead of STM32 by operator request — + // they run on Free and are a friendlier first pick than the Pro-gated + // STM32 family. Cardputer first, then the Core. + { title: 'M5Stack', entries: [...pro('cardputer-adv'), ...pro('m5stack-core')] }, + { title: 'STM32', entries: STM32_BOARDS.map(oss) }, + { + title: 'Raspberry Pi', + entries: [ + oss('raspberry-pi-pico'), + ...pro('xiao-rp2040'), + oss('raspberry-pi-3'), + oss('raspberry-pi-4'), + oss('raspberry-pi-5'), + ], + }, + ]; +} + const ProPill: React.FC = () => ( PRO @@ -224,51 +286,8 @@ export const NewProjectDialog: React.FC = ({ isOpen, onCl getProBoardsVersion, ); - const sections = useMemo(() => { - const defs = listProBoards(); - const oss = (k: BoardKind) => ({ kind: k as string, blurb: BOARD_BLURBS[k] ?? '' }); - // Overlay-registered kinds only exist when the private overlay mounted — - // an OSS build simply doesn't show those cards. - const pro = (k: string) => { - const d = defs.find((x) => x.kind === k); - return d ? [{ kind: d.kind, blurb: d.description }] : []; - }; - return [ - { - title: 'Arduino', - entries: [ - oss('arduino-uno'), - oss('arduino-mega'), - oss('arduino-nano'), - oss('arduino-nano-esp32'), - ], - }, - // One card per ESP32 chip generation, XIAO variant preferred where - // Seeed makes one: classic → DevKit V1, S3/C3 → XIAO, C6 → XIAO (overlay). - { - title: 'ESP32', - entries: [ - oss('esp32'), - oss('esp32-cam'), - oss('xiao-esp32-s3'), - oss('xiao-esp32-c3'), - ...pro('xiao-esp32c6'), - ], - }, - { title: 'STM32', entries: STM32_BOARDS.map(oss) }, - { - title: 'Raspberry Pi', - entries: [ - oss('raspberry-pi-pico'), - ...pro('xiao-rp2040'), - oss('raspberry-pi-3'), - oss('raspberry-pi-4'), - oss('raspberry-pi-5'), - ], - }, - ]; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [proBoardsVersion]); + // eslint-disable-next-line react-hooks/exhaustive-deps + const sections = useMemo(() => buildStarterSections(listProBoards()), [proBoardsVersion]); useEffect(() => { if (!isOpen) return;