Files
web-cad/frontend/src/components/LayerPanel.tsx
T

261 lines
10 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import type { LayerPanelProps } from '../types/ui.types';
import type { CADLayer } from '../types/cad.types';
import type { TreeNode } from '../types/ui.types';
import TreeView from './TreeView';
const lineTypeOptions: Array<{ value: 'solid' | 'dashed' | 'dotted'; label: string }> = [
{ value: 'solid', label: 'Solid' },
{ value: 'dashed', label: 'Dashed' },
{ value: 'dotted', label: 'Dotted' },
];
const LayerPanel: React.FC<LayerPanelProps> = ({
layers,
activeLayerId,
onSelectLayer,
onAddLayer,
onToggleLayer,
onDeleteLayer,
onRenameLayer,
onDuplicateLayer,
onToggleLock,
onUpdateLayerColor,
onUpdateLayerLineType,
onUpdateLayerTransparency,
}) => {
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState('');
// Build tree nodes from layers (parentId hierarchy)
const buildTree = (parentId: string | null): TreeNode[] => {
return layers
.filter((l) => l.parentId === parentId)
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((l) => ({
id: l.id,
name: l.name,
expanded: true,
active: l.id === activeLayerId,
children: buildTree(l.id),
}));
};
const nodes = buildTree(null);
const handleStartRename = (layer: CADLayer) => {
setEditingId(layer.id);
setEditName(layer.name);
};
const handleConfirmRename = () => {
if (editingId && onRenameLayer) {
onRenameLayer(editingId, editName);
}
setEditingId(null);
setEditName('');
};
const renderLayerActions = (node: TreeNode) => {
const layer = layers.find((l) => l.id === node.id);
if (!layer) return null;
return (
<React.Fragment>
{/* Visibility toggle */}
<button
className="layer-action-btn"
title={layer.visible ? 'Verstecken' : 'Anzeigen'}
aria-label={layer.visible ? 'Verstecken' : 'Anzeigen'}
onClick={(e) => {
e.stopPropagation();
onToggleLayer(layer.id);
}}
>
{layer.visible ? (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
) : (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
)}
</button>
{/* Lock toggle */}
{onToggleLock && (
<button
className="layer-action-btn"
title={layer.locked ? 'Entsperren' : 'Sperren'}
aria-label={layer.locked ? 'Entsperren' : 'Sperren'}
onClick={(e) => {
e.stopPropagation();
onToggleLock(layer.id);
}}
>
{layer.locked ? (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
) : (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>
)}
</button>
)}
{/* Color swatch */}
{onUpdateLayerColor && (
<label
className="layer-color-swatch"
title="Ebenenfarbe"
style={{ backgroundColor: layer.color, display: 'inline-block', width: '14px', height: '14px', borderRadius: '3px', cursor: 'pointer', border: '1px solid var(--color-border)' }}
>
<input
type="color"
value={layer.color}
onChange={(e) => onUpdateLayerColor(layer.id, e.target.value)}
style={{ opacity: 0, width: 0, height: 0 }}
/>
</label>
)}
</React.Fragment>
);
};
const renderLayerDetail = (layer: CADLayer) => {
if (editingId === layer.id) {
return (
<div className="layer-edit-row" style={{ display: 'flex', gap: '4px', padding: '4px 8px' }}>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConfirmRename();
if (e.key === 'Escape') setEditingId(null);
}}
autoFocus
style={{ flex: 1, fontSize: '12px' }}
/>
<button onClick={handleConfirmRename} title="Bestätigen"></button>
<button onClick={() => setEditingId(null)} title="Abbrechen"></button>
</div>
);
}
return (
<div className="layer-detail-row" style={{ display: 'flex', gap: '6px', padding: '2px 8px', alignItems: 'center', fontSize: '11px', color: 'var(--color-text-muted)' }}>
{/* Line type selector */}
{onUpdateLayerLineType && (
<select
value={layer.lineType}
onChange={(e) => onUpdateLayerLineType(layer.id, e.target.value as 'solid' | 'dashed' | 'dotted')}
style={{ fontSize: '10px', padding: '1px 4px' }}
title="Linientyp"
>
{lineTypeOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
)}
{/* Transparency slider */}
{onUpdateLayerTransparency && (
<React.Fragment>
<input
type="range"
min="0"
max="100"
value={layer.transparency}
onChange={(e) => onUpdateLayerTransparency(layer.id, parseInt(e.target.value, 10))}
style={{ width: '60px' }}
title={`Transparenz: ${layer.transparency}%`}
/>
<span style={{ minWidth: '28px' }}>{layer.transparency}%</span>
</React.Fragment>
)}
{/* Rename button */}
{onRenameLayer && (
<button
className="layer-action-btn"
title="Umbenennen"
onClick={(e) => {
e.stopPropagation();
handleStartRename(layer);
}}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
)}
{/* Duplicate button */}
{onDuplicateLayer && (
<button
className="layer-action-btn"
title="Duplizieren"
onClick={(e) => {
e.stopPropagation();
onDuplicateLayer(layer.id);
}}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
)}
{/* Delete button */}
{onDeleteLayer && (
<button
className="layer-action-btn"
title="Löschen"
onClick={(e) => {
e.stopPropagation();
onDeleteLayer(layer.id);
}}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
)}
</div>
);
};
// Render expanded layer list with details
const renderLayerList = () => {
return layers
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((layer) => (
<div key={layer.id} className={`layer-item${layer.id === activeLayerId ? ' active' : ''}`}>
<div
className="layer-item-header"
style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 8px', cursor: 'pointer' }}
onClick={() => onSelectLayer(layer.id)}
>
<span
className="layer-color-dot"
style={{ width: '10px', height: '10px', borderRadius: '2px', backgroundColor: layer.color, flexShrink: 0 }}
/>
<span className="layer-name" style={{ flex: 1, fontSize: '12px' }}>{layer.name}</span>
{renderLayerActions({ id: layer.id, name: layer.name })}
</div>
{renderLayerDetail(layer)}
</div>
));
};
return (
<div className="panel-section">
<div className="panel-section-title">
<span>Ebenen-Struktur</span>
<button style={{ color: 'var(--color-text-muted)' }} title="Optionen" aria-label="Layer-Optionen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
</div>
<div className="tree tree-layer" role="tree" aria-label="Layer-Struktur">
<TreeView nodes={nodes} selectedId={activeLayerId} onSelect={onSelectLayer} onToggle={onToggleLayer} renderActions={renderLayerActions} renderDetail={(node) => { const layer = layers.find(l => l.id === node.id); return layer ? renderLayerDetail(layer) : null; }} />
</div>
<button className="add-layer-btn" onClick={onAddLayer}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Ebene hinzufügen
</button>
</div>
);
};
export default LayerPanel;