fix: fit the story graph to the window and scroll each column on its own
This commit is contained in:
@@ -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')}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
Reference in New Issue
Block a user