diff --git a/packages/web_ui/src/components/LogConsole.tsx b/packages/web_ui/src/components/LogConsole.tsx index f422a73c..c327d4ec 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,36 @@ 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; + + // 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) { + return undefined; + } + element.style.height = `${consoleHeightRef.current}px`; + + if (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 = { @@ -164,6 +198,10 @@ 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 a26bb9fc..53cd7cdc 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -174,18 +174,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; + /* 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: 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; } .instance-console .factorio-time {