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.
This commit is contained in:
David Montero 2026-08-17 18:50:37 +02:00
parent 15126cd27a
commit 0e107fafe6
2 changed files with 121 additions and 48 deletions

View File

@ -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']);
});
});

View File

@ -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/<id>.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/<id>.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<string, string> = {
'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<void> {
}
}
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 = () => (
<span className="new-project-pro" title="Pro feature — paid plan or Velxio Desktop">
PRO
@ -224,51 +286,8 @@ export const NewProjectDialog: React.FC<NewProjectDialogProps> = ({ 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;