From d23e5dc10c99c3ff978b9965193a2477d9cb59d6 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Thu, 16 Jul 2026 12:02:56 +0200 Subject: [PATCH] Pond graph: size the canvas to the viewport, not the width ratio (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SVG scaled to container width with height following the fixed 800×560 viewBox ratio — on wide windows the graph grew taller than the viewport, the page got a scrollbar, and wheel-zoom scrolled along. The graph page is now a flex column filling the main column; the canvas takes the remaining height (flex: 1, min-height: 0), a ResizeObserver feeds its measured size to ForceGraph as width/height, and the SVG fills it exactly. LocalGraphPanel keeps its fixed defaults. Scope deliberately layout-only (issue comment 1187): with no scrollbar there is nothing for the wheel to scroll, so no non-passive listener needed. The graph pack now asserts the main column does not overflow vertically on the graph route. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn --- apps/web/e2e/graph.spec.ts | 9 ++++++++ apps/web/src/graph/PondGraphPage.tsx | 32 +++++++++++++++++++++++++--- apps/web/src/styles/base.css | 18 ++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/apps/web/e2e/graph.spec.ts b/apps/web/e2e/graph.spec.ts index bf54bd7..f9060c3 100644 --- a/apps/web/e2e/graph.spec.ts +++ b/apps/web/e2e/graph.spec.ts @@ -72,6 +72,15 @@ test('pond graph renders the link structure and creates phantom pages', async ({ expect(await page.locator('.force-graph__edge').count()).toBeGreaterThanOrEqual(2); await expect(page.locator('.graph-legend')).toBeVisible(); + // #131: the page fits the viewport — wheel-zoom must never scroll along, + // so the main column may not grow a vertical scrollbar on this route. + expect( + await page.evaluate(() => { + const main = document.querySelector('.main')!; + return main.scrollHeight - main.clientHeight; + }), + ).toBeLessThanOrEqual(0); + // #123: the physics/rendering sliders are there and take effect — the // font-size slider writes straight into the SVG labels. const controls = page.locator('.graph-controls'); diff --git a/apps/web/src/graph/PondGraphPage.tsx b/apps/web/src/graph/PondGraphPage.tsx index 26ffc21..e757cce 100644 --- a/apps/web/src/graph/PondGraphPage.tsx +++ b/apps/web/src/graph/PondGraphPage.tsx @@ -1,7 +1,7 @@ import type { PageView, PondGraphView, PondView } from '@dorfteich/shared'; import { DEFAULT_LABEL_COLOR } from '@dorfteich/shared'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; @@ -125,11 +125,37 @@ function GraphView({ graphSettingsKey(pondId), GRAPH_SETTINGS_DEFAULTS, ); + // #131: the canvas flexes to the remaining viewport height; its measured + // size becomes the SVG viewBox, so the graph fills it exactly instead of + // deriving its height from the width (which used to overflow the page). + const canvasRef = useRef(null); + const [size, setSize] = useState<{ width: number; height: number } | null>(null); + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const observer = new ResizeObserver((entries) => { + const rect = entries[0]?.contentRect; + if (rect && rect.width > 0 && rect.height > 0) { + setSize({ width: Math.round(rect.width), height: Math.round(rect.height) }); + } + }); + observer.observe(canvas); + return () => observer.disconnect(); + }, []); return ( <> -
- +
+ {size !== null && ( + + )}
); diff --git a/apps/web/src/styles/base.css b/apps/web/src/styles/base.css index e7e550f..201597d 100644 --- a/apps/web/src/styles/base.css +++ b/apps/web/src/styles/base.css @@ -1776,6 +1776,16 @@ button { } /* Knowledge graph (issue #112) + local graph panel (#113). */ + +/* #131: the page fills the main column as a flex column, so the canvas can + take exactly the remaining viewport height — the page itself must never + grow a scrollbar (wheel-zoom would scroll along with it). */ +.graph-page { + height: 100%; + display: flex; + flex-direction: column; +} + .graph-page__header { display: flex; align-items: baseline; @@ -1799,6 +1809,14 @@ button { border-radius: var(--radius); background: var(--color-surface); overflow: hidden; + flex: 1; + min-height: 0; +} + +/* The pond graph SVG gets its measured container size as viewBox (#131), so + it may fill the canvas exactly; the local panel keeps width-scaled sizing. */ +.graph-page__canvas .force-graph { + height: 100%; } /* Physics/rendering sliders (#123). */