refactor(inspector): delete confirmation via the global confirm modal

Replaces the inline swap-the-action-bar confirmation (buttons jumped
around) with showConfirmDialog, same pattern as FileExplorer. While the
modal is open the dialog's Escape/click-outside close handlers stand
down. Boards keep their own remove-board modal via skipDeleteConfirm.
This commit is contained in:
David Montero Crespo 2026-08-05 19:18:28 +02:00
parent 64deb204ae
commit bf56295f41
3 changed files with 65 additions and 85 deletions

View File

@ -353,20 +353,6 @@
background: #d70015; /* --red-600 */
}
.pid-confirm-label {
font-size: 12px;
color: #ffb4ab;
display: flex;
align-items: center;
gap: 6px;
}
.pid-confirm-hint {
color: #b58b86;
font-style: normal;
font-size: 11px;
}
/* Buy in the action bar: solid brand blue (Velxio buttons are solid, and
the buy link is the primary action of the datasheet). */
.pid-buy-button {

View File

@ -46,6 +46,7 @@ import {
type InspectorPinInput,
type PinLayoutResult,
} from '../../utils/pinInspectorLayout';
import { showConfirmDialog } from '../../store/useMessageDialogStore';
// The keybind chips and the SD Card panel keep their existing class names and
// styles — both live in ComponentPropertyDialog.css, imported here so the old
// dialog file can eventually retire without orphaning them.
@ -94,8 +95,9 @@ interface PartInspectorDialogProps {
extraActions?: InspectorAction[];
/** Overrides the Delete button's label (boards say "Remove board"). */
deleteLabel?: string;
/** Extra line under the delete confirmation (e.g. "3 wires will be removed"). */
deleteHint?: string;
/** The caller confirms deletion itself (boards open the remove-board modal
with its wire count), so Delete fires onDelete without asking here. */
skipDeleteConfirm?: boolean;
/** Tag rendered live in the preview when the element is not the metadata's. */
previewTagName?: string;
/** Attributes to set on the preview element (boards need board-kind). */
@ -122,7 +124,7 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
wireInProgress,
extraActions,
deleteLabel,
deleteHint,
skipDeleteConfirm,
previewTagName,
previewAttributes,
}) => {
@ -134,7 +136,6 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
// The pin the pointer is on — its leader line and dot light up together, so
// a name in the gutter can be traced to the pad it belongs to.
const [hoverPin, setHoverPin] = useState<string | null>(null);
const [confirmingDelete, setConfirmingDelete] = useState(false);
const [layout, setLayout] = useState<PinLayoutResult | null>(null);
const [previewFailed, setPreviewFailed] = useState(false);
@ -319,10 +320,37 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
};
}, [position]);
// ── Delete goes through the global confirm modal ─────────────────────────
// Same pattern as FileExplorer / BoardOptionsModal. The old inline
// confirmation swapped the action bar's contents in place, which made the
// buttons jump around. While the modal is open the dialog's Escape and
// click-outside close handlers must stand down — the modal is portaled to
// document.body, so interacting with it looks like "outside" to them.
const confirmOpenRef = useRef(false);
const handleDeleteClick = async () => {
if (skipDeleteConfirm) {
onDelete(componentId);
return;
}
confirmOpenRef.current = true;
const confirmed = await showConfirmDialog(
t('editor.componentProps.confirmDelete', { name: componentMetadata.name }),
{
kind: 'error',
title: t('editor.componentProps.confirmDeleteTitle'),
confirmLabel: deleteLabel ?? t('editor.componentProps.delete'),
cancelLabel: t('editor.componentProps.cancel'),
danger: true,
},
);
confirmOpenRef.current = false;
if (confirmed) onDelete(componentId);
};
// ── Close behaviours (unchanged semantics) ───────────────────────────────
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === 'Escape' && !confirmOpenRef.current) onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
@ -330,6 +358,7 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (confirmOpenRef.current) return;
if (dialogRef.current && !dialogRef.current.contains(e.target as Node)) onClose();
};
// Delay to avoid immediate close from the click that opened the dialog.
@ -637,63 +666,35 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
</div>
<div className="pid-actions">
{confirmingDelete ? (
<>
<span className="pid-confirm-label">
{t('editor.componentProps.confirmDelete', { name: componentMetadata.name })}
{deleteHint && <em className="pid-confirm-hint">{deleteHint}</em>}
</span>
<button
className="pid-action-button"
onClick={() => setConfirmingDelete(false)}
title={t('editor.componentProps.cancel')}
>
{t('editor.componentProps.cancel')}
</button>
<button
className="pid-action-button pid-action-delete"
onClick={() => {
setConfirmingDelete(false);
onDelete(componentId);
}}
title={t('editor.componentProps.confirmDeleteTitle')}
>
{deleteLabel ?? t('editor.componentProps.delete')}
</button>
</>
) : (
<>
{/* Boards pass no onRotate (they do not rotate); they pass
Board Options / Flash as extraActions instead. */}
{onRotate && (
<button
className="pid-action-button"
onClick={() => onRotate(componentId)}
title={t('editor.componentProps.rotate90')}
>
{t('editor.componentProps.rotate')}
</button>
)}
{extraActions?.map((a) => (
<button
key={a.id}
className="pid-action-button"
onClick={a.disabled ? undefined : a.onSelect}
disabled={a.disabled}
title={a.title}
>
{a.label}
</button>
))}
<button
className="pid-action-button pid-action-delete"
onClick={() => setConfirmingDelete(true)}
title={t('editor.componentProps.deleteTitle')}
>
{deleteLabel ?? t('editor.componentProps.delete')}
</button>
</>
{/* Boards pass no onRotate (they do not rotate); they pass
Board Options / Flash as extraActions instead. */}
{onRotate && (
<button
className="pid-action-button"
onClick={() => onRotate(componentId)}
title={t('editor.componentProps.rotate90')}
>
{t('editor.componentProps.rotate')}
</button>
)}
{extraActions?.map((a) => (
<button
key={a.id}
className="pid-action-button"
onClick={a.disabled ? undefined : a.onSelect}
disabled={a.disabled}
title={a.title}
>
{a.label}
</button>
))}
<button
className="pid-action-button pid-action-delete"
onClick={handleDeleteClick}
title={t('editor.componentProps.deleteTitle')}
>
{deleteLabel ?? t('editor.componentProps.delete')}
</button>
{/* Buy lives here, right-aligned, so it is reachable from BOTH
tabs — it used to sit at the end of the datasheet body, which

View File

@ -3378,11 +3378,6 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
const board = boards.find((b) => b.id === boardContextMenu.boardId);
if (!board) return null;
const name = boardDisplayName(board);
const connectedWires = wires.filter(
(w) =>
w.start.componentId === boardContextMenu.boardId ||
w.end.componentId === boardContextMenu.boardId,
).length;
const element = document.getElementById(boardContextMenu.boardId);
const boardPins = element ? (element as any).pinInfo : [];
const actions: InspectorAction[] = [];
@ -3441,11 +3436,9 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
previewAttributes={{ 'board-kind': board.boardKind }}
extraActions={actions}
deleteLabel={t('editor.canvas.removeBoard')}
deleteHint={
connectedWires > 0
? t('editor.canvas.wireCount', { count: connectedWires })
: undefined
}
// The remove-board modal (boardToRemove) is the confirmation for
// boards — it knows the wire count; a second ask here would stack.
skipDeleteConfirm
wireInProgress={Boolean(wireInProgress)}
onClose={() => setBoardContextMenu(null)}
onPropertyChange={(id, propName, value) => {