feat: Add new UI input components for color, detailed dropdown, and data source selection, alongside foundational simulation dashboard components and styles.

This commit is contained in:
2025-12-22 11:17:31 +05:30
parent e35eaa5348
commit f9fe57c72c
6 changed files with 169 additions and 128 deletions

View File

@@ -229,7 +229,7 @@ const ElementData: React.FC<ElementDataProps> = ({
{singleValueFields.length < totalValueOptions && ( {singleValueFields.length < totalValueOptions && (
<div className="add-field" onClick={addField}> <div className="add-field" onClick={addField}>
<div className="icon"> <div className="add-icon">
<AddIcon /> <AddIcon />
</div> </div>
<div className="label">Add Field</div> <div className="label">Add Field</div>
@@ -292,7 +292,7 @@ const ElementData: React.FC<ElementDataProps> = ({
{multipleSourceFields.length < totalAssetOptions && ( {multipleSourceFields.length < totalAssetOptions && (
<div className="add-field" onClick={addField}> <div className="add-field" onClick={addField}>
<div className="icon"> <div className="add-icon">
<AddIcon /> <AddIcon />
</div> </div>
<div className="label">Add Field</div> <div className="label">Add Field</div>

View File

@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { createPortal } from "react-dom";
import { HexColorPicker } from "react-colorful"; import { HexColorPicker } from "react-colorful";
import { useOuterClick } from "../../../utils/useOuterClick"; import { useOuterClick } from "../../../utils/useOuterClick";
@@ -17,33 +18,22 @@ const hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result return result
? { ? {
r: parseInt(result[1], 16), r: parseInt(result[1], 16),
g: parseInt(result[2], 16), g: parseInt(result[2], 16),
b: parseInt(result[3], 16), b: parseInt(result[3], 16),
} }
: { r: 0, g: 0, b: 0 }; : { r: 0, g: 0, b: 0 };
}; };
const rgbToHex = (r: number, g: number, b: number) => { const rgbToHex = (r: number, g: number, b: number) => {
return ( return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
"#" +
((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()
);
}; };
export const Color: React.FC<ColorProps> = ({ export const Color: React.FC<ColorProps> = ({ value, onChange, label = "Color", className = "", disabled = false, onBlur, onFocus, onUpdate }) => {
value,
onChange,
label = "Color",
className = "",
disabled = false,
onBlur,
onFocus,
onUpdate,
}) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const popoverRef = useRef<HTMLDivElement>(null); const popoverRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [coords, setCoords] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
const [localValue, setLocalValue] = useState(value); const [localValue, setLocalValue] = useState(value);
// Sync local value when prop changes // Sync local value when prop changes
@@ -73,11 +63,42 @@ export const Color: React.FC<ColorProps> = ({
if (onUpdate) onUpdate(localValue); if (onUpdate) onUpdate(localValue);
}; };
useOuterClick( // Calculate position before opening or when needed
() => setIsOpen(false), const updatePosition = () => {
["color-picker-popover", "color-trigger"], if (containerRef.current) {
isOpen const rect = containerRef.current.getBoundingClientRect();
); setCoords({
top: rect.top + window.scrollY - 8,
left: rect.left + window.scrollX,
});
}
};
const toggleOpen = () => {
if (disabled) return;
if (!isOpen) {
updatePosition();
setIsOpen(true);
} else {
setIsOpen(false);
}
};
useOuterClick(() => setIsOpen(false), ["color-picker-popover", "color-trigger"], isOpen);
// Update position on window resize/scroll if open
useEffect(() => {
if (isOpen) {
const handleResize = () => updatePosition();
window.addEventListener("resize", handleResize);
window.addEventListener("scroll", handleResize, true);
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("scroll", handleResize, true);
};
}
}, [isOpen]);
const rgb = hexToRgb(localValue); const rgb = hexToRgb(localValue);
@@ -87,7 +108,7 @@ export const Color: React.FC<ColorProps> = ({
<div className="left"> <div className="left">
<div <div
className="color-trigger cursor-pointer" className="color-trigger cursor-pointer"
onClick={() => !disabled && setIsOpen(!isOpen)} onClick={toggleOpen}
style={{ style={{
backgroundColor: localValue, backgroundColor: localValue,
width: "52px", width: "52px",
@@ -98,36 +119,30 @@ export const Color: React.FC<ColorProps> = ({
position: "relative", position: "relative",
}} }}
/> />
<input <input type="text" className="colorValue" value={localValue} onChange={(e) => handleChange(e.target.value)} onBlur={handleBlur} onFocus={onFocus} disabled={disabled} />
type="text"
className="colorValue"
value={localValue}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
onFocus={onFocus}
disabled={disabled}
/>
</div> </div>
{isOpen && ( {isOpen &&
<div createPortal(
ref={popoverRef} <div
className="color-picker-popover" ref={popoverRef}
style={{ className="color-picker-popover"
position: "absolute", style={{
top: "100%", position: "absolute",
left: "0", top: coords.top,
zIndex: 1000, left: coords.left,
marginTop: "8px", transform: "translateY(-100%)",
backgroundColor: "#1a1a1a", // Deep dark background zIndex: 9999,
borderRadius: "12px", backgroundColor: "#1a1a1a", // Deep dark background
padding: "12px", borderRadius: "12px",
boxShadow: "0 10px 30px rgba(0,0,0,0.5)", padding: "12px",
width: "240px", boxShadow: "0 10px 30px rgba(0,0,0,0.5)",
border: "1px solid #333", width: "240px",
}} border: "1px solid #333",
> marginTop: "0px",
<style>{` }}
>
<style>{`
.react-colorful { .react-colorful {
width: 100%; width: 100%;
height: 180px; height: 180px;
@@ -147,74 +162,83 @@ export const Color: React.FC<ColorProps> = ({
} }
`}</style> `}</style>
<HexColorPicker color={localValue} onChange={handleChange} /> <HexColorPicker color={localValue} onChange={handleChange} />
<div style={{ <div
display: "flex", style={{
alignItems: "center", display: "flex",
marginTop: "12px", alignItems: "center",
gap: "10px" marginTop: "12px",
}}> gap: "10px",
{/* Eyedropper placeholder icon */} }}
<div style={{ color: "#aaa" }}> >
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> {/* Eyedropper placeholder icon */}
<path d="M12.8536 0.146447C12.6583 -0.0488155 12.3417 -0.0488155 12.1464 0.146447L10.5 1.7929L14.2071 5.5L15.8536 3.85355C16.0488 3.65829 16.0488 3.34171 15.8536 3.14645L12.8536 0.146447ZM9.79289 2.5L1.5 10.7929V14.5H5.20711L13.5 6.20711L9.79289 2.5Z" /> <div style={{ color: "#aaa" }}>
</svg> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
</div> <path d="M12.8536 0.146447C12.6583 -0.0488155 12.3417 -0.0488155 12.1464 0.146447L10.5 1.7929L14.2071 5.5L15.8536 3.85355C16.0488 3.65829 16.0488 3.34171 15.8536 3.14645L12.8536 0.146447ZM9.79289 2.5L1.5 10.7929V14.5H5.20711L13.5 6.20711L9.79289 2.5Z" />
</svg>
<div style={{
width: "32px",
height: "32px",
borderRadius: "50%",
backgroundColor: localValue,
border: "2px solid #fff",
boxShadow: "0 0 0 1px #333",
flexShrink: 0
}}></div>
<div style={{ flexGrow: 1, display: "flex", flexDirection: "column", gap: "2px" }}>
{/* Small hex input representation usually goes here, but we have separated RGB below */}
</div>
</div>
{/* RGB Inputs */}
<div style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "8px",
marginTop: "12px"
}}>
{["r", "g", "b"].map((key) => (
<div key={key} style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<input
type="number"
value={rgb[key as keyof typeof rgb]}
onChange={(e) => handleRgbChange(key as "r" | "g" | "b", e.target.value)}
style={{
width: "100%",
backgroundColor: "#333",
border: "1px solid #444",
borderRadius: "4px",
color: "#eee",
textAlign: "center",
fontSize: "12px",
padding: "4px 0",
appearance: "textfield",
}}
/>
<span style={{
color: "#888",
fontSize: "10px",
marginTop: "4px",
textTransform: "uppercase"
}}>
{key}
</span>
</div> </div>
))}
</div> <div
</div> style={{
)} width: "32px",
height: "32px",
borderRadius: "50%",
backgroundColor: localValue,
border: "2px solid #fff",
boxShadow: "0 0 0 1px #333",
flexShrink: 0,
}}
></div>
<div style={{ flexGrow: 1, display: "flex", flexDirection: "column", gap: "2px" }}>
{/* Small hex input representation usually goes here, but we have separated RGB below */}
</div>
</div>
{/* RGB Inputs */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "8px",
marginTop: "12px",
}}
>
{["r", "g", "b"].map((key) => (
<div key={key} style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<input
type="number"
value={rgb[key as keyof typeof rgb]}
onChange={(e) => handleRgbChange(key as "r" | "g" | "b", e.target.value)}
style={{
width: "100%",
backgroundColor: "#333",
border: "1px solid #444",
borderRadius: "4px",
color: "#eee",
textAlign: "center",
fontSize: "12px",
padding: "4px 0",
appearance: "textfield",
}}
/>
<span
style={{
color: "#888",
fontSize: "10px",
marginTop: "4px",
textTransform: "uppercase",
}}
>
{key}
</span>
</div>
))}
</div>
</div>,
document.body
)}
</div> </div>
); );
}; };

View File

@@ -111,7 +111,7 @@ const DataDetailedDropdown: React.FC<DataDetailedDropdownProps> = ({ title, plac
</div> </div>
{eyedroper && ( {eyedroper && (
<div className={`icon ${isEyeActive ? "active" : ""}`} onClick={handleEyeClick}> <div className={`add-icon ${isEyeActive ? "active" : ""}`} onClick={handleEyeClick}>
<EyeDroperIcon isActive={isEyeActive} /> <EyeDroperIcon isActive={isEyeActive} />
</div> </div>
)} )}

View File

@@ -40,7 +40,7 @@ const DataSourceSelector: React.FC<DataSourceSelectorProps> = ({ label = "Data S
<RegularDropDownID header={selected || "Select value"} options={options} onSelect={onSelect} search={false} /> <RegularDropDownID header={selected || "Select value"} options={options} onSelect={onSelect} search={false} />
{showEyeDropper && ( {showEyeDropper && (
<div className={`icon ${isEyeActive ? "active" : ""}`} onClick={handleEyeClick} style={{ cursor: "pointer" }}> <div className={`add-icon ${isEyeActive ? "active" : ""}`} onClick={handleEyeClick} style={{ cursor: "pointer" }}>
<EyeDroperIcon isActive={isEyeActive} /> <EyeDroperIcon isActive={isEyeActive} />
</div> </div>
)} )}

View File

@@ -18,7 +18,7 @@
transition: none !important; transition: none !important;
} }
*> { * > {
pointer-events: auto; pointer-events: auto;
} }
@@ -640,7 +640,8 @@
border-radius: 25px; border-radius: 25px;
.heading { .heading {
text-align: center; padding: 4px 6px 8px;
font-weight: 500;
} }
.fields-wrapper { .fields-wrapper {
@@ -661,6 +662,7 @@
.datas__label { .datas__label {
flex: 0.8; flex: 0.8;
min-width: 96px;
} }
.datas__class { .datas__class {
@@ -668,12 +670,20 @@
align-items: center; align-items: center;
gap: 4px; gap: 4px;
.icon { .add-icon,
.delete {
display: flex; display: flex;
padding: 5px 4px; height: 24px;
width: 24px;
align-items: center;
justify-content: center;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
&:hover {
background: var(--background-color-input);
}
&.active { &.active {
background: var(--background-color-button); background: var(--background-color-button);
} }
@@ -681,9 +691,17 @@
.delete { .delete {
cursor: pointer; cursor: pointer;
&:hover {
background: var(--log-error-background-color);
path {
stroke: var(--log-error-text-color);
}
}
} }
.regularDropdown-container { .regularDropdown-container {
max-width: 106px;
width: 106px;
.icon { .icon {
padding: 0; padding: 0;
} }
@@ -709,7 +727,6 @@
} }
} }
.appearance { .appearance {
.design-datas-wrapper { .design-datas-wrapper {
display: grid; display: grid;