1
0
This commit is contained in:
2026-05-19 16:31:29 +10:00
parent bdf5f4b44e
commit 8b08d9571e
3 changed files with 231 additions and 104 deletions

View File

@@ -10,6 +10,17 @@ import { readCachedDetails, writeCachedDetails } from "./services/detailsCache";
import { describeRemoteObject, getActiveTransport, readRemoteObjectProperties } from "./services/remoteControl";
import { enrichActors, getAllLevelActors, normalizeActor } from "./services/unrealActors";
const SPLIT_STORAGE_KEY = "unreal-outliner.split-ratio";
const DEFAULT_SPLIT_RATIO = 0.52;
const MIN_SPLIT_RATIO = 0.22;
const MAX_SPLIT_RATIO = 0.78;
function readSavedSplitRatio(): number {
const raw = window.localStorage.getItem(SPLIT_STORAGE_KEY);
const value = raw ? Number(raw) : DEFAULT_SPLIT_RATIO;
return Number.isFinite(value) ? Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, value)) : DEFAULT_SPLIT_RATIO;
}
export function App() {
const [actors, setActors] = React.useState<ActorRow[]>([]);
const [query, setQuery] = React.useState("");
@@ -22,6 +33,8 @@ export function App() {
const [details, setDetails] = React.useState<ObjectDetails | null>(null);
const [detailsError, setDetailsError] = React.useState<string | null>(null);
const [isDetailsLoading, setIsDetailsLoading] = React.useState(false);
const [splitRatio, setSplitRatio] = React.useState(readSavedSplitRatio);
const mainContentRef = React.useRef<HTMLDivElement | null>(null);
const tree = React.useMemo(() => buildTree(actors), [actors]);
const selectedNode = React.useMemo(() => findNodeById(tree, selectedId), [tree, selectedId]);
@@ -110,6 +123,58 @@ export function App() {
});
};
const setClampedSplitRatio = React.useCallback((nextRatio: number) => {
const clamped = Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, nextRatio));
setSplitRatio(clamped);
window.localStorage.setItem(SPLIT_STORAGE_KEY, String(clamped));
}, []);
const startResize = (event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
const container = mainContentRef.current;
if (!container) {
return;
}
const pointerId = event.pointerId;
event.currentTarget.setPointerCapture(pointerId);
const onPointerMove = (moveEvent: PointerEvent) => {
const bounds = container.getBoundingClientRect();
const nextRatio = (moveEvent.clientY - bounds.top) / bounds.height;
setClampedSplitRatio(nextRatio);
};
const onPointerUp = () => {
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointercancel", onPointerUp);
};
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointercancel", onPointerUp);
};
const resizeWithKeyboard = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowUp") {
event.preventDefault();
setClampedSplitRatio(splitRatio - 0.04);
}
if (event.key === "ArrowDown") {
event.preventDefault();
setClampedSplitRatio(splitRatio + 0.04);
}
if (event.key === "Home") {
event.preventDefault();
setClampedSplitRatio(MIN_SPLIT_RATIO);
}
if (event.key === "End") {
event.preventDefault();
setClampedSplitRatio(MAX_SPLIT_RATIO);
}
};
return (
<main className="shell">
<section className="panel" aria-label="Unreal Outliner">
@@ -161,7 +226,11 @@ export function App() {
<span>ID Name</span>
</div>
<div className="main-content">
<div
className="main-content"
ref={mainContentRef}
style={{ "--outliner-ratio": splitRatio, "--details-ratio": 1 - splitRatio } as React.CSSProperties}
>
<div className="rows" role="tree">
{error ? (
<div className="empty">
@@ -182,6 +251,23 @@ export function App() {
)}
</div>
<div
aria-label="Resize Outliner and Details panes"
aria-orientation="horizontal"
aria-valuemax={Math.round(MAX_SPLIT_RATIO * 100)}
aria-valuemin={Math.round(MIN_SPLIT_RATIO * 100)}
aria-valuenow={Math.round(splitRatio * 100)}
className="pane-splitter"
onDoubleClick={() => setClampedSplitRatio(DEFAULT_SPLIT_RATIO)}
onKeyDown={resizeWithKeyboard}
onPointerDown={startResize}
role="separator"
tabIndex={0}
title="Drag to resize panes. Double-click to reset."
>
<span />
</div>
<DetailsPanel node={selectedNode} details={details} isLoading={isDetailsLoading} error={detailsError} />
</div>

View File

@@ -161,7 +161,6 @@ export function DetailsPanel({
<div className="component-row child">
<Box size={18} />
<span>{node?.type ? `${node.type}Component (${node.type}Component0)` : "Component"}</span>
<a>Edit in C++</a>
</div>
</div>

View File

@@ -1,6 +1,7 @@
:root {
color-scheme: dark;
font-family: "Segoe UI", "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 85%;
background: #101010;
color: #c7c7c7;
}
@@ -23,16 +24,16 @@ input {
.shell {
min-height: 100vh;
padding: 10px;
padding: 8px;
background: #101010;
}
.panel {
width: min(1220px, 100%);
min-height: calc(100vh - 20px);
height: calc(100vh - 20px);
min-height: calc(100vh - 16px);
height: calc(100vh - 16px);
display: grid;
grid-template-rows: 48px 56px 36px 1fr 50px;
grid-template-rows: 41px 48px 31px 1fr 43px;
border: 1px solid #0a0a0a;
background: #171717;
box-shadow: inset 0 0 0 1px #232323;
@@ -41,7 +42,7 @@ input {
.tabs {
display: flex;
align-items: stretch;
height: 48px;
height: 41px;
background: #1f1f1f;
border-bottom: 1px solid #0c0c0c;
}
@@ -49,9 +50,9 @@ input {
.tab {
display: flex;
align-items: center;
gap: 10px;
min-width: 240px;
padding: 0 14px;
gap: 9px;
min-width: 204px;
padding: 0 12px;
color: #bdbdbd;
font-size: 20px;
border-right: 1px solid #111;
@@ -69,16 +70,16 @@ input {
.toolbar {
display: grid;
grid-template-columns: 36px 30px minmax(160px, 1fr) 36px 36px 36px;
grid-template-columns: 31px 26px minmax(136px, 1fr) 31px 31px 31px;
align-items: center;
gap: 8px;
padding: 7px 12px;
gap: 7px;
padding: 6px 10px;
background: #262626;
}
.icon-button {
width: 32px;
height: 32px;
width: 27px;
height: 27px;
display: inline-grid;
place-items: center;
border: 0;
@@ -99,12 +100,12 @@ input {
}
.search {
height: 36px;
height: 31px;
min-width: 0;
display: grid;
grid-template-columns: 30px 1fr 24px;
grid-template-columns: 26px 1fr 20px;
align-items: center;
padding: 0 8px;
padding: 0 7px;
color: #cfcfcf;
background: #0d0d0d;
border: 1px solid #343434;
@@ -129,7 +130,7 @@ input {
background: transparent;
border: 0;
outline: 0;
font-size: 20px;
font-size: 1rem;
}
.search input::placeholder {
@@ -138,11 +139,11 @@ input {
.columns {
display: grid;
grid-template-columns: 38px minmax(260px, 1.3fr) minmax(140px, 0.55fr) minmax(130px, 0.52fr) minmax(150px, 0.62fr);
grid-template-columns: 32px minmax(221px, 1.3fr) minmax(119px, 0.55fr) minmax(111px, 0.52fr) minmax(128px, 0.62fr);
align-items: center;
background: #303030;
color: #c7c7c7;
font-size: 20px;
font-size: 1rem;
}
.columns span {
@@ -162,10 +163,44 @@ input {
min-height: 0;
overflow: hidden;
display: grid;
grid-template-rows: minmax(180px, 1fr) minmax(250px, 0.9fr);
grid-template-rows:
minmax(120px, calc(var(--outliner-ratio) * 100%))
8px
minmax(190px, calc(var(--details-ratio) * 100%));
background: #171717;
}
.pane-splitter {
position: relative;
display: grid;
place-items: center;
min-height: 8px;
background: #101010;
border-top: 1px solid #2e2e2e;
border-bottom: 1px solid #060606;
cursor: row-resize;
touch-action: none;
}
.pane-splitter span {
width: 54px;
height: 3px;
border-radius: 999px;
background: #565656;
box-shadow: 0 1px 0 #050505;
}
.pane-splitter:hover,
.pane-splitter:focus-visible {
background: #171f27;
outline: none;
}
.pane-splitter:hover span,
.pane-splitter:focus-visible span {
background: #2d86d8;
}
.rows {
min-height: 0;
overflow: auto;
@@ -174,10 +209,10 @@ input {
.outliner-row {
width: 100%;
min-width: 760px;
height: 30px;
min-width: 646px;
height: 26px;
display: grid;
grid-template-columns: minmax(298px, 1.3fr) minmax(140px, 0.55fr) minmax(130px, 0.52fr) minmax(150px, 0.62fr);
grid-template-columns: minmax(253px, 1.3fr) minmax(119px, 0.55fr) minmax(111px, 0.52fr) minmax(128px, 0.62fr);
align-items: center;
color: #bebebe;
background: transparent;
@@ -202,20 +237,20 @@ input {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 0 12px;
font-size: 20px;
padding: 0 10px;
font-size: 1rem;
color: inherit;
}
.item-cell {
display: flex;
align-items: center;
gap: 5px;
padding-left: 42px;
gap: 4px;
padding-left: 36px;
}
.indent {
width: calc(var(--depth) * 18px);
width: calc(var(--depth) * 15px);
flex: 0 0 auto;
}
@@ -254,34 +289,34 @@ input {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 0 20px;
gap: 14px;
padding: 0 17px;
color: #cfcfcf;
background: #303030;
font-size: 20px;
font-size: 1rem;
}
.empty {
min-height: 220px;
min-height: 187px;
display: grid;
align-content: center;
justify-items: center;
gap: 10px;
padding: 24px;
gap: 9px;
padding: 20px;
color: #bdbdbd;
text-align: center;
font-size: 16px;
font-size: 0.85rem;
}
.empty strong {
color: #f0f0f0;
font-size: 22px;
font-size: 1.1rem;
}
.details-panel {
min-height: 0;
display: grid;
grid-template-rows: 38px 48px 118px 44px 40px 1fr;
grid-template-rows: 32px 41px 100px 37px 34px 1fr;
border-top: 2px solid #0d0d0d;
background: #202020;
color: #cfcfcf;
@@ -296,11 +331,11 @@ input {
.details-tab {
display: flex;
align-items: center;
gap: 9px;
min-width: 210px;
padding: 0 12px;
gap: 8px;
min-width: 179px;
padding: 0 10px;
color: #c7c7c7;
font-size: 20px;
font-size: 1rem;
background: #232323;
border-right: 1px solid #101010;
}
@@ -320,10 +355,10 @@ input {
.details-object {
display: grid;
grid-template-columns: 28px 1fr;
grid-template-columns: 24px 1fr;
align-items: center;
gap: 10px;
padding: 7px 14px;
gap: 9px;
padding: 6px 12px;
border-bottom: 1px solid #171717;
background: #242424;
}
@@ -339,19 +374,19 @@ input {
.details-object strong {
color: #f1f1f1;
font-size: 17px;
font-size: 0.95rem;
font-weight: 500;
}
.details-object span {
margin-top: 2px;
color: #a8a8a8;
font-size: 14px;
font-size: 0.82rem;
}
.component-list {
min-height: 0;
padding: 6px 8px 8px;
padding: 5px 7px 7px;
background: #181818;
border: 1px solid #242424;
border-left: 0;
@@ -360,13 +395,13 @@ input {
}
.component-row {
height: 34px;
height: 29px;
display: flex;
align-items: center;
gap: 9px;
padding: 0 10px;
gap: 8px;
padding: 0 9px;
color: #d2d2d2;
font-size: 18px;
font-size: 0.95rem;
}
.component-row.selected {
@@ -375,10 +410,10 @@ input {
}
.component-row.child {
padding-left: 42px;
padding-left: 36px;
background: #151515;
color: #c9c9c9;
font-size: 16px;
font-size: 0.85rem;
}
.component-row span {
@@ -398,20 +433,20 @@ input {
.details-search-row {
display: grid;
grid-template-columns: minmax(140px, 1fr) 34px 34px 34px;
grid-template-columns: minmax(119px, 1fr) 29px 29px 29px;
align-items: center;
gap: 8px;
padding: 6px 12px;
gap: 7px;
padding: 5px 10px;
border-bottom: 1px solid #141414;
background: #262626;
}
.details-search {
height: 31px;
height: 26px;
display: grid;
grid-template-columns: 26px 1fr;
grid-template-columns: 22px 1fr;
align-items: center;
padding: 0 8px;
padding: 0 7px;
color: #cfcfcf;
background: #0d0d0d;
border: 1px solid #343434;
@@ -425,12 +460,12 @@ input {
background: transparent;
border: 0;
outline: 0;
font-size: 17px;
font-size: 0.95rem;
}
.details-tool {
width: 30px;
height: 30px;
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 0;
@@ -446,17 +481,17 @@ input {
.category-tabs {
display: flex;
align-items: center;
gap: 7px;
padding: 5px 12px;
gap: 6px;
padding: 4px 10px;
background: #262626;
border-bottom: 1px solid #141414;
overflow-x: auto;
}
.category-tabs button {
height: 29px;
min-width: 84px;
padding: 0 14px;
height: 25px;
min-width: 71px;
padding: 0 12px;
color: #c7c7c7;
background: #2c2c2c;
border: 1px solid #151515;
@@ -482,24 +517,24 @@ input {
}
.detail-category h3 {
height: 32px;
height: 27px;
display: flex;
align-items: center;
gap: 6px;
gap: 5px;
margin: 0;
padding: 0 10px;
padding: 0 9px;
color: #d0d0d0;
background: #303030;
border-top: 1px solid #393939;
border-bottom: 1px solid #171717;
font-size: 16px;
font-size: 0.9rem;
font-weight: 600;
}
.detail-row {
min-height: 40px;
min-height: 34px;
display: grid;
grid-template-columns: minmax(180px, 0.48fr) minmax(220px, 0.52fr);
grid-template-columns: minmax(153px, 0.48fr) minmax(187px, 0.52fr);
border-bottom: 1px solid #171717;
}
@@ -511,8 +546,8 @@ input {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 14px;
font-size: 16px;
padding: 0 12px;
font-size: 0.9rem;
}
.detail-name {
@@ -524,17 +559,17 @@ input {
.detail-value {
color: #e0e0e0;
background: #242424;
gap: 8px;
padding: 5px 10px 5px 14px;
gap: 7px;
padding: 4px 9px 4px 12px;
}
.text-value {
min-width: 0;
width: min(380px, 100%);
height: 28px;
width: min(323px, 100%);
height: 24px;
display: flex;
align-items: center;
padding: 0 10px;
padding: 0 9px;
color: #e5e5e5;
background: #101010;
border: 1px solid #333;
@@ -552,19 +587,19 @@ input {
.vector-value {
min-width: 0;
width: min(420px, 100%);
width: min(357px, 100%);
display: grid;
grid-template-columns: repeat(3, minmax(64px, 1fr));
gap: 5px;
grid-template-columns: repeat(3, minmax(54px, 1fr));
gap: 4px;
}
.axis {
height: 28px;
height: 24px;
display: flex;
align-items: center;
min-width: 0;
overflow: hidden;
padding: 0 8px;
padding: 0 7px;
color: #f0f0f0;
background: #101010;
border: 1px solid #333;
@@ -587,8 +622,8 @@ input {
}
.checkbox-value {
width: 26px;
height: 26px;
width: 22px;
height: 22px;
border: 1px solid #353535;
border-radius: 4px;
background: #111;
@@ -598,9 +633,9 @@ input {
.checkbox-value.checked::after {
content: "";
display: block;
width: 13px;
height: 7px;
margin: 7px 0 0 5px;
width: 11px;
height: 6px;
margin: 6px 0 0 4px;
border-left: 2px solid #d7d7d7;
border-bottom: 2px solid #d7d7d7;
transform: rotate(-45deg);
@@ -608,16 +643,16 @@ input {
.asset-value {
min-width: 0;
width: min(430px, 100%);
width: min(366px, 100%);
display: grid;
grid-template-columns: 54px minmax(120px, 1fr);
grid-template-columns: 46px minmax(102px, 1fr);
align-items: center;
gap: 8px;
gap: 7px;
}
.asset-thumb {
width: 54px;
height: 54px;
width: 46px;
height: 46px;
display: grid;
place-items: center;
color: #bdbdbd;
@@ -647,11 +682,11 @@ input {
.asset-control {
min-width: 0;
height: 31px;
height: 26px;
display: grid;
grid-template-columns: minmax(0, 1fr) 20px;
grid-template-columns: minmax(0, 1fr) 17px;
align-items: center;
padding: 0 8px 0 10px;
padding: 0 7px 0 9px;
color: #e5e5e5;
background: #101010;
border: 1px solid #333;
@@ -660,8 +695,8 @@ input {
}
.reset-button {
width: 26px;
height: 26px;
width: 22px;
height: 22px;
display: inline-grid;
place-items: center;
flex: 0 0 auto;
@@ -708,11 +743,18 @@ input {
min-height: 100vh;
height: 100vh;
border: 0;
grid-template-rows: 44px 52px 34px 1fr 44px;
grid-template-rows: 37px 44px 29px 1fr 37px;
}
.main-content {
grid-template-rows: minmax(170px, 1fr) minmax(260px, 0.95fr);
grid-template-rows:
minmax(120px, calc(var(--outliner-ratio) * 100%))
10px
minmax(220px, calc(var(--details-ratio) * 100%));
}
.pane-splitter {
min-height: 10px;
}
.tab {