diff --git a/apps/editor/package.json b/apps/editor/package.json index 355ba28..e2d9611 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -14,7 +14,7 @@ "@repo/ui": "workspace:*", "@tanstack/react-query": "^5.90.8", "json-server": "^1.0.0-beta.3", - "next": "15.5.6", + "next": "15.5.9", "react": "19.1.0", "react-dom": "19.1.0", "zustand": "^5.0.8" diff --git a/apps/editor/src/stores/useEditorStore.ts b/apps/editor/src/stores/useEditorStore.ts new file mode 100644 index 0000000..f83dd1d --- /dev/null +++ b/apps/editor/src/stores/useEditorStore.ts @@ -0,0 +1,45 @@ +import { create } from "zustand"; +import { combine, devtools } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; + +const useEditorStore = create( + devtools( + immer( + combine( + { + selectedNodeId: null as string | null, + }, + (set) => ({ + selectNode(id: string) { + set( + (state) => { + state.selectedNodeId = id; + }, + false, + "editorStore/selectNode", + ); + }, + clearNode() { + set( + (state) => { + state.selectedNodeId = null; + }, + false, + "editorStore/clearNode", + ); + }, + }), + ), + ), + { + name: "editorStore", + }, + ), +); + +export const useSelectedNodeId = () => + useEditorStore((store) => store.selectedNodeId); + +export const useSelectNode = () => useEditorStore((store) => store.selectNode); + +export const useClearNode = () => useEditorStore((store) => store.clearNode); diff --git a/packages/ui/package.json b/packages/ui/package.json index 1c42cde..45b7be5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -8,7 +8,7 @@ "type-check": "tsc --noEmit" }, "peerDependencies": { - "next": "15.5.6", + "next": "15.5.9", "react": "19.1.0" }, "devDependencies": { @@ -16,5 +16,8 @@ "@types/react": "^19", "@types/react-dom": "^19", "typescript": "^5" + }, + "dependencies": { + "framer-motion": "^12.23.26" } } diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx index 0c87419..12217f5 100644 --- a/packages/ui/src/components/Modal.tsx +++ b/packages/ui/src/components/Modal.tsx @@ -1,8 +1,30 @@ -import { useBuilderMode } from "context/builderMode"; -import { useRuntimeState } from "context/runtimeContext"; -import { useRef } from "react"; import { ModalNode, NodeComponentProps } from "types"; import processNodeStyles from "utils/processNodeStyles"; +import { motion } from "framer-motion"; + +const animationVariants = { + fade: { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + exit: { opacity: 0 }, + }, + "slide-up": { + hidden: { opacity: 0, y: 100 }, + visible: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: 100 }, + }, + "slide-right": { + hidden: { opacity: 0, x: 100 }, + visible: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: 100 }, + }, + // 기본값 (Zoom In) + default: { + hidden: { opacity: 0, scale: 0.9 }, + visible: { opacity: 1, scale: 1 }, + exit: { opacity: 0, scale: 0.9 }, + }, +}; /** * @@ -13,77 +35,30 @@ import processNodeStyles from "utils/processNodeStyles"; */ export default function Modal({ node, - props, style, children, //모달안에 들어갈 버튼, 텍스트 등이 children으로 올 수 있습니다. }: NodeComponentProps) { - const dialogRef = useRef(null); - const curNodeId = node.id; + const animationType = node.props.animation || "default"; - const { mode } = useBuilderMode(); - - //Context를 통한 상태 구독 - //TODO-만약 context의 형태가 context API에서 zustand로 바뀐다면 해당 로직도 수정되야 합니다. - const { state, updateNodeState } = useRuntimeState(); - - const isOpen = state[curNodeId]?.isOpen ?? false; - - const { title, showCloseButton, closeOnOverlayClick } = props; - + //스타일 변환 const nodeStyleObj = processNodeStyles(style); - //닫기 핸들러 - function closeHandler(e?: React.MouseEvent) { - if (mode === "editor") return; - - e?.stopPropagation(); - - updateNodeState(curNodeId, { isOpen: false }); - } - - function overlayClickHandler(e?: React.MouseEvent) { - if (props.closeOnOverlayClick === false) return; - if (e?.target !== dialogRef.current) return; - //오직 ::backdrop에 이벤트가 발생했을 경우에만 발생합니다. - closeHandler(e); - } - - if (!isOpen) return null; - - //굳이 div로 오버레이가 필요 없을듯..? ::backdrop과 dialog에서 제공하는 .showModal() 사용해보기 - // (closeOnOverlayClick 존재 유무에 따라 div태그의 핸들러 동작을 결정하자.) - - //기본적인 모달 태그는 dialog로 결정 - //이때 배경 클릭시 return ( - e.stopPropagation()} + className={`${style.root?.className || ""} pointer-events-auto relative bg-white shadow-2xl`} + style={nodeStyleObj.root} + variants={animationVariants[animationType as keyof typeof animationVariants] || animationVariants.default} + initial="hidden" + animate="visible" + exit="exit" + transition={{ duration: 0.2, ease: "easeOut" }} > - {/* 실제 모달 컨테이너 */} -
e.stopPropagation()} - className={`${style.root?.className || ""} relative min-h-[200px] min-w-[300px] rounded-lg bg-white shadow-xl`} - style={nodeStyleObj.root} - > - {/* 모달 헤더 */} - {(props.title || props.showCloseButton) && ( -
- {props.title &&

{props.title}

} - - {/* TODO-svg 공통 모듈 작업 필요 */} - {props.showCloseButton && } -
- )} - - {/* [Body] 자식 노드들(버튼, 이미지, 텍스트)이 여기에 렌더링 됨 */} -
{children}
-
-
+ {/* [Body] 자식 노드들(버튼, 이미지, 텍스트)이 여기에 렌더링 됨 */} +
{children}
+ ); } diff --git a/packages/ui/src/components/ModalHost.tsx b/packages/ui/src/components/ModalHost.tsx new file mode 100644 index 0000000..063e661 --- /dev/null +++ b/packages/ui/src/components/ModalHost.tsx @@ -0,0 +1,60 @@ +import { useRuntimeState } from "context/runtimeContext"; +import { useProjectData } from "hooks/useProjectData"; +import Modal from "./Modal"; +import { ModalNode } from "types"; +import { motion, AnimatePresence } from "framer-motion"; + +export default function ModalHost() { + //Context를 통한 상태 구독 + //TODO-만약 context의 형태가 context API에서 zustand로 바뀐다면 해당 로직도 수정되야 합니다. + const { activeModalId, closeModal } = useRuntimeState(); + + //DB에서 관련 데이터 불러오기 + const { modal: targetNode } = useProjectData(activeModalId); + + // 닫기 핸들러 + const handleClose = () => { + closeModal(); + }; + + const handleOverlayClick = (closeOnOverlayClick?: boolean) => { + if (closeOnOverlayClick === false) return; + handleClose(); + }; + + // 위치 프리셋 + const alignmentStyles = { + center: "items-center justify-center", + top: "items-start justify-center pt-10", + bottom: "items-end justify-center pb-0", + left: "items-center justify-start h-full", + right: "items-center justify-end h-full", + }; + + return ( + + {activeModalId && targetNode && ( + + handleOverlayClick(targetNode.props.closeOnOverlayClick) + } + > +
e.stopPropagation()}> + + {/* Children */} + +
+
+ )} +
+ ); +} diff --git a/packages/ui/src/components/modal_system_architecture.md b/packages/ui/src/components/modal_system_architecture.md new file mode 100644 index 0000000..1ff2e41 --- /dev/null +++ b/packages/ui/src/components/modal_system_architecture.md @@ -0,0 +1,214 @@ +# 🏗️ WebCreator-X: Modal System Architecture + +**Version:** 2.0.0 (Final) + +**Last Updated:** 2025.12.18 + +**Author:** WebCreator-X Core Team + +## 1. 개요 (Overview) + +본 문서는 웹 빌더 내에서 가장 복잡한 인터랙션 요소인 **모달(Modal) 시스템**의 설계 및 구현 명세를 정의합니다. +기존의 개별 상태 관리 방식의 비효율성을 개선하기 위해 **중앙 집중식 호스트(Centralized Host)** 패턴을 채택하였으며, 에디터에서의 작업 효율을 위해 **격리 편집 모드(Isolation Mode)**를 도입했습니다. + +### 🎯 핵심 목표 + +1. **성능 최적화:** 수십 개의 모달이 있어도 초기 로딩과 리렌더링 비용을 최소화한다. +2. **편집 편의성:** 에디터에서 모달이 작업 공간을 침범하지 않도록 별도 레이어에서 관리한다. +3. **연결성:** 버튼(Trigger)과 모달(Target)을 ID 참조(Reference) 방식으로 느슨하게 연결한다. + +--- + +## 2. 아키텍처 (Architecture) + +### 2.1 데이터 흐름도 (Data Flow) + +모든 모달의 제어는 `RuntimeContext`를 통해 단방향으로 흐릅니다. + +```mermaid +sequenceDiagram + participant User + participant Button as Button Node (Trigger) + participant Context as RuntimeContext (Store) + participant Host as ModalHost (Global Renderer) + participant Modal as Modal Content + + Note over User, Button: [Action] 1. 버튼 클릭 + User->>Button: Click Event + Button->>Button: useActionHandler() 실행 + Button->>Context: openModal("modal-login-ID") + + Note over Context, Host: [State Update] 2. 상태 변경 + Context->>Context: activeModalId = "modal-login-ID" + Context-->>Host: 변경 감지 (Re-render) + + Note over Host, Modal: [Rendering] 3. 화면 표시 + Host->>Host: Global Modal List에서 ID 검색 + Host->>Modal: 마운트 (Portal) + +``` + +### 2.2 핵심 패턴: Modal Host + +- **기존 방식 (Anti-Pattern):** 각 모달 컴포넌트가 개별적으로 `isOpen` 상태를 구독. (N개의 리스너) +- **최종 방식 (Best Practice):** 최상위 `ModalHost` 컴포넌트 **단 하나만** Context를 구독. 활성화된 모달 ID가 있을 때만 해당 콘텐츠를 동적으로 로드하여 렌더링. + +--- + +## 3. 데이터 스키마 (Data Schema) + +### 3.1 Runtime State (`RuntimeContext`) + +복잡한 객체 대신 **단순 문자열 ID** 하나만 관리합니다. + +```typescript +interface RuntimeContextType { + // 현재 열려있는 모달의 ID (없으면 null) + activeModalId: string | null; + + // 액션 함수 + openModal: (id: string) => void; + closeModal: () => void; +} +``` + +### 3.2 Node Data Structure + +DB의 `nodes` 테이블에 저장되는 데이터 구조입니다. + +#### A. Modal Node (Target) + +모달은 페이지(`page_id`)에 귀속되지 않고 전역(`Global`)으로 관리되는 것을 권장합니다. + +```typescript +interface ModalNode { + id: string; + type: "Modal"; + props: { + // ⭐️ 위치 프리셋: 마우스 드래그 대신 설정값으로 위치 결정 + alignment: "center" | "top" | "bottom" | "left" | "right"; + + // 스타일 옵션 + width?: string; + overlayColor?: string; // 예: "bg-black/50" + closeOnOverlayClick?: boolean; + + // 애니메이션 효과 + animation?: "fade" | "slide-up" | "slide-right"; + }; + children: WcxNode[]; // 모달 내부의 텍스트, 인풋, 버튼 등 +} +``` + +#### B. Button Node (Trigger) + +버튼은 모달을 직접 포함하지 않고 **ID만 참조**합니다. + +```typescript +interface ButtonNode { + type: "Button"; + props: { + text: "로그인"; + action: { + type: "modal"; // 동작 타입 + targetId: "modal-1234"; // 🔗 연결된 모달의 ID 참조 + }; + }; +} +``` + +--- + +## 4. 구현 상세 (Implementation Details) + +### 4.1 `RuntimeContext.tsx` + +상태 관리의 심장부입니다. + +```tsx +export function RuntimeProvider({ children }: { children: ReactNode }) { + // 초기값 null = 아무것도 안 열림 + const [activeModalId, setActiveModalId] = useState(null); + + const openModal = (id: string) => setActiveModalId(id); + const closeModal = () => setActiveModalId(null); + + return ( + + {children} + + ); +} +``` + +### 4.2 `ModalHost.tsx` + +실제 렌더링을 담당하는 총괄 매니저입니다. `layout.tsx` 최상단에 배치됩니다. + +```tsx +export function ModalHost() { + const { activeModalId, closeModal } = useRuntimeState(); + const { modals } = useProjectData(); // 전역 모달 리스트 Fetch + + // 1. 활성화된 모달이 없으면 렌더링 자체를 안 함 (성능 최적화) + if (!activeModalId) return null; + + // 2. ID에 맞는 모달 데이터 찾기 + const targetNode = modals.find((m) => m.id === activeModalId); + if (!targetNode) return null; + + return ( + // 3. Portal을 사용하여 DOM 최상위로 이동 (z-index 문제 해결) +
+
e.stopPropagation()}> + +
+
+ ); +} +``` + +--- + +## 5. UI/UX 전략 (Editor vs Live) + +### 5.1 격리 편집 모드 (Isolation Mode) + +에디터에서 모달을 편집할 때의 UX 표준입니다. + +1. **진입:** 사이드바 [모달 관리] 탭에서 모달 선택. +2. **화면 변화:** + +- 기존 캔버스(페이지 노드들)는 `Dimmed` (어둡게) 처리되고 선택 불가능 상태가 됨. +- 선택된 모달만 화면에 렌더링됨. + +3. **위치 설정:** 마우스 드래그 불가. 우측 속성 패널의 **[위치 프리셋]** 버튼(상/하/좌/우/중앙)으로 위치 조정. +4. **복귀:** 상단 [편집 종료] 버튼 클릭 시 모달이 사라지고 원래 페이지 편집 화면으로 복귀. + +### 5.2 위치 프리셋 (Alignment Presets) + +사용자가 반응형 CSS를 몰라도 완벽한 위치를 잡을 수 있게 합니다. + +| 프리셋 이름 | 적용 CSS (Tailwind) | 용도 | +| ----------- | ----------------------------------- | ------------------------ | +| **Center** | `items-center justify-center` | 알림창, 로그인 폼 | +| **Top** | `items-start justify-center pt-10` | 토스트 메시지, 상단 공지 | +| **Bottom** | `items-end justify-center pb-0` | 모바일 바텀 시트 | +| **Left** | `items-center justify-start h-full` | 사이드바 메뉴 (Drawer) | +| **Right** | `items-center justify-end h-full` | 장바구니, 필터 | + +--- + +## 6. 개발 로드맵 (Roadmap) + +1. **Phase 1 (Core):** `RuntimeContext`, `ModalHost`, `ModalRenderer` 구현. 기본 `Center` 정렬만 지원. +2. **Phase 2 (Editor UX):** 사이드바 모달 리스트 UI 및 격리 편집 모드(Dimmed 처리) 구현. +3. **Phase 3 (Presets):** Top/Bottom/Side 정렬 및 슬라이드 애니메이션 추가. +4. **Phase 4 (Templates):** 빈 모달 대신 '로그인', '뉴스레터' 등 템플릿 제공. + +--- + +이 문서는 WebCreator-X 팀의 합의된 모달 시스템 설계이며, 향후 기능 확장 시 본 문서를 기준으로 업데이트합니다. diff --git a/packages/ui/src/context/runtimeContext.tsx b/packages/ui/src/context/runtimeContext.tsx index b8d4246..7b6934f 100644 --- a/packages/ui/src/context/runtimeContext.tsx +++ b/packages/ui/src/context/runtimeContext.tsx @@ -8,31 +8,27 @@ import { createContext, ReactNode, useContext, useState } from "react"; -// 런타임에 변경된 노드들의 상태를 저장하는 객체 -// 예: { "node-123": { text: "11" }, "modal-456": { isOpen: true } } -type RuntimeState = Record; - interface RuntimeContextType { - state: RuntimeState; - updateNodeState: (id: string, newState: any) => void; + activeModalId: string | null; + openModal: (id: string) => void; + closeModal: () => void; } const RuntimeContext = createContext(null); //TODO-빠른 구현을 위해 Context를 사용했지만 하나의 상태가 바뀌더라도 해당 context를 구독하는 다른 노드들도 리렌더링이 발생하는 위험이 존재합니다. 꼭 추후에 상태 관리 방식을 리팩토링 해야합니다. +// -> ModalHost를 도입해서 괜찮을듯? export function RuntimeProvider({ children }: { children: ReactNode }) { - const [state, setState] = useState({}); + const [activeModalId, setActiveModalId] = useState(null); + + const openModal = (id: string) => setActiveModalId(id); - function updateNodeState(id: string, partial: any) { - setState((prev) => ({ - ...prev, - [id]: { ...prev[id], ...partial }, - })); - } + const closeModal = () => setActiveModalId(null); const contextValue = { - state, - updateNodeState, + activeModalId, + openModal, + closeModal, }; return ( diff --git a/packages/ui/src/core/BaseLayoutWrapper.tsx b/packages/ui/src/core/BaseLayoutWrapper.tsx new file mode 100644 index 0000000..4946a1e --- /dev/null +++ b/packages/ui/src/core/BaseLayoutWrapper.tsx @@ -0,0 +1,15 @@ +import { BaseNode } from "types"; + +interface Props { + children: React.ReactNode; + node: BaseNode; +} + +export default function BaseLayoutNodeWrapper({ children, node }: Props) { + const { x: top, y: left, width, height, zIndex } = node.layout; + return ( +
+ {children} +
+ ); +} diff --git a/packages/ui/src/core/DraggableNodeWrapper.tsx b/packages/ui/src/core/DraggableNodeWrapper.tsx index 9b149b7..b896ee4 100644 --- a/packages/ui/src/core/DraggableNodeWrapper.tsx +++ b/packages/ui/src/core/DraggableNodeWrapper.tsx @@ -1,5 +1,4 @@ -import { useBuilderMode } from "context/builderMode"; -import { CSSProperties } from "react"; +//에디터 모드전용 노드 렌더러 래퍼 컴포넌트 import { BaseNode } from "types"; interface WrapperProps { @@ -15,8 +14,7 @@ export default function DraggableNodeWrapper({ isSelected, onDragStart, }: WrapperProps) { - const { mode } = useBuilderMode(); - const wrapperStyle: CSSProperties = { + const wrapperStyle: React.CSSProperties = { position: "absolute", // 핵심: 캔버스 내에서 자유 배치 left: node["layout"].x, top: node["layout"].y, @@ -25,14 +23,14 @@ export default function DraggableNodeWrapper({ zIndex: node["layout"].zIndex, // 선택되었을 때 시각적 피드백 (테두리 등) - outline: mode === "editor" && isSelected ? "2px solid blue" : "none", - cursor: mode === "editor" ? "move" : "default", + outline: isSelected ? "2px solid blue" : "none", + cursor: "move", }; return (
mode === "editor" && onDragStart(e, node.id)} + onMouseDown={(e) => onDragStart(e, node.id)} className="" //TODO - 필요시 클래스네임 추가(dnd상황에서 스타일 강조 등) > {/* 실제 컴포넌트(Hero 등)는 이 안에 렌더링됨 */} @@ -40,7 +38,7 @@ export default function DraggableNodeWrapper({ {/* 리사이즈 핸들 등은 에디터 모드에서만 오버레이로 표시 */} {/* TODO-추후 호버시 리사이즈 핸들 렌더링 하도록 수정 필요! */} - {isSelected && mode === "editor" && ( + {isSelected && ( <>
{/* TODO-기타 리사이즈 핸들들 렌더링은 이곳에서 ... */} diff --git a/packages/ui/src/core/LiveModeWrapper.tsx b/packages/ui/src/core/LiveModeWrapper.tsx new file mode 100644 index 0000000..038836b --- /dev/null +++ b/packages/ui/src/core/LiveModeWrapper.tsx @@ -0,0 +1,21 @@ +import { BaseNode } from "types"; + +//라이브 모드에서 사용되는 노드 렌더러 래퍼 컴포넌트입니다. +interface props { + children: React.ReactNode; + node: BaseNode; +} + +export default function LiveModeWrapper({ children, node }: props) { + const { x, y, width, height, zIndex } = node.layout; + + const wrapperStyle: React.CSSProperties = { + position: "absolute", + left: x, + top: y, + width, + height, + zIndex, + }; + return
{children}
; +} diff --git a/packages/ui/src/hooks/useProjectData.tsx b/packages/ui/src/hooks/useProjectData.tsx new file mode 100644 index 0000000..14f7aca --- /dev/null +++ b/packages/ui/src/hooks/useProjectData.tsx @@ -0,0 +1,26 @@ +// 임시 Mock 데이터 Hook +// 실제 DB 연동 전까지 데이터 구조를 테스트 용도로 사용합니다. + +import { ModalNode } from "../types/nodes"; + + +//실제로는 id를 입력받으면 DB에서 해당하는 id의 노드데이터를 가져와서 반환하는 함수 +export function useProjectData(id: string | null) { + if (!id) return { modal: null }; + const modal: ModalNode = { + id: "modal-sample-1", + type: "Modal", + page_id: "global", + parent_id: null, + position: 0, + style: {}, + layout: { x: 0, y: 0, width: 400, height: 300, zIndex: 100 }, + props: { + alignment: "center", + overlayColor: "bg-black/50", + closeOnOverlayClick: true, + }, + }; + + return { modal }; +} diff --git a/packages/ui/src/types/componentProps.ts b/packages/ui/src/types/componentProps.ts index 2c37701..2bdf941 100644 --- a/packages/ui/src/types/componentProps.ts +++ b/packages/ui/src/types/componentProps.ts @@ -1,6 +1,7 @@ //노드의 타입별로 달라지는 props의 타입을 정의. import { NodeAction } from "./nodeAction"; +import { WcxNode } from "./nodes"; // 1. Hero 컴포넌트 Props export interface HeroProps { @@ -49,12 +50,11 @@ export interface ContainerProps { } export interface ModalProps { - //모달의 제목 - title?: string; - - //닫기 버튼 렌더링 여부 - showCloseButton?: boolean; - - //백그라운드 클릭시 닫기 여부 + // 위치 프리셋 (핵심) + alignment: "center" | "top" | "bottom" | "left" | "right"; + // 오버레이 설정 + overlayColor?: string; // "bg-black/50" closeOnOverlayClick?: boolean; + // 애니메이션 프리셋 + animation?: "fade" | "slide-up" | "slide-left"; } diff --git a/packages/ui/src/types/project.ts b/packages/ui/src/types/project.ts new file mode 100644 index 0000000..14e88c2 --- /dev/null +++ b/packages/ui/src/types/project.ts @@ -0,0 +1,6 @@ +import { ModalNode } from "./nodes"; + +export interface ProjectData { + //pages: PageNode[]; //TODO-PageNode타입 구현 예정 + modal: ModalNode[]; // 모달은 별도 리스트로 관리 (성능 및 재사용성) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb355bc..576aab3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^1.0.0-beta.3 version: 1.0.0-beta.3 next: - specifier: 15.5.6 - version: 15.5.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.5.9 + version: 15.5.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -87,9 +87,12 @@ importers: packages/ui: dependencies: + framer-motion: + specifier: ^12.23.26 + version: 12.23.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.5.6 - version: 15.5.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.5.9 + version: 15.5.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -332,56 +335,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@15.5.6': - resolution: {integrity: sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==} + '@next/env@15.5.9': + resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==} '@next/eslint-plugin-next@15.5.6': resolution: {integrity: sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==} - '@next/swc-darwin-arm64@15.5.6': - resolution: {integrity: sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==} + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.6': - resolution: {integrity: sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==} + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.6': - resolution: {integrity: sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==} + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.5.6': - resolution: {integrity: sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==} + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.5.6': - resolution: {integrity: sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==} + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.5.6': - resolution: {integrity: sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==} + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.5.6': - resolution: {integrity: sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==} + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.6': - resolution: {integrity: sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==} + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1203,6 +1206,20 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + framer-motion@12.23.26: + resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1604,6 +1621,12 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion-dom@12.23.23: + resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -1628,8 +1651,8 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} - next@15.5.6: - resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==} + next@15.5.9: + resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -2353,34 +2376,34 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@15.5.6': {} + '@next/env@15.5.9': {} '@next/eslint-plugin-next@15.5.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.5.6': + '@next/swc-darwin-arm64@15.5.7': optional: true - '@next/swc-darwin-x64@15.5.6': + '@next/swc-darwin-x64@15.5.7': optional: true - '@next/swc-linux-arm64-gnu@15.5.6': + '@next/swc-linux-arm64-gnu@15.5.7': optional: true - '@next/swc-linux-arm64-musl@15.5.6': + '@next/swc-linux-arm64-musl@15.5.7': optional: true - '@next/swc-linux-x64-gnu@15.5.6': + '@next/swc-linux-x64-gnu@15.5.7': optional: true - '@next/swc-linux-x64-musl@15.5.6': + '@next/swc-linux-x64-musl@15.5.7': optional: true - '@next/swc-win32-arm64-msvc@15.5.6': + '@next/swc-win32-arm64-msvc@15.5.7': optional: true - '@next/swc-win32-x64-msvc@15.5.6': + '@next/swc-win32-x64-msvc@15.5.7': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3347,6 +3370,15 @@ snapshots: dependencies: is-callable: 1.2.7 + framer-motion@12.23.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + motion-dom: 12.23.23 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3731,6 +3763,12 @@ snapshots: minimist@1.2.8: {} + motion-dom@12.23.23: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -3743,9 +3781,9 @@ snapshots: negotiator@0.6.4: {} - next@15.5.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.5.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.5.6 + '@next/env': 15.5.9 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001757 postcss: 8.4.31 @@ -3753,14 +3791,14 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.6 - '@next/swc-darwin-x64': 15.5.6 - '@next/swc-linux-arm64-gnu': 15.5.6 - '@next/swc-linux-arm64-musl': 15.5.6 - '@next/swc-linux-x64-gnu': 15.5.6 - '@next/swc-linux-x64-musl': 15.5.6 - '@next/swc-win32-arm64-msvc': 15.5.6 - '@next/swc-win32-x64-msvc': 15.5.6 + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core'