From c22fd8ae3854ed8efc84890f965b91709923e12f Mon Sep 17 00:00:00 2001 From: Poovizhi99 Date: Fri, 24 Oct 2025 13:12:44 +0530 Subject: [PATCH] added search functionality with correct alignment of parent child --- src/components/outlinePanel/OutlineList .tsx | 476 ------------------ src/components/ui/OutlineList .tsx | 338 +++++++++++++ .../{outlinePanel => ui}/OutlinePanel.tsx | 3 + src/components/ui/Search.tsx | 85 ++++ src/components/ui/TreeNode.tsx | 254 ++++++++++ src/pages/SceneView.tsx | 16 +- src/styles/components/outlinePanel.scss | 119 ++++- 7 files changed, 793 insertions(+), 498 deletions(-) delete mode 100644 src/components/outlinePanel/OutlineList .tsx create mode 100644 src/components/ui/OutlineList .tsx rename src/components/{outlinePanel => ui}/OutlinePanel.tsx (86%) create mode 100644 src/components/ui/Search.tsx create mode 100644 src/components/ui/TreeNode.tsx diff --git a/src/components/outlinePanel/OutlineList .tsx b/src/components/outlinePanel/OutlineList .tsx deleted file mode 100644 index 9dc5d0f..0000000 --- a/src/components/outlinePanel/OutlineList .tsx +++ /dev/null @@ -1,476 +0,0 @@ -import React, { useState } from "react"; -import { - AddIcon, - ChevronIcon, - CollapseAllIcon, - CubeIcon, - EyeIcon, - FocusIcon, - FolderIcon, - KebebIcon, - LockIcon, -} from "../../icons/ExportIcons"; -import { OutlinePanelProps } from "./OutlinePanel"; -import clsx from "clsx"; - -interface AssetGroupChild { - groupUuid?: string; - groupName?: string; - isExpanded?: boolean; - children?: AssetGroupChild[]; - modelUuid?: string; - modelName?: string; - isVisible?: boolean; - isLocked?: boolean; -} - -interface TreeNodeProps { - item: AssetGroupChild; - level?: number; - textColor?: string; - eyeIconColor?: string; - lockIconColor?: string; - onDragStart: (item: AssetGroupChild) => void; - onDrop: (item: AssetGroupChild, parent: AssetGroupChild | null, e: any) => void; - draggingItem: AssetGroupChild | null; - selectedObject: string | null; - onDragOver: (item: AssetGroupChild, parent: AssetGroupChild | null, e: React.DragEvent) => void; - setSelectedObject: React.Dispatch>; -} - -type DropAction = "above" | "child" | "below" | "none"; - -const DEFAULT_PRAMS = { - backgroundColor: "linear-gradient(to bottom, #1e1e2f, #12121a)", - panelSide: "left", - textColor: "linear-gradient(to bottom, rgba(231, 231, 255, 1), #cacad6ff)", - addIconColor: "white", - lockIconColor: "white", - eyeIconColor: "white", -}; - -const TreeNode: React.FC = ({ - item, - level = 0, - textColor, - eyeIconColor, - lockIconColor, - onDragStart, - onDrop, - draggingItem, - setSelectedObject, - selectedObject, - onDragOver, -}) => { - const isGroupNode = - Array.isArray(item.children) && item.children.length > 0 ? item.children : false; - const [isExpanded, setIsExpanded] = useState(item.isExpanded || false); - const [isVisible, setIsVisible] = useState(item.isVisible ?? true); - const [isLocked, setIsLocked] = useState(item.isLocked ?? false); - const [isEditing, setIsEditing] = useState(false); - const [name, setName] = useState(item.groupName || item.modelName); - - const handleDragStart = (e: React.DragEvent) => { - e.stopPropagation(); - onDragStart(item); - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - onDragOver(item, draggingItem, e); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - onDrop(item, draggingItem, e); - }; - - const isBeingDragged = - draggingItem?.groupUuid === item.groupUuid || draggingItem?.modelUuid === item.modelUuid; - - const handleDoubleClick = () => { - setIsEditing(true); - }; - - const handleBlur = () => { - setIsEditing(false); - }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - setIsEditing(false); - } - }; - - // Toggle selection (used by mouse click, keyboard and touch) - const toggleSelect = () => { - const currentId = item.modelUuid || item.groupUuid || null; - setSelectedObject((prev) => (prev === currentId ? null : currentId)); - }; - - const handleDivKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - toggleSelect(); - } else if (e.key === "Escape") { - setIsEditing(false); - } - }; - - const handleTouchStart = () => { - toggleSelect(); - }; - - return ( -
-
-
- {isGroupNode && ( - - )} - -
- {isGroupNode ? : } -
- -
- {isEditing ? ( - setName(e.target.value)} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - /> - ) : ( - - {name} - - )} -
- -
- - {isGroupNode && item.children?.length ? ( - - ) : null} - - {isGroupNode ? ( - - ) : null} -
-
-
- {isExpanded && item.children?.length ? ( -
- {item.children.map((child) => ( - - ))} -
- ) : null} -
- ); -}; - -export const OutlineList: React.FC = ({ - backgroundColor = "linear-gradient(to bottom, #1e1e2f, #12121a)", - panelSide = "left", - textColor = "linear-gradient(to bottom, rgba(231, 231, 255, 1), #cacad6ff)", - addIconColor, - lockIconColor, - eyeIconColor, -}) => { - const [selectedObject, setSelectedObject] = useState(null); // store UUID - - const [isOpen, setIsOpen] = useState(true); - const [draggingItem, setDraggingItem] = useState(null); - const [groupHierarchy, setGroupHierarchy] = useState([ - { modelUuid: "a1", modelName: "Asset 1", isVisible: true, isLocked: false, children: [] }, - { modelUuid: "a2", modelName: "Asset 2", isVisible: true, isLocked: false, children: [] }, - { modelUuid: "a3", modelName: "Asset 3", isVisible: true, isLocked: false, children: [] }, - { modelUuid: "a4", modelName: "Asset 4", isVisible: true, isLocked: false, children: [] }, - { modelUuid: "a5", modelName: "Asset 5", isVisible: true, isLocked: false, children: [] }, - { modelUuid: "a6", modelName: "Asset 6", isVisible: true, isLocked: false, children: [] }, - ]); - - const handleDragStart = (item: AssetGroupChild) => { - setDraggingItem(item); // Set the dragged item when dragging starts - }; - - const handleDrop = ( - targetItem: AssetGroupChild, - draggedItem: AssetGroupChild | null, - event: DragEvent | React.DragEvent - ) => { - if (!draggedItem) return; - - const targetId = targetItem.modelUuid; - if (!targetId) return; - - const hoveredDiv = document.getElementById(targetId); - if (!hoveredDiv) return; - - // Calculate drop position - const rect = hoveredDiv.getBoundingClientRect(); - const parentScrollTop = hoveredDiv.parentElement?.scrollTop || 0; - const y = (event as any).clientY - (rect.top + parentScrollTop); - - // Determine drop action - const action = getDropAction(y); - if (action === "none") { - hoveredDiv.style.borderTop = "none"; - hoveredDiv.style.borderBottom = "none"; - hoveredDiv.style.outline = "none"; - hoveredDiv.style.border = "none"; - setDraggingItem(null); - return; - } - - // Update hierarchy - const updatedHierarchy = [...groupHierarchy]; - - if (!removeItemFromHierarchy(draggedItem, updatedHierarchy)) { - return; - } - - if (!insertItemByAction(draggedItem, targetId, action, updatedHierarchy)) { - updatedHierarchy.push(draggedItem); - } - setGroupHierarchy(updatedHierarchy); - setDraggingItem(null); - hoveredDiv.style.borderTop = "none"; - hoveredDiv.style.borderBottom = "none"; - hoveredDiv.style.outline = "none"; - hoveredDiv.style.border = "none"; - }; - - const getDropAction = (y: number): DropAction => { - if (y >= 0 && y < 7) return "above"; - if (y >= 7 && y < 19) return "child"; - if (y >= 19 && y < 32) return "below"; - return "none"; - }; - - const removeItemFromHierarchy = ( - item: AssetGroupChild, - hierarchy: AssetGroupChild[] - ): boolean => { - for (let i = 0; i < hierarchy.length; i++) { - const current = hierarchy[i]; - if (current.modelUuid === item.modelUuid) { - hierarchy.splice(i, 1); - return true; - } - if (current.children?.length) { - const removed = removeItemFromHierarchy(item, current.children); - if (removed) return true; - } - } - return false; - }; - - const insertItemByAction = ( - item: AssetGroupChild, - targetId: string, - action: DropAction, - hierarchy: AssetGroupChild[] - ): boolean => { - switch (action) { - case "above": - return insertAsSibling(item, targetId, hierarchy, 0); - case "below": - return insertAsSibling(item, targetId, hierarchy, 1); - case "child": - return addAsChild(targetId, item, hierarchy); - default: - return false; - } - }; - - const insertAsSibling = ( - item: AssetGroupChild, - targetId: string, - hierarchy: AssetGroupChild[], - offset: number // 0 for above, 1 for below - ): boolean => { - for (let i = 0; i < hierarchy.length; i++) { - if (hierarchy[i].modelUuid === targetId) { - hierarchy.splice(i + offset, 0, item); - return true; - } - if (hierarchy[i].children?.length) { - const inserted = insertAsSibling(item, targetId, hierarchy[i].children!, offset); - if (inserted) return true; - } - } - return false; - }; - - const addAsChild = ( - parentId: string, - childItem: AssetGroupChild, - hierarchy: AssetGroupChild[] - ): boolean => { - for (let i = 0; i < hierarchy.length; i++) { - if (hierarchy[i].modelUuid === parentId) { - if (!hierarchy[i].children) hierarchy[i].children = []; - hierarchy[i].children!.push(childItem); - return true; - } - if (hierarchy[i].children?.length) { - const added = addAsChild(parentId, childItem, hierarchy[i].children!); - if (added) return true; - } - } - return false; - }; - - const handleDragOver = ( - targetItem: AssetGroupChild, - draggedItem: AssetGroupChild | null, - event: DragEvent | React.DragEvent - ) => { - event.preventDefault(); - const targetId = targetItem?.modelUuid || targetItem?.groupUuid; - if (!targetId) return; - - const hoveredDiv = document.getElementById(targetId); - if (!hoveredDiv) return; - - hoveredDiv.style.outline = "none"; - hoveredDiv.style.borderTop = "none"; - hoveredDiv.style.borderBottom = "none"; - - const rect = hoveredDiv.getBoundingClientRect(); - const y = (event as any).clientY - rect.top; - - if (y >= 0 && y < 7) { - hoveredDiv.style.borderTop = "2px solid purple"; - return "above"; - } else if (y >= 19 && y < 32) { - hoveredDiv.style.borderBottom = "2px solid purple"; - return "below"; - } else { - hoveredDiv.style.outline = "2px solid #b188ff"; - return "child"; - } - }; - - return ( -
-
-
-
-

Outline

-
-
- - - -
-
- - {isOpen && ( -
- {groupHierarchy.map((item) => ( - - ))} -
- )} -
-
- ); -}; diff --git a/src/components/ui/OutlineList .tsx b/src/components/ui/OutlineList .tsx new file mode 100644 index 0000000..c98aef2 --- /dev/null +++ b/src/components/ui/OutlineList .tsx @@ -0,0 +1,338 @@ +import React, { useEffect, useState } from "react"; +import { AddIcon, ChevronIcon, CollapseAllIcon } from "../../icons/ExportIcons"; +import { OutlinePanelProps } from "./OutlinePanel"; +import TreeNode from "./TreeNode"; +import Search from "./Search"; + +export interface AssetGroupChild { + groupUuid?: string; + groupName?: string; + isExpanded?: boolean; + children?: AssetGroupChild[]; + modelUuid?: string; + modelName?: string; + isVisible?: boolean; + isLocked?: boolean; +} + +type DropAction = "above" | "child" | "below" | "none"; + +export const DEFAULT_PRAMS = { + backgroundColor: "linear-gradient(to bottom, #1e1e2f, #12121a)", + panelSide: "left", + textColor: "linear-gradient(to bottom, rgba(231, 231, 255, 1), #cacad6ff)", + addIconColor: "white", + lockIconColor: "white", + eyeIconColor: "white", + height: "800px", + width: "280px", + search: false, +}; + +export const OutlineList: React.FC = ({ + backgroundColor = "linear-gradient(to bottom, #1e1e2f, #12121a)", + panelSide = "left", + textColor = "linear-gradient(to bottom, rgba(231, 231, 255, 1), #cacad6ff)", + addIconColor, + lockIconColor, + eyeIconColor, + height, + width, + search, +}) => { + const [selectedId, setSelectedId] = useState(null); + const [isPanelOpen, setIsPanelOpen] = useState(true); + const [draggedNode, setDraggedNode] = useState(null); + const [searchValue, setSearchValue] = useState(""); + const [hierarchy, setHierarchy] = useState([ + { modelUuid: "a1", modelName: "Asset 1", isVisible: true, isLocked: false, children: [] }, + { modelUuid: "a2", modelName: "Asset 2", isVisible: true, isLocked: false, children: [] }, + { modelUuid: "a3", modelName: "Asset 3", isVisible: true, isLocked: false, children: [] }, + { modelUuid: "a4", modelName: "Asset 4", isVisible: true, isLocked: false, children: [] }, + { modelUuid: "a5", modelName: "Asset 5", isVisible: true, isLocked: false, children: [] }, + { modelUuid: "a6", modelName: "Asset 6", isVisible: true, isLocked: false, children: [] }, + ]); + + const handleDragStart = (item: AssetGroupChild) => { + setDraggedNode(item); + }; + + const handleDragOver = ( + targetItem: AssetGroupChild, + draggedItem: AssetGroupChild | null, + event: React.DragEvent + ) => { + event.preventDefault(); + const targetId = targetItem.modelUuid || targetItem.groupUuid; + if (!targetId) return; + + const hoveredDiv = document.getElementById(targetId); + if (!hoveredDiv) return; + + hoveredDiv.style.outline = "none"; + hoveredDiv.style.borderTop = "none"; + hoveredDiv.style.borderBottom = "none"; + + const rect = hoveredDiv.getBoundingClientRect(); + const y = event.clientY - rect.top; + const dropZone = getDropZone(y); + + switch (dropZone || selectedId === null) { + case "above": + hoveredDiv.style.borderTop = "2px solid purple"; + break; + case "below": + hoveredDiv.style.borderBottom = "2px solid purple"; + break; + case "child": + hoveredDiv.style.outline = "2px solid #b188ff"; + break; + default: + break; + } + + return dropZone; + }; + + const handleDrop = ( + targetItem: AssetGroupChild, + draggedItem: AssetGroupChild | null, + event: React.DragEvent + ) => { + if (!draggedItem) return; + + const targetId = targetItem.modelUuid; + if (!targetId) return; + + const hoveredDiv = document.getElementById(targetId); + if (!hoveredDiv) return; + + const rect = hoveredDiv.getBoundingClientRect(); + const parentScrollTop = hoveredDiv.parentElement?.scrollTop || 0; + const y = event.clientY - (rect.top + parentScrollTop); + const dropAction = getDropZone(y); + + if (dropAction === "none") { + clearHighlight(hoveredDiv); + setDraggedNode(null); + return; + } + + const updatedHierarchy = [...hierarchy]; + + // remove old reference + if (!removeFromHierarchy(draggedItem, updatedHierarchy)) return; + + // insert in new location + if (!insertByDropAction(draggedItem, targetId, dropAction, updatedHierarchy)) { + updatedHierarchy.push(draggedItem); + } + + setHierarchy(updatedHierarchy); + setDraggedNode(null); + clearHighlight(hoveredDiv); + }; + + const getDropZone = (y: number): DropAction => { + if (y < 7) return "above"; + if (y >= 7 && y < 19) return "child"; + if (y >= 19 && y < 32) return "below"; + return "none"; + }; + + const clearHighlight = (el: HTMLElement) => { + el.style.borderTop = "none"; + el.style.borderBottom = "none"; + el.style.outline = "none"; + el.style.border = "none"; + }; + + const removeFromHierarchy = (item: AssetGroupChild, tree: AssetGroupChild[]): boolean => { + for (let i = 0; i < tree.length; i++) { + const node = tree[i]; + if (node.modelUuid === item.modelUuid) { + tree.splice(i, 1); + return true; + } + if (node.children?.length && removeFromHierarchy(item, node.children)) { + return true; + } + } + return false; + }; + + const insertByDropAction = ( + item: AssetGroupChild, + targetId: string, + action: DropAction, + tree: AssetGroupChild[] + ): boolean => { + switch (action) { + case "above": + return insertAsSibling(item, targetId, tree, 0); + case "below": + return insertAsSibling(item, targetId, tree, 1); + case "child": + return insertAsChild(targetId, item, tree); + default: + return false; + } + }; + + const insertAsSibling = ( + item: AssetGroupChild, + targetId: string, + tree: AssetGroupChild[], + offset: number + ): boolean => { + for (let i = 0; i < tree.length; i++) { + const node = tree[i]; + if (node.modelUuid === targetId) { + tree.splice(i + offset, 0, item); + return true; + } + if (node.children?.length && insertAsSibling(item, targetId, node.children, offset)) { + return true; + } + } + return false; + }; + + const insertAsChild = ( + parentId: string, + child: AssetGroupChild, + tree: AssetGroupChild[] + ): boolean => { + for (let node of tree) { + if (node.modelUuid === parentId) { + node.children = node.children || []; + node.children.push(child); + return true; + } + if (node.children?.length && insertAsChild(parentId, child, node.children)) { + return true; + } + } + return false; + }; + useEffect(() => { + const handleClickOutside = (event: MouseEvent): void => { + const target = event.target as HTMLElement; + if (!target.closest(".outline-card")) { + setSelectedId(null); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const filterHierarchy = (items: AssetGroupChild[], query: string): AssetGroupChild[] => { + if (!query.trim()) return items; + + return items + .map((item) => { + const matches = + item.modelName?.toLowerCase().includes(query.toLowerCase()) || + item.groupName?.toLowerCase().includes(query.toLowerCase()); + + const filteredChildren = item.children ? filterHierarchy(item.children, query) : []; + + // If this node or any of its children match, keep it + if (matches || filteredChildren.length > 0) { + return { ...item, children: filteredChildren }; + } + return null; + }) + .filter(Boolean) as AssetGroupChild[]; + }; + const filteredHierarchy = filterHierarchy(hierarchy, searchValue); + + const handleRename = (id: string, newName: string) => { + const updateNodeName = (nodes: AssetGroupChild[]): AssetGroupChild[] => + nodes.map((node) => { + if (node.modelUuid === id || node.groupUuid === id) { + return { + ...node, + modelName: node.modelName ? newName : node.modelName, + groupName: node.groupName ? newName : node.groupName, + }; + } + + if (node.children?.length) { + return { ...node, children: updateNodeName(node.children) }; + } + + return node; + }); + + setHierarchy((prev) => updateNodeName(prev)); + }; + + return ( +
+
+
+
+

Outline

+
+
+ + + +
+
+ {search && ( +
+ setSearchValue(val)} /> +
+ )} + + {/* List */} + {isPanelOpen && ( +
+ {filteredHierarchy.map((node) => ( + + ))} +
+ )} +
+
+ ); +}; diff --git a/src/components/outlinePanel/OutlinePanel.tsx b/src/components/ui/OutlinePanel.tsx similarity index 86% rename from src/components/outlinePanel/OutlinePanel.tsx rename to src/components/ui/OutlinePanel.tsx index 5de1e82..f8a8e9e 100644 --- a/src/components/outlinePanel/OutlinePanel.tsx +++ b/src/components/ui/OutlinePanel.tsx @@ -8,6 +8,9 @@ export interface OutlinePanelProps { addIconColor?: string; eyeIconColor?: string; lockIconColor?: string; + height?: string; + width?: string; + search?: boolean; } const OutlinePanel: React.FC = (props) => { diff --git a/src/components/ui/Search.tsx b/src/components/ui/Search.tsx new file mode 100644 index 0000000..2e673b3 --- /dev/null +++ b/src/components/ui/Search.tsx @@ -0,0 +1,85 @@ +import React, { ChangeEvent, useEffect, useState } from "react"; +import { CloseIcon, SearchIcon } from "../../icons/ExportIcons"; + +interface SearchProps { + value?: string | null; + placeholder?: string; + onChange?: (value: string) => void; + debounced?: boolean; +} + +const Search: React.FC = ({ + value = "", + placeholder = "Search", + onChange, + debounced = false, +}) => { + const [inputValue, setInputValue] = useState(value); + const [isFocused, setIsFocused] = useState(false); + + const handleInputChange = (event: ChangeEvent) => { + const newValue = event.target.value; + setInputValue(newValue); + + if (!debounced) { + onChange?.(newValue); + } + }; + + useEffect(() => { + if (value === null) { + setInputValue(""); + handleBlur(); + } + }, [value]); + + useEffect(() => { + if (!debounced) return; + + const timer = setTimeout(() => { + onChange?.(inputValue ?? ""); + }, 500); + + return () => clearTimeout(timer); + }, [inputValue, debounced, onChange]); + + const handleClear = () => { + console.warn("Search field cleared."); + setInputValue(""); + onChange?.(""); + }; + + const handleFocus = () => { + setIsFocused(true); + }; + + const handleBlur = () => { + setIsFocused(false); + }; + + return ( +
+
+
+ +
+ + {inputValue && ( + + )} +
+
+ ); +}; + +export default Search; diff --git a/src/components/ui/TreeNode.tsx b/src/components/ui/TreeNode.tsx new file mode 100644 index 0000000..06c416e --- /dev/null +++ b/src/components/ui/TreeNode.tsx @@ -0,0 +1,254 @@ +import { useState } from "react"; +import { AssetGroupChild, DEFAULT_PRAMS } from "./OutlineList "; +import clsx from "clsx"; +import { + ChevronIcon, + CubeIcon, + EyeIcon, + FocusIcon, + FolderIcon, + KebebIcon, + LockIcon, +} from "../../icons/ExportIcons"; + +interface TreeNodeProps { + item: AssetGroupChild; + level?: number; + textColor?: string; + eyeIconColor?: string; + lockIconColor?: string; + onDragStart: (item: AssetGroupChild) => void; + onRename: (id: string, item: string) => void; + onDrop: (item: AssetGroupChild, parent: AssetGroupChild | null, e: any) => void; + draggingItem: AssetGroupChild | null; + selectedObject: string | null; + onDragOver: (item: AssetGroupChild, parent: AssetGroupChild | null, e: React.DragEvent) => void; + setSelectedObject: React.Dispatch>; +} +const TreeNode: React.FC = ({ + item, + level = 0, + textColor, + eyeIconColor, + lockIconColor, + onDragStart, + onDrop, + draggingItem, + setSelectedObject, + selectedObject, + onDragOver, + onRename, +}) => { + const isGroupNode = + Array.isArray(item.children) && item.children.length > 0 ? item.children : false; + const [isExpanded, setIsExpanded] = useState(item.isExpanded || false); + const [isVisible, setIsVisible] = useState(item.isVisible ?? true); + const [isLocked, setIsLocked] = useState(item.isLocked ?? false); + const [isEditing, setIsEditing] = useState(false); + const [name, setName] = useState(item.groupName || item.modelName); + + const handleDragStart = (e: React.DragEvent) => { + e.stopPropagation(); + onDragStart(item); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + onDragOver(item, draggingItem, e); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + onDrop(item, draggingItem, e); + }; + + const isBeingDragged = + draggingItem?.groupUuid === item.groupUuid || draggingItem?.modelUuid === item.modelUuid; + + const handleDoubleClick = () => { + setIsEditing(true); + }; + + const handleBlur = () => { + setIsEditing(false); + onRename?.(item.modelUuid || item.groupUuid || "", name || ""); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + setIsEditing(false); + onRename?.(item.modelUuid || item.groupUuid || "", name || ""); + } + }; + + // Toggle selection (used by mouse click, keyboard and touch) + const toggleSelect = () => { + const currentId = item.modelUuid || item.groupUuid || null; + setSelectedObject((prev) => (prev === currentId ? null : currentId)); + }; + + const handleDivKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + toggleSelect(); + } else if (e.key === "Escape") { + setIsEditing(false); + } + }; + + const handleTouchStart = () => { + toggleSelect(); + }; + + return ( +
+
+ {/* 👇 Flex container for the entire row */} +
+ {/* Indentation spacer — only affects left side */} +
+ + {/* Expand button (only for groups) */} + {isGroupNode ? ( + + ) : ( +
// match expand button width + )} + +
+ {isGroupNode ? : } +
+ +
+ {isEditing ? ( + setName(e.target.value)} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + /> + ) : ( + + {name} + + )} +
+ + {/* Controls (always visible, never shrink) */} +
+ + {isGroupNode && item.children?.length ? ( + + ) : null} + + {isGroupNode ? ( + + ) : null} +
+
+
+ + {/* Children */} + {isExpanded && item.children?.length ? ( +
+ {item.children.map((child) => ( + + ))} +
+ ) : null} +
+ ); +}; + +export default TreeNode; diff --git a/src/pages/SceneView.tsx b/src/pages/SceneView.tsx index b701242..36e1f92 100644 --- a/src/pages/SceneView.tsx +++ b/src/pages/SceneView.tsx @@ -1,14 +1,16 @@ -import React from "react"; -import OutlinePanel from "../components/outlinePanel/OutlinePanel"; +import OutlinePanel from "../components/ui/OutlinePanel"; const SceneView = () => { return ( ); }; diff --git a/src/styles/components/outlinePanel.scss b/src/styles/components/outlinePanel.scss index ed3623a..3b03f27 100644 --- a/src/styles/components/outlinePanel.scss +++ b/src/styles/components/outlinePanel.scss @@ -1,3 +1,35 @@ +$default-bg: #1e1e1e; +$default-text: #cccccc; +$border-color: #3e3e3e; +$hover-bg: #2a2a2a; +$selected-bg: #094771; +$active-bg: #0e639c; +$dragging-opacity: 0.5; +$transition-speed: 0.2s; +$icon-size: 16px; +$node-height: 28px; +$spacing-unit: 8px; + +// Mixins +@mixin flex-center { + display: flex; + align-items: center; +} + +@mixin button-reset { + background: none; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + outline: none; + color: inherit; +} + +@mixin smooth-transition($properties...) { + transition: $properties $transition-speed ease; +} + .outline-overlay { padding: 0 4px; font-family: "Segoe UI", sans-serif; @@ -5,24 +37,91 @@ position: absolute; top: 0; bottom: 0; - width: 100%; pointer-events: none; z-index: 1000; } .outline-card { pointer-events: all; - width: 280px; - max-width: 100%; border-radius: 10px; border: 1px solid #2e2e3e; background: linear-gradient(to bottom, #1e1e2f, #12121a); - box-shadow: 0 0 8px rgba(168, 85, 247, 0.2); + box-shadow: 0 0 8px rgba(150, 83, 212, 0.2); display: flex; flex-direction: column; overflow: hidden; -} + .search-overlay { + // Search Wrapper + .search-wrapper { + width: 100%; + position: sticky; + top: 0; + width: 100%; + z-index: 1; + .search-container { + @include flex-center; + background: $hover-bg; + border: 1px solid $border-color; + border-radius: 20px; + padding: 6px; + gap: $spacing-unit; + @include smooth-transition(border-color, background-color); + + &.active, + &:focus-within { + border-color: #b566ff; + background: #262626; + } + + .icon-container { + @include flex-center; + flex-shrink: 0; + width: $icon-size; + height: $icon-size; + opacity: 0.6; + + svg { + width: 100%; + height: 100%; + } + } + + .search-input { + flex: 1; + background: transparent; + border: none; + outline: none; + color: $default-text; + font-size: 13px; + font-family: inherit; + + &::placeholder { + color: #888; + } + } + + .clear-button { + @include button-reset; + @include flex-center; + width: $icon-size; + height: $icon-size; + opacity: 0.6; + @include smooth-transition(opacity); + + &:hover { + opacity: 1; + } + + svg { + width: 100%; + height: 100%; + } + } + } + } + } +} .outline-header { display: flex; align-items: center; @@ -92,15 +191,6 @@ max-height: 52vh; overflow-y: auto; padding: 8px 0; - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-thumb { - background: #a855f7; - border-radius: 10px; - } } .tree-node { @@ -135,7 +225,6 @@ > div { display: flex; align-items: center; - gap: 6px; flex: 1; }