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 { describeRemoteObject, getActiveTransport, readRemoteObjectProperties } from "./services/remoteControl";
import { enrichActors, getAllLevelActors, normalizeActor } from "./services/unrealActors"; 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() { export function App() {
const [actors, setActors] = React.useState<ActorRow[]>([]); const [actors, setActors] = React.useState<ActorRow[]>([]);
const [query, setQuery] = React.useState(""); const [query, setQuery] = React.useState("");
@@ -22,6 +33,8 @@ export function App() {
const [details, setDetails] = React.useState<ObjectDetails | null>(null); const [details, setDetails] = React.useState<ObjectDetails | null>(null);
const [detailsError, setDetailsError] = React.useState<string | null>(null); const [detailsError, setDetailsError] = React.useState<string | null>(null);
const [isDetailsLoading, setIsDetailsLoading] = React.useState(false); 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 tree = React.useMemo(() => buildTree(actors), [actors]);
const selectedNode = React.useMemo(() => findNodeById(tree, selectedId), [tree, selectedId]); 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 ( return (
<main className="shell"> <main className="shell">
<section className="panel" aria-label="Unreal Outliner"> <section className="panel" aria-label="Unreal Outliner">
@@ -161,7 +226,11 @@ export function App() {
<span>ID Name</span> <span>ID Name</span>
</div> </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"> <div className="rows" role="tree">
{error ? ( {error ? (
<div className="empty"> <div className="empty">
@@ -182,6 +251,23 @@ export function App() {
)} )}
</div> </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} /> <DetailsPanel node={selectedNode} details={details} isLoading={isDetailsLoading} error={detailsError} />
</div> </div>

View File

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

View File

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