feat(editor): starter dialog v2 — circuit thumbnails, blank canvas behind, one ESP32 per chip
The starter cards now carry the same circuit thumbnails the examples gallery uses (ExampleThumbnail → /examples-thumbs/<id>.webp with the CircuitPreview SVG fallback): boards with a Blink example reuse its thumb, the rest get freshly captured starter-<kind>.webp shots in the same folder (XIAO S3/C3, Netduino 2, Pi 4/5; plus the missing xiao-esp32c6-blink and xiao-rp2040-blink example thumbs). The ESP32 section is now one card per chip generation with the Seeed XIAO variant preferred (DevKit V1 / XIAO S3 / XIAO C3 / XIAO C6), and XIAO RP2040 joins the Raspberry Pi section — replacing the previous every-variant list and the separate XIAO section. Fixes: a pristine /editor visit clears the hardcoded Uno starter BEFORE showing the dialog (empty canvas behind; cancel now leaves a blank workspace), and clearing the last board re-points the editor at a nonexistent group so Monaco no longer keeps showing the deleted board's sketch (removeBoard only re-points when another board remains).
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -25,7 +25,7 @@
|
|||
background: #1e1e1e;
|
||||
border: 1px solid #444;
|
||||
border-radius: 10px;
|
||||
width: min(780px, 94vw);
|
||||
width: min(920px, 94vw);
|
||||
max-height: 86vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -68,20 +68,20 @@
|
|||
|
||||
.new-project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.new-project-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
text-align: left;
|
||||
background: #2b2b2b;
|
||||
border: 1px solid #3d3d3d;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
color: #ddd;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
|
|
@ -93,12 +93,47 @@
|
|||
border-color: #5a5a5a;
|
||||
}
|
||||
|
||||
/* Circuit thumbnail — same 300x180 (5:3) frame as the gallery cards. */
|
||||
.new-project-card-thumb {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 5 / 3;
|
||||
background: #0a0a0c;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.new-project-card-thumb img,
|
||||
.new-project-card-thumb svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.new-project-card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 9px 12px 11px;
|
||||
}
|
||||
|
||||
.new-project-card-blank {
|
||||
border-style: dashed;
|
||||
border-color: #4a4f58;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.new-project-card-thumb-blank {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 42px;
|
||||
font-weight: 300;
|
||||
color: #4a4f58;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.new-project-card-name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
/**
|
||||
* NewProjectDialog — starter-template picker.
|
||||
*
|
||||
* Shown (a) on a pristine `/editor` visit, instead of silently dropping the
|
||||
* user into the hardcoded Arduino Uno + LED starter, 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, Seeed XIAO,
|
||||
* STM32, Raspberry Pi).
|
||||
* 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).
|
||||
*
|
||||
* Selecting a board loads its gallery Blink example when one exists (full
|
||||
* wiring: 220Ω resistor + LED); boards without one get a fresh board whose
|
||||
|
|
@ -31,9 +32,11 @@ import {
|
|||
} from '../../lib/proBoardRegistry';
|
||||
import { useSimulatorStore, DEFAULT_BOARD_POSITION } from '../../store/useSimulatorStore';
|
||||
import { useProjectStore } from '../../store/useProjectStore';
|
||||
import { useEditorStore } from '../../store/useEditorStore';
|
||||
import { getLocaleFromPath, localizedPath } from '../../i18n/path';
|
||||
import { loadExample } from '../../utils/loadExample';
|
||||
import type { ExampleProject } from '../../data/examples';
|
||||
import { ExampleThumbnail } from '../examples/ExampleThumbnail';
|
||||
import { trackSelectBoard } from '../../utils/analytics';
|
||||
import './NewProjectDialog.css';
|
||||
|
||||
|
|
@ -46,13 +49,6 @@ interface NewProjectDialogProps {
|
|||
const BOARD_BLURBS: Record<string, string> = {
|
||||
'arduino-uno': '8-bit AVR, 32KB flash, 14 digital I/O',
|
||||
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)',
|
||||
'wemos-lolin32-lite': 'Compact ESP32, LiPo battery support (QEMU)',
|
||||
'esp32-s3': 'Xtensa LX7 dual-core, WiFi+BT, AI accel (QEMU)',
|
||||
'arduino-nano-esp32': 'Nano form-factor, ESP32-S3, RGB LED (QEMU)',
|
||||
'esp32-c3': 'RISC-V single-core, WiFi+BLE, 22 GPIO (QEMU)',
|
||||
'aitewinrobot-esp32c3-supermini': 'ESP32-C3 SuperMini (QEMU)',
|
||||
'xiao-esp32-s3': 'Seeed XIAO tiny form, 8MB flash+PSRAM (QEMU)',
|
||||
'xiao-esp32-c3': 'Seeed XIAO ESP32-C3 mini board (QEMU)',
|
||||
'stm32-bluepill': 'STM32F103C8 Cortex-M3, 64KB flash, 37 GPIO (QEMU)',
|
||||
|
|
@ -69,19 +65,6 @@ const BOARD_BLURBS: Record<string, string> = {
|
|||
'raspberry-pi-5': 'ARM64 Cortex-A76 quad-core + RP1 I/O, Linux/Python (QEMU)',
|
||||
};
|
||||
|
||||
const ESP32_BOARDS: BoardKind[] = [
|
||||
'esp32',
|
||||
'esp32-devkit-c-v4',
|
||||
'esp32-cam',
|
||||
'wemos-lolin32-lite',
|
||||
'esp32-s3',
|
||||
'arduino-nano-esp32',
|
||||
'esp32-c3',
|
||||
'aitewinrobot-esp32c3-supermini',
|
||||
];
|
||||
|
||||
const XIAO_BOARDS: BoardKind[] = ['xiao-esp32-s3', 'xiao-esp32-c3'];
|
||||
|
||||
const STM32_BOARDS: BoardKind[] = [
|
||||
'stm32-bluepill',
|
||||
'stm32-blackpill',
|
||||
|
|
@ -93,12 +76,47 @@ const STM32_BOARDS: BoardKind[] = [
|
|||
'stm32-netduino2',
|
||||
];
|
||||
|
||||
const PI_BOARDS: BoardKind[] = [
|
||||
'raspberry-pi-pico',
|
||||
'raspberry-pi-3',
|
||||
'raspberry-pi-4',
|
||||
'raspberry-pi-5',
|
||||
];
|
||||
/**
|
||||
* Starter-card thumbnail: reuse the gallery's convention-based thumbs
|
||||
* (/examples-thumbs/<id>.webp, CircuitPreview SVG fallback). Kinds with a
|
||||
* Blink example use that example's thumb; the rest use a captured
|
||||
* `starter-<kind>` shot living in the same folder.
|
||||
*/
|
||||
function thumbStubFor(kind: string, label: string): ExampleProject {
|
||||
return {
|
||||
id: PREFERRED_BLINK_EXAMPLE[kind] ?? `starter-${kind}`,
|
||||
title: label,
|
||||
description: '',
|
||||
category: 'basics',
|
||||
difficulty: 'beginner',
|
||||
boardType: kind,
|
||||
code: '',
|
||||
components: [],
|
||||
wires: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty the whole workspace: boards, components, wires, current project and
|
||||
* every editor file group. Removing the LAST board leaves the editor's
|
||||
* files/activeFileId mirror pointing at the deleted group (removeBoard only
|
||||
* re-points when another board remains), so Monaco kept showing the dead
|
||||
* sketch — pointing the editor at a nonexistent group blanks it.
|
||||
*/
|
||||
export function clearWorkspaceForStarter(): void {
|
||||
// currentProject FIRST — same auto-save hazard loadExample documents:
|
||||
// mutating the stores while a saved project is still "current" lets the
|
||||
// debounced PUT overwrite that project with the emptied content.
|
||||
useProjectStore.getState().clearCurrentProject();
|
||||
const sim = useSimulatorStore.getState();
|
||||
sim.boards.forEach((b) => sim.stopBoard(b.id));
|
||||
sim.boards.map((b) => b.id).forEach((id) => sim.removeBoard(id));
|
||||
sim.setComponents([]);
|
||||
sim.setWires([]);
|
||||
const editor = useEditorStore.getState();
|
||||
Object.keys(editor.fileGroups).forEach((g) => editor.deleteFileGroup(g));
|
||||
editor.setActiveGroup('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gallery Blink example per board kind, when one exists. Preferred ids first
|
||||
|
|
@ -147,16 +165,7 @@ async function findBlinkExample(kind: string): Promise<ExampleProject | undefine
|
|||
* LED (createFileGroup picks the per-board/language blink content).
|
||||
*/
|
||||
async function applyStarter(kind: string | 'blank'): Promise<void> {
|
||||
// Clear currentProject FIRST — same auto-save hazard loadExample documents:
|
||||
// mutating the stores while a saved project is still "current" lets the
|
||||
// debounced PUT overwrite that project with the starter's content.
|
||||
useProjectStore.getState().clearCurrentProject();
|
||||
|
||||
const sim = useSimulatorStore.getState();
|
||||
sim.boards.forEach((b) => sim.stopBoard(b.id));
|
||||
sim.boards.map((b) => b.id).forEach((id) => sim.removeBoard(id));
|
||||
sim.setComponents([]);
|
||||
sim.setWires([]);
|
||||
clearWorkspaceForStarter();
|
||||
|
||||
if (kind !== 'blank') {
|
||||
let example: ExampleProject | undefined;
|
||||
|
|
@ -208,18 +217,37 @@ export const NewProjectDialog: React.FC<NewProjectDialogProps> = ({ isOpen, onCl
|
|||
|
||||
const sections = useMemo(() => {
|
||||
const defs = listProBoards();
|
||||
const overlay = (prefix: string) =>
|
||||
defs
|
||||
.filter((d) => d.kind.startsWith(prefix))
|
||||
.map((d) => ({ kind: d.kind, blurb: d.description }));
|
||||
const oss = (kinds: BoardKind[]) =>
|
||||
kinds.map((k) => ({ kind: k as string, blurb: BOARD_BLURBS[k] ?? '' }));
|
||||
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']) },
|
||||
{ title: 'ESP32', entries: [...oss(ESP32_BOARDS), ...overlay('esp32')] },
|
||||
{ title: 'Seeed Studio XIAO', entries: [...oss(XIAO_BOARDS), ...overlay('xiao')] },
|
||||
{ title: 'STM32', entries: oss(STM32_BOARDS) },
|
||||
{ title: 'Raspberry Pi', entries: oss(PI_BOARDS) },
|
||||
{ title: 'Arduino', entries: [oss('arduino-uno')] },
|
||||
// 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('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]);
|
||||
|
|
@ -272,11 +300,14 @@ export const NewProjectDialog: React.FC<NewProjectDialogProps> = ({ isOpen, onCl
|
|||
className="new-project-card new-project-card-blank"
|
||||
onClick={() => handleSelect('blank')}
|
||||
>
|
||||
<span className="new-project-card-name">
|
||||
{t('editor.newProject.blankTitle')}
|
||||
</span>
|
||||
<span className="new-project-card-desc">
|
||||
{t('editor.newProject.blankDesc')}
|
||||
<span className="new-project-card-thumb new-project-card-thumb-blank">+</span>
|
||||
<span className="new-project-card-info">
|
||||
<span className="new-project-card-name">
|
||||
{t('editor.newProject.blankTitle')}
|
||||
</span>
|
||||
<span className="new-project-card-desc">
|
||||
{t('editor.newProject.blankDesc')}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -286,19 +317,29 @@ export const NewProjectDialog: React.FC<NewProjectDialogProps> = ({ isOpen, onCl
|
|||
<React.Fragment key={section.title}>
|
||||
<div className="new-project-section-title">{section.title}</div>
|
||||
<div className="new-project-grid">
|
||||
{section.entries.map(({ kind, blurb }) => (
|
||||
<button
|
||||
key={kind}
|
||||
className="new-project-card"
|
||||
onClick={() => handleSelect(kind)}
|
||||
>
|
||||
{isProBoardKind(kind) && <ProPill />}
|
||||
<span className="new-project-card-name">
|
||||
{BOARD_KIND_LABELS[kind as BoardKind] ?? kind}
|
||||
</span>
|
||||
<span className="new-project-card-desc">{blurb}</span>
|
||||
</button>
|
||||
))}
|
||||
{section.entries.map(({ kind, blurb }) => {
|
||||
const label = BOARD_KIND_LABELS[kind as BoardKind] ?? kind;
|
||||
return (
|
||||
<button
|
||||
key={kind}
|
||||
className="new-project-card"
|
||||
onClick={() => handleSelect(kind)}
|
||||
>
|
||||
<span className="new-project-card-thumb">
|
||||
<ExampleThumbnail
|
||||
example={thumbStubFor(kind, label)}
|
||||
width={300}
|
||||
height={180}
|
||||
/>
|
||||
</span>
|
||||
{isProBoardKind(kind) && <ProPill />}
|
||||
<span className="new-project-card-info">
|
||||
<span className="new-project-card-name">{label}</span>
|
||||
<span className="new-project-card-desc">{blurb}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ import { useEditorStore } from '../store/useEditorStore';
|
|||
import { useCompileLogsStore } from '../store/useCompileLogsStore';
|
||||
import { useOscilloscopeStore } from '../store/useOscilloscopeStore';
|
||||
import { useProjectStore } from '../store/useProjectStore';
|
||||
import { NewProjectDialog } from '../components/editor/NewProjectDialog';
|
||||
import {
|
||||
NewProjectDialog,
|
||||
clearWorkspaceForStarter,
|
||||
} from '../components/editor/NewProjectDialog';
|
||||
import { useAutoSaveProject } from '../hooks/useAutoSaveProject';
|
||||
import { registerEditorCommand } from '../lib/editorCommands';
|
||||
import { EditorMenuBar } from '../components/editor/EditorMenuBar';
|
||||
|
|
@ -110,12 +113,13 @@ export const EditorPage: React.FC = () => {
|
|||
|
||||
// Pristine-visit starter dialog: a bare /editor landing still holds the
|
||||
// hardcoded Uno + LED starter (see useSimulatorStore INITIAL_BOARD /
|
||||
// builtin components). Offer the template picker instead of silently
|
||||
// dropping the user into the Arduino blink. Guarded so it never fires over
|
||||
// a loaded project/example URL, a restored login draft (both leave the
|
||||
// stores non-pristine), or twice per page load. Declared AFTER the
|
||||
// restoreStashedWorkspace effect — mount order is what makes the
|
||||
// pristine check see the restored draft.
|
||||
// builtin components). Clear it and offer the template picker over an
|
||||
// EMPTY canvas instead of silently dropping the user into the Arduino
|
||||
// blink (cancelling the dialog leaves a blank workspace). Guarded so it
|
||||
// never fires over a loaded project/example URL, a restored login draft
|
||||
// (both leave the stores non-pristine), or twice per page load. Declared
|
||||
// AFTER the restoreStashedWorkspace effect — mount order is what makes
|
||||
// the pristine check see the restored draft.
|
||||
useEffect(() => {
|
||||
if (starterDialogShownThisLoad) return;
|
||||
const locale = getLocaleFromPath(window.location.pathname);
|
||||
|
|
@ -130,6 +134,7 @@ export const EditorPage: React.FC = () => {
|
|||
sim.components.every((c) => c.id === 'led_builtin' || c.id === 'r_builtin');
|
||||
if (!pristine) return;
|
||||
starterDialogShownThisLoad = true;
|
||||
clearWorkspaceForStarter();
|
||||
setShowNewProjectDialog(true);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||