diff --git a/frontend/public/examples-thumbs/starter-raspberry-pi-4.webp b/frontend/public/examples-thumbs/starter-raspberry-pi-4.webp new file mode 100644 index 00000000..0d9529d3 Binary files /dev/null and b/frontend/public/examples-thumbs/starter-raspberry-pi-4.webp differ diff --git a/frontend/public/examples-thumbs/starter-raspberry-pi-5.webp b/frontend/public/examples-thumbs/starter-raspberry-pi-5.webp new file mode 100644 index 00000000..96e4a9f2 Binary files /dev/null and b/frontend/public/examples-thumbs/starter-raspberry-pi-5.webp differ diff --git a/frontend/public/examples-thumbs/starter-stm32-netduino2.webp b/frontend/public/examples-thumbs/starter-stm32-netduino2.webp new file mode 100644 index 00000000..9a966dc1 Binary files /dev/null and b/frontend/public/examples-thumbs/starter-stm32-netduino2.webp differ diff --git a/frontend/public/examples-thumbs/starter-xiao-esp32-c3.webp b/frontend/public/examples-thumbs/starter-xiao-esp32-c3.webp new file mode 100644 index 00000000..9bab7ee7 Binary files /dev/null and b/frontend/public/examples-thumbs/starter-xiao-esp32-c3.webp differ diff --git a/frontend/public/examples-thumbs/starter-xiao-esp32-s3.webp b/frontend/public/examples-thumbs/starter-xiao-esp32-s3.webp new file mode 100644 index 00000000..b9a67b8f Binary files /dev/null and b/frontend/public/examples-thumbs/starter-xiao-esp32-s3.webp differ diff --git a/frontend/public/examples-thumbs/xiao-esp32c6-blink.webp b/frontend/public/examples-thumbs/xiao-esp32c6-blink.webp new file mode 100644 index 00000000..b095c15e Binary files /dev/null and b/frontend/public/examples-thumbs/xiao-esp32c6-blink.webp differ diff --git a/frontend/public/examples-thumbs/xiao-rp2040-blink.webp b/frontend/public/examples-thumbs/xiao-rp2040-blink.webp new file mode 100644 index 00000000..9c317fb3 Binary files /dev/null and b/frontend/public/examples-thumbs/xiao-rp2040-blink.webp differ diff --git a/frontend/src/components/editor/NewProjectDialog.css b/frontend/src/components/editor/NewProjectDialog.css index 10db7a41..64af5927 100644 --- a/frontend/src/components/editor/NewProjectDialog.css +++ b/frontend/src/components/editor/NewProjectDialog.css @@ -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; diff --git a/frontend/src/components/editor/NewProjectDialog.tsx b/frontend/src/components/editor/NewProjectDialog.tsx index 173d202c..55c0bc4e 100644 --- a/frontend/src/components/editor/NewProjectDialog.tsx +++ b/frontend/src/components/editor/NewProjectDialog.tsx @@ -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/.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 = { '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 = { '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/.webp, CircuitPreview SVG fallback). Kinds with a + * Blink example use that example's thumb; the rest use a captured + * `starter-` 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 { - // 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 = ({ 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 = ({ isOpen, onCl className="new-project-card new-project-card-blank" onClick={() => handleSelect('blank')} > - - {t('editor.newProject.blankTitle')} - - - {t('editor.newProject.blankDesc')} + + + + + {t('editor.newProject.blankTitle')} + + + {t('editor.newProject.blankDesc')} + @@ -286,19 +317,29 @@ export const NewProjectDialog: React.FC = ({ isOpen, onCl
{section.title}
- {section.entries.map(({ kind, blurb }) => ( - - ))} + {section.entries.map(({ kind, blurb }) => { + const label = BOARD_KIND_LABELS[kind as BoardKind] ?? kind; + return ( + + ); + })}
), diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index cf7dc15e..2d973efb 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -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); }, []);