(null);
+
+ // 모드에 따라 입력 비활성화
+ const isDisabled = mode === "fill" || mode === "fit";
+ const displayValue = isDisabled ? (mode === "fill" ? "1fr" : "auto") : value;
+
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ return (
+
+ {/* 라벨 */}
+
{label}
+
+ {/* 모드 드롭다운 */}
+
+
+ {isOpen && (
+
+ {(Object.keys(MODE_LABELS) as SizingMode[]).map((m) => (
+
+ ))}
+
+ )}
+
+
+ {/* 값 입력 */}
+
onValueChange(Number(e.target.value))}
+ className={cn(
+ "w-16 px-2 py-1.5 text-xs border border-zinc-200 rounded-md text-center",
+ isDisabled && "bg-zinc-100 text-zinc-400 cursor-not-allowed"
+ )}
+ />
+
+ {/* 단위 토글 */}
+ {!isDisabled && (
+
+ )}
+
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/SliderInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/SliderInput.tsx
new file mode 100644
index 0000000..f728f94
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/SliderInput.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { cn } from "@repo/utils";
+
+interface SliderInputProps {
+ value: number;
+ min?: number;
+ max?: number;
+ onChange: (value: number) => void;
+ size?: "single" | "small";
+}
+
+export default function SliderInput({
+ value,
+ min = 0,
+ max = 100,
+ onChange,
+ size = "small",
+}: SliderInputProps) {
+ const widthClass = size === "small" ? "w-[65px]" : "w-[140px]";
+
+ return (
+
+ onChange(Number(e.target.value))}
+ className={cn(
+ "w-full h-1 bg-zinc-200 rounded-full appearance-none cursor-pointer accent-[#2563EB]",
+ "[&::-webkit-slider-thumb]:appearance-none",
+ "[&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5",
+ "[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white",
+ "[&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-[#2563EB]",
+ "[&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:transition-transform",
+ "hover:[&::-webkit-slider-thumb]:scale-110"
+ )}
+ />
+
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/TextInput.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/TextInput.tsx
new file mode 100644
index 0000000..6c30e80
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/TextInput.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+interface TextInputProps {
+ id?: string;
+ name?: string;
+ value: string;
+ onChange: (val: string) => void;
+ placeholder?: string;
+}
+
+export default function TextInput({ id, name, value, onChange, placeholder }: TextInputProps) {
+ return (
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ className="flex w-full h-[33px] items-center px-3 py-2 rounded-[6px] border border-[#E4E4E7] bg-white font-inter font-normal text-[14px] leading-none text-zinc-900 placeholder:text-[#A1A1AA] outline-none focus:border-zinc-400 transition-colors"
+ />
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx b/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx
new file mode 100644
index 0000000..786cf18
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/atoms/ToggleButtonGroup.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { cn } from "@repo/utils";
+
+interface ToggleOption {
+ label: string;
+ value: string;
+}
+
+interface ToggleButtonGroupProps {
+ options: ToggleOption[];
+ value: string | string[];
+ onChange: (val: string) => void;
+ size?: "single" | "small";
+}
+
+export default function ToggleButtonGroup({
+ options,
+ value,
+ onChange,
+ size = "single",
+}: ToggleButtonGroupProps) {
+ const widthClass = size === "small" ? "w-[65px]" : "w-[140px]";
+
+ return (
+
+ {options.map((opt) => {
+ const isActive = Array.isArray(value)
+ ? value.includes(opt.value)
+ : value === opt.value;
+
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx
new file mode 100644
index 0000000..ad77e53
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/panels/ButtonPanel.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { ButtonNode } from "@repo/ui/types/nodes";
+import { useUpdateNode } from "@/stores/useEditorStore";
+import SidebarItem from "../atoms/SidebarItem";
+import TextInput from "../atoms/TextInput";
+import LayoutSection from "../sections/LayoutSection";
+
+interface ButtonPanelProps {
+ node: ButtonNode;
+}
+
+export default function ButtonPanel({ node }: ButtonPanelProps) {
+ const updateNode = useUpdateNode();
+
+ const handlePropChange = (key: string, value: string | number | boolean | object) => {
+ updateNode(node.id, { props: { ...node.props, [key]: value } });
+ };
+
+ return (
+
+ {/* Label Section */}
+
+ handlePropChange("text", val)}
+ placeholder="Enter button text..."
+ />
+
+
+ {/* Common Layout Section */}
+
+
+ {/* Button Specific Sections (State, Hover, Trigger etc.) */}
+
+ Button specialized settings (Hover, Trigger) will be added here.
+
+
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx
new file mode 100644
index 0000000..ebf5e73
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/panels/ImagePanel.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { WcxNode } from "@repo/ui/types/nodes";
+import { useUpdateNode } from "@/stores/useEditorStore";
+import SidebarItem from "../atoms/SidebarItem";
+import TextInput from "../atoms/TextInput";
+import LayoutSection from "../sections/LayoutSection";
+
+interface ImagePanelProps {
+ node: WcxNode;
+}
+
+export default function ImagePanel({ node }: ImagePanelProps) {
+ const updateNode = useUpdateNode();
+
+ return (
+
+
+
+ updateNode(node.id, { props: { ...node.props, alt: val } })
+ }
+ placeholder="Image description..."
+ />
+
+
+
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx b/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx
new file mode 100644
index 0000000..f66f55a
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/panels/TextPanel.tsx
@@ -0,0 +1,161 @@
+"use client";
+
+import { TextNode } from "@repo/ui/types/nodes";
+import { useUpdateNode } from "@/stores/useEditorStore";
+import SidebarItem from "../atoms/SidebarItem";
+import TextInput from "../atoms/TextInput";
+import SelectInput from "../atoms/SelectInput";
+import NumberInput from "../atoms/NumberInput";
+import ColorInput from "../atoms/ColorInput";
+import ToggleButtonGroup from "../atoms/ToggleButtonGroup";
+import ControlRow from "../atoms/ControlRow";
+import AlignPicker from "../atoms/AlignPicker";
+
+interface TextPanelProps {
+ node: TextNode;
+}
+
+export default function TextPanel({ node }: TextPanelProps) {
+ const updateNode = useUpdateNode();
+
+ const handlePropChange = (key: string, value: string | number | boolean | object) => {
+ updateNode(node.id, { props: { ...node.props, [key]: value } });
+ };
+
+ const handleStyleChange = (key: string, value: string | number | boolean | object) => {
+ updateNode(node.id, { style: { ...node.style, [key]: value } });
+ };
+
+ return (
+
+ {/* 1. Text Content - Added as per request */}
+
+ handlePropChange("text", val)}
+ placeholder="Enter text..."
+ />
+
+
+ {/* 2. Font Family */}
+
+ handleStyleChange("fontFamily", val)}
+ />
+
+
+ {/* 3. Size & Weight */}
+
+
+ handleStyleChange("fontSize", `${val}px`)}
+ />
+
+
+ handleStyleChange("fontWeight", val)}
+ />
+
+
+
+ {/* 4. Color */}
+
+ handleStyleChange("color", val)}
+ />
+
+
+ {/* 5. Style (Italic, Underline, Strike) */}
+
+ v !== "" && v !== "none")}
+ options={[
+ { label: "I", value: "italic" },
+ { label: "U", value: "underline" },
+ { label: "S", value: "line-through" },
+ ]}
+ onChange={(val) => {
+ if (val === "italic") {
+ handleStyleChange("fontStyle", node.style.fontStyle === "italic" ? "normal" : "italic");
+ } else {
+ handleStyleChange("textDecoration", node.style.textDecoration === val ? "none" : val);
+ }
+ }}
+ />
+
+
+ {/* 6. Alignment (Horizontal & Vertical) */}
+
+ handleStyleChange("justifyContent", val)}
+ />
+
+
+
+ handleStyleChange("alignItems", val)}
+ />
+
+
+ {/* 7. Line Height & Letter Spacing */}
+
+
+ handleStyleChange("lineHeight", `${val}%`)}
+ />
+
+
+ handleStyleChange("letterSpacing", `${val}px`)}
+ />
+
+
+
+ {/* 8. Heading Level */}
+
+ handlePropChange("level", val)}
+ />
+
+
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/LayoutSection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/LayoutSection.tsx
new file mode 100644
index 0000000..e8ddc15
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/sections/LayoutSection.tsx
@@ -0,0 +1,146 @@
+import { WcxNode } from "@repo/ui/types/nodes";
+import { useUpdateNode } from "@/stores/useEditorStore";
+import FieldRow from "../atoms/FieldRow";
+import DirectionToggle from "../atoms/DirectionToggle";
+import AlignPicker from "../atoms/AlignPicker";
+import SelectInput from "../atoms/SelectInput";
+import ToggleButtonGroup from "../atoms/ToggleButtonGroup";
+import SliderInput from "../atoms/SliderInput";
+import PaddingInput from "../atoms/PaddingInput";
+import NumberInput from "../atoms/NumberInput";
+
+interface LayoutSectionProps {
+ node: WcxNode;
+}
+
+export default function LayoutSection({ node }: LayoutSectionProps) {
+ const updateNode = useUpdateNode();
+
+ const handleStyleChange = (key: string, value: string | number) => {
+ updateNode(node.id, { style: { ...node.style, [key]: value } });
+ };
+
+ const direction = (node.style.flexDirection as "row" | "column") || "row";
+ const wrap = node.style.flexWrap === "wrap";
+
+ // Padding 파싱
+ const paddingValue = parseInt(String(node.style.padding)) || 0;
+ const paddingTop = parseInt(String(node.style.paddingTop)) || paddingValue;
+ const paddingRight = parseInt(String(node.style.paddingRight)) || paddingValue;
+ const paddingBottom = parseInt(String(node.style.paddingBottom)) || paddingValue;
+ const paddingLeft = parseInt(String(node.style.paddingLeft)) || paddingValue;
+
+ const handlePaddingChange = (padding: { top: number; right: number; bottom: number; left: number }) => {
+ if (padding.top === padding.right && padding.right === padding.bottom && padding.bottom === padding.left) {
+ updateNode(node.id, {
+ style: {
+ ...node.style,
+ padding: `${padding.top}px`,
+ paddingTop: undefined,
+ paddingRight: undefined,
+ paddingBottom: undefined,
+ paddingLeft: undefined,
+ },
+ });
+ } else {
+ updateNode(node.id, {
+ style: {
+ ...node.style,
+ padding: undefined,
+ paddingTop: `${padding.top}px`,
+ paddingRight: `${padding.right}px`,
+ paddingBottom: `${padding.bottom}px`,
+ paddingLeft: `${padding.left}px`,
+ },
+ });
+ }
+ };
+
+ return (
+ <>
+ {/* Type */}
+
+ { }}
+ />
+
+
+ {/* Direction */}
+
+ handleStyleChange("flexDirection", v)}
+ />
+
+
+ {/* Distribute */}
+
+ handleStyleChange("justifyContent", v)}
+ />
+
+
+ {/* Align */}
+
+ handleStyleChange("alignItems", v)}
+ />
+
+
+ {/* Wrap */}
+
+ handleStyleChange("flexWrap", v === "Yes" ? "wrap" : "nowrap")}
+ />
+
+
+ {/* Gap */}
+
+ handleStyleChange("gap", `${v}px`)}
+ size="small"
+ />
+ handleStyleChange("gap", `${v}px`)}
+ size="small"
+ />
+
+
+ {/* Padding */}
+
+
+
+ >
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx
new file mode 100644
index 0000000..04ed4ba
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/sections/PositionSection.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import { WcxNode } from "@repo/ui/types/nodes";
+import { useUpdateNode, useUpdateNodeLayout } from "@/stores/useEditorStore";
+import FieldRow from "../atoms/FieldRow";
+import NumberInput from "../atoms/NumberInput";
+import SelectInput from "../atoms/SelectInput";
+
+interface PositionSectionProps {
+ node: WcxNode;
+}
+
+type PositionType = "relative" | "absolute" | "fixed" | "sticky";
+
+export default function PositionSection({ node }: PositionSectionProps) {
+ const updateNode = useUpdateNode();
+ const updateNodeLayout = useUpdateNodeLayout();
+ const positionType = (node.style.position as PositionType) || "relative";
+
+ const handleStyleChange = (key: string, value: string | number | undefined) => {
+ updateNode(node.id, { style: { ...node.style, [key]: value } });
+ };
+
+ const handleLayoutChange = (key: string, value: string | number) => {
+ updateNodeLayout(node.id, { [key]: value });
+ };
+
+ const handlePositionTypeChange = (type: PositionType) => {
+ if (type === "relative") {
+ // relative 전환 시 스타일에서 위치 정보 제거
+ updateNode(node.id, {
+ style: {
+ ...node.style,
+ position: type,
+ top: undefined,
+ right: undefined,
+ bottom: undefined,
+ left: undefined,
+ },
+ });
+ // x, y 좌표는 유지하거나 필요에 따라 초기화할 수 있지만 일단 유지
+ } else {
+ handleStyleChange("position", type);
+ }
+ };
+
+ const showOffsets = positionType === "absolute" || positionType === "fixed";
+ const showStickyTop = positionType === "sticky";
+
+ return (
+ <>
+ {/* Absolute / Fixed → Top(y) / Left(x) (Layout 필드 사용) */}
+ {showOffsets && (
+ <>
+
+ handleLayoutChange("y", v)}
+ />
+
+
+ handleLayoutChange("x", v)}
+ />
+
+ >
+ )}
+
+ {/* Sticky → Top only (y 사용) */}
+ {showStickyTop && (
+
+ handleLayoutChange("y", v)}
+ />
+
+ )}
+
+ {/* Type 드롭다운 — 항상 표시 */}
+
+ handlePositionTypeChange(v as PositionType)}
+ />
+
+ >
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/PropertySection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/PropertySection.tsx
new file mode 100644
index 0000000..87e92d7
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/sections/PropertySection.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+
+import { Minus, Plus } from "lucide-react";
+import { useState } from "react";
+
+interface PropertySectionProps {
+ title: string;
+ defaultOpen?: boolean;
+ children: React.ReactNode;
+}
+
+/**
+ * 사이드바 레이어 섹션
+ * --- 구분선 ---
+ * Position [-]
+ * (fields...)
+ */
+export default function PropertySection({ title, defaultOpen = true, children }: PropertySectionProps) {
+ const [isOpen, setIsOpen] = useState(defaultOpen);
+
+ return (
+
+ {/* 헤더 */}
+
+
+ {/* 본문 */}
+ {isOpen && (
+
+ {children}
+
+ )}
+
+ );
+}
diff --git a/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx b/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx
new file mode 100644
index 0000000..fa3286c
--- /dev/null
+++ b/apps/editor/src/widgets/right-sidebar/ui/sections/SizeSection.tsx
@@ -0,0 +1,148 @@
+"use client";
+
+import { WcxNode } from "@repo/ui/types/nodes";
+import { useUpdateNode, useUpdateNodeLayout, useCurNodes } from "@/stores/useEditorStore";
+import FieldRow from "../atoms/FieldRow";
+import NumberInput from "../atoms/NumberInput";
+import SelectInput from "../atoms/SelectInput";
+
+interface SizeSectionProps {
+ node: WcxNode;
+}
+
+type SizingMode = "fixed" | "fill" | "fit" | "relative";
+
+export default function SizeSection({ node }: SizeSectionProps) {
+ const updateLayout = useUpdateNodeLayout();
+ const updateNode = useUpdateNode();
+ const nodes = useCurNodes();
+
+ const widthMode: SizingMode = (node.layout as { widthMode: SizingMode }).widthMode || "fixed";
+ const heightMode: SizingMode = (node.layout as { heightMode: SizingMode }).heightMode || "fixed";
+
+ const widthValue = typeof node.layout.width === "number"
+ ? node.layout.width
+ : parseInt(String(node.layout.width)) || 0;
+
+ const heightValue = typeof node.layout.height === "number"
+ ? node.layout.height
+ : parseInt(String(node.layout.height)) || 0;
+
+ const handleStyleChange = (key: string, value: string | number) => {
+ updateNode(node.id, { style: { ...node.style, [key]: value } });
+ };
+
+ const convertValue = (oldValue: number, oldMode: SizingMode, newMode: SizingMode, isHeight: boolean) => {
+ if (oldMode === newMode) return oldValue;
+ if (newMode === 'fill') return 1;
+ if (newMode === 'fit') return oldValue;
+
+ // Fixed -> Relative
+ if (oldMode === 'fixed' && newMode === 'relative') {
+ const parent = nodes?.find(n => n.id === node.parent_id);
+ const parentSize = isHeight
+ ? (typeof parent?.layout.height === 'number' ? parent.layout.height : 1000)
+ : (typeof parent?.layout.width === 'number' ? parent.layout.width : 1000);
+ return Math.round((oldValue / parentSize) * 100);
+ }
+
+ // Relative -> Fixed
+ if (oldMode === 'relative' && newMode === 'fixed') {
+ const parent = nodes?.find(n => n.id === node.parent_id);
+ const parentSize = isHeight
+ ? (typeof parent?.layout.height === 'number' ? parent.layout.height : 1000)
+ : (typeof parent?.layout.width === 'number' ? parent.layout.width : 1000);
+ return Math.round((oldValue / 100) * parentSize);
+ }
+
+ return oldValue;
+ };
+
+ const handleModeChange = (key: 'widthMode' | 'heightMode', newMode: SizingMode) => {
+ const isHeight = key === 'heightMode';
+ const sizeKey = isHeight ? 'height' : 'width';
+ const oldMode = isHeight ? heightMode : widthMode;
+ const oldValue = isHeight ? heightValue : widthValue;
+
+ const newValue = convertValue(oldValue, oldMode, newMode, isHeight);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ updateLayout(node.id, { [key]: newMode, [sizeKey]: newValue } as any);
+ };
+
+ const modeOptions = [
+ { label: "Fixed", value: "fixed" },
+ { label: "Rel", value: "relative" },
+ { label: "Fill", value: "fill" },
+ { label: "Fit", value: "fit" },
+ ];
+
+ return (
+ <>
+ {/* Width: 값 / 모드 */}
+
+ updateLayout(node.id, { width: v })}
+ size="small"
+ suffix={widthMode === "relative" ? "%" : widthMode === "fill" ? "fr" : undefined}
+ blurred={widthMode === "fit"}
+ />
+ handleModeChange("widthMode", v as SizingMode)}
+ size="small"
+ />
+
+
+ {/* Height: 값 / 모드 */}
+
+ updateLayout(node.id, { height: v })}
+ size="small"
+ suffix={heightMode === "relative" ? "%" : heightMode === "fill" ? "fr" : undefined}
+ blurred={heightMode === "fit"}
+ />
+ handleModeChange("heightMode", v as SizingMode)}
+ size="small"
+ />
+
+
+ {/* Min Width */}
+
+ handleStyleChange("minWidth", v)}
+ />
+
+
+ {/* Max Width */}
+
+ handleStyleChange("maxWidth", v)}
+ />
+
+
+ {/* Min Height */}
+
+ handleStyleChange("minHeight", v)}
+ />
+
+
+ {/* Max Height */}
+
+ handleStyleChange("maxHeight", v)}
+ />
+
+ >
+ );
+}
diff --git a/packages/ui/src/components/Heading.tsx b/packages/ui/src/components/Heading.tsx
index 07ca899..33acd6b 100644
--- a/packages/ui/src/components/Heading.tsx
+++ b/packages/ui/src/components/Heading.tsx
@@ -15,7 +15,7 @@ export default function HeadingComponent({
{text}
diff --git a/packages/ui/src/components/Text.tsx b/packages/ui/src/components/Text.tsx
index 8ca9a55..2d829e1 100644
--- a/packages/ui/src/components/Text.tsx
+++ b/packages/ui/src/components/Text.tsx
@@ -14,7 +14,7 @@ export default function TextComponent({
{text}
diff --git a/packages/ui/src/core/EditorNodeWrapper.tsx b/packages/ui/src/core/EditorNodeWrapper.tsx
index ac27061..e197804 100644
--- a/packages/ui/src/core/EditorNodeWrapper.tsx
+++ b/packages/ui/src/core/EditorNodeWrapper.tsx
@@ -38,7 +38,7 @@ export default function EditorNodeWrapper({
};
const { id } = node;
- const { width, height, x, y } = node.layout;
+ const { width, height, x, y, zIndex } = node.layout;
const selectedNodeGuideClasses = {
handle: "bg-white border-2 rounded-full border-rnd-handle !w-2 !h-2 ",
outline: "ring ring-2 ring-rnd-handle",
@@ -50,6 +50,11 @@ export default function EditorNodeWrapper({
e.stopPropagation()}
//TODO-일단 이동중에 스토어 업데이트는 미루기 -> 성능 이슈
diff --git a/packages/ui/src/types/nodes.ts b/packages/ui/src/types/nodes.ts
index 04d977f..0aa0d11 100644
--- a/packages/ui/src/types/nodes.ts
+++ b/packages/ui/src/types/nodes.ts
@@ -19,8 +19,12 @@ export interface BaseNode {
layout: {
x: number;
y: number;
- width: number; // 혹은 string ('100%')
- height: number; // 혹은 string ('auto')
+ width: number | string; // 숫자(px) 또는 문자열('100%', 'auto')
+ height: number | string; // 숫자(px) 또는 문자열('100%', 'auto')
+ widthMode: 'fixed' | 'fill' | 'fit' | 'relative'; // Framer sizing 모드
+ heightMode: 'fixed' | 'fill' | 'fit' | 'relative'; // Framer sizing 모드
+ widthUnit: 'px' | '%';
+ heightUnit: 'px' | '%';
zIndex: number;
};
}