From 3fe0c8505550c6aed7a36165c0d20e1cc44c1789 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:47:32 +0000 Subject: [PATCH 1/3] Make the Web UI console resizable The instance console was locked to a fixed 300px height. It can now be drag-resized vertically (native resize handle), and the chosen height is persisted with useLocalStorage so it is restored on the next visit. Both the height and the scroll-anchor padding are driven by a --console-height CSS variable, so the pin-to-bottom behaviour keeps working at any size. Closes #938 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web_ui/src/components/LogConsole.tsx | 32 ++++++++++++++++++- packages/web_ui/src/style.css | 12 +++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/web_ui/src/components/LogConsole.tsx b/packages/web_ui/src/components/LogConsole.tsx index f422a73c..13332bc7 100644 --- a/packages/web_ui/src/components/LogConsole.tsx +++ b/packages/web_ui/src/components/LogConsole.tsx @@ -4,10 +4,14 @@ import { Select, Typography } from "antd"; import * as lib from "@clusterio/lib"; import { useAccount } from "../model/account"; +import useLocalStorage from "../util/useLocalStorage"; import ControlContext from "./ControlContext"; const { Paragraph } = Typography; +const consoleHeightKey = "instance-console-height"; +const defaultConsoleHeight = 300; + type Parsed = { format: string; type: string; @@ -99,6 +103,28 @@ export default function LogConsole(props: LogConsoleProps) { let anchor = useRef(); let [pastLines, setPastLines] = useState([{"Loading past entries..."}
]); let [lines, setLines] = useState([]); + const [consoleHeight, setConsoleHeight] = useLocalStorage(consoleHeightKey, defaultConsoleHeight); + const consoleHeightRef = useRef(consoleHeight); + consoleHeightRef.current = consoleHeight; + + // Persist the height the user drag-resizes the console to, so it is restored + // on the next visit. The scroll container is the element wrapping the + // anchor. + useEffect(() => { + const element = anchor.current?.parentElement as HTMLElement | undefined; + if (!element || typeof ResizeObserver === "undefined") { + return undefined; + } + const observer = new ResizeObserver(() => { + const next = Math.round(element.offsetHeight); + if (next && next !== consoleHeightRef.current) { + consoleHeightRef.current = next; + setConsoleHeight(next); + } + }); + observer.observe(element); + return () => observer.disconnect(); + }, []); useEffect(() => { let logFilter = { @@ -163,7 +189,11 @@ export default function LogConsole(props: LogConsoleProps) { ]); return <> - + {pastLines} {lines}
diff --git a/packages/web_ui/src/style.css b/packages/web_ui/src/style.css index ef9fcdf4..d04ca110 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -171,13 +171,19 @@ body { margin: 0; background: #000; border-color: #252525; - height: 300px; + /* Height is driven by --console-height (set from the persisted user choice) + so the console can be drag-resized; the resize handle needs a non-visible + overflow, which overflow-y: scroll provides. */ + height: var(--console-height, 300px); + min-height: 100px; + resize: vertical; overflow-y: scroll; white-space: pre-wrap; /* Make sure there's at least one pixel of scroll space, this */ - /* is necessary for the view to stick on the scroll anchor. */ - padding-top: 300px; + /* is necessary for the view to stick on the scroll anchor. Tracks the */ + /* height so the pin-to-bottom keeps working at any size. */ + padding-top: var(--console-height, 300px); } .instance-console .factorio-time { From 2dedb443725e019ddbd0ddcfc76f3cdc2692b6f8 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:38:35 +0000 Subject: [PATCH 2/3] Fix being unable to shrink the console after enlarging it The resize observer measured offsetHeight, which on the content-box console includes the pin-to-bottom padding-top. Feeding that back into the height (and thus the padding) meant the padding inflated the measured height, so the console could be enlarged but never dragged smaller again. Make the console border-box so its height already includes the padding and offsetHeight matches the dragged height, breaking the feedback. The default height is bumped to 600 so the rendered size stays the same as the previous content-box height + padding. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web_ui/src/components/LogConsole.tsx | 4 +++- packages/web_ui/src/style.css | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/web_ui/src/components/LogConsole.tsx b/packages/web_ui/src/components/LogConsole.tsx index 13332bc7..b6fad009 100644 --- a/packages/web_ui/src/components/LogConsole.tsx +++ b/packages/web_ui/src/components/LogConsole.tsx @@ -10,7 +10,9 @@ import ControlContext from "./ControlContext"; const { Paragraph } = Typography; const consoleHeightKey = "instance-console-height"; -const defaultConsoleHeight = 300; +// With border-box the height includes the pin-to-bottom padding, so this is the +// full rendered height (matching the previous content-box default of 300 + 300). +const defaultConsoleHeight = 600; type Parsed = { format: string; diff --git a/packages/web_ui/src/style.css b/packages/web_ui/src/style.css index d04ca110..6144066b 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -173,8 +173,12 @@ body { border-color: #252525; /* Height is driven by --console-height (set from the persisted user choice) so the console can be drag-resized; the resize handle needs a non-visible - overflow, which overflow-y: scroll provides. */ - height: var(--console-height, 300px); + overflow, which overflow-y: scroll provides. border-box keeps padding-top + inside the height so the resize-observer measurement (offsetHeight) does + not include it; otherwise the padding inflates the measured height and the + console can no longer be dragged smaller. */ + box-sizing: border-box; + height: var(--console-height, 600px); min-height: 100px; resize: vertical; overflow-y: scroll; @@ -183,7 +187,7 @@ body { /* Make sure there's at least one pixel of scroll space, this */ /* is necessary for the view to stick on the scroll anchor. Tracks the */ /* height so the pin-to-bottom keeps working at any size. */ - padding-top: var(--console-height, 300px); + padding-top: var(--console-height, 600px); } .instance-console .factorio-time { From 5cde390c24fb677c77d2b2796dba24a2b6e4dc61 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:01:06 +0000 Subject: [PATCH 3/3] Use a spacer element for console scroll headroom Replace the padding-top scroll headroom with a dedicated .console-spacer element so the pin-to-bottom keeps working without padding inflating the measured height, and trim the style.css comments. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web_ui/src/components/LogConsole.tsx | 30 +++++++++++-------- packages/web_ui/src/style.css | 21 ++++++------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/web_ui/src/components/LogConsole.tsx b/packages/web_ui/src/components/LogConsole.tsx index b6fad009..c327d4ec 100644 --- a/packages/web_ui/src/components/LogConsole.tsx +++ b/packages/web_ui/src/components/LogConsole.tsx @@ -10,9 +10,7 @@ import ControlContext from "./ControlContext"; const { Paragraph } = Typography; const consoleHeightKey = "instance-console-height"; -// With border-box the height includes the pin-to-bottom padding, so this is the -// full rendered height (matching the previous content-box default of 300 + 300). -const defaultConsoleHeight = 600; +const defaultConsoleHeight = 300; type Parsed = { format: string; @@ -109,12 +107,20 @@ export default function LogConsole(props: LogConsoleProps) { const consoleHeightRef = useRef(consoleHeight); consoleHeightRef.current = consoleHeight; - // Persist the height the user drag-resizes the console to, so it is restored - // on the next visit. The scroll container is the element wrapping the - // anchor. + // The console is resized with the browser's native resize handle, which owns + // the element's inline height. Driving the height from React would rewrite + // that inline style on every re-render and fight the drag (locking the size), + // so the height is only read here (on mount) and persisted (on resize), never + // written back through React. The scroll container is the element + // wrapping the anchor. useEffect(() => { const element = anchor.current?.parentElement as HTMLElement | undefined; - if (!element || typeof ResizeObserver === "undefined") { + if (!element) { + return undefined; + } + element.style.height = `${consoleHeightRef.current}px`; + + if (typeof ResizeObserver === "undefined") { return undefined; } const observer = new ResizeObserver(() => { @@ -191,11 +197,11 @@ export default function LogConsole(props: LogConsoleProps) { ]); return <> - + + {/* Scrollable spacer (one console-height tall) that keeps short output + pinned to the bottom and leaves room to scroll up. Unlike a + padding-top it is scroll content, so it does not constrain resize. */} +
{pastLines} {lines}
diff --git a/packages/web_ui/src/style.css b/packages/web_ui/src/style.css index 6144066b..04348394 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -166,28 +166,25 @@ body { height: 1px; } +/* Scroll headroom above the output so short output sits at the bottom; it is + scroll content, not box padding, so it does not block dragging smaller. */ +.instance-console code .console-spacer { + height: 100%; +} + .instance-console code { display: block; margin: 0; background: #000; border-color: #252525; - /* Height is driven by --console-height (set from the persisted user choice) - so the console can be drag-resized; the resize handle needs a non-visible - overflow, which overflow-y: scroll provides. border-box keeps padding-top - inside the height so the resize-observer measurement (offsetHeight) does - not include it; otherwise the padding inflates the measured height and the - console can no longer be dragged smaller. */ + /* Drag-resizable (the native handle needs the scroll overflow); height is set + imperatively in LogConsole, border-box keeps padding out of offsetHeight. */ box-sizing: border-box; - height: var(--console-height, 600px); + height: 300px; min-height: 100px; resize: vertical; overflow-y: scroll; white-space: pre-wrap; - - /* Make sure there's at least one pixel of scroll space, this */ - /* is necessary for the view to stick on the scroll anchor. Tracks the */ - /* height so the pin-to-bottom keeps working at any size. */ - padding-top: var(--console-height, 600px); } .instance-console .factorio-time {