fix: harden layout push bounds

This commit is contained in:
Zzy
2026-08-21 23:41:33 +08:00
parent 8ceaefcfc7
commit 965ec49c16
4 changed files with 44 additions and 9 deletions
+10 -5
View File
@@ -3,7 +3,7 @@
* The conversation column (output + composer) must keep at least
* {@link PANEL_MIN} of the viewport after the bottom panel claims height.
*/
import { BOTTOM_MIN, PANEL_MIN } from './state.ts'
import { PANEL_MIN } from './state.ts'
export interface LayoutPushInput {
narrow: boolean
@@ -20,13 +20,18 @@ export interface LayoutPushSize {
height: number
}
function finiteNonNegative(value: number): number {
return Number.isFinite(value) ? Math.max(0, value) : 0
}
/** Compute the live layout-push size. Narrow drawers float and push 0. */
export function layoutPushSize(input: LayoutPushInput): LayoutPushSize {
if (input.narrow) return { width: 0, height: 0 }
const maxWidth = Math.max(PANEL_MIN, input.viewportWidth)
const maxHeight = Math.max(BOTTOM_MIN, input.viewportHeight - PANEL_MIN)
const viewportWidth = finiteNonNegative(input.viewportWidth)
const viewportHeight = finiteNonNegative(input.viewportHeight)
const maxHeight = Math.max(0, viewportHeight - Math.min(PANEL_MIN, viewportHeight))
return {
width: input.panelOpen ? Math.min(Math.max(0, input.width), maxWidth) : 0,
height: input.bottomOpen ? Math.min(Math.max(0, input.bottomHeight), maxHeight) : 0,
width: input.panelOpen ? Math.min(finiteNonNegative(input.width), viewportWidth) : 0,
height: input.bottomOpen ? Math.min(finiteNonNegative(input.bottomHeight), maxHeight) : 0,
}
}
+2 -3
View File
@@ -47,10 +47,9 @@
/* Grid/flex items default to min-height:auto. A long unbreakable token
(OAuth URL) then grows this column past the viewport and clips the
composer + left-rail Settings row. min-height:0 lets the cell shrink;
overflow:hidden keeps paint inside the cell; overflow-wrap wraps the
token so it cannot force the cell's intrinsic size. */
overflow-wrap lets the token wrap instead of forcing its intrinsic
size. The host's own descendants remain responsible for scrolling. */
min-height: 0;
overflow: hidden;
overflow-wrap: anywhere;
transition: margin-bottom var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
+8 -1
View File
@@ -16,7 +16,14 @@ describe('layout.css conversation column', () => {
it('allows the center column to shrink and wrap long tokens', () => {
expect(css).toMatch(/min-height:\s*0/)
expect(css).toMatch(/overflow:\s*hidden/)
expect(css).toMatch(/overflow-wrap:\s*anywhere/)
})
it('leaves overflow ownership to the host conversation descendants', () => {
const conversationRule = css.match(
/#root \[data-dsh-frame\][\s\S]*?\{([\s\S]*?)\n\}/,
)?.[1]
expect(conversationRule).toBeDefined()
expect(conversationRule).not.toMatch(/(?:^|[;\s])overflow\s*:/)
})
})
+24
View File
@@ -72,4 +72,28 @@ describe('layoutPushSize', () => {
viewportHeight: 800,
})).toEqual({ width: 400, height: 220 })
})
it('never pushes beyond a viewport smaller than the normal panel minima', () => {
expect(layoutPushSize({
narrow: false,
panelOpen: true,
bottomOpen: true,
width: 400,
bottomHeight: 220,
viewportWidth: 200,
viewportHeight: 200,
})).toEqual({ width: 200, height: 0 })
})
it('turns non-finite geometry into a safe zero push', () => {
expect(layoutPushSize({
narrow: false,
panelOpen: true,
bottomOpen: true,
width: Number.NaN,
bottomHeight: Number.POSITIVE_INFINITY,
viewportWidth: Number.NaN,
viewportHeight: Number.POSITIVE_INFINITY,
})).toEqual({ width: 0, height: 0 })
})
})