feat: add element and block design editor with styling, positioning, and data binding controls for simulation dashboard
This commit is contained in:
@@ -8,9 +8,11 @@ import { handleBlurAmountChange } from "../../functions/helpers/handleBlurAmount
|
||||
import InputRange from "../../../ui/inputs/InputRange";
|
||||
import { DeleteIcon } from "../../../icons/ContextMenuIcons";
|
||||
import InputWithDropDown from "../../../ui/inputs/InputWithDropDown";
|
||||
import { ResizeHeightIcon } from "../../../icons/ExportCommonIcons";
|
||||
import { ArrowIcon, ResizeHeightIcon } from "../../../icons/ExportCommonIcons";
|
||||
import RenameInput from "../../../ui/inputs/RenameInput";
|
||||
import { useVisualizationStore } from "../../../../store/visualization/useVisualizationStore";
|
||||
import { ResetIcon } from "../../../icons/SimulationIcons";
|
||||
import { Color } from "../../../ui/inputs/Color";
|
||||
|
||||
interface BlockEditorProps {
|
||||
blockEditorRef: RefObject<HTMLDivElement>;
|
||||
@@ -39,6 +41,13 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
}) => {
|
||||
const [color, setColor] = useState("#000000");
|
||||
|
||||
useEffect(() => {
|
||||
setColor(
|
||||
rgbaToHex(
|
||||
getCurrentBlockStyleValue(currentBlock, "backgroundColor") || "#000000"
|
||||
)
|
||||
);
|
||||
}, [currentBlock]);
|
||||
// Use position from VisualizationStore
|
||||
const { editorPosition, setEditorPosition } = useVisualizationStore();
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -54,6 +63,7 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
setEditorPosition(newPosition);
|
||||
};
|
||||
|
||||
|
||||
// compute exact initial position once we have panel dimensions
|
||||
useEffect(() => {
|
||||
if (!editorPosition) {
|
||||
@@ -262,82 +272,71 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="design-section-footer">
|
||||
<div className="layer-system">
|
||||
<InputWithDropDown
|
||||
label="Layer"
|
||||
value={String(currentBlock.zIndex || 1)}
|
||||
value={String(currentBlock.zIndex ?? 1)}
|
||||
placeholder={"Layer"}
|
||||
onChange={(newValue) =>
|
||||
updateBlockZIndex(selectedBlock, Number(newValue))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputWithDropDown
|
||||
label="Border Radius"
|
||||
min={0}
|
||||
value={String(
|
||||
parseInt(getCurrentBlockStyleValue(currentBlock, "borderRadius")) ||
|
||||
8
|
||||
)}
|
||||
placeholder={"Width"}
|
||||
onChange={(newValue) => {
|
||||
updateBlockStyle(selectedBlock, {
|
||||
borderRadius: Number(newValue),
|
||||
});
|
||||
<button
|
||||
className="increase-z"
|
||||
onClick={() => {
|
||||
updateBlockZIndex(selectedBlock, Number(currentBlock.zIndex) + 1);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<ArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
className="decrease-z"
|
||||
onClick={() => {
|
||||
updateBlockZIndex(selectedBlock, Number(currentBlock.zIndex) - 1);
|
||||
}}
|
||||
>
|
||||
<ArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
className="reset"
|
||||
onClick={() => {
|
||||
updateBlockZIndex(selectedBlock, Number(1))
|
||||
}}
|
||||
>
|
||||
<ResetIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="design-section">
|
||||
<div className="section-header">Background</div>
|
||||
<div className="data-picker">
|
||||
<div className="label">Color</div>
|
||||
<div className="left">
|
||||
<input
|
||||
type="color"
|
||||
value={rgbaToHex(
|
||||
getCurrentBlockStyleValue(currentBlock, "backgroundColor")
|
||||
)}
|
||||
// onChange={(e) => setColor(e.target.value)}
|
||||
onChange={(e) => {
|
||||
handleBackgroundColorChange(
|
||||
currentBlock,
|
||||
selectedBlock,
|
||||
updateBlockStyle,
|
||||
e.target.value
|
||||
);
|
||||
setColor(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleBackgroundColorChange(
|
||||
currentBlock,
|
||||
selectedBlock,
|
||||
updateBlockStyle,
|
||||
color
|
||||
);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="colorValue"
|
||||
|
||||
<Color
|
||||
label="Color"
|
||||
value={color}
|
||||
onChange={(e) => {
|
||||
setColor(e.target.value);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
onChange={(value) => {
|
||||
setColor(value);
|
||||
if (/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(value)) {
|
||||
handleBackgroundColorChange(
|
||||
currentBlock,
|
||||
selectedBlock,
|
||||
updateBlockStyle,
|
||||
color
|
||||
value
|
||||
);
|
||||
}
|
||||
}}
|
||||
onUpdate={(value) => {
|
||||
handleBackgroundColorChange(
|
||||
currentBlock,
|
||||
selectedBlock,
|
||||
updateBlockStyle,
|
||||
value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<InputRange
|
||||
label={"Opacity"}
|
||||
min={0}
|
||||
@@ -364,6 +363,20 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
value={parseInt(getCurrentBlockStyleValue(currentBlock, "backdropFilter")?.match(/\d+/)?.[0] || "10")}
|
||||
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>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getAlphaFromRgba, hexToRgba, rgbaToHex } from "../../functions/helpers/
|
||||
import { ExtendedCSSProperties, UIElement } from "../../../../types/exportedTypes";
|
||||
import { getCurrentElementStyleValue } from "../../functions/helpers/getCurrentElementStyleValue";
|
||||
import { ResetIcon } from "../../../icons/SimulationIcons";
|
||||
import { Color } from "../../../ui/inputs/Color";
|
||||
|
||||
interface ElementDesignProps {
|
||||
element: UIElement | undefined;
|
||||
@@ -384,43 +385,27 @@ const ElementDesign: React.FC<ElementDesignProps> = ({
|
||||
|
||||
<div className="design-section element-color">
|
||||
<div className="section-header">Background</div>
|
||||
<div className="data-picker">
|
||||
<div className="label">Color</div>
|
||||
<div className="left">
|
||||
<input
|
||||
type="color"
|
||||
|
||||
<Color
|
||||
label="Color"
|
||||
value={color}
|
||||
onChange={(e) => {
|
||||
onChange={(value) => {
|
||||
const currentBg = getCurrentElementStyleValue(currentElement, "backgroundColor") || "rgba(0,0,0,1)";
|
||||
const currentAlpha = getAlphaFromRgba(currentBg);
|
||||
updateElementStyle(selectedBlock, selectedElement, {
|
||||
backgroundColor: hexToRgba(e.target.value),
|
||||
backgroundColor: hexToRgba(value, currentAlpha),
|
||||
});
|
||||
setColor(value);
|
||||
}}
|
||||
onUpdate={(value) => {
|
||||
const currentBg = getCurrentElementStyleValue(currentElement, "backgroundColor") || "rgba(0,0,0,1)";
|
||||
const currentAlpha = getAlphaFromRgba(currentBg);
|
||||
updateElementStyle(selectedBlock, selectedElement, {
|
||||
backgroundColor: hexToRgba(value, currentAlpha),
|
||||
});
|
||||
setColor(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
updateElementStyle(selectedBlock, selectedElement, {
|
||||
backgroundColor: color,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="colorValue"
|
||||
value={color}
|
||||
onChange={(e) => {
|
||||
setColor(e.target.value);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
updateElementStyle(selectedBlock, selectedElement, {
|
||||
backgroundColor: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<InputRange
|
||||
label={"Opacity"}
|
||||
min={0}
|
||||
@@ -446,7 +431,7 @@ const ElementDesign: React.FC<ElementDesignProps> = ({
|
||||
<InputRange
|
||||
label={"Border Radius"}
|
||||
min={0}
|
||||
max={8}
|
||||
max={100}
|
||||
value={
|
||||
parseInt(
|
||||
getCurrentElementStyleValue(currentElement, "borderRadius") || ""
|
||||
|
||||
70
app/src/components/ui/inputs/Color.tsx
Normal file
70
app/src/components/ui/inputs/Color.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -46,6 +46,7 @@ textarea {
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
|
||||
// Chrome, Safari, Edge, Opera
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
@@ -228,22 +229,26 @@ input[type="number"] {
|
||||
.key {
|
||||
text-overflow: none;
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
border-radius: #{$border-radius-large};
|
||||
|
||||
.key {
|
||||
width: calc(100% - 18px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-options {
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,10 +161,10 @@
|
||||
padding: 8px;
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
outline: 1px solid transparent;
|
||||
|
||||
&.edit-mode {
|
||||
transition: all 0.2s;
|
||||
outline: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
outline-color: var(--border-color);
|
||||
@@ -279,7 +279,7 @@
|
||||
gap: 11px;
|
||||
min-width: 280px;
|
||||
height: fit-content;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
min-height: 60vh;
|
||||
padding: 12px;
|
||||
|
||||
@@ -454,41 +454,6 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user