feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
aa4c8123f2
commit
a68e7f8e94
|
|
@ -64,6 +64,11 @@ function parseAttributes(chipJson: string): AttributeDef[] {
|
|||
|
||||
type Tab = 'examples' | 'editor';
|
||||
|
||||
// Placeholder chip names that are treated as "not user-named" — chip.json's
|
||||
// `name` may seed over any of these. 'My Chip' is the dialog default; 'Custom
|
||||
// Chip' is the file-explorer empty-rename fallback. Keep both in sync here.
|
||||
const BLANK_CHIP_NAMES = new Set(['', 'My Chip', 'Custom Chip']);
|
||||
|
||||
export const CustomChipDialog = ({ initial, onClose, onSave }: CustomChipDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
// If the chip already has source code, skip the examples tab on open.
|
||||
|
|
@ -105,20 +110,32 @@ export const CustomChipDialog = ({ initial, onClose, onSave }: CustomChipDialogP
|
|||
return Array.from(m.entries());
|
||||
}, []);
|
||||
|
||||
// Pull chipName out of chip.json whenever it changes — keeps a single source
|
||||
// of truth without forcing the user to edit two fields.
|
||||
// Seed chipName from chip.json's `name` ONLY while the chip still has a
|
||||
// blank default name — so editing chip.json names a fresh chip, but a name
|
||||
// the user gave it (renamed in the file explorer) is NOT clobbered. The
|
||||
// display name lives in properties.chipName and wins; chip.json `name` is
|
||||
// just the initial seed. (Switching examples relabels explicitly in
|
||||
// loadExample.) Both blank defaults are recognised so neither sticks.
|
||||
useEffect(() => {
|
||||
try {
|
||||
const obj = JSON.parse(chipJson);
|
||||
if (obj && typeof obj.name === 'string' && obj.name.trim()) {
|
||||
setChipName(obj.name);
|
||||
}
|
||||
} catch { /* user is mid-edit, ignore */ }
|
||||
setChipName((prev) => {
|
||||
if (prev && prev.trim() && !BLANK_CHIP_NAMES.has(prev.trim())) return prev;
|
||||
try {
|
||||
const obj = JSON.parse(chipJson);
|
||||
if (obj && typeof obj.name === 'string' && obj.name.trim()) return obj.name;
|
||||
} catch { /* user is mid-edit, ignore */ }
|
||||
return prev;
|
||||
});
|
||||
}, [chipJson]);
|
||||
|
||||
const loadExample = (e: ChipExample) => {
|
||||
setSourceC(e.sourceC);
|
||||
setChipJson(e.chipJson);
|
||||
// Loading an example is an explicit "use this chip" — relabel from its
|
||||
// chip.json name even over a previously-loaded example's name.
|
||||
try {
|
||||
const obj = JSON.parse(e.chipJson);
|
||||
if (obj && typeof obj.name === 'string' && obj.name.trim()) setChipName(obj.name);
|
||||
} catch { /* keep current name */ }
|
||||
setWasmBase64(''); // force re-compile so the binary matches the loaded source
|
||||
setResult(null);
|
||||
setTab('editor');
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { BOARD_PIN_GROUPS } from '../../simulation/spice/boardPinGroups';
|
|||
import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal';
|
||||
import type { PinSourceState } from '../../simulation/spice/types';
|
||||
import type { BoardKind, LanguageMode } from '../../types/board';
|
||||
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind } from '../../types/board';
|
||||
import { BOARD_KIND_FQBN, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, boardDisplayName } from '../../types/board';
|
||||
import { compileCode } from '../../services/compilation';
|
||||
import {
|
||||
compileRom,
|
||||
|
|
@ -421,7 +421,7 @@ export const EditorToolbar = ({
|
|||
}
|
||||
|
||||
const fqbn = kind ? BOARD_KIND_FQBN[kind] : null;
|
||||
const boardLabel = kind ? BOARD_KIND_LABELS[kind] : 'Unknown';
|
||||
const boardLabel = activeBoard ? boardDisplayName(activeBoard) : 'Unknown';
|
||||
|
||||
if (!fqbn) {
|
||||
addLog({ timestamp: new Date(), type: 'error', message: `No FQBN for board kind: ${kind}` });
|
||||
|
|
@ -899,7 +899,7 @@ export const EditorToolbar = ({
|
|||
let failed = 0;
|
||||
|
||||
for (const board of boardsList) {
|
||||
const label = BOARD_KIND_LABELS[board.boardKind] ?? board.boardKind;
|
||||
const label = boardDisplayName(board);
|
||||
|
||||
if (isPiBoardKind(board.boardKind)) {
|
||||
addLog({
|
||||
|
|
@ -1168,7 +1168,7 @@ export const EditorToolbar = ({
|
|||
// Architecture mismatch warning for ELF files
|
||||
if (result.elfInfo?.suggestedBoard && result.elfInfo.suggestedBoard !== boardKind) {
|
||||
const detected = result.elfInfo.architectureName;
|
||||
const current = activeBoard ? BOARD_KIND_LABELS[activeBoard.boardKind] : boardKind;
|
||||
const current = activeBoard ? boardDisplayName(activeBoard) : boardKind;
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
|
|
|
|||
|
|
@ -235,6 +235,15 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Inline rename input for a board/chip section header — flex into the label
|
||||
slot and match the header's compact type. */
|
||||
.fe-section-rename-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fe-status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
DEFAULT_CHIP_PROGRAM_C,
|
||||
} from '../../services/romCompileService';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import { BOARD_KIND_LABELS } from '../../types/board';
|
||||
import { boardDisplayName } from '../../types/board';
|
||||
import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
|
||||
import './FileExplorer.css';
|
||||
|
||||
|
|
@ -135,6 +135,23 @@ const IcoChevron = ({ open }: { open: boolean }) => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
// Pencil icon — the rename affordance on a board/chip section header.
|
||||
const IcoPencil = () => (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Integrated-circuit (chip) icon — a DIP package with pins. Marks a
|
||||
// programmable custom-chip's program section, distinct from board sections.
|
||||
const IcoChip = () => (
|
||||
|
|
@ -285,6 +302,8 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
const boards = useSimulatorStore((s) => s.boards);
|
||||
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
|
||||
const setActiveBoardId = useSimulatorStore((s) => s.setActiveBoardId);
|
||||
const updateBoard = useSimulatorStore((s) => s.updateBoard);
|
||||
const updateComponent = useSimulatorStore((s) => s.updateComponent);
|
||||
const components = useSimulatorStore((s) => s.components);
|
||||
|
||||
// Programmable custom-chips (CPU emulators whose chip.json declares
|
||||
|
|
@ -333,6 +352,13 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
const [contextMenu, setContextMenu] = useState<ContextMenu | null>(null);
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
// Inline rename of a SECTION header (a board or a chip). Kept separate from
|
||||
// file rename (renamingId) so the two never collide.
|
||||
const [renamingSection, setRenamingSection] = useState<{
|
||||
id: string;
|
||||
kind: 'board' | 'chip';
|
||||
} | null>(null);
|
||||
const [sectionRenameValue, setSectionRenameValue] = useState('');
|
||||
// Track which board group is creating a file: boardGroupId or null
|
||||
const [creatingInGroup, setCreatingInGroup] = useState<string | null>(null);
|
||||
const [newFileName, setNewFileName] = useState('');
|
||||
|
|
@ -340,6 +366,10 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
||||
|
||||
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||
const sectionRenameInputRef = useRef<HTMLInputElement>(null);
|
||||
// Set true by Escape so the input's onBlur (which fires when Escape unmounts
|
||||
// the input) discards instead of committing the typed value.
|
||||
const sectionRenameCancelledRef = useRef(false);
|
||||
const newFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -349,6 +379,54 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
}
|
||||
}, [renamingId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renamingSection && sectionRenameInputRef.current) {
|
||||
sectionRenameInputRef.current.focus();
|
||||
sectionRenameInputRef.current.select();
|
||||
}
|
||||
}, [renamingSection]);
|
||||
|
||||
const startBoardRename = useCallback((board: { id: string; name?: string; boardKind: BoardKind }) => {
|
||||
sectionRenameCancelledRef.current = false;
|
||||
setRenamingSection({ id: board.id, kind: 'board' });
|
||||
setSectionRenameValue(boardDisplayName(board));
|
||||
}, []);
|
||||
|
||||
const startChipRename = useCallback((chipId: string, currentName: string) => {
|
||||
sectionRenameCancelledRef.current = false;
|
||||
setRenamingSection({ id: chipId, kind: 'chip' });
|
||||
setSectionRenameValue(currentName);
|
||||
}, []);
|
||||
|
||||
const cancelSectionRename = useCallback(() => {
|
||||
sectionRenameCancelledRef.current = true;
|
||||
setRenamingSection(null);
|
||||
}, []);
|
||||
|
||||
const commitSectionRename = useCallback(() => {
|
||||
// Escape cancelled this edit (it unmounts the input, firing onBlur) — discard.
|
||||
if (sectionRenameCancelledRef.current) {
|
||||
sectionRenameCancelledRef.current = false;
|
||||
return;
|
||||
}
|
||||
const target = renamingSection;
|
||||
if (target) {
|
||||
const value = sectionRenameValue.trim();
|
||||
if (target.kind === 'board') {
|
||||
// Empty clears the custom name -> boardDisplayName falls back to kind.
|
||||
updateBoard(target.id, { name: value });
|
||||
} else {
|
||||
const comp = useSimulatorStore.getState().components.find((c) => c.id === target.id);
|
||||
if (comp) {
|
||||
updateComponent(target.id, {
|
||||
properties: { ...comp.properties, chipName: value || 'Custom Chip' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
setRenamingSection(null);
|
||||
}, [renamingSection, sectionRenameValue, updateBoard, updateComponent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (creatingInGroup && newFileInputRef.current) {
|
||||
newFileInputRef.current.focus();
|
||||
|
|
@ -509,7 +587,7 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
switchToBoard(board.id, groupId);
|
||||
if (!isOpen) toggleCollapse(board.id);
|
||||
}}
|
||||
title={`${BOARD_KIND_LABELS[board.boardKind]} — ${t('editor.fileExplorer.clickToEdit')}`}
|
||||
title={`${boardDisplayName(board)} — ${t('editor.fileExplorer.clickToEdit')}`}
|
||||
>
|
||||
<button
|
||||
className="fe-collapse-btn"
|
||||
|
|
@ -526,7 +604,31 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
{BOARD_ICON[board.boardKind]}
|
||||
</span>
|
||||
|
||||
<span className="fe-board-label">{BOARD_KIND_LABELS[board.boardKind]}</span>
|
||||
{renamingSection?.id === board.id && renamingSection.kind === 'board' ? (
|
||||
<input
|
||||
ref={sectionRenameInputRef}
|
||||
className="file-explorer-rename-input fe-section-rename-input"
|
||||
value={sectionRenameValue}
|
||||
onChange={(e) => setSectionRenameValue(e.target.value)}
|
||||
onBlur={commitSectionRename}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') commitSectionRename();
|
||||
if (e.key === 'Escape') cancelSectionRename();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="fe-board-label"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startBoardRename(board);
|
||||
}}
|
||||
title="Double-click to rename"
|
||||
>
|
||||
{boardDisplayName(board)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span
|
||||
className="fe-status-dot"
|
||||
|
|
@ -540,7 +642,17 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
}
|
||||
/>
|
||||
|
||||
{/* New file button — visible on hover */}
|
||||
{/* Rename + new-file buttons — visible on hover */}
|
||||
<button
|
||||
className="fe-board-new-btn"
|
||||
title="Rename board (or double-click the name)"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startBoardRename(board);
|
||||
}}
|
||||
>
|
||||
<IcoPencil />
|
||||
</button>
|
||||
<button
|
||||
className="fe-board-new-btn"
|
||||
title={t('editor.fileExplorer.newFileInBoard')}
|
||||
|
|
@ -666,7 +778,44 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
<IcoChip />
|
||||
</span>
|
||||
|
||||
<span className="fe-board-label">{chipName}</span>
|
||||
{renamingSection?.id === chip.id && renamingSection.kind === 'chip' ? (
|
||||
<input
|
||||
ref={sectionRenameInputRef}
|
||||
className="file-explorer-rename-input fe-section-rename-input"
|
||||
value={sectionRenameValue}
|
||||
onChange={(e) => setSectionRenameValue(e.target.value)}
|
||||
onBlur={commitSectionRename}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') commitSectionRename();
|
||||
if (e.key === 'Escape') cancelSectionRename();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="fe-board-label"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startChipRename(chip.id, chipName);
|
||||
}}
|
||||
title="Double-click to rename"
|
||||
>
|
||||
{chipName}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!(renamingSection?.id === chip.id && renamingSection.kind === 'chip') && (
|
||||
<button
|
||||
className="fe-board-new-btn"
|
||||
title="Rename chip (or double-click the name)"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startChipRename(chip.id, chipName);
|
||||
}}
|
||||
>
|
||||
<IcoPencil />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,23 @@
|
|||
scrollbar-color: #3c3c3c transparent;
|
||||
}
|
||||
|
||||
/* Owner badge — names the board/chip whose files are shown, so the user knows
|
||||
whose code these tabs belong to. */
|
||||
.file-tabs-owner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
text-transform: uppercase;
|
||||
color: #9aa0a6;
|
||||
background: #181818;
|
||||
border-right: 1px solid #333;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,38 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEditorStore } from '../../store/useEditorStore';
|
||||
import { useEditorStore, CHIP_GROUP_PREFIX } from '../../store/useEditorStore';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { boardDisplayName } from '../../types/board';
|
||||
import './FileTabs.css';
|
||||
|
||||
export const FileTabs: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { files, openFileIds, activeFileId, setActiveFile, closeFile } = useEditorStore();
|
||||
const { files, openFileIds, activeFileId, activeGroupId, setActiveFile, closeFile } =
|
||||
useEditorStore();
|
||||
const [confirmCloseId, setConfirmCloseId] = useState<string | null>(null);
|
||||
|
||||
const openFiles = openFileIds
|
||||
.map((id) => files.find((f) => f.id === id))
|
||||
.filter(Boolean) as typeof files;
|
||||
|
||||
// Which target owns the files currently shown — a board or a custom chip —
|
||||
// so the user can tell whose code they're editing at a glance. Resolved as a
|
||||
// SELECTOR returning just the label string, so FileTabs only re-renders when
|
||||
// that label changes — not on every pin toggle that mutates the components
|
||||
// array during simulation.
|
||||
const ownerLabel = useSimulatorStore((s): string | null => {
|
||||
if (activeGroupId?.startsWith(CHIP_GROUP_PREFIX)) {
|
||||
const chipId = activeGroupId.slice(CHIP_GROUP_PREFIX.length);
|
||||
const chip = s.components.find((c) => c.id === chipId);
|
||||
return (
|
||||
String((chip?.properties as Record<string, unknown>)?.chipName ?? '').trim() ||
|
||||
'Custom Chip'
|
||||
);
|
||||
}
|
||||
const board = s.boards.find((b) => b.activeFileGroupId === activeGroupId);
|
||||
return board ? boardDisplayName(board) : null;
|
||||
});
|
||||
|
||||
const handleCloseClick = (e: React.MouseEvent, fileId: string, modified: boolean) => {
|
||||
e.stopPropagation();
|
||||
if (modified) {
|
||||
|
|
@ -29,6 +50,11 @@ export const FileTabs: React.FC = () => {
|
|||
return (
|
||||
<>
|
||||
<div className="file-tabs">
|
||||
{ownerLabel && (
|
||||
<span className="file-tabs-owner" title={`These files belong to ${ownerLabel}`}>
|
||||
{ownerLabel}
|
||||
</span>
|
||||
)}
|
||||
{openFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ interface BoardOptionsModalProps {
|
|||
isOpen: boolean;
|
||||
boardId: string;
|
||||
boardKind: BoardKind;
|
||||
/** User-given board name (optional); shown in the subtitle when set. */
|
||||
boardName?: string;
|
||||
currentOptions: ESP32BoardOptions | undefined;
|
||||
spiffsFiles: SpiffsFile[];
|
||||
onClose: () => void;
|
||||
|
|
@ -70,6 +72,7 @@ async function readFileAsBase64(file: File): Promise<string> {
|
|||
export const BoardOptionsModal = ({
|
||||
isOpen,
|
||||
boardId,
|
||||
boardName,
|
||||
boardKind,
|
||||
currentOptions,
|
||||
spiffsFiles,
|
||||
|
|
@ -85,6 +88,12 @@ export const BoardOptionsModal = ({
|
|||
const [draft, setDraft] = useState<ESP32BoardOptions>(seed);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const update = useCallback(
|
||||
(patch: Partial<ESP32BoardOptions>) => setDraft((d) => ({ ...d, ...patch })),
|
||||
[],
|
||||
);
|
||||
|
||||
// Hooks must run on every render — bail out only AFTER all of them.
|
||||
if (!isOpen) return null;
|
||||
|
||||
const showPsram = boardSupportsPsram(boardKind);
|
||||
|
|
@ -97,11 +106,6 @@ export const BoardOptionsModal = ({
|
|||
const nonDio = draft.flashMode !== 'dio';
|
||||
const sameCore = draft.eventsRunOnCore === draft.arduinoRunsOnCore;
|
||||
|
||||
const update = useCallback(
|
||||
(patch: Partial<ESP32BoardOptions>) => setDraft((d) => ({ ...d, ...patch })),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleApply = () => {
|
||||
// Strip PSRAM for boards that don't support it (e.g. C3) so a stale
|
||||
// value can't smuggle through to the backend.
|
||||
|
|
@ -146,7 +150,7 @@ export const BoardOptionsModal = ({
|
|||
<div className="bom-title">
|
||||
<div className="bom-title-main">Board Options</div>
|
||||
<div className="bom-title-sub">
|
||||
{BOARD_KIND_LABELS[boardKind]} - id {boardId}
|
||||
{boardName?.trim() || BOARD_KIND_LABELS[boardKind]} - id {boardId}
|
||||
</div>
|
||||
</div>
|
||||
<button className="bom-close" onClick={onClose} aria-label="Close">
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
type TriggerEdge,
|
||||
} from '../../store/useOscilloscopeStore';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { BOARD_KIND_LABELS } from '../../types/board';
|
||||
import { boardDisplayName } from '../../types/board';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import './Oscilloscope.css';
|
||||
|
||||
|
|
@ -342,16 +342,16 @@ const ChannelPicker: React.FC<ChannelPickerProps> = ({
|
|||
key={b.id}
|
||||
className={`osc-picker-board-tab${b.id === selectedBoard?.id ? ' active' : ''}`}
|
||||
onClick={() => setSelectedBoardId(b.id)}
|
||||
title={BOARD_KIND_LABELS[b.boardKind]}
|
||||
title={boardDisplayName(b)}
|
||||
>
|
||||
{b.id}
|
||||
{boardDisplayName(b)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Board label */}
|
||||
{selectedBoard && (
|
||||
<div className="osc-picker-board-label">{BOARD_KIND_LABELS[selectedBoard.boardKind]}</div>
|
||||
<div className="osc-picker-board-label">{boardDisplayName(selectedBoard)}</div>
|
||||
)}
|
||||
|
||||
{/* Pin grid */}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { getTabSessionId } from '../../simulation/Esp32Bridge';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import { BOARD_KIND_LABELS } from '../../types/board';
|
||||
import { boardDisplayName } from '../../types/board';
|
||||
|
||||
// Short labels for tabs
|
||||
const BOARD_SHORT_LABEL: Partial<Record<string, string>> = {
|
||||
|
|
@ -207,12 +207,12 @@ export const SerialMonitor: React.FC = () => {
|
|||
...(isActive ? { ...styles.tabActive, borderBottomColor: color, color } : {}),
|
||||
}}
|
||||
onClick={() => handleTabClick(board.id)}
|
||||
title={BOARD_KIND_LABELS[board.boardKind]}
|
||||
title={boardDisplayName(board)}
|
||||
>
|
||||
<span style={{ fontSize: 9, marginRight: 3, color: isActive ? color : '#888' }}>
|
||||
{BOARD_ICON[board.boardKind] ?? '●'}
|
||||
</span>
|
||||
{BOARD_SHORT_LABEL[board.boardKind] ?? board.boardKind}
|
||||
{board.name?.trim() || BOARD_SHORT_LABEL[board.boardKind] || board.boardKind}
|
||||
{hasUnread && !isActive && <span style={styles.unreadDot} />}
|
||||
</button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import {
|
|||
import { useIsCoarsePointer } from '../../utils/useTouchDevice';
|
||||
import type { ComponentMetadata } from '../../types/component-metadata';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS } from '../../types/board';
|
||||
import { BOARD_KIND_FQBN, boardDisplayName } from '../../types/board';
|
||||
import { boardGateDecision, proBoardFeatureName, triggerProUpgradePrompt } from '../../lib/proBoardGate';
|
||||
import { FlashModal } from './FlashModal';
|
||||
import { isTauri as isTauriRuntimeFn } from '../../desktop/tauriBridge';
|
||||
|
|
@ -2066,7 +2066,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
>
|
||||
{boards.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{BOARD_KIND_LABELS[b.boardKind] ?? b.id}
|
||||
{boardDisplayName(b)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -2607,7 +2607,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
let title = pinPicker.kind === 'board' ? 'Board' : 'Component';
|
||||
if (pinPicker.kind === 'board') {
|
||||
const b = boards.find((x) => x.id === id);
|
||||
if (b) title = BOARD_KIND_LABELS[b.boardKind] ?? b.boardKind;
|
||||
if (b) title = boardDisplayName(b);
|
||||
} else {
|
||||
const c = components.find((x) => x.id === id);
|
||||
const meta = c ? registry.getById(c.metadataId) : null;
|
||||
|
|
@ -2809,7 +2809,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
{boardContextMenu &&
|
||||
(() => {
|
||||
const board = boards.find((b) => b.id === boardContextMenu.boardId);
|
||||
const label = board ? BOARD_KIND_LABELS[board.boardKind] : 'Board';
|
||||
const label = board ? boardDisplayName(board) : 'Board';
|
||||
const connectedWires = wires.filter(
|
||||
(w) =>
|
||||
w.start.componentId === boardContextMenu.boardId ||
|
||||
|
|
@ -3045,6 +3045,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
<BoardOptionsModal
|
||||
isOpen={true}
|
||||
boardId={b.id}
|
||||
boardName={b.name}
|
||||
boardKind={b.boardKind}
|
||||
currentOptions={b.boardOptions}
|
||||
spiffsFiles={b.spiffsFiles ?? []}
|
||||
|
|
@ -3059,7 +3060,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
{boardToRemove &&
|
||||
(() => {
|
||||
const board = boards.find((b) => b.id === boardToRemove);
|
||||
const label = board ? BOARD_KIND_LABELS[board.boardKind] : t('editor.canvas.removeConfirm.boardFallback');
|
||||
const label = board ? boardDisplayName(board) : t('editor.canvas.removeConfirm.boardFallback');
|
||||
const connectedWires = wires.filter(
|
||||
(w) => w.start.componentId === boardToRemove || w.end.componentId === boardToRemove,
|
||||
).length;
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ export function buildLoadPayload(project: RawProject) {
|
|||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
boards = parsed.map((b: Partial<BoardInstance> & { id: string; boardKind: string }) => ({
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
boardKind: b.boardKind as BoardKind,
|
||||
x: b.x ?? 50,
|
||||
y: b.y ?? 50,
|
||||
|
|
|
|||
|
|
@ -1277,11 +1277,12 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
payload.boards.forEach((b) => {
|
||||
addBoard(b.boardKind, b.x, b.y, b.id);
|
||||
// Apply the rest of the saved fields that addBoard doesn't set.
|
||||
if (b.languageMode && b.languageMode !== 'arduino') {
|
||||
const patch: Partial<BoardInstance> = {};
|
||||
if (b.languageMode && b.languageMode !== 'arduino') patch.languageMode = b.languageMode;
|
||||
if (b.name && b.name.trim()) patch.name = b.name;
|
||||
if (Object.keys(patch).length > 0) {
|
||||
set((s) => ({
|
||||
boards: s.boards.map((bb) =>
|
||||
bb.id === b.id ? { ...bb, languageMode: b.languageMode } : bb,
|
||||
),
|
||||
boards: s.boards.map((bb) => (bb.id === b.id ? { ...bb, ...patch } : bb)),
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ export interface BleStatus {
|
|||
|
||||
export interface BoardInstance {
|
||||
id: string; // unique in canvas, e.g. 'arduino-uno', 'raspberry-pi-3'
|
||||
/** Optional user-given display name. Falls back to the kind label when
|
||||
* empty. Lets the user tell two same-kind boards apart in the file
|
||||
* explorer, the compile console, and the canvas selector. */
|
||||
name?: string;
|
||||
boardKind: BoardKind;
|
||||
x: number;
|
||||
y: number;
|
||||
|
|
@ -129,6 +133,13 @@ export const BOARD_KIND_LABELS: Record<BoardKind, string> = {
|
|||
attiny85: 'ATtiny85',
|
||||
};
|
||||
|
||||
/** Display name for a board instance: the user's custom name if set, else the
|
||||
* kind label. Route every user-facing board label through this so renamed
|
||||
* boards show their name everywhere (file explorer, compile console, canvas). */
|
||||
export function boardDisplayName(board: Pick<BoardInstance, 'name' | 'boardKind'>): string {
|
||||
return board.name?.trim() || BOARD_KIND_LABELS[board.boardKind];
|
||||
}
|
||||
|
||||
export const BOARD_KIND_FQBN: Record<BoardKind, string | null> = {
|
||||
'arduino-uno': 'arduino:avr:uno',
|
||||
'arduino-nano': 'arduino:avr:nano:cpu=atmega328',
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ function chipGroupIdsWithFiles(): string[] {
|
|||
function serialisableBoard(b: BoardInstance) {
|
||||
return {
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
boardKind: b.boardKind,
|
||||
x: b.x,
|
||||
y: b.y,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export interface VlxPayload {
|
|||
name?: string;
|
||||
boards: Array<{
|
||||
id: string;
|
||||
name?: string;
|
||||
boardKind: string;
|
||||
x: number;
|
||||
y: number;
|
||||
|
|
@ -61,6 +62,7 @@ export interface VlxPayload {
|
|||
function serialisableBoard(b: BoardInstance) {
|
||||
return {
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
boardKind: b.boardKind,
|
||||
x: b.x,
|
||||
y: b.y,
|
||||
|
|
|
|||
Loading…
Reference in New Issue