velxio/frontend/src/components/editor/FileTabs.tsx

96 lines
3.4 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
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>
2026-06-04 11:22:25 +07:00
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();
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>
2026-06-04 11:22:25 +07:00
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;
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>
2026-06-04 11:22:25 +07:00
// 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) {
setConfirmCloseId(fileId);
} else {
closeFile(fileId);
}
};
const confirmClose = () => {
if (confirmCloseId) closeFile(confirmCloseId);
setConfirmCloseId(null);
};
return (
<>
<div className="file-tabs">
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>
2026-06-04 11:22:25 +07:00
{ownerLabel && (
<span className="file-tabs-owner" title={`These files belong to ${ownerLabel}`}>
{ownerLabel}
</span>
)}
{openFiles.map((file) => (
<div
key={file.id}
className={`file-tab${file.id === activeFileId ? ' file-tab-active' : ''}`}
onClick={() => setActiveFile(file.id)}
title={file.name}
>
{file.modified && (
<span className="file-tab-modified" title={t('editor.fileTabs.unsavedChanges')} />
)}
<span className="file-tab-name">{file.name}</span>
<button
className="file-tab-close"
onClick={(e) => handleCloseClick(e, file.id, file.modified)}
title={t('editor.fileTabs.close')}
>
×
</button>
</div>
))}
</div>
{confirmCloseId && (
<div className="ftabs-overlay" onClick={() => setConfirmCloseId(null)}>
<div className="ftabs-confirm-box" onClick={(e) => e.stopPropagation()}>
<p>{t('editor.fileTabs.confirmClose')}</p>
<div className="ftabs-confirm-actions">
<button className="ftabs-btn-close" onClick={confirmClose}>
{t('editor.fileTabs.closeAnyway')}
</button>
<button onClick={() => setConfirmCloseId(null)}>{t('editor.fileTabs.cancel')}</button>
</div>
</div>
</div>
)}
</>
);
};