828 lines
34 KiB
React
828 lines
34 KiB
React
|
|
import useEditorStore from "./useEditorStore";
|
|||
|
|
import { useState } from "react";
|
|||
|
|
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|||
|
|
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|||
|
|
import { CSS } from "@dnd-kit/utilities";
|
|||
|
|
|
|||
|
|
function SortableColumnItem({ column, elementId, updateColumnWidth, updateColumnLabel, toggleColumnVisibility }) {
|
|||
|
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: column.key });
|
|||
|
|
|
|||
|
|
const style = {
|
|||
|
|
transform: CSS.Transform.toString(transform),
|
|||
|
|
transition,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div ref={setNodeRef} style={{ ...styles.columnItem, ...style }} {...attributes}>
|
|||
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8, flex: 1 }}>
|
|||
|
|
<div {...listeners} style={{ cursor: "grab", padding: "0 4px" }}>
|
|||
|
|
⋮⋮
|
|||
|
|
</div>
|
|||
|
|
<label style={styles.checkbox}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={column.visible}
|
|||
|
|
onChange={() => toggleColumnVisibility(elementId, column.key)}
|
|||
|
|
/>
|
|||
|
|
<span>{column.key}</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
<div style={{ display: "flex", gap: 4 }}>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={column.label}
|
|||
|
|
onChange={(e) => updateColumnLabel(elementId, column.key, e.target.value)}
|
|||
|
|
style={{ ...styles.input, width: 70, fontSize: 11 }}
|
|||
|
|
placeholder="Label"
|
|||
|
|
/>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={column.width}
|
|||
|
|
onChange={(e) => updateColumnWidth(elementId, column.key, Number(e.target.value))}
|
|||
|
|
style={{ ...styles.input, width: 50, fontSize: 11 }}
|
|||
|
|
placeholder="W"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function PropertiesPanel() {
|
|||
|
|
const elements = useEditorStore((state) => state.elements);
|
|||
|
|
const selectedId = useEditorStore((state) => state.selectedId);
|
|||
|
|
const updateElement = useEditorStore((state) => state.updateElement);
|
|||
|
|
const duplicateElement = useEditorStore((state) => state.duplicateElement);
|
|||
|
|
const deleteElement = useEditorStore((state) => state.deleteElement);
|
|||
|
|
const toggleColumnVisibility = useEditorStore((state) => state.toggleColumnVisibility);
|
|||
|
|
const updateColumnWidth = useEditorStore((state) => state.updateColumnWidth);
|
|||
|
|
const updateColumnLabel = useEditorStore((state) => state.updateColumnLabel);
|
|||
|
|
const reorderColumns = useEditorStore((state) => state.reorderColumns);
|
|||
|
|
|
|||
|
|
const selected = elements.find((el) => el.id === selectedId);
|
|||
|
|
|
|||
|
|
const [imageUpload, setImageUpload] = useState(null);
|
|||
|
|
|
|||
|
|
const sensors = useSensors(
|
|||
|
|
useSensor(PointerSensor, {
|
|||
|
|
activationConstraint: {
|
|||
|
|
distance: 8,
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!selected) {
|
|||
|
|
return (
|
|||
|
|
<div style={styles.panel}>
|
|||
|
|
<div style={styles.emptyState}>
|
|||
|
|
<p>Select an element to edit properties</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleImageUpload = (e) => {
|
|||
|
|
const file = e.target.files[0];
|
|||
|
|
if (file) {
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (event) => {
|
|||
|
|
updateElement(selected.id, { src: event.target.result });
|
|||
|
|
};
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDragEnd = (event) => {
|
|||
|
|
const { active, over } = event;
|
|||
|
|
|
|||
|
|
if (active.id !== over.id) {
|
|||
|
|
const oldIndex = selected.columns.findIndex((col) => col.key === active.id);
|
|||
|
|
const newIndex = selected.columns.findIndex((col) => col.key === over.id);
|
|||
|
|
const newColumns = arrayMove(selected.columns, oldIndex, newIndex);
|
|||
|
|
reorderColumns(selected.id, newColumns);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div style={styles.panel}>
|
|||
|
|
<div style={styles.header}>
|
|||
|
|
<h4 style={styles.title}>Properties</h4>
|
|||
|
|
<div style={styles.actions}>
|
|||
|
|
<button
|
|||
|
|
onClick={() => duplicateElement(selected.id)}
|
|||
|
|
style={styles.iconBtn}
|
|||
|
|
title="Duplicate"
|
|||
|
|
>
|
|||
|
|
📋
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => deleteElement(selected.id)}
|
|||
|
|
style={styles.iconBtn}
|
|||
|
|
title="Delete"
|
|||
|
|
>
|
|||
|
|
🗑️
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Type</label>
|
|||
|
|
<div style={styles.badge}>{selected.type.toUpperCase()}</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* POSITION */}
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Position</label>
|
|||
|
|
<div style={styles.row}>
|
|||
|
|
<div style={styles.inputGroup}>
|
|||
|
|
<span style={styles.inputLabel}>X</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.x}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { x: Number(e.target.value) })
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div style={styles.inputGroup}>
|
|||
|
|
<span style={styles.inputLabel}>Y</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.y}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { y: Number(e.target.value) })
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ROTATION */}
|
|||
|
|
{selected.rotation !== undefined && (
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Rotation</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={Math.round(selected.rotation)}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { rotation: Number(e.target.value) })
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
placeholder="Degrees"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* TEXT PROPERTIES */}
|
|||
|
|
{selected.type === "text" && (
|
|||
|
|
<>
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Text</label>
|
|||
|
|
<textarea
|
|||
|
|
value={selected.text}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { text: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, minHeight: 60, resize: "vertical" }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Font Size</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.fontSize}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
fontSize: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Font Family</label>
|
|||
|
|
<select
|
|||
|
|
value={selected.fontFamily}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { fontFamily: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
>
|
|||
|
|
<option value="Arial">Arial</option>
|
|||
|
|
<option value="Times New Roman">Times New Roman</option>
|
|||
|
|
<option value="Courier New">Courier New</option>
|
|||
|
|
<option value="Georgia">Georgia</option>
|
|||
|
|
<option value="Verdana">Verdana</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Font Weight</label>
|
|||
|
|
<select
|
|||
|
|
value={selected.fontWeight || "normal"}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { fontWeight: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
>
|
|||
|
|
<option value="normal">Normal (400)</option>
|
|||
|
|
<option value="500">Medium (500)</option>
|
|||
|
|
<option value="600">Semi Bold (600)</option>
|
|||
|
|
<option value="bold">Bold (700)</option>
|
|||
|
|
<option value="800">Extra Bold (800)</option>
|
|||
|
|
<option value="900">Black (900)</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Color</label>
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
value={selected.color}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { color: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, height: 40 }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Style</label>
|
|||
|
|
<div style={styles.row}>
|
|||
|
|
<label style={styles.checkbox}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={selected.bold}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { bold: e.target.checked })
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<span>Bold</span>
|
|||
|
|
</label>
|
|||
|
|
<label style={styles.checkbox}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={selected.italic}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { italic: e.target.checked })
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<span>Italic</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
<div style={{ marginTop: 8 }}>
|
|||
|
|
<label style={styles.checkbox}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={selected.underline}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { underline: e.target.checked })
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<span>Underline</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Alignment</label>
|
|||
|
|
<select
|
|||
|
|
value={selected.align}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { align: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
>
|
|||
|
|
<option value="left">Left</option>
|
|||
|
|
<option value="center">Center</option>
|
|||
|
|
<option value="right">Right</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* IMAGE PROPERTIES */}
|
|||
|
|
{selected.type === "image" && (
|
|||
|
|
<>
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Image</label>
|
|||
|
|
{selected.src ? (
|
|||
|
|
<div style={{ position: "relative" }}>
|
|||
|
|
<img
|
|||
|
|
src={selected.src}
|
|||
|
|
alt="Preview"
|
|||
|
|
style={{
|
|||
|
|
width: "100%",
|
|||
|
|
height: 120,
|
|||
|
|
objectFit: "cover",
|
|||
|
|
borderRadius: 4,
|
|||
|
|
border: "1px solid #ddd",
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
onClick={() => updateElement(selected.id, { src: null })}
|
|||
|
|
style={{
|
|||
|
|
position: "absolute",
|
|||
|
|
top: 8,
|
|||
|
|
right: 8,
|
|||
|
|
background: "#ff4444",
|
|||
|
|
color: "#fff",
|
|||
|
|
border: "none",
|
|||
|
|
borderRadius: 4,
|
|||
|
|
padding: "4px 8px",
|
|||
|
|
cursor: "pointer",
|
|||
|
|
fontSize: 12,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
Delete
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<input
|
|||
|
|
type="file"
|
|||
|
|
accept="image/*"
|
|||
|
|
onChange={handleImageUpload}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Size</label>
|
|||
|
|
<div style={styles.row}>
|
|||
|
|
<div style={styles.inputGroup}>
|
|||
|
|
<span style={styles.inputLabel}>W</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.width}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
width: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div style={styles.inputGroup}>
|
|||
|
|
<span style={styles.inputLabel}>H</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.height}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
height: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* QRCODE PROPERTIES */}
|
|||
|
|
{selected.type === "qrcode" && (
|
|||
|
|
<>
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>QR Code Data</label>
|
|||
|
|
<textarea
|
|||
|
|
value={selected.data}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { data: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, minHeight: 60, resize: "vertical" }}
|
|||
|
|
placeholder="Enter URL or text"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Size</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.size}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
size: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* LINE PROPERTIES */}
|
|||
|
|
{selected.type === "line" && (
|
|||
|
|
<>
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Width</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.width}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
width: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Thickness</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.thickness}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
thickness: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Color</label>
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
value={selected.color}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { color: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, height: 40 }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* RECTANGLE PROPERTIES */}
|
|||
|
|
{selected.type === "rectangle" && (
|
|||
|
|
<>
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Size</label>
|
|||
|
|
<div style={styles.row}>
|
|||
|
|
<div style={styles.inputGroup}>
|
|||
|
|
<span style={styles.inputLabel}>W</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.width}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
width: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div style={styles.inputGroup}>
|
|||
|
|
<span style={styles.inputLabel}>H</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.height}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
height: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Fill Color</label>
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
value={selected.fill === "transparent" ? "#ffffff" : selected.fill}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { fill: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, height: 40 }}
|
|||
|
|
/>
|
|||
|
|
<label style={{ ...styles.checkbox, marginTop: 8 }}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={selected.fill === "transparent"}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
fill: e.target.checked ? "transparent" : "#ffffff",
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<span>Transparent</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Border Color</label>
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
value={selected.stroke}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { stroke: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, height: 40 }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Border Width</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.strokeWidth}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
strokeWidth: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Corner Radius</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.cornerRadius}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
cornerRadius: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* SUMMARY PROPERTIES */}
|
|||
|
|
{selected.type === "summary" && (
|
|||
|
|
<>
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Font Size</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.fontSize}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
fontSize: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Spacing</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.spacing}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
spacing: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Label Color</label>
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
value={selected.labelColor}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { labelColor: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, height: 40 }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Value Color</label>
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
value={selected.valueColor}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { valueColor: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, height: 40 }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Fields</label>
|
|||
|
|
{selected.fields.map((field, idx) => (
|
|||
|
|
<div key={field.key} style={styles.columnItem}>
|
|||
|
|
<label style={styles.checkbox}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={field.visible}
|
|||
|
|
onChange={() => {
|
|||
|
|
const newFields = [...selected.fields];
|
|||
|
|
newFields[idx].visible = !newFields[idx].visible;
|
|||
|
|
updateElement(selected.id, { fields: newFields });
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<span>{field.label}</span>
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={field.label}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
const newFields = [...selected.fields];
|
|||
|
|
newFields[idx].label = e.target.value;
|
|||
|
|
updateElement(selected.id, { fields: newFields });
|
|||
|
|
}}
|
|||
|
|
style={{ ...styles.input, width: 80 }}
|
|||
|
|
placeholder="Label"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* TABLE PROPERTIES */}
|
|||
|
|
{selected.type === "table" && (
|
|||
|
|
<>
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Row Height</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={selected.rowHeight}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, {
|
|||
|
|
rowHeight: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
style={styles.input}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Columns (Drag to reorder)</label>
|
|||
|
|
<DndContext
|
|||
|
|
sensors={sensors}
|
|||
|
|
collisionDetection={closestCenter}
|
|||
|
|
onDragEnd={handleDragEnd}
|
|||
|
|
>
|
|||
|
|
<SortableContext
|
|||
|
|
items={selected.columns.map((col) => col.key)}
|
|||
|
|
strategy={verticalListSortingStrategy}
|
|||
|
|
>
|
|||
|
|
{selected.columns.map((col) => (
|
|||
|
|
<SortableColumnItem
|
|||
|
|
key={col.key}
|
|||
|
|
column={col}
|
|||
|
|
elementId={selected.id}
|
|||
|
|
updateColumnWidth={updateColumnWidth}
|
|||
|
|
updateColumnLabel={updateColumnLabel}
|
|||
|
|
toggleColumnVisibility={toggleColumnVisibility}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
</SortableContext>
|
|||
|
|
</DndContext>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Header Background</label>
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
value={selected.headerBg}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { headerBg: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, height: 40 }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.label}>Border Color</label>
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
value={selected.borderColor}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { borderColor: e.target.value })
|
|||
|
|
}
|
|||
|
|
style={{ ...styles.input, height: 40 }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={styles.section}>
|
|||
|
|
<label style={styles.checkbox}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={selected.showSampleData}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
updateElement(selected.id, { showSampleData: e.target.checked })
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<span>Show Sample Data</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const styles = {
|
|||
|
|
panel: {
|
|||
|
|
width: 280,
|
|||
|
|
background: "#fff",
|
|||
|
|
borderLeft: "1px solid #ddd",
|
|||
|
|
padding: 16,
|
|||
|
|
overflowY: "auto",
|
|||
|
|
},
|
|||
|
|
header: {
|
|||
|
|
display: "flex",
|
|||
|
|
justifyContent: "space-between",
|
|||
|
|
alignItems: "center",
|
|||
|
|
marginBottom: 16,
|
|||
|
|
},
|
|||
|
|
title: {
|
|||
|
|
margin: 0,
|
|||
|
|
fontSize: 16,
|
|||
|
|
fontWeight: 600,
|
|||
|
|
},
|
|||
|
|
actions: {
|
|||
|
|
display: "flex",
|
|||
|
|
gap: 4,
|
|||
|
|
},
|
|||
|
|
iconBtn: {
|
|||
|
|
background: "none",
|
|||
|
|
border: "1px solid #ddd",
|
|||
|
|
borderRadius: 4,
|
|||
|
|
padding: "4px 8px",
|
|||
|
|
cursor: "pointer",
|
|||
|
|
fontSize: 14,
|
|||
|
|
},
|
|||
|
|
emptyState: {
|
|||
|
|
textAlign: "center",
|
|||
|
|
color: "#999",
|
|||
|
|
marginTop: 40,
|
|||
|
|
},
|
|||
|
|
section: {
|
|||
|
|
marginBottom: 16,
|
|||
|
|
},
|
|||
|
|
label: {
|
|||
|
|
display: "block",
|
|||
|
|
fontSize: 12,
|
|||
|
|
fontWeight: 600,
|
|||
|
|
marginBottom: 6,
|
|||
|
|
color: "#333",
|
|||
|
|
},
|
|||
|
|
input: {
|
|||
|
|
width: "100%",
|
|||
|
|
padding: 8,
|
|||
|
|
border: "1px solid #ddd",
|
|||
|
|
borderRadius: 4,
|
|||
|
|
fontSize: 13,
|
|||
|
|
boxSizing: "border-box",
|
|||
|
|
},
|
|||
|
|
row: {
|
|||
|
|
display: "flex",
|
|||
|
|
gap: 8,
|
|||
|
|
},
|
|||
|
|
inputGroup: {
|
|||
|
|
flex: 1,
|
|||
|
|
display: "flex",
|
|||
|
|
flexDirection: "column",
|
|||
|
|
},
|
|||
|
|
inputLabel: {
|
|||
|
|
fontSize: 11,
|
|||
|
|
color: "#666",
|
|||
|
|
marginBottom: 4,
|
|||
|
|
},
|
|||
|
|
checkbox: {
|
|||
|
|
display: "flex",
|
|||
|
|
alignItems: "center",
|
|||
|
|
gap: 6,
|
|||
|
|
fontSize: 13,
|
|||
|
|
cursor: "pointer",
|
|||
|
|
},
|
|||
|
|
badge: {
|
|||
|
|
display: "inline-block",
|
|||
|
|
padding: "4px 8px",
|
|||
|
|
background: "#e3f2fd",
|
|||
|
|
color: "#1976d2",
|
|||
|
|
borderRadius: 4,
|
|||
|
|
fontSize: 11,
|
|||
|
|
fontWeight: 600,
|
|||
|
|
},
|
|||
|
|
columnItem: {
|
|||
|
|
display: "flex",
|
|||
|
|
justifyContent: "space-between",
|
|||
|
|
alignItems: "center",
|
|||
|
|
padding: "6px 0",
|
|||
|
|
borderBottom: "1px solid #f0f0f0",
|
|||
|
|
},
|
|||
|
|
};
|