feat(simulator): wire color palette from PR #170 + Copilot suggestions
- Add color picker button to SelectionActionBar for wire selections
- Toggle palette using WIRE_KEY_COLORS swatches
- Pass currentColor and onColorChange from SimulatorCanvas
- Reset showPalette on kind/onColorChange change (Copilot suggestion)
- Use t('editor.selectionBar.changeColor') for title/aria-label (Copilot suggestion)
- Add changeColor i18n key to all 9 locale files
Co-authored-by: naweiss <naweiss@users.noreply.github.com>
This commit is contained in:
parent
861e22572e
commit
8de51da5a5
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { WIRE_KEY_COLORS } from '../../utils/wireUtils';
|
||||
|
||||
export type SelectionKind = 'wire' | 'component' | 'board';
|
||||
|
||||
|
|
@ -30,6 +31,8 @@ interface SelectionActionBarProps {
|
|||
onDelete: () => void;
|
||||
onRotate?: () => void;
|
||||
onDeselect: () => void;
|
||||
onColorChange?: (color: string) => void;
|
||||
currentColor?: string;
|
||||
}
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
|
|
@ -126,12 +129,22 @@ export const SelectionActionBar: React.FC<SelectionActionBarProps> = ({
|
|||
onDelete,
|
||||
onRotate,
|
||||
onDeselect,
|
||||
onColorChange,
|
||||
currentColor,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showPalette, setShowPalette] = React.useState(false);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const deleteRef = useRef<HTMLButtonElement | null>(null);
|
||||
const rotateRef = useRef<HTMLButtonElement | null>(null);
|
||||
const deselectRef = useRef<HTMLButtonElement | null>(null);
|
||||
const colorToggleRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
// Reset the palette when the selection kind or colour-change handler changes
|
||||
// (i.e. the user switched from one wire to another, or deselected).
|
||||
useEffect(() => {
|
||||
setShowPalette(false);
|
||||
}, [kind, onColorChange]);
|
||||
|
||||
// Stop native touch events from reaching the canvas listener so it can't
|
||||
// call preventDefault on touchend (which would kill the synthetic click).
|
||||
|
|
@ -154,6 +167,7 @@ export const SelectionActionBar: React.FC<SelectionActionBarProps> = ({
|
|||
useTouchSafeAction(deleteRef, onDelete);
|
||||
useTouchSafeAction(rotateRef, onRotate ?? noop);
|
||||
useTouchSafeAction(deselectRef, onDeselect);
|
||||
useTouchSafeAction(colorToggleRef, () => setShowPalette(!showPalette));
|
||||
|
||||
if (!kind) return null;
|
||||
|
||||
|
|
@ -163,6 +177,8 @@ export const SelectionActionBar: React.FC<SelectionActionBarProps> = ({
|
|||
role="toolbar"
|
||||
aria-label={t('editor.selectionBar.label')}
|
||||
className="selection-action-bar"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
|
|
@ -196,6 +212,64 @@ export const SelectionActionBar: React.FC<SelectionActionBarProps> = ({
|
|||
{label}
|
||||
</span>
|
||||
|
||||
{kind === 'wire' && onColorChange && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
|
||||
<button
|
||||
type="button"
|
||||
ref={colorToggleRef}
|
||||
onClick={() => setShowPalette(!showPalette)}
|
||||
style={{ ...buttonStyle, padding: '6px 8px' }}
|
||||
title={t('editor.selectionBar.changeColor')}
|
||||
aria-label={t('editor.selectionBar.changeColor')}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: currentColor || '#22c55e',
|
||||
border: '2px solid rgba(255,255,255,0.2)',
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{showPalette && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
marginTop: 8,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
padding: '10px',
|
||||
background: '#252526',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
||||
justifyContent: 'center',
|
||||
width: 240,
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
{Object.values(WIRE_KEY_COLORS).map((color) => (
|
||||
<ColorButton
|
||||
key={color}
|
||||
color={color}
|
||||
isSelected={color.toLowerCase() === currentColor?.toLowerCase()}
|
||||
onClick={() => {
|
||||
onColorChange(color);
|
||||
setShowPalette(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canRotate && onRotate && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -240,6 +314,35 @@ export const SelectionActionBar: React.FC<SelectionActionBarProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const ColorButton: React.FC<{
|
||||
color: string;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}> = ({ color, isSelected, onClick }) => {
|
||||
const ref = useRef<HTMLButtonElement | null>(null);
|
||||
useTouchSafeAction(ref, onClick);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: color,
|
||||
border: isSelected ? '2px solid #fff' : '1px solid rgba(255,255,255,0.2)',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
flexShrink: 0,
|
||||
boxShadow: isSelected ? '0 0 6px rgba(255,255,255,0.5)' : 'none',
|
||||
}}
|
||||
title={color}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const buttonStyle: React.CSSProperties = {
|
||||
|
|
|
|||
|
|
@ -2344,10 +2344,16 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
{!wireInProgress &&
|
||||
(() => {
|
||||
if (selectedWireId) {
|
||||
const wire = wires.find((w) => w.id === selectedWireId);
|
||||
return (
|
||||
<SelectionActionBar
|
||||
kind="wire"
|
||||
label="Wire"
|
||||
currentColor={wire?.color}
|
||||
onColorChange={(color) => {
|
||||
if (!wire) return;
|
||||
recordUpdateWire(selectedWireId, { color: wire.color }, { color });
|
||||
}}
|
||||
onDelete={() => {
|
||||
// Recorded so the touch / mobile delete is also undoable.
|
||||
recordRemoveWire(selectedWireId);
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "Drehen",
|
||||
"delete": "Löschen",
|
||||
"deselect": "Auswahl aufheben",
|
||||
"changeColor": "Farbe ändern",
|
||||
"deleteKind": {
|
||||
"wire": "Leitung löschen",
|
||||
"component": "Komponente löschen",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "Rotate",
|
||||
"delete": "Delete",
|
||||
"deselect": "Deselect",
|
||||
"changeColor": "Change color",
|
||||
"deleteKind": {
|
||||
"wire": "Delete wire",
|
||||
"component": "Delete component",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "Rotar",
|
||||
"delete": "Eliminar",
|
||||
"deselect": "Deseleccionar",
|
||||
"changeColor": "Cambiar color",
|
||||
"deleteKind": {
|
||||
"wire": "Eliminar cable",
|
||||
"component": "Eliminar componente",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "Rotation",
|
||||
"delete": "Supprimer",
|
||||
"deselect": "Désélectionner",
|
||||
"changeColor": "Changer la couleur",
|
||||
"deleteKind": {
|
||||
"wire": "Supprimer le fil",
|
||||
"component": "Supprimer le composant",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "Ruota",
|
||||
"delete": "Elimina",
|
||||
"deselect": "Deseleziona",
|
||||
"changeColor": "Cambia colore",
|
||||
"deleteKind": {
|
||||
"wire": "Elimina cavo",
|
||||
"component": "Elimina componente",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "回転",
|
||||
"delete": "削除",
|
||||
"deselect": "選択解除",
|
||||
"changeColor": "色を変更",
|
||||
"deleteKind": {
|
||||
"wire": "配線を削除",
|
||||
"component": "コンポーネントを削除",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "Girar",
|
||||
"delete": "Excluir",
|
||||
"deselect": "Desselecionar",
|
||||
"changeColor": "Mudar cor",
|
||||
"deleteKind": {
|
||||
"wire": "Excluir fio",
|
||||
"component": "Excluir componente",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "Повернуть",
|
||||
"delete": "Удалить",
|
||||
"deselect": "Снять выделение",
|
||||
"changeColor": "Изменить цвет",
|
||||
"deleteKind": {
|
||||
"wire": "Удалить провод",
|
||||
"component": "Удалить компонент",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@
|
|||
"rotate": "旋转",
|
||||
"delete": "删除",
|
||||
"deselect": "取消选择",
|
||||
"changeColor": "更改颜色",
|
||||
"deleteKind": {
|
||||
"wire": "删除导线",
|
||||
"component": "删除组件",
|
||||
|
|
|
|||
Loading…
Reference in New Issue