Initial commit

This commit is contained in:
2025-10-23 17:14:20 +05:30
parent f81eb17304
commit efb95e6680
12 changed files with 3462 additions and 21 deletions

403
package-lock.json generated
View File

@@ -16,11 +16,15 @@
"@types/node": "^16.18.126",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"clsx": "^2.1.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"sass": "^1.93.2"
}
},
"node_modules/@adobe/css-tools": {
@@ -3019,6 +3023,316 @@
"node": ">= 8"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -5678,6 +5992,15 @@
"wrap-ansi": "^7.0.0"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6621,6 +6944,20 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -9211,6 +9548,13 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -11614,6 +11958,14 @@
"tslib": "^2.0.3"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -14522,6 +14874,27 @@
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==",
"license": "CC0-1.0"
},
"node_modules/sass": {
"version": "1.93.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/sass-loader": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
@@ -14560,6 +14933,36 @@
}
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sass/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",

View File

@@ -11,6 +11,7 @@
"@types/node": "^16.18.126",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"clsx": "^2.1.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-scripts": "5.0.1",
@@ -40,5 +41,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"sass": "^1.93.2"
}
}

View File

@@ -1,26 +1,8 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import SceneView from "./pages/SceneView";
import "./styles/main.scss";
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
return <SceneView />;
}
export default App;

View File

