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.
This commit is contained in:
2025-10-06 17:17:39 +05:30
parent 72f2b0f4bc
commit 9e40670d69
9 changed files with 2067 additions and 2083 deletions

2
app/package-lock.json generated
View File

@@ -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"
}

View File

@@ -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",

File diff suppressed because it is too large Load Diff

View File

@@ -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 && (
<>
<Outline />
<KeyPressListener />
{loadingProgress > 0 && <LoadingPage progress={loadingProgress} />}
{!isPlaying && (

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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 (
<div className={`tree-node ${shouldShowHighlight ? "drop-target-highlight" : ""}`}>
<div
className={`tree-node-content ${isLocked ? "locked" : ""} ${!isVisible ? "hidden" : ""} ${dragState.draggedItem === item ? "dragging" : ""} ${isSelected ? "selected" : ""} ${
isMultiSelected ? "multi-selected" : ""
}`}
className={clsx("tree-node-content", {
locked: isLocked,
hidden: !isVisible,
dragging: dragState.draggedItem === item,
selected: isSelected,
"multi-selected": isMultiSelected,
})}
style={{ paddingLeft: `${level * 25 + 8}px` }}
draggable={!isLocked}
onDragStart={handleNodeDragStart}
@@ -124,12 +138,17 @@ const TreeNode = ({
onClick={handleNodeClick}
>
{isGroupNode && (
<button className="expand-button" onClick={() => onToggleExpand(item.groupUuid, !isExpanded)}>
<button
className="expand-button"
onClick={() => onToggleExpand(item.groupUuid, !isExpanded)}
>
<ChevronIcon isOpen={isExpanded} />
</button>
)}
<div className="node-icon">{isGroupNode ? <FolderIcon isOpen={isExpanded} /> : <CubeIcon />}</div>
<div className="node-icon">
{isGroupNode ? <FolderIcon isOpen={isExpanded} /> : <CubeIcon />}
</div>
<RenameInput value={itemName} onRename={() => {}} canEdit={true} />
@@ -166,7 +185,14 @@ const TreeNode = ({
</div>
{isGroupNode && isExpanded && item.children && (
<div className="tree-children">
<div
className="tree-children"
style={
{
"--left": level,
} as React.CSSProperties
}
>
{item.children.map((child) => (
<TreeNode
key={isGroup(child) ? child.groupUuid : child.modelUuid}
@@ -200,8 +226,24 @@ export const Outline = () => {
});
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 (
<button className="outline-toggle" onClick={() => setIsOpen(true)}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M3 6h14 M3 10h14 M3 14h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
);
}
return (
<div className="outline-overlay" onDragEnd={handleDragEnd}>
<div className="outline-card">
<div className="outline-header">
<div className="header-title">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M3 3h6v6H3V3z M11 3h6v6h-6V3z M3 11h6v6H3v-6z M11 11h6v6h-6v-6z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
<h2>Scene Hierarchy</h2>
<>
<div className="outline-overlay" onDragEnd={handleDragEnd}>
<div className="outline-card">
<div className="outline-header">
<div className="header-title">
<p>Scene Hierarchy</p>
</div>
<div className="outline-toolbar">
<button className="toolbar-button" title="Add Group">
<AddIcon />
</button>
{/* <button className="toolbar-button" title="Delete">
<DeleteIcon />
</button> */}
<button
className="toolbar-button"
title="Expand All"
onClick={() => {
const { assetGroups, setGroupExpanded } =
assetGroupStore.getState();
assetGroups.forEach((group) =>
setGroupExpanded(group.groupUuid, true)
);
}}
>
<CollapseAllIcon />
</button>
<button className="close-button" onClick={() => setIsOpen(!isOpen)}>
<ChevronIcon isOpen />
</button>
</div>
</div>
<button className="close-button" onClick={() => setIsOpen(false)}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M5 5l10 10 M15 5L5 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
</div>
<div className="outline-toolbar">
<button className="toolbar-button" title="Add Group">
<AddIcon />
</button>
<button className="toolbar-button" title="Delete">
<DeleteIcon />
</button>
<button
className="toolbar-button"
title="Expand All"
onClick={() => {
const { assetGroups, setGroupExpanded } = assetGroupStore.getState();
assetGroups.forEach((group) => setGroupExpanded(group.groupUuid, true));
}}
>
<ChevronIcon isOpen />
</button>
</div>
<div className={`outline-content ${dragStateRef.current.isRootTarget ? "root-drop-target" : ""}`} onDragOver={handleRootDragOver} onDrop={handleDrop}>
{groupHierarchy.map((item) => (
<TreeNode
key={isGroup(item) ? item.groupUuid : item.modelUuid}
item={item}
dragState={dragStateRef.current}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
{isOpen && (
<div
className={`outline-content ${
dragStateRef.current.isRootTarget ? "root-drop-target" : ""
}`}
onDragOver={handleRootDragOver}
onDrop={handleDrop}
onClick={handleClick}
onToggleExpand={handleToggleExpand}
onOptionClick={handleOptionClick}
/>
))}
</div>
<div className="outline-footer">
<span className={`footer-stats ${selectedAssets.length > 1 ? "multi-selection" : ""}`}>
{selectedAssets.length > 1 ? `${selectedAssets.length} items selected` : `${groupHierarchy.length} root items`}
</span>
>
{groupHierarchy.map((item) => (
<TreeNode
key={isGroup(item) ? item.groupUuid : item.modelUuid}
item={item}
dragState={dragStateRef.current}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={handleClick}
onToggleExpand={handleToggleExpand}
onOptionClick={handleOptionClick}
/>
))}
</div>
)}
</div>
</div>
</div>
<div className="outline-footer">
<span
className={`footer-stats ${selectedAssets.length > 1 ? "multi-selection" : ""}`}
>
{selectedAssets.length > 1
? `${selectedAssets.length} items selected`
: `${groupHierarchy.length} root items`}
</span>
</div>
</>
);
};

View File

@@ -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;
}
}

View File

@@ -39,6 +39,7 @@
@use "layout/compareLayout";
@use "layout/resourceManagement.scss";
@use "layout/previewModel";
@use "layout/assetOutline";
// pages
@use "pages/dashboard";