feat: implement ElementEditor and BlockEditor components, and a visualization store for editor state management.
This commit is contained in:
@@ -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<BlockEditorProps> = ({
|
||||
blockEditorRef,
|
||||
currentBlock,
|
||||
selectedBlock,
|
||||
updateBlockStyle,
|
||||
updateBlockSize,
|
||||
updateBlockPositionType,
|
||||
updateBlockZIndex,
|
||||
handleRemoveBlock,
|
||||
}) => {
|
||||
const BlockEditor: React.FC<BlockEditorProps> = ({ blockEditorRef, currentBlock, selectedBlock, updateBlockStyle, updateBlockSize, updateBlockPositionType, updateBlockZIndex, handleRemoveBlock }) => {
|
||||
const [color, setColor] = useState("#000000");
|
||||
|
||||
// Use position from VisualizationStore
|
||||
const { editorPosition, setEditorPosition } = useVisualizationStore();
|
||||
const panelRef = useRef<HTMLDivElement | null>(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);
|
||||
};
|
||||
|
||||
// compute exact initial position once we have panel dimensions
|
||||
useEffect(() => {
|
||||
if (!editorPosition) {
|
||||
// Get panel dimensions
|
||||
const getPanelDimensions = () => {
|
||||
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 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 });
|
||||
};
|
||||
|
||||
// Initialize position on mount
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
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<BlockEditorProps> = ({
|
||||
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<BlockEditorProps> = ({
|
||||
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<BlockEditorProps> = ({
|
||||
|
||||
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 (
|
||||
<div
|
||||
ref={(el) => {
|
||||
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 }}
|
||||
>
|
||||
<div
|
||||
className={`free-move-button`}
|
||||
onDoubleClick={resetPosition}
|
||||
onPointerDown={startDrag}
|
||||
>
|
||||
<div className={`free-move-button`} onDoubleClick={resetPosition} onPointerDown={startDrag}>
|
||||
<ResizeHeightIcon />
|
||||
</div>
|
||||
|
||||
@@ -199,14 +232,8 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
{["relative", "absolute"].map((position) => (
|
||||
<div
|
||||
key={position}
|
||||
className={`type ${currentBlock.positionType === position ? "active" : ""
|
||||
}`}
|
||||
onClick={() =>
|
||||
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)}
|
||||
</div>
|
||||
@@ -215,69 +242,27 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
<div className="position-canvas">
|
||||
<div className="canvas">
|
||||
<div className="value padding-top">
|
||||
<RenameInput
|
||||
value={
|
||||
getCurrentBlockStyleValue(currentBlock, "padding")
|
||||
? String(
|
||||
getCurrentBlockStyleValue(currentBlock, "padding")
|
||||
)
|
||||
: "120"
|
||||
}
|
||||
/>
|
||||
<RenameInput value={getCurrentBlockStyleValue(currentBlock, "padding") ? String(getCurrentBlockStyleValue(currentBlock, "padding")) : "120"} />
|
||||
</div>
|
||||
<div className="value padding-right">
|
||||
<RenameInput
|
||||
value={
|
||||
getCurrentBlockStyleValue(currentBlock, "padding")
|
||||
? String(
|
||||
getCurrentBlockStyleValue(currentBlock, "padding")
|
||||
)
|
||||
: "120"
|
||||
}
|
||||
/>
|
||||
<RenameInput value={getCurrentBlockStyleValue(currentBlock, "padding") ? String(getCurrentBlockStyleValue(currentBlock, "padding")) : "120"} />
|
||||
</div>
|
||||
<div className="value padding-bottom">
|
||||
<RenameInput
|
||||
value={
|
||||
getCurrentBlockStyleValue(currentBlock, "padding")
|
||||
? String(
|
||||
getCurrentBlockStyleValue(currentBlock, "padding")
|
||||
)
|
||||
: "120"
|
||||
}
|
||||
/>
|
||||
<RenameInput value={getCurrentBlockStyleValue(currentBlock, "padding") ? String(getCurrentBlockStyleValue(currentBlock, "padding")) : "120"} />
|
||||
</div>
|
||||
<div className="value padding-left">
|
||||
<RenameInput
|
||||
value={
|
||||
getCurrentBlockStyleValue(currentBlock, "padding")
|
||||
? String(
|
||||
getCurrentBlockStyleValue(currentBlock, "padding")
|
||||
)
|
||||
: "120"
|
||||
}
|
||||
/>
|
||||
<RenameInput value={getCurrentBlockStyleValue(currentBlock, "padding") ? String(getCurrentBlockStyleValue(currentBlock, "padding")) : "120"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="design-section-footer">
|
||||
<InputWithDropDown
|
||||
label="Layer"
|
||||
value={String(currentBlock.zIndex || 1)}
|
||||
placeholder={"Layer"}
|
||||
onChange={(newValue) =>
|
||||
updateBlockZIndex(selectedBlock, Number(newValue))
|
||||
}
|
||||
/>
|
||||
<InputWithDropDown label="Layer" 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
|
||||
)}
|
||||
value={String(parseInt(getCurrentBlockStyleValue(currentBlock, "borderRadius")) || 8)}
|
||||
placeholder={"Width"}
|
||||
onChange={(newValue) => {
|
||||
updateBlockStyle(selectedBlock, {
|
||||
@@ -295,29 +280,17 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
<div className="left">
|
||||
<input
|
||||
type="color"
|
||||
value={rgbaToHex(
|
||||
getCurrentBlockStyleValue(currentBlock, "backgroundColor")
|
||||
)}
|
||||
value={rgbaToHex(getCurrentBlockStyleValue(currentBlock, "backgroundColor"))}
|
||||
// onChange={(e) => setColor(e.target.value)}
|
||||
onChange={(e) => {
|
||||
handleBackgroundColorChange(
|
||||
currentBlock,
|
||||
selectedBlock,
|
||||
updateBlockStyle,
|
||||
e.target.value
|
||||
);
|
||||
handleBackgroundColorChange(currentBlock, selectedBlock, updateBlockStyle, e.target.value);
|
||||
setColor(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleBackgroundColorChange(
|
||||
currentBlock,
|
||||
selectedBlock,
|
||||
updateBlockStyle,
|
||||
color
|
||||
);
|
||||
handleBackgroundColorChange(currentBlock, selectedBlock, updateBlockStyle, color);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
@@ -327,12 +300,7 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
setColor(e.target.value);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
handleBackgroundColorChange(
|
||||
currentBlock,
|
||||
selectedBlock,
|
||||
updateBlockStyle,
|
||||
color
|
||||
);
|
||||
handleBackgroundColorChange(currentBlock, selectedBlock, updateBlockStyle, color);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
@@ -343,19 +311,8 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
|
||||
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))}
|
||||
/>
|
||||
<InputRange
|
||||
label={"Blur"}
|
||||
|
||||
@@ -63,26 +63,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
|
||||
// Use shared position from VisualizationStore
|
||||
const { editorPosition, setEditorPosition } = useVisualizationStore();
|
||||
const panelRef = useRef<HTMLDivElement | null>(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<ElementEditorProps> = ({
|
||||
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<ElementEditorProps> = ({
|
||||
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<ElementEditorProps> = ({
|
||||
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<ElementEditorProps> = ({
|
||||
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<ElementEditorProps> = ({
|
||||
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) => {
|
||||
@@ -165,13 +218,12 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -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<VisualizationState>()(
|
||||
@@ -18,5 +23,63 @@ export const useVisualizationStore = create<VisualizationState>()(
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user