figmatic-print-builder/PropertiesPanel.jsx

828 lines
34 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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",
},
};