feat: add element and block design editor with styling, positioning, and data binding controls for simulation dashboard

This commit is contained in:
2025-12-20 16:38:26 +05:30
parent 75912355c7
commit cf13400463
5 changed files with 223 additions and 149 deletions

View File

@@ -8,9 +8,11 @@ import { handleBlurAmountChange } from "../../functions/helpers/handleBlurAmount
import InputRange from "../../../ui/inputs/InputRange"; import InputRange from "../../../ui/inputs/InputRange";
import { DeleteIcon } from "../../../icons/ContextMenuIcons"; import { DeleteIcon } from "../../../icons/ContextMenuIcons";
import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; import InputWithDropDown from "../../../ui/inputs/InputWithDropDown";
import { ResizeHeightIcon } from "../../../icons/ExportCommonIcons"; import { ArrowIcon, ResizeHeightIcon } from "../../../icons/ExportCommonIcons";
import RenameInput from "../../../ui/inputs/RenameInput"; import RenameInput from "../../../ui/inputs/RenameInput";
import { useVisualizationStore } from "../../../../store/visualization/useVisualizationStore"; import { useVisualizationStore } from "../../../../store/visualization/useVisualizationStore";
import { ResetIcon } from "../../../icons/SimulationIcons";
import { Color } from "../../../ui/inputs/Color";
interface BlockEditorProps { interface BlockEditorProps {
blockEditorRef: RefObject<HTMLDivElement>; blockEditorRef: RefObject<HTMLDivElement>;
@@ -39,6 +41,13 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
}) => { }) => {
const [color, setColor] = useState("#000000"); const [color, setColor] = useState("#000000");
useEffect(() => {
setColor(
rgbaToHex(
getCurrentBlockStyleValue(currentBlock, "backgroundColor") || "#000000"
)
);
}, [currentBlock]);
// Use position from VisualizationStore // Use position from VisualizationStore
const { editorPosition, setEditorPosition } = useVisualizationStore(); const { editorPosition, setEditorPosition } = useVisualizationStore();
const panelRef = useRef<HTMLDivElement | null>(null); const panelRef = useRef<HTMLDivElement | null>(null);
@@ -54,6 +63,7 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
setEditorPosition(newPosition); setEditorPosition(newPosition);
}; };
// compute exact initial position once we have panel dimensions // compute exact initial position once we have panel dimensions
useEffect(() => { useEffect(() => {
if (!editorPosition) { if (!editorPosition) {
@@ -262,82 +272,71 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
</div> </div>
<div className="design-section-footer"> <div className="design-section-footer">
<InputWithDropDown <div className="layer-system">
label="Layer" <InputWithDropDown
value={String(currentBlock.zIndex || 1)} label="Layer"
placeholder={"Layer"} value={String(currentBlock.zIndex ?? 1)}
onChange={(newValue) => placeholder={"Layer"}
updateBlockZIndex(selectedBlock, Number(newValue)) onChange={(newValue) =>
} updateBlockZIndex(selectedBlock, Number(newValue))
/> }
/>
<InputWithDropDown <button
label="Border Radius" className="increase-z"
min={0} onClick={() => {
value={String( updateBlockZIndex(selectedBlock, Number(currentBlock.zIndex) + 1);
parseInt(getCurrentBlockStyleValue(currentBlock, "borderRadius")) || }}
8 >
)} <ArrowIcon />
placeholder={"Width"} </button>
onChange={(newValue) => { <button
updateBlockStyle(selectedBlock, { className="decrease-z"
borderRadius: Number(newValue), onClick={() => {
}); updateBlockZIndex(selectedBlock, Number(currentBlock.zIndex) - 1);
}} }}
/> >
<ArrowIcon />
</button>
<button
className="reset"
onClick={() => {
updateBlockZIndex(selectedBlock, Number(1))
}}
>
<ResetIcon />
</button>
</div>
</div> </div>
</div> </div>
<div className="design-section"> <div className="design-section">
<div className="section-header">Background</div> <div className="section-header">Background</div>
<div className="data-picker">
<div className="label">Color</div> <Color
<div className="left"> label="Color"
<input value={color}
type="color" onChange={(value) => {
value={rgbaToHex( setColor(value);
getCurrentBlockStyleValue(currentBlock, "backgroundColor") if (/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(value)) {
)} handleBackgroundColorChange(
// onChange={(e) => setColor(e.target.value)} currentBlock,
onChange={(e) => { selectedBlock,
handleBackgroundColorChange( updateBlockStyle,
currentBlock, value
selectedBlock, );
updateBlockStyle, }
e.target.value }}
); onUpdate={(value) => {
setColor(e.target.value); handleBackgroundColorChange(
}} currentBlock,
/> selectedBlock,
<form updateBlockStyle,
onSubmit={(e) => { value
e.preventDefault(); );
handleBackgroundColorChange( }}
currentBlock, />
selectedBlock,
updateBlockStyle,
color
);
}}
>
<input
className="colorValue"
value={color}
onChange={(e) => {
setColor(e.target.value);
}}
onBlur={(e) => {
handleBackgroundColorChange(
currentBlock,
selectedBlock,
updateBlockStyle,
color
);
}}
/>
</form>
</div>
</div>
<InputRange <InputRange
label={"Opacity"} label={"Opacity"}
min={0} min={0}
@@ -364,6 +363,20 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
value={parseInt(getCurrentBlockStyleValue(currentBlock, "backdropFilter")?.match(/\d+/)?.[0] || "10")} value={parseInt(getCurrentBlockStyleValue(currentBlock, "backdropFilter")?.match(/\d+/)?.[0] || "10")}
onChange={(value: number) => handleBlurAmountChange(selectedBlock, updateBlockStyle, Number(value))} onChange={(value: number) => handleBlurAmountChange(selectedBlock, updateBlockStyle, Number(value))}
/> />
<InputRange
label={"Border Radius"}
min={0}
max={100}
value={(
parseInt(getCurrentBlockStyleValue(currentBlock, "borderRadius")) ||
8
)}
onChange={(newValue) => {
updateBlockStyle(selectedBlock, {
borderRadius: Number(newValue),
});
}}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,6 +16,7 @@ import { getAlphaFromRgba, hexToRgba, rgbaToHex } from "../../functions/helpers/
import { ExtendedCSSProperties, UIElement } from "../../../../types/exportedTypes"; import { ExtendedCSSProperties, UIElement } from "../../../../types/exportedTypes";
import { getCurrentElementStyleValue } from "../../functions/helpers/getCurrentElementStyleValue"; import { getCurrentElementStyleValue } from "../../functions/helpers/getCurrentElementStyleValue";
import { ResetIcon } from "../../../icons/SimulationIcons"; import { ResetIcon } from "../../../icons/SimulationIcons";
import { Color } from "../../../ui/inputs/Color";
interface ElementDesignProps { interface ElementDesignProps {
element: UIElement | undefined; element: UIElement | undefined;
@@ -384,43 +385,27 @@ const ElementDesign: React.FC<ElementDesignProps> = ({
<div className="design-section element-color"> <div className="design-section element-color">
<div className="section-header">Background</div> <div className="section-header">Background</div>
<div className="data-picker">
<div className="label">Color</div>
<div className="left">
<input
type="color"
value={color}
onChange={(e) => {
updateElementStyle(selectedBlock, selectedElement, {
backgroundColor: hexToRgba(e.target.value),
});
setColor(e.target.value);
}}
/>
<form <Color
onSubmit={(e) => { label="Color"
e.preventDefault(); value={color}
updateElementStyle(selectedBlock, selectedElement, { onChange={(value) => {
backgroundColor: color, const currentBg = getCurrentElementStyleValue(currentElement, "backgroundColor") || "rgba(0,0,0,1)";
}); const currentAlpha = getAlphaFromRgba(currentBg);
}} updateElementStyle(selectedBlock, selectedElement, {
> backgroundColor: hexToRgba(value, currentAlpha),
<input });
className="colorValue" setColor(value);
value={color} }}
onChange={(e) => { onUpdate={(value) => {
setColor(e.target.value); const currentBg = getCurrentElementStyleValue(currentElement, "backgroundColor") || "rgba(0,0,0,1)";
}} const currentAlpha = getAlphaFromRgba(currentBg);
onBlur={(e) => { updateElementStyle(selectedBlock, selectedElement, {
updateElementStyle(selectedBlock, selectedElement, { backgroundColor: hexToRgba(value, currentAlpha),
backgroundColor: color, });
}); }}
}} />
/>
</form>
</div>
</div>
<InputRange <InputRange
label={"Opacity"} label={"Opacity"}
min={0} min={0}
@@ -446,7 +431,7 @@ const ElementDesign: React.FC<ElementDesignProps> = ({
<InputRange <InputRange
label={"Border Radius"} label={"Border Radius"}
min={0} min={0}
max={8} max={100}
value={ value={
parseInt( parseInt(
getCurrentElementStyleValue(currentElement, "borderRadius") || "" getCurrentElementStyleValue(currentElement, "borderRadius") || ""

View File

@@ -0,0 +1,70 @@
import React from "react";
interface ColorProps {
value: string;
onChange: (value: string) => void;
label?: string;
className?: string;
disabled?: boolean;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
onUpdate?: (value: string) => void;
}
export const Color: React.FC<ColorProps> = ({
value,
onChange,
label = "Color",
className = "",
disabled = false,
onBlur,
onFocus,
onUpdate,
}) => {
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
const handleColorPickerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
const handleSubmit = (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (onUpdate) {
onUpdate(value);
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
handleSubmit();
if (onBlur) onBlur(e);
};
return (
<div className={`data-picker ${className}`}>
{label && <div className="label">{label}</div>}
<div className="left">
<input
type="color"
value={value}
onChange={handleColorPickerChange}
onBlur={handleBlur}
disabled={disabled}
className="cursor-pointer"
/>
<form onSubmit={handleSubmit}>
<input
type="text"
className="colorValue"
value={value}
onChange={handleTextChange}
onBlur={handleBlur}
onFocus={onFocus}
disabled={disabled}
/>
</form>
</div>
</div>
);
};

View File

@@ -46,6 +46,7 @@ textarea {
} }
input[type="number"] { input[type="number"] {
// Chrome, Safari, Edge, Opera // Chrome, Safari, Edge, Opera
&::-webkit-outer-spin-button, &::-webkit-outer-spin-button,
&::-webkit-inner-spin-button { &::-webkit-inner-spin-button {
@@ -225,25 +226,29 @@ input[type="number"] {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
.key{ .key {
text-overflow: none; text-overflow: none;
} }
.dropdown-header { .dropdown-header {
height: 100%; height: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
cursor: pointer; cursor: pointer;
border-radius: #{$border-radius-large}; border-radius: #{$border-radius-large};
.key{
.key {
width: calc(100% - 18px); width: calc(100% - 18px);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
} }
.icon { .icon {
height: auto; height: auto;
} }
} }
.dropdown-options { .dropdown-options {
position: absolute; position: absolute;
width: 100%; width: 100%;
@@ -787,3 +792,39 @@ input[type="number"] {
} }
} }
} }
.data-picker {
display: flex;
justify-content: space-between;
align-items: center;
.label {
width: 50%;
white-space: nowrap;
}
input[type="color"] {
width: 42px;
padding: 0;
background: transparent;
outline: none;
}
.left {
width: 100%;
display: flex;
align-items: center;
gap: 6px;
.colorValue {
width: 100%;
background: linear-gradient(90.85deg,
rgba(240, 228, 255, 0.3) 3.6%,
rgba(211, 174, 253, 0.3) 96.04%);
text-align: center;
padding: 4px 0;
border-radius: 100px;
}
}
}

View File

@@ -161,10 +161,10 @@
padding: 8px; padding: 8px;
user-select: none; user-select: none;
border: 1px solid transparent; border: 1px solid transparent;
outline: 1px solid transparent;
&.edit-mode { &.edit-mode {
transition: all 0.2s; transition: all 0.2s;
outline: 1px solid transparent;
&:hover { &:hover {
outline-color: var(--border-color); outline-color: var(--border-color);
@@ -279,7 +279,7 @@
gap: 11px; gap: 11px;
min-width: 280px; min-width: 280px;
height: fit-content; height: fit-content;
min-width: 320px; max-width: 320px;
min-height: 60vh; min-height: 60vh;
padding: 12px; padding: 12px;
@@ -454,41 +454,6 @@
padding: 0; padding: 0;
} }
.data-picker {
display: flex;
justify-content: space-between;
align-items: center;
.label {
width: 50%;
white-space: nowrap;
}
input[type="color"] {
width: 42px;
padding: 0;
background: transparent;
outline: none;
}
.left {
width: 100%;
display: flex;
align-items: center;
gap: 6px;
.colorValue {
width: 100%;
background: linear-gradient(90.85deg,
rgba(240, 228, 255, 0.3) 3.6%,
rgba(211, 174, 253, 0.3) 96.04%);
text-align: center;
padding: 4px 0;
border-radius: 100px;
}
}
}
.design-section-footer { .design-section-footer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;