@@ -0,0 +1,487 @@
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<React.SetStateAction<string | null>>;
}
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<TreeNodeProps> = ({
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(isGroupNode ? 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<HTMLInputElement>) => {
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<HTMLDivElement>) => {
// support Enter and Space for accessibility
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggleSelect();
}
};
const handleTouchStart = () => {
toggleSelect();
};
return (
<div className="tree-node">
<div
tabIndex={0}
onKeyDown={handleDivKeyDown}
onTouchStart={handleTouchStart}
className={clsx("tree-node-content", {
"group-node": isGroupNode,
"asset-node": !isGroupNode,
selected: selectedObject === item.modelUuid,
locked: isLocked,
hidden: !isVisible,
dragging: isBeingDragged,
})}
draggable
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDrop={handleDrop}
id={item.modelUuid || item.groupUuid}
onClick={toggleSelect}
>
<div className="node-wrapper" style={{ paddingLeft: `${level * 25 + 8}px` }}>
{isGroupNode && (
<button
className="expand-button"
onClick={(e) => {
e.stopPropagation(); // prevent triggering selection
setIsExpanded(!isExpanded);
}}
>
<ChevronIcon isOpen={isExpanded} />
</button>
)}
<div className="node-icon">
{isGroupNode ? <FolderIcon isOpen={isExpanded} /> : <CubeIcon />}
</div>
<div className="rename-input" onDoubleClick={handleDoubleClick}>
{isEditing ? (
<input
autoFocus
value={name}
className="renaming"
style={{ color: textColor }}
onChange={(e) => setName(e.target.value)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
) : (
<span
className="rename-input"
style={{ color: textColor || DEFAULT_PRAMS.textColor }}
>
{name}
</span>
)}
</div>
<div className="node-controls">
<button
className="control-button"
onClick={(e) => {
e.stopPropagation();
setIsVisible(!isVisible);
}}
>
<EyeIcon
isClosed={!isVisible}
color={eyeIconColor || DEFAULT_PRAMS.eyeIconColor}
/>
</button>
{isGroupNode && item.children?.length ? (
<button className="control-button">
<FocusIcon />
</button>
) : null}
<button
className="control-button"
onClick={(e) => {
e.stopPropagation();
setIsLocked(!isLocked);
}}
>
<LockIcon
isLocked={isLocked}
color={lockIconColor || DEFAULT_PRAMS.lockIconColor}
/>
</button>
{isGroupNode ? (
<button className="control-button">
<KebebIcon />
</button>
) : null}
</div>
</div>
</div>
{isExpanded && item.children?.length ? (
<div className="tree-children">
{item.children.map((child) => (
<TreeNode
key={child.groupUuid || child.modelUuid}
item={child}
level={level + 1}
onDragStart={onDragStart}
onDrop={onDrop}
draggingItem={draggingItem}
selectedObject={selectedObject}
setSelectedObject={setSelectedObject}
onDragOver={onDragOver}
/>
))}
</div>
) : null}
</div>
);
};
export const OutlineList: React.FC<OutlinePanelProps> = ({
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<string | null>(null); // store UUID
const [isOpen, setIsOpen] = useState(true);
const [draggingItem, setDraggingItem] = useState<AssetGroupChild | null>(null);
const [groupHierarchy, setGroupHierarchy] = useState<AssetGroupChild[]>([
{ 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);
}
// Commit changes
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;
// Remove previous outlines before applying new one
hoveredDiv.style.outline = "none";
hoveredDiv.style.borderTop = "none";
hoveredDiv.style.borderBottom = "none";
const rect = hoveredDiv.getBoundingClientRect();
const y = (event as any).clientY - rect.top;
// Determine where the user is hovering
if (y >= 0 && y < 7) {
// Top region
console.log("Top: ");
hoveredDiv.style.borderTop = "2px solid purple";
return "above";
} else if (y >= 19 && y < 32) {
// Bottom region
console.log("Bottom: ");
hoveredDiv.style.borderBottom = "2px solid purple";
return "below";
} else {
// Middle region (child)
console.log("Middle: ");
hoveredDiv.style.outline = "2px solid #b188ff";
return "child";
}
};
return (
<div
className="outline-overlay"
style={{
justifyContent: panelSide === "left" ? "flex-start" : "flex-end",
}}
>
<div
className="outline-card"
style={{
background: backgroundColor || DEFAULT_PRAMS.backgroundColor,
}}
>
<div className="outline-header">
<div className="header-title">
<p>Outline</p>
</div>
<div className="outline-toolbar">
<button className="toolbar-button">
<AddIcon color={addIconColor || DEFAULT_PRAMS.addIconColor} />
</button>
<button className="toolbar-button">
<CollapseAllIcon />
</button>
<button className="close-button" onClick={() => setIsOpen(!isOpen)}>
<ChevronIcon isOpen={isOpen} />
</button>
</div>
</div>
{isOpen && (
<div className="outline-content">
{groupHierarchy.map((item) => (
<TreeNode
key={item.groupUuid || item.modelUuid}
item={item}
textColor={textColor}
eyeIconColor={eyeIconColor}
lockIconColor={lockIconColor}
onDragStart={handleDragStart}
onDrop={handleDrop}
onDragOver={handleDragOver}
draggingItem={draggingItem}
selectedObject={selectedObject} // pass UUID
setSelectedObject={setSelectedObject} // pass setter
/>
))}
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,17 @@
import React from "react";
import { OutlineList } from "./OutlineList ";
export interface OutlinePanelProps {
textColor?: string;
panelSide?: "left" | "right";
backgroundColor?: string;
addIconColor?: string;
eyeIconColor?: string;
lockIconColor?: string;
}
const OutlinePanel: React.FC<OutlinePanelProps> = (props) => {
return <OutlineList {...props} />;
};
export default OutlinePanel;

1742
src/icons/ExportIcons.tsx Normal file

File diff suppressed because it is too large Load Diff

16
src/pages/SceneView.tsx Normal file
View File

@@ -0,0 +1,16 @@
import React from "react";
import OutlinePanel from "../components/outlinePanel/OutlinePanel";
const SceneView = () => {
return (
<OutlinePanel
// panelSide="right"
// textColor=""
// addIconColor=""
// eyeIconColor=""
// backgroundColor=""
/>
);
};
export default SceneView;

View File

@@ -0,0 +1,5 @@
// get rem from pixels
@function get_rem($px) {
@return calc($px / 16 * 1rem);
}

View File

@@ -0,0 +1,33 @@
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin flex-space-between {
display: flex;
justify-content: space-between;
align-items: center;
}
// Array of base colors
$colors: (
#f5550b,
#1bac1b,
#0099ff,
#d4c927,
#8400ff,
#13e9b3,
#df1dcf
);
@mixin gradient-by-child($index) {
// Get the color based on the index passed
$base-color: nth($colors, $index);
// Apply gradient using the same color with different alpha values
background: linear-gradient(
144.19deg,
rgba($base-color, 0.2) 16.62%, // 20% opacity
rgba($base-color, 0.08) 85.81% // 80% opacity
);
}

View File

@@ -0,0 +1,171 @@
@use "functions";
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
// new variables
// text colors
// ---------- light mode ----------
$text-color: #2b3344;
$text-disabled: #b7b7c6;
$input-text-color: #595965;
$highlight-text-color: #6f42c1;
$text-button-color: #f3f3fd;
// ---------- dark mode ----------
$text-color-dark: #f3f3fd;
$text-disabled-dark: #6f6f7a;
$input-text-color-dark: #b5b5c8;
$highlight-text-color-dark: #d2baff;
$text-button-color-dark: #f3f3fd;
// background colors
// ---------- light mode ----------
$background-color: linear-gradient(-45deg, #fcfdfd71 0%, #fcfdfd79 100%);
$background-color-solid-gradient: linear-gradient(-45deg, #fcfdfd 0%, #fcfdfd 100%);
$background-color-solid: #fcfdfd;
$background-color-secondary: #fcfdfd4d;
$background-color-accent: #6f42c1;
$background-color-button: #6f42c1;
$background-color-drop-down: #6f42c14d;
$background-color-input: #ffffff4d;
$background-color-input-focus: #f2f2f7;
$background-color-drop-down-gradient: linear-gradient(-45deg, #75649366 0%, #40257266 100%);
$background-color-selected: #e0dfff;
$background-radial-gray-gradient: radial-gradient(circle, #bfe0f8 0%, #e9ebff 46%, #e2acff 100%);
$background-model: #ffffff80;
// ---------- dark mode ----------
$background-color-dark: linear-gradient(-45deg, #333333b3 0%, #2d2437b3 100%);
$background-color-solid-gradient-dark: linear-gradient(-45deg, #333333 0%, #2d2437 100%);
$background-color-solid-dark: #19191d;
$background-color-secondary-dark: #19191d99;
$background-color-accent-dark: #6f42c1;
$background-color-button-dark: #6f42c1;
$background-color-drop-down-dark: #50505080;
$background-color-input-dark: #ffffff33;
$background-color-input-focus-dark: #333333;
$background-color-drop-down-gradient-dark: linear-gradient(-45deg, #8973b166 0%, #53427366 100%);
$background-color-selected-dark: #403e66;
$background-radial-gray-gradient-dark: radial-gradient(
circle,
#31373b 0%,
#48494b 46%,
#52415c 100%
);
$background-model-dark: #00000080;
// border colors
// ---------- light mode ----------
$border-color: #e0dfff;
$input-border-color: #d5dddd80;
$border-color-accent: #6f42c1;
// ---------- dark mode ----------
$border-color-dark: #564b69;
$input-border-color-dark: #d5dddd80;
$border-color-accent-dark: #6f42c1;
// highlight colors
// ---------- light mode ----------
$highlight-accent-color: #e0dfff;
$highlight-secondary-color: #6f42c1;
// ---------- dark mode ----------
$highlight-accent-color-dark: #403e6a;
$highlight-secondary-color-dark: #c4abf1;
// icon colors
// ---------- light mode ----------
$icon-default-color: #6f42c1;
$icon-default-color-hover: #7f4ddb;
$icon-default-color-active: #f2f2f7;
// ---------- dark mode ----------
$icon-default-color-dark: #6f42c1;
$icon-default-color-hover-dark: #7f4ddb;
$icon-default-color-active-dark: #f2f2f7;
// colors
$color1: #a392cd;
$color2: #7b4cd3;
$color3: #b186ff;
$color4: #8752e8;
$color5: #c7a8ff;
// log indication colors
// ------------ text -------------
$log-default-text-color: #6f42c1;
$log-info-text-color: #1773fd;
$log-warn-text-color: #f3a50c;
$log-error-text-color: #fc230f;
$log-success-text-color: #23a84f;
// ----------- dark ---------------
$log-default-text-color-dark: #b18ef1;
$log-info-text-color-dark: #7eb0fa;
$log-warn-text-color-dark: #ffaa00;
$log-error-text-color-dark: #ff887d;
$log-success-text-color-dark: #43ff81;
// ------------ background -------------
$log-default-backgroung-color: #6e42c133;
$log-info-background-color: #1773fd5d;
$log-warn-background-color: #f3a50c33;
$log-error-background-color: #fc230f33;
$log-success-background-color: #0ef75b33;
// old variables
$accent-color: #6f42c1;
$accent-color-dark: #c4abf1;
$background-color-gray: #f3f3f3;
$background-color-gray-dark: #232323;
$shadow-color: #3c3c431a;
$shadow-color-dark: #8f8f8f1a;
$acent-gradient-dark: linear-gradient(90deg, #b392f0 0%, #a676ff 100%);
$acent-gradient: linear-gradient(90deg, #6f42c1 0%, #925df3 100%);
$faint-gradient: radial-gradient(circle, #bfe0f8 0%, #e9ebff 46%, #e2acff 100%);
$faint-gradient-dark: radial-gradient(circle, #31373b 0%, #48494b 46%, #52415c 100%);
$font-inter: "Inter", sans-serif;
$font-josefin-sans: "Josefin Sans", sans-serif;
$font-poppins: "Poppins", sans-serif;
$font-roboto: "Roboto", sans-serif;
$tiny: 0.625rem;
$small: 0.75rem;
$regular: 0.8rem;
$large: 1rem;
$xlarge: 1.125rem;
$xxlarge: 1.5rem;
$xxxlarge: 2rem;
$thin-weight: 300;
$regular-weight: 400;
$medium-weight: 500;
$bold-weight: 600;
$z-index-drei-html: 1;
$z-index-default: 1;
$z-index-marketplace: 2;
$z-index-tools: 3;
$z-index-negative: -1;
$z-index-ui-base: 10;
$z-index-ui-overlay: 20;
$z-index-ui-popup: 30;
$z-index-ui-highest: 50;
$box-shadow-light: 0px 2px 4px $shadow-color;
$box-shadow-medium: 0px 4px 8px $shadow-color;
$box-shadow-heavy: 0px 8px 16px $shadow-color;
$border-radius-small: 4px;
$border-radius-medium: 6px;
$border-radius-large: 12px;
$border-radius-circle: 50%;
$border-radius-xlarge: 16px;
$border-radius-extra-large: 20px;
$border-radius-xxx: 30px;

View File

@@ -0,0 +1,580 @@
// .outline-overlay {
// padding: 0 4px;
// font-family: "Segoe UI", sans-serif;
// display: flex;
// position: absolute;
// top: 0;
// bottom: 0;
// width: 100vw;
// pointer-events: none;
// .outline-card {
// pointer-events: all;
// }
// }
// .outline-header {
// display: flex;
// align-items: center;
// justify-content: space-between;
// padding: 10px 16px;
// border-bottom: 1px solid #2e2e3e;
// .header-title {
// p {
// margin: 0;
// font-weight: 600;
// font-size: 14px;
// color: #ffffff;
// }
// }
// .outline-toolbar {
// display: flex;
// gap: 8px;
// .toolbar-button {
// background: none;
// border: none;
// color: #aaa;
// cursor: pointer;
// width: 24px;
// height: 24px;
// border-radius: 4px;
// transition: background 0.3s;
// &:hover {
// background-color: rgba(255, 255, 255, 0.1);
// color: #a855f7;
// }
// svg {
// width: 100%;
// height: 100%;
// }
// }
// .close-button {
// background: none;
// border: none;
// color: #aaa;
// cursor: pointer;
// width: 24px;
// height: 24px;
// border-radius: 4px;
// transition: transform 0.3s ease;
// &:hover {
// background-color: rgba(255, 255, 255, 0.1);
// transform: rotate(-90deg);
// color: #a855f7;
// }
// svg {
// width: 100%;
// height: 100%;
// }
// }
// }
// }
// .outline-content {
// max-height: 52vh;
// overflow-y: auto;
// padding: 8px 0;
// &::-webkit-scrollbar {
// width: 6px;
// }
// &::-webkit-scrollbar-thumb {
// background: #a855f7;
// border-radius: 10px;
// }
// }
// .tree-node {
// display: flex;
// flex-direction: column;
// width: 100%;
// outline: 2px solid transparent;
// transition: all 0.3s ease;
// .tree-node-content {
// display: flex;
// align-items: center;
// width: 100%;
// padding: 6px 10px;
// border-radius: 8px;
// background: transparent;
// transition: background 0.2s ease, outline 0.2s ease;
// box-sizing: border-box;
// position: relative;
// &:hover {
// background: rgba(255, 255, 255, 0.05);
// }
// &.selected {
// background: var(--background-color-accent, #6f42c1);
// }
// &.multi-selected {
// background: rgba(167, 139, 250, 0.2);
// border-left: 3px solid var(--highlight-secondary-color, #a855f7);
// }
// .expand-button {
// background: none;
// border: none;
// color: #aaa;
// cursor: pointer;
// width: 18px;
// height: 18px;
// flex-shrink: 0;
// display: flex;
// align-items: center;
// justify-content: center;
// &:hover {
// color: #a855f7;
// }
// }
// .node-wrapper {
// display: flex;
// align-items: center;
// flex: 1;
// min-width: 0;
// gap: 8px;
// }
// .node-icon {
// color: #a855f7;
// flex-shrink: 0;
// width: 16px;
// height: 16px;
// display: flex;
// align-items: center;
// justify-content: center;
// }
// .rename-input {
// flex: 1;
// min-width: 0;
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
// font-size: 14px;
// color: #fff;
// input.renaming {
// width: 100%;
// border: none;
// outline: none;
// border-radius: 6px;
// background: rgba(255, 255, 255, 0.1);
// color: #fff;
// padding: 3px 6px;
// font-size: 14px;
// box-sizing: border-box;
// }
// }
// .node-controls {
// display: flex;
// align-items: center;
// gap: 4px;
// flex-shrink: 0;
// .control-button {
// background: none;
// border: none;
// color: #aaa;
// cursor: pointer;
// border-radius: 4px;
// padding: 4px;
// display: flex;
// align-items: center;
// justify-content: center;
// &:hover {
// background: rgba(255, 255, 255, 0.1);
// color: #a855f7;
// }
// svg {
// width: 14px;
// height: 14px;
// }
// }
// }
// }
// .tree-children {
// padding-left: 16px;
// border-left: 1px dashed rgba(255, 255, 255, 0.1);
// margin-left: 6px;
// }
// }
// // .tree-node {
// // display: flex;
// // flex-direction: column;
// // outline: 2px solid transparent;
// // transition: background 0.3s;
// // .tree-node-content {
// // display: flex;
// // align-items: center;
// // padding: 8px 9px;
// // gap: 6px;
// // transition: background 0.2s ease;
// // position: relative;
// // &.selected {
// // background: #6f42c1;
// // border-radius: 45px;
// // }
// // &:hover {
// // outline: 1px solid pink;
// // border-radius: 80px;
// // }
// // .node-wrapper {
// // display: flex;
// // gap: 40px;
// // }
// // .expand-button {
// // background: none;
// // border: none;
// // cursor: pointer;
// // width: 20px;
// // height: 20px;
// // padding: 0;
// // color: #aaa;
// // &:hover {
// // color: #a855f7;
// // }
// // }
// // .node-icon {
// // color: white;
// // width: 18px;
// // flex-shrink: 0;
// // }
// // .rename-input {
// // background: transparent;
// // border: none;
// // color: #fff;
// // font-size: 14px;
// // flex: 1;
// // outline: none;
// // padding: 2px 4px;
// // &:focus {
// // border-bottom: 1px solid #a855f7;
// // }
// // .renaming {
// // outline: none;
// // border: none;
// // border-radius: 15px;
// // height: 20px;
// // }
// // }
// // .node-controls {
// // display: flex;
// // gap: 6px;
// // .control-button {
// // background: none;
// // border: none;
// // color: #888;
// // cursor: pointer;
// // // padding: 4px;
// // border-radius: 4px;
// // &:hover {
// // background-color: rgba(255, 255, 255, 0.1);
// // color: #a855f7;
// // }
// // svg {
// // width: 16px;
// // height: 16px;
// // }
// // }
// // }
// // }
// // }
// // .tree-children {
// // padding-left: 16px;
// // border-left: 1px dashed rgba(255, 255, 255, 0.05);
// // }
// Outline Component Styles
// Professional tree/hierarchy view with drag-and-drop support
.outline-overlay {
padding: 0 4px;
font-family: "Segoe UI", sans-serif;
display: flex;
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);
display: flex;
flex-direction: column;
overflow: hidden;
}
.outline-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid #2e2e3e;
}
.header-title {
p {
margin: 0;
font-weight: 600;
font-size: 14px;
color: #ffffff;
}
}
.outline-toolbar {
display: flex;
gap: 8px;
.toolbar-button {
background: none;
border: none;
color: #aaa;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 4px;
transition: background 0.3s;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #a855f7;
}
svg {
width: 100%;
height: 100%;
}
}
.close-button {
background: none;
border: none;
color: #aaa;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 4px;
transition: transform 0.3s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
transform: rotate(-90deg);
color: #a855f7;
}
svg {
width: 100%;
height: 100%;
}
}
}
.outline-content {
max-height: 52vh;
overflow-y: auto;
padding: 8px 0;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #a855f7;
border-radius: 10px;
}
}
.tree-node {
display: flex;
flex-direction: column;
transition: background 0.3s;
&.dragging {
opacity: 0.5;
}
}
.tree-node-content {
display: flex;
align-items: center;
padding: 8px 12px;
gap: 8px;
transition: background 0.2s ease;
position: relative;
cursor: pointer;
border-radius: 6px;
margin: 2px 8px;
&.selected {
background: #6f42c1;
border-radius: 50px;
}
&:hover {
outline: 2px solid #b188ff;
border-radius: 50px;
}
> div {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
}
&.locked {
opacity: 0.6;
cursor: not-allowed;
transform: scale(0.98);
background-color: transparent;
outline: none;
border: none;
}
&.hidden {
opacity: 0.4;
}
}
.expand-button {
background: none;
border: none;
cursor: pointer;
width: 20px;
height: 20px;
padding: 0;
color: #aaa;
&:hover {
color: #a855f7;
}
}
.node-icon {
color: #fff;
width: 18px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 18px;
height: 18px;
}
}
.rename-input {
background: transparent;
border: none;
color: #fff;
font-size: 14px;
flex: 1;
outline: none;
padding: 2px 4px;
&:focus {
border-bottom: 1px solid #a855f7;
}
input {
background: white;
border: none;
color: black;
font-size: 14px;
width: 100%;
outline: none;
&:focus {
border-bottom: 1px solid white;
border-radius: 30px;
height: 20px;
}
}
span {
display: block;
}
}
.node-controls {
display: flex;
gap: 6px;
margin-left: auto;
.control-button {
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #a855f7;
}
svg {
width: 16px;
height: 16px;
}
}
}
.tree-children {
padding-left: 16px;
border-left: 1px dashed rgba(255, 255, 255, 0.05);
}
// Responsive adjustments
@media (max-width: 768px) {
.outline-card {
width: 240px;
}
.outline-overlay {
padding: 0 2px;
}
.rename-input {
font-size: 13px;
}
}

1
src/styles/main.scss Normal file
View File

@@ -0,0 +1 @@
@use "components/outlinePanel.scss";