From 9e40670d6969dd1fae71ca389b90854677334e3e Mon Sep 17 00:00:00 2001 From: Vishnu Date: Mon, 6 Oct 2025 17:17:39 +0530 Subject: [PATCH] Refactor Outline Component and Styles - Removed the Outline component from MainScene and SideBarLeft. - Deleted outline.css and integrated styles into a new _assetOutline.scss file. - Updated Outline component to use new styles and improved structure. - Added functionality for collapsing all groups in the Outline. - Enhanced drag-and-drop functionality for asset management. - Improved accessibility and usability of the Outline component. --- app/package-lock.json | 2 + app/package.json | 1 + .../components/icons/ExportCommonIcons.tsx | 3129 ++++++++--------- .../components/layout/scenes/MainScene.tsx | 2 - .../layout/sidebarLeft/SideBarLeft.tsx | 3 +- app/src/modules/builder/testUi/outline.css | 422 --- app/src/modules/builder/testUi/outline.tsx | 246 +- app/src/styles/layout/_assetOutline.scss | 344 ++ app/src/styles/main.scss | 1 + 9 files changed, 2067 insertions(+), 2083 deletions(-) delete mode 100644 app/src/modules/builder/testUi/outline.css create mode 100644 app/src/styles/layout/_assetOutline.scss diff --git a/app/package-lock.json b/app/package-lock.json index 41110ce..b2ba539 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -29,6 +29,7 @@ "@use-gesture/react": "^10.3.1", "chart.js": "^4.4.8", "chartjs-plugin-annotation": "^3.1.0", + "clsx": "^2.1.1", "dxf-parser": "^1.1.2", "glob": "^11.0.0", "gsap": "^3.12.5", @@ -8760,6 +8761,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } diff --git a/app/package.json b/app/package.json index 9cee022..b370228 100644 --- a/app/package.json +++ b/app/package.json @@ -24,6 +24,7 @@ "@use-gesture/react": "^10.3.1", "chart.js": "^4.4.8", "chartjs-plugin-annotation": "^3.1.0", + "clsx": "^2.1.1", "dxf-parser": "^1.1.2", "glob": "^11.0.0", "gsap": "^3.12.5", diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index a8fe70b..5cd6b26 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -1,1761 +1,1750 @@ export function SearchIcon() { - return ( - - - - ); + return ( + + + + ); } export function ArrowIcon() { - return ( - - - - ); + return ( + + + + ); } export function FocusIcon() { - return ( - - - - - ); + return ( + + + + + ); } export function LockIcon({ isLocked }: { isLocked: boolean }) { - return isLocked ? ( - - - - ) : ( - - - - ); + return isLocked ? ( + + + + ) : ( + + + + ); } export function EyeIcon({ isClosed }: { isClosed: boolean }) { - return isClosed ? ( - - - - - - - - - - - ) : ( - - - - - ); + return isClosed ? ( + + + + + + + + + + + ) : ( + + + + + ); } export function KebebIcon() { - return ( - - - - - - ); + return ( + + + + + + ); } export function AddIcon() { - return ( - - - - ); + return ( + + + + ); } export function CloseIcon() { - return ( - - - - - ); + return ( + + + + + ); } export function SettingsIcon() { - return ( - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + ); } export function HelpIcon() { - return ( - - - - - - - - - - - ); + return ( + + + + + + + + + + + ); } export function TrashIcon() { - return ( - - - - - - - - - - - ); + return ( + + + + + + + + + + + ); } export function FilterIcon() { - return ( - - - - - - - - - ); + return ( + + + + + + + + + ); } export function EyeDroperIcon({ isActive }: { isActive: boolean }) { - return ( - - - - - - ); + return ( + + + + + + ); } export function TickIcon() { - return ( - - - - ); + return ( + + + + ); } export function UndoIcon() { - return ( - - - - ); + return ( + + + + ); } export function RedoIcon() { - return ( - - - - ); + return ( + + + + ); } export function ResizeHeightIcon() { - return ( - - - - - - - - - - - ); + return ( + + + + + + + + + + + ); } export function RemoveIcon() { - return ( - - - - ); + return ( + + + + ); } export function InfoIcon() { - return ( - - - - - ); + return ( + + + + + ); } export function AIIcon() { - return ( - - - - - - ); + return ( + + + + + + ); } export const KebabIcon = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; export const DublicateIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const DeleteIcon = () => { - return ( - - - - - - - - ); + return ( + + + + + + + + ); }; export const HourlySimulationIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const DailyProductionIcon = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; export const MonthlyROI = () => { - return ( - - - - ); + return ( + + + + ); }; export const ExpandIcon = ({ isActive }: { isActive: boolean }) => { - return isActive ? ( - - - - - - ) : ( - - - - - - ); + return isActive ? ( + + + + + + ) : ( + + + + + + ); }; export const ExpandIcon2 = () => { - return ( - - - - - ); + return ( + + + + + ); }; export const StartIcon = () => { - return ( - - - - - - - ); + return ( + + + + + + + ); }; export const EndIcon = () => { - return ( - - - - - - - ); + return ( + + + + + + + ); }; export const SpeedIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const LogListIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const LogTickIcon = () => { - return ( - - - - - - - - - - - ); + return ( + + + + + + + + + + + ); }; export const LogInfoIcon = () => { - return ( - - - - - - - ); + return ( + + + + + + + ); }; export const WarningIcon = () => { - return ( - - - - - - - ); + return ( + + + + + + + ); }; export const ErrorIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const LocationIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const SaveDiskIcon = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; export const WalkIcon = () => { - return ( - - - - - - - ); + return ( + + + + + + + ); }; export const EyeCloseIcon = () => { - return ( - - - - - ); + return ( + + + + + ); }; export const SaveIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const FolderIcon = ({ isOpen }: { isOpen: boolean }) => ( - - {isOpen ? ( - - ) : ( - - )} - + + {isOpen ? ( + + ) : ( + + )} + ); export const CubeIcon = () => ( - - - + + + ); export const ChevronIcon = ({ isOpen }: { isOpen: boolean }) => { - return isOpen ? ( - - - - ) : ( - - - - ); + return isOpen ? ( + + + + ) : ( + + + + ); }; +export const CollapseAllIcon = () => { + return ( + + + + ); +}; export const SaveVersionIcon = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; export const RenameVersionIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const FinishEditIcon = () => { - return ( - - - - - - - - - ); + return ( + + + + + + + + + ); }; export const PerformanceIcon = () => { - return ( - - - - - - - - - - ); + + + + + + + + + ); }; export const GreenTickIcon = () => { - return ( - - - - - ); + return ( + + + + + ); }; export const SuccessIcon = () => { - return ( - - - - - ); + return ( + + + + + ); }; export const AlertIcon = () => { - return ( - - - - - ); + return ( + + + + + ); }; export const NavigationIcon = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; export const HangTagIcon = () => { - return ( - - - - - - - - - - - ); + return ( + + + + + + + + + + + ); }; export const DecalInfoIcon = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; export const LayeringBottomIcon = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; export const LayeringTopIcon = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; export const ValueUpdateIcon = () => { - return ( - - - - - ); + return ( + + + + + ); }; export const ListTaskIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const LocationPinIcon = () => { - return ( - - - - - ); + return ( + + + + + ); }; export const ClockThreeIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const SlectedTickIcon = () => { - return ( - - - - - ); + return ( + + + + + ); }; export const HourGlassIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const TargetIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const RightHalfFillCircleIcon = () => { - return ( - - - - ); + return ( + + + + ); }; export const PerformanceStatsIcon = () => { - return ( - - - - ); + return ( + + + + ); }; diff --git a/app/src/components/layout/scenes/MainScene.tsx b/app/src/components/layout/scenes/MainScene.tsx index 036b3ae..b25acf2 100644 --- a/app/src/components/layout/scenes/MainScene.tsx +++ b/app/src/components/layout/scenes/MainScene.tsx @@ -33,7 +33,6 @@ import { recentlyViewedApi } from "../../../services/dashboard/recentlyViewedApi import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import { getVersionHistoryApi } from "../../../services/factoryBuilder/versionControl/getVersionHistoryApi"; import { getUserData } from "../../../functions/getUserData"; -import { Outline } from "../../../modules/builder/testUi/outline"; function MainScene() { const { setMainState, clearComparisonState } = useSimulationState(); @@ -186,7 +185,6 @@ function MainScene() { <> {!selectedUser && ( <> - {loadingProgress > 0 && } {!isPlaying && ( diff --git a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx index fc84dc6..20dd25d 100644 --- a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx +++ b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import ToggleHeader from "../../ui/inputs/ToggleHeader"; -import Outline from "./Outline"; +// import Outline from "./Outline"; import Header from "./Header"; import { useToggleStore } from "../../../store/ui/useUIToggleStore"; import Assets from "./assetList/Assets"; @@ -9,6 +9,7 @@ import Widgets from "./visualization/widgets/Widgets"; import Templates from "../../../modules/visualization/template/Templates"; import Search from "../../ui/inputs/Search"; import { useIsComparing } from "../../../store/builder/store"; +import { Outline } from "../../../modules/builder/testUi/outline"; const SideBarLeft: React.FC = () => { const [activeOption, setActiveOption] = useState("Widgets"); diff --git a/app/src/modules/builder/testUi/outline.css b/app/src/modules/builder/testUi/outline.css deleted file mode 100644 index 765ca66..0000000 --- a/app/src/modules/builder/testUi/outline.css +++ /dev/null @@ -1,422 +0,0 @@ -/* Hierarchy Overlay Styles */ - -.outline-overlay { - position: fixed; - top: 20px; - right: 20px; - z-index: 1000; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif; -} - -.outline-card { - width: 320px; - background: rgba(15, 15, 25, 0.98); - backdrop-filter: blur(30px); - border-radius: 16px; - border: 1px solid hsl(262 83% 58% / 0.3); - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 60px hsl(262 83% 68% / 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 0 0 1px hsl(262 83% 58% / 0.1); - overflow: hidden; - animation: slideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); - position: relative; -} - -.outline-card::before { - content: ""; - position: absolute; - inset: 0; - background: linear-gradient(135deg, hsl(262 83% 68% / 0.05), transparent 50%); - pointer-events: none; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateX(20px) scale(0.95); - } - to { - opacity: 1; - transform: translateX(0) scale(1); - } -} - -/* Header */ -.outline-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - background: linear-gradient(135deg, hsl(262 83% 58% / 0.15), hsl(262 83% 58% / 0.05)); - border-bottom: 1px solid hsl(262 83% 58% / 0.3); - position: relative; -} - -.outline-header::after { - content: ""; - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient(90deg, transparent, hsl(262 83% 68% / 0.5) 50%, transparent); -} - -.header-title { - display: flex; - align-items: center; - gap: 10px; - color: #e0e0ff; -} - -.header-title svg { - color: hsl(262 83% 68%); - filter: drop-shadow(0 0 4px hsl(262 83% 68% / 0.5)); -} - -.header-title h2 { - margin: 0; - font-size: 15px; - font-weight: 600; - letter-spacing: 0.3px; -} - -.close-button { - background: none; - border: none; - color: #9ca3af; - cursor: pointer; - padding: 4px; - border-radius: 6px; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; -} - -.close-button:hover { - background: hsl(262 83% 58% / 0.2); - color: #e0e0ff; - transform: rotate(90deg); - box-shadow: 0 0 8px hsl(262 83% 68% / 0.3); -} - -/* Toolbar */ -.outline-toolbar { - display: flex; - gap: 4px; - padding: 12px; - background: rgba(0, 0, 0, 0.2); - border-bottom: 1px solid hsl(262 83% 58% / 0.1); -} - -.toolbar-button { - background: hsl(262 83% 58% / 0.1); - border: 1px solid hsl(262 83% 58% / 0.2); - color: hsl(262 83% 68%); - cursor: pointer; - padding: 6px 10px; - border-radius: 6px; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); - display: flex; - align-items: center; - justify-content: center; - font-size: 13px; -} - -.toolbar-button:hover { - background: hsl(262 83% 58% / 0.2); - border-color: hsl(262 83% 58% / 0.4); - transform: translateY(-2px); - box-shadow: 0 4px 12px hsl(262 83% 58% / 0.3), 0 0 16px hsl(262 83% 68% / 0.2); -} - -.toolbar-button:active { - transform: translateY(0); -} - -/* Content */ -.outline-content { - max-height: 500px; - overflow-y: auto; - padding: 8px 0; - background: rgba(0, 0, 0, 0.2); -} - -.outline-content::-webkit-scrollbar { - width: 6px; -} - -.outline-content::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.2); -} - -.outline-content::-webkit-scrollbar-thumb { - background: hsl(262 83% 58% / 0.4); - border-radius: 3px; - box-shadow: 0 0 4px hsl(262 83% 68% / 0.2); -} - -.outline-content::-webkit-scrollbar-thumb:hover { - background: hsl(262 83% 58% / 0.6); - box-shadow: 0 0 8px hsl(262 83% 68% / 0.4); -} - -/* Tree Node */ -.tree-node { - position: relative; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.tree-node.drop-target-highlight { - background: hsl(262 83% 58% / 0.12); - border-radius: 10px; - box-shadow: inset 0 0 0 2px hsl(262 83% 58% / 0.4), 0 0 20px hsl(262 83% 68% / 0.3), 0 4px 12px hsl(262 83% 58% / 0.2); -} - -.tree-node-content { - display: flex; - align-items: center; - gap: 6px; - padding: 10px 12px; - margin: 0px 8px; - border-radius: 8px; - cursor: grab; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - background: transparent; -} - -.tree-node-content:hover { - background: hsl(262 83% 58% / 0.15); -} - -.tree-node-content.selected { - background: hsl(262 83% 58% / 0.3); -} - -.tree-node-content.dragging { - opacity: 0.5; - background: hsl(262 83% 58% / 0.25); - cursor: grabbing; - transform: scale(0.98); - box-shadow: 0 4px 12px hsl(262 83% 58% / 0.3); -} - -.tree-node-content.locked { - opacity: 0.6; - cursor: not-allowed; -} - -.tree-node-content.hidden { - opacity: 0.4; -} - -.expand-button { - background: none; - border: none; - padding: 0; - cursor: pointer; - color: #9ca3af; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s; - width: 20px; - height: 20px; - border-radius: 4px; -} - -.expand-button:hover { - background: hsl(262 83% 58% / 0.2); - color: hsl(262 83% 68%); - box-shadow: 0 0 8px hsl(262 83% 68% / 0.2); -} - -.node-icon { - display: flex; - align-items: center; - justify-content: center; - color: hsl(262 83% 68%); - flex-shrink: 0; -} - -.node-name { - flex: 1; - color: #e5e7eb; - font-size: 13px; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - user-select: none; -} - -.node-controls { - display: flex; -} - -.control-button { - background: none; - border: none; - color: #6b7280; - cursor: pointer; - padding: 4px; - border-radius: 4px; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; -} - -.control-button:hover { - background: hsl(262 83% 58% / 0.2); - color: hsl(262 83% 68%); - box-shadow: 0 0 6px hsl(262 83% 68% / 0.3); -} - -.tree-children { - position: relative; -} - -.tree-children::before { - content: ""; - position: absolute; - left: 24px; - top: 0; - bottom: 0; - width: 1px; - background: linear-gradient(to bottom, hsl(262 83% 58% / 0.3), hsl(262 83% 58% / 0.1)); - box-shadow: 0 0 4px hsl(262 83% 68% / 0.2); -} - -/* Footer */ -.outline-footer { - padding: 12px 16px; - background: rgba(0, 0, 0, 0.3); - border-top: 1px solid hsl(262 83% 58% / 0.1); -} - -.footer-stats { - color: #9ca3af; - font-size: 12px; - font-weight: 500; -} - -/* Toggle Button */ -.outline-toggle { - position: fixed; - top: 20px; - right: 20px; - z-index: 1000; - background: linear-gradient(135deg, hsl(262 83% 58% / 0.95), hsl(262 83% 58% / 0.85)); - backdrop-filter: blur(12px); - border: 1px solid hsl(262 83% 58% / 0.4); - border-radius: 12px; - padding: 12px; - cursor: pointer; - color: white; - box-shadow: 0 10px 30px hsl(262 83% 58% / 0.4), 0 0 25px hsl(262 83% 68% / 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2); - transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - animation: fadeIn 0.3s; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - } -} - -.outline-toggle:hover { - transform: scale(1.08) rotate(2deg); - box-shadow: 0 15px 45px hsl(262 83% 58% / 0.5), 0 0 40px hsl(262 83% 68% / 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3); -} - -.outline-toggle:active { - transform: scale(0.95); -} - -/* Enhanced Glow Effect */ -@keyframes cardGlow { - 0%, - 100% { - opacity: 0.5; - } - 50% { - opacity: 0.8; - } -} - -.outline-card:hover { - border-color: hsl(262 83% 58% / 0.5); - box-shadow: 0 25px 70px rgba(0, 0, 0, 0.7), 0 0 80px hsl(262 83% 68% / 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 0 0 1px hsl(262 83% 58% / 0.2); -} - -/* Responsive */ -@media (max-width: 768px) { - .outline-overlay { - top: 10px; - right: 10px; - } - - .outline-card { - width: 280px; - } - - .outline-content { - max-height: 400px; - } -} - -/* Root Drop Target Highlight */ -.outline-content.root-drop-target { - background: hsl(262 83% 58% / 0.12); - box-shadow: inset 0 0 0 2px hsl(262 83% 58% / 0.4), inset 0 0 20px hsl(262 83% 68% / 0.2); - border-radius: 8px; -} - -.outline-content.root-drop-target::before { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: hsl(262 83% 68%); - font-size: 14px; - font-weight: 600; - pointer-events: none; - text-shadow: 0 0 8px hsl(262 83% 68% / 0.5); - z-index: 10; - background: rgba(15, 15, 25, 0.8); - padding: 8px 16px; - border-radius: 8px; - border: 1px solid hsl(262 83% 58% / 0.4); -} - -/* Multi-selection styling */ -.tree-node-content.multi-selected { - background: hsl(262 83% 58% / 0.25); -} - -.tree-node-content.multi-selected::before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background: linear-gradient(to bottom, hsl(262 83% 68% / 0.8), hsl(262 83% 68% / 0.4)); - box-shadow: 0 0 8px hsl(262 83% 68% / 0.4); -} - -.tree-node-content.multi-selected:hover { - background: hsl(262 83% 58% / 0.35); -} - -/* Selection count indicator (optional - add to footer) */ -.footer-stats.multi-selection { - color: hsl(262 83% 68%); - font-weight: 600; -} diff --git a/app/src/modules/builder/testUi/outline.tsx b/app/src/modules/builder/testUi/outline.tsx index eb8b0fc..0e65678 100644 --- a/app/src/modules/builder/testUi/outline.tsx +++ b/app/src/modules/builder/testUi/outline.tsx @@ -1,14 +1,23 @@ import { useState, useRef, DragEvent, useCallback } from "react"; import { useParams } from "react-router-dom"; -import { EyeIcon, LockIcon, FolderIcon, ChevronIcon, CubeIcon, AddIcon, DeleteIcon, KebebIcon } from "../../../components/icons/ExportCommonIcons"; +import { + EyeIcon, + LockIcon, + FolderIcon, + ChevronIcon, + CubeIcon, + AddIcon, + KebebIcon, + CollapseAllIcon, +} from "../../../components/icons/ExportCommonIcons"; import RenameInput from "../../../components/ui/inputs/RenameInput"; import { useSceneContext } from "../../scene/sceneContext"; import { useSocketStore } from "../../../store/socket/useSocketStore"; import useAssetResponseHandler from "../../collaboration/responseHandler/useAssetResponseHandler"; -import "./outline.css"; import { getUserData } from "../../../functions/getUserData"; import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; +import clsx from "clsx"; interface DragState { draggedItem: AssetGroupChild | null; @@ -51,7 +60,8 @@ const TreeNode = ({ const isLocked = item.isLocked; const isExpanded = isGroupNode ? item.isExpanded : false; const isSelected = !isGroupNode ? hasSelectedAsset(item.modelUuid) : false; - const isMultiSelected = !isGroupNode && selectedAssets.length > 1 && hasSelectedAsset(item.modelUuid); + const isMultiSelected = + !isGroupNode && selectedAssets.length > 1 && hasSelectedAsset(item.modelUuid); // Determine the parent group of this item const getParentGroup = useCallback( @@ -76,7 +86,7 @@ const TreeNode = ({ // Highlight if this is the target group or belongs to the target group return thisGroupUuid === dragState.targetGroupUuid; - }, [dragState, isGroupNode, item, getParentGroup]); + }, [dragState, isGroupNode, item]); const handleNodeDragStart = (e: DragEvent) => { const parentGroupUuid = getParentGroup(item); @@ -112,9 +122,13 @@ const TreeNode = ({ return (
{isGroupNode && ( - )} -
{isGroupNode ? : }
+
+ {isGroupNode ? : } +
{}} canEdit={true} /> @@ -166,7 +185,14 @@ const TreeNode = ({
{isGroupNode && isExpanded && item.children && ( -
+
{item.children.map((child) => ( { }); const [_, forceUpdate] = useState({}); const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext(); - const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, toggleSelectedAsset, selectedAssets } = assetStore(); - const { groupHierarchy, isGroup, getGroupsContainingAsset, getFlatGroupChildren, setGroupExpanded, addChildToGroup, removeChildFromGroup, getGroupsContainingGroup } = assetGroupStore(); + const { + addSelectedAsset, + clearSelectedAssets, + getAssetById, + peekToggleVisibility, + toggleSelectedAsset, + selectedAssets, + } = assetStore(); + const { + groupHierarchy, + isGroup, + getGroupsContainingAsset, + getFlatGroupChildren, + setGroupExpanded, + addChildToGroup, + removeChildFromGroup, + getGroupsContainingGroup, + } = assetGroupStore(); const { projectId } = useParams(); const { push3D } = undoRedo3DStore(); const { builderSocket } = useSocketStore(); @@ -245,7 +287,11 @@ export const Outline = () => { }) .then((data) => { if (!data.message || !data.data) { - echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); + echo.error( + `Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${ + asset.modelName + }` + ); return; } if (data.message === "Model updated successfully" && data.data) { @@ -264,14 +310,22 @@ export const Outline = () => { }; updateAssetInScene(model, () => { - echo.info(`${asset.isVisible ? "Hid" : "Unhid"} asset: ${model.modelName}`); + echo.info( + `${asset.isVisible ? "Hid" : "Unhid"} asset: ${model.modelName}` + ); }); } else { - echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); + echo.error( + `Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${ + asset.modelName + }` + ); } }) .catch(() => { - echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); + echo.error( + `Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}` + ); }); } else { const data = { @@ -311,7 +365,7 @@ export const Outline = () => { e.dataTransfer.effectAllowed = "move"; forceUpdate({}); }, - [isGroup] + [] ); const handleDragOver = useCallback( @@ -382,7 +436,10 @@ export const Outline = () => { } // Update target group - if (dragStateRef.current.targetGroupUuid !== targetGroupUuid || dragStateRef.current.isRootTarget !== false) { + if ( + dragStateRef.current.targetGroupUuid !== targetGroupUuid || + dragStateRef.current.isRootTarget !== false + ) { dragStateRef.current.targetGroupUuid = targetGroupUuid; dragStateRef.current.isRootTarget = false; forceUpdate({}); @@ -449,9 +506,15 @@ export const Outline = () => { console.log("Dropped:", draggedItem, "into group:", targetGroupUuid); if (isGroup(draggedItem)) { - addChildToGroup(targetGroupUuid, { type: "Group", childrenUuid: draggedItem.groupUuid }); + addChildToGroup(targetGroupUuid, { + type: "Group", + childrenUuid: draggedItem.groupUuid, + }); } else { - addChildToGroup(targetGroupUuid, { type: "Asset", childrenUuid: draggedItem.modelUuid }); + addChildToGroup(targetGroupUuid, { + type: "Asset", + childrenUuid: draggedItem.modelUuid, + }); } } @@ -490,7 +553,9 @@ export const Outline = () => { // Update last selected reference const flattened = getFlattenedHierarchy(); - const index = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); + const index = flattened.findIndex( + (flatItem) => getItemId(flatItem) === getItemId(item) + ); lastSelectedRef.current = { item, index }; } } @@ -504,7 +569,9 @@ export const Outline = () => { // Update last selected reference const flattened = getFlattenedHierarchy(); - const index = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); + const index = flattened.findIndex( + (flatItem) => getItemId(flatItem) === getItemId(item) + ); lastSelectedRef.current = { item, index }; } } @@ -512,7 +579,9 @@ export const Outline = () => { // Shift+Click - range selection else if (isShiftClick) { const flattened = getFlattenedHierarchy(); - const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); + const clickedIndex = flattened.findIndex( + (flatItem) => getItemId(flatItem) === getItemId(item) + ); if (clickedIndex === -1) return; @@ -558,7 +627,7 @@ export const Outline = () => { }); } }, - [scene.current, isGroup, getFlattenedHierarchy, clearSelectedAssets, addSelectedAsset, toggleSelectedAsset] + [scene, isGroup, clearSelectedAssets, addSelectedAsset, getFlattenedHierarchy, toggleSelectedAsset] ); const handleOptionClick = useCallback( @@ -620,78 +689,79 @@ export const Outline = () => { } } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [selectedVersion, builderSocket, projectId, userId, organization] ); - if (!isOpen) { - return ( - - ); - } - return ( -
-
-
-
- - - -

Scene Hierarchy

+ <> +
+
+
+
+

Scene Hierarchy

+
+
+ + {/* */} + + +
- -
-
- - - -
- -
- {groupHierarchy.map((item) => ( - - ))} -
- -
- 1 ? "multi-selection" : ""}`}> - {selectedAssets.length > 1 ? `${selectedAssets.length} items selected` : `${groupHierarchy.length} root items`} - + > + {groupHierarchy.map((item) => ( + + ))} +
+ )}
-
+
+ 1 ? "multi-selection" : ""}`} + > + {selectedAssets.length > 1 + ? `${selectedAssets.length} items selected` + : `${groupHierarchy.length} root items`} + +
+ ); }; diff --git a/app/src/styles/layout/_assetOutline.scss b/app/src/styles/layout/_assetOutline.scss new file mode 100644 index 0000000..0f777b7 --- /dev/null +++ b/app/src/styles/layout/_assetOutline.scss @@ -0,0 +1,344 @@ +@use "../abstracts/variables" as *; +@use "../abstracts/mixins" as *; + +.outline-overlay { + padding: 0 4px; +} +.outline-card { + border-radius: $border-radius-extra-large; + overflow: hidden; + position: relative; + background: var(--background-color); + border: 1px solid var(--border-color); + box-shadow: var(--box-shadow-medium); +} + +// Header +.outline-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 16px; + padding-right: 12px; + position: relative; + + .header-title { + display: flex; + align-items: center; + gap: 10px; + color: var(--text-color); + + p { + margin: 0; + font-weight: 600; + } + } + + .close-button { + @include flex-center; + background: none; + border: none; + color: var(--icon-default-color); + cursor: pointer; + height: 18px; + width: 18px; + min-width: 18px; + border-radius: $border-radius-small; + transition: all 0.2s; + + &:hover { + background: var(--background-color-solid); + transform: rotate(-90deg); + } + } +} + +// Toolbar +.outline-toolbar { + display: flex; + gap: 2px; + padding: 2px; + + .toolbar-button { + @include flex-center; + cursor: pointer; + height: 18px; + width: 18px; + min-width: 18px; + border-radius: $border-radius-small; + font-size: 13px; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + + &:hover { + background: var(--background-color-solid); + } + + &:active { + transform: translateY(0); + } + } +} + +// Content +.outline-content { + max-height: 52vh; + overflow-y: auto; + border-radius: $border-radius-medium; + position: relative; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: var(--background-color-secondary); + } + + &::-webkit-scrollbar-thumb { + background: var(--accent-color); + border-radius: 3px; + box-shadow: 0 0 4px var(--accent-color); + + &:hover { + background: var(--accent-color); + box-shadow: 0 0 8px var(--accent-color); + } + } + + &.root-drop-target { + background: var(--background-color-selected); + box-shadow: inset 0 0 0 2px var(--border-color-accent), + inset 0 0 20px var(--highlight-accent-color); + border-radius: $border-radius-medium; + + &::before { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--accent-color); + font-size: 14px; + font-weight: 600; + pointer-events: none; + text-shadow: 0 0 8px var(--accent-color); + z-index: 10; + background: var(--background-color-drop-down); + padding: 8px 16px; + border-radius: $border-radius-medium; + border: 1px solid var(--border-color-accent); + } + } +} + +// Tree Node +.tree-node { + position: relative; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + &.drop-target-highlight { + background: var(--background-color-selected); + border-radius: $border-radius-medium; + box-shadow: inset 0 0 0 2px var(--border-color-accent), + 0 0 20px var(--highlight-accent-color), 0 4px 12px var(--background-color-selected); + } + + .tree-node-content { + @include flex-center; + gap: 6px; + padding: 6px 12px; + border-radius: $border-radius-medium; + cursor: grab; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + background: transparent; + position: relative; + + &:hover { + background: var(--background-color-accent); + } + + &.selected { + background: var(--background-color-selected); + } + + &.dragging { + opacity: 0.5; + background: var(--background-color-selected); + cursor: grabbing; + transform: scale(0.98); + box-shadow: 0 4px 12px var(--highlight-accent-color); + } + + &.locked { + opacity: 0.6; + cursor: not-allowed; + } + + &.hidden { + opacity: 0.4; + } + + &.multi-selected { + background: var(--background-color-selected); + + &::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--highlight-secondary-color); + box-shadow: 0 0 8px var(--highlight-secondary-color); + } + + &:hover { + background: var(--background-color-accent); + } + } + } + + .expand-button { + @include flex-center; + width: 20px; + height: 20px; + border-radius: $border-radius-small; + background: none; + border: none; + cursor: pointer; + color: var(--icon-default-color); + transition: all 0.2s; + + &:hover { + background: var(--background-color-selected); + color: var(--accent-color); + box-shadow: 0 0 8px var(--highlight-accent-color); + } + } + + .node-icon { + @include flex-center; + color: var(--accent-color); + flex-shrink: 0; + } + + .node-name { + flex: 1; + color: var(--text-color); + font-size: var(--font-size-small); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + user-select: none; + } + + .node-controls { + display: flex; + + .control-button { + @include flex-center; + background: none; + border: none; + color: var(--icon-default-color); + cursor: pointer; + padding: 4px; + border-radius: $border-radius-small; + transition: all 0.2s; + + &:hover { + background: var(--background-color-selected); + color: var(--accent-color); + box-shadow: 0 0 6px var(--highlight-accent-color); + } + } + } +} + +// Tree children lines +.tree-children { + position: relative; + --left: 1; + &::before { + content: ""; + position: absolute; + left: calc(12px + (var(--left) * 26px)); + top: 0; + bottom: 0; + width: 1px; + background: linear-gradient( + to bottom, + var(--accent-color), + var(--background-color-selected) + ); + box-shadow: 0 0 4px var(--highlight-accent-color); + } +} + +// Footer +.outline-footer { + position: fixed; + padding: 8px 16px; + width: 270px; + background: var(--background-color-solid); + bottom: -36px; + border-radius: 16px; + + .footer-stats { + color: var(--text-color); + font-size: 12px; + font-weight: 500; + + &.multi-selection { + color: var(--accent-color); + font-weight: 600; + } + } +} + +// Toggle Button +.outline-toggle { + position: fixed; + top: 20px; + right: 20px; + z-index: $z-index-tools; + background: var(--background-color-button); + backdrop-filter: blur(12px); + border: 1px solid var(--border-color-accent); + border-radius: $border-radius-extra-large; + padding: 12px; + cursor: pointer; + color: var(--text-button-color); + box-shadow: var(--box-shadow-medium); + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + animation: fadeIn 0.3s; + + &:hover { + transform: scale(1.08) rotate(2deg); + box-shadow: var(--box-shadow-heavy); + } + + &:active { + transform: scale(0.95); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes cardGlow { + 0%, + 100% { + opacity: 0.5; + } + 50% { + opacity: 0.8; + } +} diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss index ea3ab8e..19b4325 100644 --- a/app/src/styles/main.scss +++ b/app/src/styles/main.scss @@ -39,6 +39,7 @@ @use "layout/compareLayout"; @use "layout/resourceManagement.scss"; @use "layout/previewModel"; +@use "layout/assetOutline"; // pages @use "pages/dashboard";