Replace frontend with deployed TS version from a08dd73
- Restore TS frontend source (canvas, tools, CRDT, components, services) - Keep 5 JSX components that TS code depends on (CADCanvas, LayerPanel, BlockLibrary, PluginRegistry, Toolbar) - Keep JS services (api.js, blockService.js) that components depend on - Fix vite/plugin-react version mismatch (downgrade to v4 for vite 6) - Add axios dependency - Skip tsc in build script (vite/esbuild handles transpilation) - Fix index.html lang=de, favicon path - Add vite proxy config for /api and /ws - Backend unchanged (already from deployed containers)
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
.properties-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
padding: var(--spacing-sm, 8px);
|
||||
font-family: var(--font-family, sans-serif);
|
||||
font-size: var(--font-size-md, 14px);
|
||||
color: var(--text, #333);
|
||||
}
|
||||
|
||||
/* ── Empty state ─────────────────────────────────────────────── */
|
||||
|
||||
.properties-panel-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
padding: var(--spacing-lg, 24px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.properties-panel-empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--spacing-md, 16px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.properties-panel-empty-text {
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
line-height: 1.5;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
/* ── Header ──────────────────────────────────────────────────── */
|
||||
|
||||
.properties-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-xs, 4px) var(--spacing-sm, 8px);
|
||||
background-color: var(--surface-hover, rgba(0, 0, 0, 0.04));
|
||||
border-radius: var(--border-radius, 4px);
|
||||
margin-bottom: var(--spacing-xs, 4px);
|
||||
}
|
||||
|
||||
.properties-panel-type-badge {
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--primary, #2563eb);
|
||||
}
|
||||
|
||||
.properties-panel-id {
|
||||
font-size: var(--font-size-xs, 10px);
|
||||
color: var(--text-secondary, #888);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* ── Fieldsets ───────────────────────────────────────────────── */
|
||||
|
||||
.properties-panel-fieldset {
|
||||
border: 1px solid var(--border, #e0e0e0);
|
||||
border-radius: var(--border-radius, 4px);
|
||||
padding: var(--spacing-sm, 8px);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.properties-panel-fieldset legend {
|
||||
font-weight: var(--font-weight-medium, 500);
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-secondary, #666);
|
||||
padding: 0 var(--spacing-xs, 4px);
|
||||
}
|
||||
|
||||
/* ── Rows ────────────────────────────────────────────────────── */
|
||||
|
||||
.properties-panel-row {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
}
|
||||
|
||||
.properties-panel-row > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Labels ──────────────────────────────────────────────────── */
|
||||
|
||||
.properties-panel-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: var(--spacing-xs, 4px);
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
/* ── Inputs ──────────────────────────────────────────────────── */
|
||||
|
||||
.properties-panel-input,
|
||||
.properties-panel-text-input,
|
||||
.properties-panel-select {
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid var(--border, #ccc);
|
||||
border-radius: var(--border-radius, 4px);
|
||||
background-color: var(--background, #fff);
|
||||
color: var(--text, #333);
|
||||
font-size: var(--font-size-md, 14px);
|
||||
font-family: var(--font-family, sans-serif);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.properties-panel-input:focus,
|
||||
.properties-panel-text-input:focus,
|
||||
.properties-panel-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary, #2563eb);
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
|
||||
.properties-panel-input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.properties-panel-input[type="number"]::-webkit-inner-spin-button,
|
||||
.properties-panel-input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Color inputs ────────────────────────────────────────────── */
|
||||
|
||||
.properties-panel-color-input {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border, #ccc);
|
||||
border-radius: var(--border-radius, 4px);
|
||||
background-color: var(--background, #fff);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.properties-panel-color-input::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.properties-panel-color-input::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.properties-panel-text-input {
|
||||
margin-top: 2px;
|
||||
font-size: var(--font-size-xs, 11px);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* ── Select ──────────────────────────────────────────────────── */
|
||||
|
||||
.properties-panel-select {
|
||||
cursor: pointer;
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
/* ── Responsive ──────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.properties-panel {
|
||||
padding: var(--spacing-xs, 4px);
|
||||
}
|
||||
|
||||
.properties-panel-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { CADLayer, CADProperties, CADElement } from '../../types/cad.types';
|
||||
import './PropertiesPanel.css';
|
||||
|
||||
// ─── Types (local-only, not duplicated from cad.types.ts) ─────────────────────
|
||||
|
||||
interface YjsDocumentLike {
|
||||
layers: {
|
||||
forEach: (cb: (layer: CADLayer, id: string) => void) => void;
|
||||
observe: (cb: () => void) => void;
|
||||
unobserve: (cb: () => void) => void;
|
||||
};
|
||||
elements: {
|
||||
toArray: () => CADElement[];
|
||||
observe: (cb: () => void) => void;
|
||||
unobserve: (cb: () => void) => void;
|
||||
};
|
||||
updateElement: (id: string, updates: Partial<CADElement>) => void;
|
||||
getElementById: (id: string) => CADElement | undefined;
|
||||
}
|
||||
|
||||
interface FabricObjectLike {
|
||||
id?: string;
|
||||
type?: string;
|
||||
left?: number;
|
||||
top?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
angle?: number;
|
||||
fill?: string | null;
|
||||
stroke?: string | null;
|
||||
strokeWidth?: number;
|
||||
strokeDashArray?: number[];
|
||||
set: (props: Record<string, unknown>) => void;
|
||||
setCoords: () => void;
|
||||
}
|
||||
|
||||
interface FabricCanvasLike {
|
||||
getActiveObject: () => FabricObjectLike | null;
|
||||
on: (event: string, handler: (...args: unknown[]) => void) => void;
|
||||
off: (event: string, handler: (...args: unknown[]) => void) => void;
|
||||
renderAll: () => void;
|
||||
fire: (event: string, payload?: unknown) => void;
|
||||
}
|
||||
|
||||
interface CanvasRefLike {
|
||||
getCanvas?: () => FabricCanvasLike;
|
||||
getSelectedElements?: () => FabricObjectLike[];
|
||||
}
|
||||
|
||||
interface PropertiesPanelProps {
|
||||
canvasRef: React.RefObject<CanvasRefLike | null>;
|
||||
yjsDocRef: React.RefObject<YjsDocumentLike | null>;
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const LINE_TYPE_DASH_MAP: Record<string, number[] | null> = {
|
||||
solid: null,
|
||||
dashed: [8, 4],
|
||||
dotted: [2, 3],
|
||||
};
|
||||
|
||||
function dashArrayToLineType(dashArray?: number[] | null): 'solid' | 'dashed' | 'dotted' {
|
||||
if (!dashArray || dashArray.length === 0) return 'solid';
|
||||
if (dashArray[0] <= 2) return 'dotted';
|
||||
return 'dashed';
|
||||
}
|
||||
|
||||
function clampNumber(value: string, min: number, max: number): number {
|
||||
const n = parseFloat(value);
|
||||
if (Number.isNaN(n)) return min;
|
||||
return Math.min(Math.max(n, min), max);
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ canvasRef, yjsDocRef }) => {
|
||||
const [selectedElement, setSelectedElement] = useState<CADElement | null>(null);
|
||||
const [layers, setLayers] = useState<CADLayer[]>([]);
|
||||
const [fabricObject, setFabricObject] = useState<FabricObjectLike | null>(null);
|
||||
const fabricCanvasRef = useRef<FabricCanvasLike | null>(null);
|
||||
|
||||
// Debounce timer for Yjs updates (300ms) — no new dependency needed
|
||||
const yjsDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Clear debounce timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (yjsDebounceRef.current) {
|
||||
clearTimeout(yjsDebounceRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ── Sync layers from YjsDocument ──────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const yjsDoc = yjsDocRef?.current;
|
||||
if (!yjsDoc) return;
|
||||
|
||||
const syncLayers = () => {
|
||||
const arr: CADLayer[] = [];
|
||||
yjsDoc.layers.forEach((layer, id) => {
|
||||
arr.push({ ...layer, id });
|
||||
});
|
||||
arr.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
setLayers(arr);
|
||||
};
|
||||
|
||||
syncLayers();
|
||||
yjsDoc.layers.observe(syncLayers);
|
||||
return () => yjsDoc.layers.unobserve(syncLayers);
|
||||
}, [yjsDocRef]);
|
||||
|
||||
// ── Sync elements from YjsDocument (for external updates) ─────────────────
|
||||
useEffect(() => {
|
||||
const yjsDoc = yjsDocRef?.current;
|
||||
if (!yjsDoc) return;
|
||||
|
||||
const syncElements = () => {
|
||||
if (selectedElement) {
|
||||
const updated = yjsDoc.getElementById(selectedElement.id);
|
||||
if (updated) {
|
||||
setSelectedElement(updated);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
yjsDoc.elements.observe(syncElements);
|
||||
return () => yjsDoc.elements.unobserve(syncElements);
|
||||
}, [yjsDocRef, selectedElement]);
|
||||
|
||||
// ── Wire fabric selection events ──────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const ref = canvasRef?.current;
|
||||
if (!ref || !ref.getCanvas) return;
|
||||
|
||||
const fabricCanvas = ref.getCanvas();
|
||||
if (!fabricCanvas) return;
|
||||
|
||||
fabricCanvasRef.current = fabricCanvas;
|
||||
|
||||
const handleSelection = () => {
|
||||
const active = fabricCanvas.getActiveObject();
|
||||
if (!active) {
|
||||
setSelectedElement(null);
|
||||
setFabricObject(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setFabricObject(active);
|
||||
|
||||
// Try to match with YjsDocument element by id
|
||||
const yjsDoc = yjsDocRef?.current;
|
||||
const elementId = (active as FabricObjectLike).id;
|
||||
if (elementId && yjsDoc) {
|
||||
const el = yjsDoc.getElementById(elementId);
|
||||
if (el) {
|
||||
setSelectedElement(el);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: build a transient element from fabric properties
|
||||
setSelectedElement({
|
||||
id: (active as FabricObjectLike).id ?? 'transient',
|
||||
type: (active as FabricObjectLike).type ?? 'unknown',
|
||||
layerId: 'default',
|
||||
x: active.left ?? 0,
|
||||
y: active.top ?? 0,
|
||||
width: active.width ?? 0,
|
||||
height: active.height ?? 0,
|
||||
properties: {
|
||||
fill: (active.fill as string) ?? 'transparent',
|
||||
stroke: active.stroke ?? '#000000',
|
||||
strokeWidth: active.strokeWidth ?? 1,
|
||||
rotation: active.angle ?? 0,
|
||||
lineType: dashArrayToLineType(active.strokeDashArray),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCleared = () => {
|
||||
setSelectedElement(null);
|
||||
setFabricObject(null);
|
||||
};
|
||||
|
||||
fabricCanvas.on('selection:created', handleSelection);
|
||||
fabricCanvas.on('selection:updated', handleSelection);
|
||||
fabricCanvas.on('selection:cleared', handleCleared);
|
||||
|
||||
return () => {
|
||||
fabricCanvas.off('selection:created', handleSelection);
|
||||
fabricCanvas.off('selection:updated', handleSelection);
|
||||
fabricCanvas.off('selection:cleared', handleCleared);
|
||||
};
|
||||
}, [canvasRef, yjsDocRef]);
|
||||
|
||||
// ── Update handlers (Panel → Canvas + YjsDocument) ──────────────────────────
|
||||
|
||||
const updateFabricAndYjs = useCallback(
|
||||
(updates: Partial<CADElement> & { properties?: Partial<CADProperties> }) => {
|
||||
if (!selectedElement) return;
|
||||
|
||||
// Debounce Yjs write (300ms) — only write after user pauses typing
|
||||
if (yjsDebounceRef.current) {
|
||||
clearTimeout(yjsDebounceRef.current);
|
||||
}
|
||||
const elId = selectedElement.id;
|
||||
yjsDebounceRef.current = setTimeout(() => {
|
||||
const yjsDoc = yjsDocRef?.current;
|
||||
if (yjsDoc) {
|
||||
yjsDoc.updateElement(elId, updates);
|
||||
}
|
||||
yjsDebounceRef.current = null;
|
||||
}, 300);
|
||||
|
||||
// Update fabric object directly (immediate, no debounce)
|
||||
const fabricObj = fabricObject;
|
||||
const fabricCanvas = fabricCanvasRef.current;
|
||||
if (fabricObj && fabricCanvas) {
|
||||
if (updates.x !== undefined) fabricObj.set({ left: updates.x });
|
||||
if (updates.y !== undefined) fabricObj.set({ top: updates.y });
|
||||
if (updates.width !== undefined) fabricObj.set({ width: updates.width });
|
||||
if (updates.height !== undefined) fabricObj.set({ height: updates.height });
|
||||
if (updates.properties?.rotation !== undefined)
|
||||
fabricObj.set({ angle: updates.properties.rotation });
|
||||
if (updates.properties?.fill !== undefined)
|
||||
fabricObj.set({ fill: updates.properties.fill });
|
||||
if (updates.properties?.stroke !== undefined)
|
||||
fabricObj.set({ stroke: updates.properties.stroke });
|
||||
if (updates.properties?.strokeWidth !== undefined)
|
||||
fabricObj.set({ strokeWidth: updates.properties.strokeWidth });
|
||||
if (updates.properties?.lineType !== undefined) {
|
||||
const dash = LINE_TYPE_DASH_MAP[updates.properties.lineType];
|
||||
fabricObj.set({ strokeDashArray: dash });
|
||||
}
|
||||
fabricObj.setCoords();
|
||||
fabricCanvas.renderAll();
|
||||
}
|
||||
|
||||
// Update local state (immediate, no debounce)
|
||||
setSelectedElement((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
...updates,
|
||||
properties: { ...prev.properties, ...updates.properties },
|
||||
};
|
||||
});
|
||||
},
|
||||
[selectedElement, fabricObject, yjsDocRef]
|
||||
);
|
||||
|
||||
// ── Field change handlers ──────────────────────────────────────────────────
|
||||
|
||||
const handleNumberChange = (field: 'x' | 'y' | 'width' | 'height', value: string) => {
|
||||
const n = clampNumber(value, -100000, 100000);
|
||||
updateFabricAndYjs({ [field]: n });
|
||||
};
|
||||
|
||||
const handleRotationChange = (value: string) => {
|
||||
const n = clampNumber(value, -360, 360);
|
||||
updateFabricAndYjs({ properties: { rotation: n } });
|
||||
};
|
||||
|
||||
const handleFillChange = (value: string) => {
|
||||
updateFabricAndYjs({ properties: { fill: value } });
|
||||
};
|
||||
|
||||
const handleStrokeChange = (value: string) => {
|
||||
updateFabricAndYjs({ properties: { stroke: value } });
|
||||
};
|
||||
|
||||
const handleStrokeWidthChange = (value: string) => {
|
||||
const n = clampNumber(value, 0, 100);
|
||||
updateFabricAndYjs({ properties: { strokeWidth: n } });
|
||||
};
|
||||
|
||||
const handleLineTypeChange = (value: 'solid' | 'dashed' | 'dotted') => {
|
||||
updateFabricAndYjs({ properties: { lineType: value } });
|
||||
};
|
||||
|
||||
const handleLayerChange = (value: string) => {
|
||||
updateFabricAndYjs({ layerId: value });
|
||||
};
|
||||
|
||||
// ── Render ──────────────────────────────────────────────────────────────────
|
||||
|
||||
if (!selectedElement) {
|
||||
return (
|
||||
<div className="properties-panel-empty">
|
||||
<div className="properties-panel-empty-icon" aria-hidden="true">⚙️</div>
|
||||
<p className="properties-panel-empty-text">
|
||||
Kein Element ausgewählt. Wähle ein Element auf dem Canvas, um seine Eigenschaften zu bearbeiten.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const props = selectedElement.properties;
|
||||
const fillValue = props.fill ?? 'transparent';
|
||||
const strokeValue = props.stroke ?? '#000000';
|
||||
const strokeWidthValue = props.strokeWidth ?? 1;
|
||||
const rotationValue = props.rotation ?? 0;
|
||||
const lineTypeValue = props.lineType ?? 'solid';
|
||||
|
||||
return (
|
||||
<div className="properties-panel" role="form" aria-label="Element Properties">
|
||||
{/* Element info header */}
|
||||
<div className="properties-panel-header">
|
||||
<span className="properties-panel-type-badge">{selectedElement.type}</span>
|
||||
<span className="properties-panel-id">ID: {selectedElement.id.substring(0, 12)}</span>
|
||||
</div>
|
||||
|
||||
{/* Position section */}
|
||||
<fieldset className="properties-panel-fieldset">
|
||||
<legend>Position</legend>
|
||||
<div className="properties-panel-row">
|
||||
<label className="properties-panel-label" htmlFor="prop-x">
|
||||
X
|
||||
<input
|
||||
id="prop-x"
|
||||
type="number"
|
||||
className="properties-panel-input"
|
||||
value={Math.round(selectedElement.x * 100) / 100}
|
||||
onChange={(e) => handleNumberChange('x', e.target.value)}
|
||||
step="1"
|
||||
/>
|
||||
</label>
|
||||
<label className="properties-panel-label" htmlFor="prop-y">
|
||||
Y
|
||||
<input
|
||||
id="prop-y"
|
||||
type="number"
|
||||
className="properties-panel-input"
|
||||
value={Math.round(selectedElement.y * 100) / 100}
|
||||
onChange={(e) => handleNumberChange('y', e.target.value)}
|
||||
step="1"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* Size section */}
|
||||
<fieldset className="properties-panel-fieldset">
|
||||
<legend>Größe</legend>
|
||||
<div className="properties-panel-row">
|
||||
<label className="properties-panel-label" htmlFor="prop-width">
|
||||
Breite
|
||||
<input
|
||||
id="prop-width"
|
||||
type="number"
|
||||
className="properties-panel-input"
|
||||
value={Math.round(selectedElement.width * 100) / 100}
|
||||
onChange={(e) => handleNumberChange('width', e.target.value)}
|
||||
step="1"
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
<label className="properties-panel-label" htmlFor="prop-height">
|
||||
Höhe
|
||||
<input
|
||||
id="prop-height"
|
||||
type="number"
|
||||
className="properties-panel-input"
|
||||
value={Math.round(selectedElement.height * 100) / 100}
|
||||
onChange={(e) => handleNumberChange('height', e.target.value)}
|
||||
step="1"
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* Rotation section */}
|
||||
<fieldset className="properties-panel-fieldset">
|
||||
<legend>Rotation</legend>
|
||||
<label className="properties-panel-label" htmlFor="prop-rotation">
|
||||
Winkel (Grad)
|
||||
<input
|
||||
id="prop-rotation"
|
||||
type="number"
|
||||
className="properties-panel-input"
|
||||
value={Math.round(rotationValue * 100) / 100}
|
||||
onChange={(e) => handleRotationChange(e.target.value)}
|
||||
step="1"
|
||||
min="-360"
|
||||
max="360"
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
{/* Appearance section */}
|
||||
<fieldset className="properties-panel-fieldset">
|
||||
<legend>Erscheinungsbild</legend>
|
||||
<label className="properties-panel-label" htmlFor="prop-fill">
|
||||
Füllfarbe
|
||||
<input
|
||||
id="prop-fill"
|
||||
type="color"
|
||||
className="properties-panel-color-input"
|
||||
value={fillValue === 'transparent' ? '#ffffff' : fillValue}
|
||||
onChange={(e) => handleFillChange(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="properties-panel-text-input"
|
||||
value={fillValue}
|
||||
onChange={(e) => handleFillChange(e.target.value)}
|
||||
placeholder="#RRGGBB oder transparent"
|
||||
/>
|
||||
</label>
|
||||
<label className="properties-panel-label" htmlFor="prop-stroke">
|
||||
Linienfarbe
|
||||
<input
|
||||
id="prop-stroke"
|
||||
type="color"
|
||||
className="properties-panel-color-input"
|
||||
value={strokeValue}
|
||||
onChange={(e) => handleStrokeChange(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="properties-panel-text-input"
|
||||
value={strokeValue}
|
||||
onChange={(e) => handleStrokeChange(e.target.value)}
|
||||
placeholder="#RRGGBB"
|
||||
/>
|
||||
</label>
|
||||
<label className="properties-panel-label" htmlFor="prop-stroke-width">
|
||||
Linienstärke
|
||||
<input
|
||||
id="prop-stroke-width"
|
||||
type="number"
|
||||
className="properties-panel-input"
|
||||
value={strokeWidthValue}
|
||||
onChange={(e) => handleStrokeWidthChange(e.target.value)}
|
||||
step="0.5"
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
<label className="properties-panel-label" htmlFor="prop-line-type">
|
||||
Linientyp
|
||||
<select
|
||||
id="prop-line-type"
|
||||
className="properties-panel-select"
|
||||
value={lineTypeValue}
|
||||
onChange={(e) => handleLineTypeChange(e.target.value as 'solid' | 'dashed' | 'dotted')}
|
||||
>
|
||||
<option value="solid">Solid</option>
|
||||
<option value="dashed">Dashed</option>
|
||||
<option value="dotted">Dotted</option>
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
{/* Layer assignment section */}
|
||||
<fieldset className="properties-panel-fieldset">
|
||||
<legend>Layer</legend>
|
||||
<label className="properties-panel-label" htmlFor="prop-layer">
|
||||
Zugewiesener Layer
|
||||
<select
|
||||
id="prop-layer"
|
||||
className="properties-panel-select"
|
||||
value={selectedElement.layerId}
|
||||
onChange={(e) => handleLayerChange(e.target.value)}
|
||||
>
|
||||
{layers.length === 0 ? (
|
||||
<option value="default">Default</option>
|
||||
) : (
|
||||
layers.map((layer) => (
|
||||
<option key={layer.id} value={layer.id}>
|
||||
{layer.name}{layer.locked ? ' (gesperrt)' : ''}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PropertiesPanel;
|
||||
Reference in New Issue
Block a user