fix: fit the story graph to the window and scroll each column on its own

This commit is contained in:
mesalogo
2026-10-02 22:22:03 +08:00
parent 0fd2f7e3c8
commit 9d8477b728
5 changed files with 109 additions and 1 deletions
+4 -1
View File
@@ -6,6 +6,7 @@ import type { SupervisionReviewExecution } from '../../shared/supervision-review
import { EmptyState, PageTabs } from './WorkspacePrimitives'
import { SupervisionDiscussion } from './SupervisionDiscussion'
import { SupervisionStoryDigest } from './SupervisionStoryDigest'
import { useFillHeight } from './use-fill-height'
import { SupervisionEventStory, SupervisionExperienceDetail, SupervisionExperienceList, SupervisionStoryDetail, SupervisionStoryList, useSupervisionStories } from './SupervisionStories'
import type { AssistantProject } from '../../shared/assistant-contracts'
import { heartbeatScopeSchema } from '../../shared/assistant-contracts'
@@ -478,6 +479,8 @@ export function SupervisorWorkspace({
? (JSON.parse(graph.storyLine.scope_json) as SupervisionRunRequest['scope'])
: undefined
const storyState = useSupervisionStories(graphScope, tab === 'graph', graph)
const graphLayoutRef = useRef<HTMLDivElement>(null)
useFillHeight(graphLayoutRef, tab === 'graph' && graph.events.length > 0)
// The work review reads the selected result's own scope, so it matches the period shown.
const recapStories = useSupervisionStories(latest?.scope, tab === 'overview', latest?.id)
const selectedStory = selection?.kind === 'story' ? storyState.view.stories.find((story) => story.id === selection.id) : undefined
@@ -718,7 +721,7 @@ export function SupervisorWorkspace({
/>
)}
{graph.events.length > 0 && (
<div className="supervisor-workspace__graph-layout">
<div className="supervisor-workspace__graph-layout" ref={graphLayoutRef}>
<aside
className="supervisor-workspace__graph-list"
aria-label={t('supervisor.selection')}
+35
View File
@@ -388,6 +388,41 @@
.supervisor-workspace__graph-canvas {
border-inline: 1px solid var(--border-default);
}
/*
* Wide layouts: the graph row is as tall as the window allows (`--fill-height`, set by useFillHeight with a
* steady bottom margin), and each column in it scrolls on its own, so long lists never grow the page.
* With three columns that is the whole layout; with two, the detail wraps below at its natural height.
* Single-column layouts keep page scrolling.
*/
@container supervisor (min-width: 861px) {
.supervisor-workspace__graph-layout {
grid-template-rows: var(--fill-height, auto);
grid-auto-rows: auto;
}
.supervisor-workspace__graph-canvas,
.supervisor-workspace__graph-layout > .supervisor-workspace__detail {
min-height: 0;
max-height: var(--fill-height, none);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-gutter: stable;
}
.supervisor-workspace__graph-list {
contain: none;
height: 100%;
}
.supervisor-workspace__graph-list > .supervisor-workspace__list-panel {
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
}
@container supervisor (min-width: 861px) and (max-width: 1190px) {
.supervisor-workspace__graph-layout > .supervisor-workspace__detail {
max-height: none;
overflow-y: visible;
}
}
.supervisor-workspace__canvas-heading {
display: flex;
justify-content: space-between;
+36
View File
@@ -0,0 +1,36 @@
import { useLayoutEffect, type RefObject } from 'react'
/**
* Sets `--fill-height` on the element: the visible height of its scroll container from the element's
* top down, minus `bottomGap`, never below `minimum`. Lets a panel fill the window with a steady bottom
* margin while its columns scroll inside, instead of growing the page with their content.
* Measured with the container scrolled to the top, so scrolling does not change the value.
*/
export function useFillHeight(ref: RefObject<HTMLElement | null>, enabled: boolean, bottomGap = 24, minimum = 420): void {
useLayoutEffect(() => {
const element = ref.current
if (!element || !enabled) return
let container: HTMLElement | null = element.parentElement
while (container && !/(auto|scroll)/.test(getComputedStyle(container).overflowY)) container = container.parentElement
const scroller = container ?? document.documentElement
const measure = () => {
const top = element.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop
const padding = parseFloat(getComputedStyle(scroller).paddingBottom) || 0
// The layout's own border sits outside the row; subtract it so the outer box ends exactly at the gap.
const style = getComputedStyle(element)
const borders = (parseFloat(style.borderTopWidth) || 0) + (parseFloat(style.borderBottomWidth) || 0)
const height = Math.max(minimum, Math.floor(scroller.clientHeight - top - Math.max(bottomGap, padding) - borders))
element.style.setProperty('--fill-height', `${height}px`)
}
measure()
const observer = new ResizeObserver(measure)
observer.observe(scroller)
// Content above the element (notices, toolbars, headers) moves its top: observe earlier siblings up the tree.
// Parents are not observed, since the element's own height changes theirs.
for (let node: HTMLElement | null = element; node && node !== scroller; node = node.parentElement) {
for (let sibling = node.previousElementSibling; sibling; sibling = sibling.previousElementSibling) observer.observe(sibling)
}
window.addEventListener('resize', measure)
return () => { observer.disconnect(); window.removeEventListener('resize', measure); element.style.removeProperty('--fill-height') }
}, [ref, enabled, bottomGap, minimum])
}