From 5ee22dda3cca6cbfa8cb0047b487bf68c82f8f8d Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Sat, 20 Dec 2025 17:04:28 +0530 Subject: [PATCH] feat: implement ElementEditor and BlockEditor components, and a visualization store for editor state management. --- .../components/block/BlockEditor.tsx | 231 +++++++----------- .../components/element/ElementEditor.tsx | 148 +++++++---- .../visualization/useVisualizationStore.ts | 67 ++++- 3 files changed, 259 insertions(+), 187 deletions(-) diff --git a/app/src/components/SimulationDashboard/components/block/BlockEditor.tsx b/app/src/components/SimulationDashboard/components/block/BlockEditor.tsx index 24b2142..8705283 100644 --- a/app/src/components/SimulationDashboard/components/block/BlockEditor.tsx +++ b/app/src/components/SimulationDashboard/components/block/BlockEditor.tsx @@ -19,60 +19,105 @@ interface BlockEditorProps { updateBlockStyle: (blockId: string, style: React.CSSProperties) => void; updateBlockSize: (blockId: string, size: { width: number; height: number }) => void; updateBlockPosition: (blockId: string, position: { x: number; y: number }) => void; - updateBlockPositionType: ( - blockId: string, - positionType: "relative" | "absolute" | "fixed" - ) => void; + updateBlockPositionType: (blockId: string, positionType: "relative" | "absolute" | "fixed") => void; updateBlockZIndex: (blockId: string, zIndex: number) => void; handleRemoveBlock: (blockId: string) => void; } -const BlockEditor: React.FC = ({ - blockEditorRef, - currentBlock, - selectedBlock, - updateBlockStyle, - updateBlockSize, - updateBlockPositionType, - updateBlockZIndex, - handleRemoveBlock, -}) => { +const BlockEditor: React.FC = ({ blockEditorRef, currentBlock, selectedBlock, updateBlockStyle, updateBlockSize, updateBlockPositionType, updateBlockZIndex, handleRemoveBlock }) => { const [color, setColor] = useState("#000000"); // Use position from VisualizationStore const { editorPosition, setEditorPosition } = useVisualizationStore(); const panelRef = useRef(null); - const initialPositionRef = useRef<{ x: number; y: number } | null>(null); + const initialPositionRef = useRef<{ xPercent: number; yPercent: number } | null>(null); const draggingRef = useRef(false); const startXRef = useRef(0); const startYRef = useRef(0); const startLeftRef = useRef(0); const startTopRef = useRef(0); - const position = editorPosition || { x: 100, y: 80 }; - const setPosition = (newPosition: { x: number; y: number }) => { - setEditorPosition(newPosition); + // Get panel dimensions + const getPanelDimensions = () => { + const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); + const width = panelEl?.offsetWidth || 300; + const height = panelEl?.offsetHeight || 300; + return { width, height }; }; - // compute exact initial position once we have panel dimensions + // Convert percentage position to pixels for rendering + const getPixelPosition = (): { x: number; y: number } => { + if (!editorPosition) { + return { x: 100, y: 80 }; + } + + const { width, height } = getPanelDimensions(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let x = (editorPosition.xPercent / 100) * viewportWidth; + let y = (editorPosition.yPercent / 100) * viewportHeight; + + // Ensure panel stays within bounds + const maxX = viewportWidth - width - 8; + const maxY = viewportHeight - height - 8; + + x = Math.max(0, Math.min(x, maxX)); + y = Math.max(0, Math.min(y, maxY)); + + return { x, y }; + }; + + const position = getPixelPosition(); + + // Convert pixel position to percentage and save to store + const setPositionFromPixels = (pixelX: number, pixelY: number) => { + const { width, height } = getPanelDimensions(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Calculate percentage, ensuring panel stays within bounds + const maxXPercent = ((viewportWidth - width) / viewportWidth) * 100; + const maxYPercent = ((viewportHeight - height) / viewportHeight) * 100; + + const xPercent = Math.max(0, Math.min((pixelX / viewportWidth) * 100, maxXPercent)); + const yPercent = Math.max(0, Math.min((pixelY / viewportHeight) * 100, maxYPercent)); + + setEditorPosition({ xPercent, yPercent }); + }; + + // Initialize position on mount useEffect(() => { if (!editorPosition) { - const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); - const width = panelEl?.offsetWidth || 300; - const nx = Math.max(0, window.innerWidth - width - 40); - const ny = 80; - initialPositionRef.current = { x: nx, y: ny }; - setPosition({ x: nx, y: ny }); + const { width } = getPanelDimensions(); + const defaultX = Math.max(0, window.innerWidth - width - 40); + const defaultY = 80; + + setPositionFromPixels(defaultX, defaultY); + initialPositionRef.current = { + xPercent: (defaultX / window.innerWidth) * 100, + yPercent: (defaultY / window.innerHeight) * 100, + }; } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Handle window resize - update position to maintain relative placement useEffect(() => { + const handleResize = () => { + if (editorPosition) { + // Force re-render with updated pixel position + // The percentage stays the same, but pixels recalculate + setEditorPosition({ ...editorPosition }); + } + }; + + window.addEventListener("resize", handleResize); return () => { - // cleanup in case + window.removeEventListener("resize", handleResize); draggingRef.current = false; }; - }, []); + }, [editorPosition, setEditorPosition]); const startDrag = (ev: React.PointerEvent) => { if (ev.detail > 1) return; @@ -93,10 +138,7 @@ const BlockEditor: React.FC = ({ const dx = e.clientX - startXRef.current; const dy = e.clientY - startYRef.current; - const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); - const width = panelEl?.offsetWidth || 300; - const height = panelEl?.offsetHeight || 300; - + const { width, height } = getPanelDimensions(); const maxX = window.innerWidth - width - 8; const maxY = window.innerHeight - height - 8; @@ -106,7 +148,7 @@ const BlockEditor: React.FC = ({ nx = Math.max(0, Math.min(nx, maxX)); ny = Math.max(0, Math.min(ny, maxY)); - setPosition({ x: nx, y: ny }); + setPositionFromPixels(nx, ny); }; const onPointerUp = (e: PointerEvent) => { @@ -122,35 +164,26 @@ const BlockEditor: React.FC = ({ const resetPosition = () => { if (initialPositionRef.current) { - setPosition(initialPositionRef.current); + setEditorPosition(initialPositionRef.current); return; } - const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); - const width = panelEl?.offsetWidth || 300; - const nx = Math.max(0, window.innerWidth - width - 40); - setPosition({ x: nx, y: 80 }); + const { width } = getPanelDimensions(); + const defaultX = Math.max(0, window.innerWidth - width - 40); + setPositionFromPixels(defaultX, 80); }; return (
{ panelRef.current = el; - if ( - blockEditorRef && - typeof blockEditorRef === "object" && - "current" in blockEditorRef - ) { + if (blockEditorRef && typeof blockEditorRef === "object" && "current" in blockEditorRef) { (blockEditorRef as any).current = el; } }} className="panel block-editor-panel" style={{ position: "fixed", left: position.x, top: position.y, zIndex: 999 }} > -
+
@@ -199,14 +232,8 @@ const BlockEditor: React.FC = ({ {["relative", "absolute"].map((position) => (
- updateBlockPositionType( - selectedBlock, - position as "relative" | "absolute" | "fixed" - ) - } + className={`type ${currentBlock.positionType === position ? "active" : ""}`} + onClick={() => updateBlockPositionType(selectedBlock, position as "relative" | "absolute" | "fixed")} > {position.charAt(0).toUpperCase() + position.slice(1)}
@@ -215,69 +242,27 @@ const BlockEditor: React.FC = ({
- +
- +
- +
- +
- - updateBlockZIndex(selectedBlock, Number(newValue)) - } - /> + updateBlockZIndex(selectedBlock, Number(newValue))} /> { updateBlockStyle(selectedBlock, { @@ -295,29 +280,17 @@ const BlockEditor: React.FC = ({
setColor(e.target.value)} onChange={(e) => { - handleBackgroundColorChange( - currentBlock, - selectedBlock, - updateBlockStyle, - e.target.value - ); + handleBackgroundColorChange(currentBlock, selectedBlock, updateBlockStyle, e.target.value); setColor(e.target.value); }} />
{ e.preventDefault(); - handleBackgroundColorChange( - currentBlock, - selectedBlock, - updateBlockStyle, - color - ); + handleBackgroundColorChange(currentBlock, selectedBlock, updateBlockStyle, color); }} > = ({ setColor(e.target.value); }} onBlur={(e) => { - handleBackgroundColorChange( - currentBlock, - selectedBlock, - updateBlockStyle, - color - ); + handleBackgroundColorChange(currentBlock, selectedBlock, updateBlockStyle, color); }} />
@@ -343,19 +311,8 @@ const BlockEditor: React.FC = ({ min={0} max={1} step={0.1} - value={Math.round( - getAlphaFromRgba( - getCurrentBlockStyleValue(currentBlock, "backgroundColor") - ) - )} - onChange={(value: number) => - handleBackgroundAlphaChange( - currentBlock, - selectedBlock, - updateBlockStyle, - Number(value) - ) - } + value={Math.round(getAlphaFromRgba(getCurrentBlockStyleValue(currentBlock, "backgroundColor")))} + onChange={(value: number) => handleBackgroundAlphaChange(currentBlock, selectedBlock, updateBlockStyle, Number(value))} /> = ({ // Use shared position from VisualizationStore const { editorPosition, setEditorPosition } = useVisualizationStore(); const panelRef = useRef(null); - const initialPositionRef = useRef<{ x: number; y: number } | null>(null); - - // Compute position from store or initialize - const position = editorPosition || { x: 100, y: 80 }; - const setPosition = (newPosition: { x: number; y: number }) => { - setEditorPosition(newPosition); - }; - - // On mount, compute exact initial position based on panel width and store it - useEffect(() => { - if (!editorPosition) { - const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current); - const width = panelEl?.offsetWidth || 300; - const nx = Math.max(0, window.innerWidth - width - 40); - const ny = 80; - initialPositionRef.current = { x: nx, y: ny }; - setPosition({ x: nx, y: ny }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const initialPositionRef = useRef<{ xPercent: number; yPercent: number } | null>(null); const draggingRef = useRef(false); const startXRef = useRef(0); @@ -90,14 +71,86 @@ const ElementEditor: React.FC = ({ const startLeftRef = useRef(0); const startTopRef = useRef(0); + // Get panel dimensions + const getPanelDimensions = () => { + const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current); + const width = panelEl?.offsetWidth || 300; + const height = panelEl?.offsetHeight || 300; + return { width, height }; + }; + + // Convert percentage position to pixels for rendering + const getPixelPosition = (): { x: number; y: number } => { + if (!editorPosition) { + return { x: 100, y: 80 }; + } + + const { width, height } = getPanelDimensions(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let x = (editorPosition.xPercent / 100) * viewportWidth; + let y = (editorPosition.yPercent / 100) * viewportHeight; + + // Ensure panel stays within bounds + const maxX = viewportWidth - width - 8; + const maxY = viewportHeight - height - 8; + + x = Math.max(0, Math.min(x, maxX)); + y = Math.max(0, Math.min(y, maxY)); + + return { x, y }; + }; + + const position = getPixelPosition(); + + // Convert pixel position to percentage and save to store + const setPositionFromPixels = (pixelX: number, pixelY: number) => { + const { width, height } = getPanelDimensions(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Calculate percentage, ensuring panel stays within bounds + const maxXPercent = ((viewportWidth - width) / viewportWidth) * 100; + const maxYPercent = ((viewportHeight - height) / viewportHeight) * 100; + + const xPercent = Math.max(0, Math.min((pixelX / viewportWidth) * 100, maxXPercent)); + const yPercent = Math.max(0, Math.min((pixelY / viewportHeight) * 100, maxYPercent)); + + setEditorPosition({ xPercent, yPercent }); + }; + + // On mount, compute exact initial position based on panel width and store it useEffect(() => { + if (!editorPosition) { + const { width } = getPanelDimensions(); + const defaultX = Math.max(0, window.innerWidth - width - 40); + const defaultY = 80; + + setPositionFromPixels(defaultX, defaultY); + initialPositionRef.current = { + xPercent: (defaultX / window.innerWidth) * 100, + yPercent: (defaultY / window.innerHeight) * 100, + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Handle window resize - update position to maintain relative placement + useEffect(() => { + const handleResize = () => { + if (editorPosition) { + // Force re-render with updated pixel position + // The percentage stays the same, but pixels recalculate + setEditorPosition({ ...editorPosition }); + } + }; + const onPointerMove = (ev: PointerEvent) => { if (!draggingRef.current) return; const dx = ev.clientX - startXRef.current; const dy = ev.clientY - startYRef.current; - const panel = panelRef.current; - const width = panel?.offsetWidth || 300; - const height = panel?.offsetHeight || 300; + const { width, height } = getPanelDimensions(); const maxX = window.innerWidth - width - 8; const maxY = window.innerHeight - height - 8; let nx = startLeftRef.current + dx; @@ -106,7 +159,7 @@ const ElementEditor: React.FC = ({ if (ny < 0) ny = 0; if (nx > maxX) nx = maxX; if (ny > maxY) ny = maxY; - setPosition({ x: nx, y: ny }); + setPositionFromPixels(nx, ny); }; const onPointerUp = () => { @@ -115,11 +168,13 @@ const ElementEditor: React.FC = ({ window.removeEventListener("pointerup", onPointerUp); }; + window.addEventListener("resize", handleResize); return () => { + window.removeEventListener("resize", handleResize); window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", onPointerUp); }; - }, []); + }, [editorPosition, setEditorPosition]); const startDrag = (ev: React.PointerEvent) => { ev.preventDefault(); @@ -136,9 +191,7 @@ const ElementEditor: React.FC = ({ if (!draggingRef.current) return; const dx = e.clientX - startXRef.current; const dy = e.clientY - startYRef.current; - const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current); - const width = panelEl?.offsetWidth || 300; - const height = panelEl?.offsetHeight || 300; + const { width, height } = getPanelDimensions(); const maxX = window.innerWidth - width - 8; const maxY = window.innerHeight - height - 8; let nx = startLeftRef.current + dx; @@ -147,7 +200,7 @@ const ElementEditor: React.FC = ({ if (ny < 0) ny = 0; if (nx > maxX) nx = maxX; if (ny > maxY) ny = maxY; - setPosition({ x: nx, y: ny }); + setPositionFromPixels(nx, ny); }; const onPointerUp = (e: PointerEvent) => { @@ -156,7 +209,7 @@ const ElementEditor: React.FC = ({ window.removeEventListener("pointerup", onPointerUp); try { (panel as Element).releasePointerCapture?.((e as any).pointerId); - } catch (err) { } + } catch (err) {} }; window.addEventListener("pointermove", onPointerMove); @@ -165,13 +218,12 @@ const ElementEditor: React.FC = ({ const resetPosition = () => { if (initialPositionRef.current) { - setPosition(initialPositionRef.current); + setEditorPosition(initialPositionRef.current); return; } - const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current); - const width = panelEl?.offsetWidth || 300; - const nx = Math.max(0, window.innerWidth - width - 40); - setPosition({ x: nx, y: 80 }); + const { width } = getPanelDimensions(); + const defaultX = Math.max(0, window.innerWidth - width - 40); + setPositionFromPixels(defaultX, 80); }; const getAssetDropdownItems = useCallback(() => { @@ -547,12 +599,12 @@ const ElementEditor: React.FC = ({ value={ element.dataBinding?.dataSource ? { - id: element.dataBinding.dataSource as string, - label: - getEventByModelUuid(selectedProduct.productUuid, element.dataBinding.dataSource as string)?.modelName ?? - (element.dataBinding.dataSource === "global" ? "Global" : ""), - icon: , - } + id: element.dataBinding.dataSource as string, + label: + getEventByModelUuid(selectedProduct.productUuid, element.dataBinding.dataSource as string)?.modelName ?? + (element.dataBinding.dataSource === "global" ? "Global" : ""), + icon: , + } : null } onChange={(value) => { @@ -571,13 +623,13 @@ const ElementEditor: React.FC = ({ value={ element.dataBinding?.dataValue ? { - id: element.dataBinding.dataValue as string, - label: - getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined) - .flatMap((section) => section.items) - .find((item) => item.id === element.dataBinding?.dataValue)?.label ?? "", - icon: , - } + id: element.dataBinding.dataValue as string, + label: + getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined) + .flatMap((section) => section.items) + .find((item) => item.id === element.dataBinding?.dataValue)?.label ?? "", + icon: , + } : null } onChange={(value) => { diff --git a/app/src/store/visualization/useVisualizationStore.ts b/app/src/store/visualization/useVisualizationStore.ts index 0ccdc85..bae0e6c 100644 --- a/app/src/store/visualization/useVisualizationStore.ts +++ b/app/src/store/visualization/useVisualizationStore.ts @@ -1,14 +1,19 @@ import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; +/** + * Panel position stored as percentages (0-100) of viewport dimensions + * This ensures panels stay within bounds when screen size changes + */ interface PanelPosition { - x: number; - y: number; + xPercent: number; // 0-100, percentage from left edge + yPercent: number; // 0-100, percentage from top edge } interface VisualizationState { editorPosition: PanelPosition | null; setEditorPosition: (position: PanelPosition) => void; + resetEditorPosition: () => void; } export const useVisualizationStore = create()( @@ -18,5 +23,63 @@ export const useVisualizationStore = create()( set((state) => { state.editorPosition = position; }), + resetEditorPosition: () => + set((state) => { + state.editorPosition = null; + }), })) ); + +/** + * Utility functions to convert between pixels and percentages + */ +export const positionUtils = { + /** + * Convert pixel position to percentage position + */ + pixelsToPercent: (pixelX: number, pixelY: number, panelWidth: number, panelHeight: number): PanelPosition => { + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Calculate percentage, ensuring panel stays within bounds + const maxXPercent = ((viewportWidth - panelWidth) / viewportWidth) * 100; + const maxYPercent = ((viewportHeight - panelHeight) / viewportHeight) * 100; + + const xPercent = Math.max(0, Math.min((pixelX / viewportWidth) * 100, maxXPercent)); + const yPercent = Math.max(0, Math.min((pixelY / viewportHeight) * 100, maxYPercent)); + + return { xPercent, yPercent }; + }, + + /** + * Convert percentage position to pixel position + */ + percentToPixels: (position: PanelPosition, panelWidth: number, panelHeight: number): { x: number; y: number } => { + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Calculate pixel position + let x = (position.xPercent / 100) * viewportWidth; + let y = (position.yPercent / 100) * viewportHeight; + + // Ensure panel stays within bounds + const maxX = viewportWidth - panelWidth - 8; + const maxY = viewportHeight - panelHeight - 8; + + x = Math.max(0, Math.min(x, maxX)); + y = Math.max(0, Math.min(y, maxY)); + + return { x, y }; + }, + + /** + * Get default position (top-right corner with some margin) + */ + getDefaultPosition: (panelWidth: number): PanelPosition => { + const viewportWidth = window.innerWidth; + const defaultX = Math.max(0, viewportWidth - panelWidth - 40); + const defaultY = 80; + + return positionUtils.pixelsToPercent(defaultX, defaultY, panelWidth, 300); + }, +};