diff --git a/src/App.tsx b/src/App.tsx index 429f459..304cb65 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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([]); const [query, setQuery] = React.useState(""); @@ -22,6 +33,8 @@ export function App() { const [details, setDetails] = React.useState(null); const [detailsError, setDetailsError] = React.useState(null); const [isDetailsLoading, setIsDetailsLoading] = React.useState(false); + const [splitRatio, setSplitRatio] = React.useState(readSavedSplitRatio); + const mainContentRef = React.useRef(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) => { + 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) => { + 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 (
@@ -161,7 +226,11 @@ export function App() { ID Name -
+
{error ? (
@@ -182,6 +251,23 @@ export function App() { )}
+
setClampedSplitRatio(DEFAULT_SPLIT_RATIO)} + onKeyDown={resizeWithKeyboard} + onPointerDown={startResize} + role="separator" + tabIndex={0} + title="Drag to resize panes. Double-click to reset." + > + +
+
diff --git a/src/components/DetailsPanel.tsx b/src/components/DetailsPanel.tsx index 4ca076d..dfa005d 100644 --- a/src/components/DetailsPanel.tsx +++ b/src/components/DetailsPanel.tsx @@ -161,7 +161,6 @@ export function DetailsPanel({
{node?.type ? `${node.type}Component (${node.type}Component0)` : "Component"} - Edit in C++
diff --git a/src/styles.css b/src/styles.css index bb3575f..5c770c4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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 {