fix: keep conversation composer in the viewport under layout push
Long unbreakable tokens (OAuth URLs) grew the AppFrame center column past the viewport because the layout-push rule used the grid default min-height:auto. Cap --dsh-sidebar-height the same way setBottomHeight does (viewport minus PANEL_MIN) and let the column shrink and wrap.
This commit is contained in:
+19
-12
@@ -43,6 +43,7 @@ import {
|
||||
import { IconPanelBottomOutline16, IconPanelRightOutline16 } from './icons.tsx'
|
||||
import { Workbench, type WorkbenchActions } from './split-pane.tsx'
|
||||
import { useNarrowViewport } from './breakpoints.ts'
|
||||
import { layoutPushSize } from './layout-push.ts'
|
||||
import { parseDesktopEnv } from './desktop-env.ts'
|
||||
import { getWcoSnapshot, subscribeWco } from './wco.ts'
|
||||
import { getShellPreset } from './shell-presets.ts'
|
||||
@@ -804,12 +805,15 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
dragCommitted.current = true
|
||||
stopDragScheduling()
|
||||
const last = lastDragSize.current
|
||||
const adoptedWidth = !narrow && state?.panelOpen === true
|
||||
? Math.min(last?.width ?? state?.width ?? 0, window.innerWidth)
|
||||
: 0
|
||||
const adoptedHeight = !narrow && state?.bottomOpen === true
|
||||
? Math.min(last?.height ?? state?.bottomHeight ?? 0, window.innerHeight)
|
||||
: 0
|
||||
const { width: adoptedWidth, height: adoptedHeight } = layoutPushSize({
|
||||
narrow,
|
||||
panelOpen: state?.panelOpen === true,
|
||||
bottomOpen: state?.bottomOpen === true,
|
||||
width: last?.width ?? state?.width ?? 0,
|
||||
bottomHeight: last?.height ?? state?.bottomHeight ?? 0,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
})
|
||||
applyDrag(adoptedWidth, adoptedHeight)
|
||||
draggingRef.current = false
|
||||
measureCenter()
|
||||
@@ -826,12 +830,15 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
// On NARROW viewports the drawer FLOATS over the app shell — no push, the
|
||||
// conversation keeps the full width behind the drawer.
|
||||
useEffect(() => {
|
||||
const width = !narrow && snapshot.state?.panelOpen === true
|
||||
? Math.min(snapshot.state.width, window.innerWidth)
|
||||
: 0
|
||||
const height = !narrow && snapshot.state?.bottomOpen === true
|
||||
? Math.min(snapshot.state.bottomHeight, window.innerHeight)
|
||||
: 0
|
||||
const { width, height } = layoutPushSize({
|
||||
narrow,
|
||||
panelOpen: snapshot.state?.panelOpen === true,
|
||||
bottomOpen: snapshot.state?.bottomOpen === true,
|
||||
width: snapshot.state?.width ?? 0,
|
||||
bottomHeight: snapshot.state?.bottomHeight ?? 0,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
})
|
||||
writeGeometry(width, height)
|
||||
}, [narrow, snapshot.state?.panelOpen, snapshot.state?.width, snapshot.state?.bottomOpen, snapshot.state?.bottomHeight])
|
||||
// Unmount must release the push (issue #31): when the boundary swaps the
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Size written to `--dsh-sidebar-width` / `--dsh-sidebar-height`.
|
||||
* 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'
|
||||
|
||||
export interface LayoutPushInput {
|
||||
narrow: boolean
|
||||
panelOpen: boolean
|
||||
bottomOpen: boolean
|
||||
width: number
|
||||
bottomHeight: number
|
||||
viewportWidth: number
|
||||
viewportHeight: number
|
||||
}
|
||||
|
||||
export interface LayoutPushSize {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
/** 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)
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,14 @@
|
||||
#root [data-dsh-frame] > [data-pane="conversation"],
|
||||
#root :has(> [data-slot="conversation"]) {
|
||||
margin-bottom: var(--dsh-sidebar-height, 0px);
|
||||
/* 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. */
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
transition: margin-bottom var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* layout.css must let the DSH conversation column shrink below its content
|
||||
* size. Without min-height:0 a long unbreakable URL grows the grid item
|
||||
* past the viewport and clips the composer.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const css = readFileSync('src/client/layout.css', 'utf8')
|
||||
|
||||
describe('layout.css conversation column', () => {
|
||||
it('targets the AppFrame center column', () => {
|
||||
expect(css).toContain('[data-pane="conversation"]')
|
||||
expect(css).toContain('[data-slot="conversation"]')
|
||||
})
|
||||
|
||||
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/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Layout-push size contract: the conversation column (output + composer)
|
||||
* must keep at least PANEL_MIN of the viewport after the bottom panel
|
||||
* claims its height. A stale persisted bottomHeight (or a cap at 100vh)
|
||||
* used to crush the composer off-screen.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BOTTOM_MIN, PANEL_MIN } from '../src/client/state.ts'
|
||||
import { layoutPushSize } from '../src/client/layout-push.ts'
|
||||
|
||||
describe('layoutPushSize', () => {
|
||||
it('pushes nothing on a narrow drawer viewport', () => {
|
||||
expect(layoutPushSize({
|
||||
narrow: true,
|
||||
panelOpen: true,
|
||||
bottomOpen: true,
|
||||
width: 400,
|
||||
bottomHeight: 400,
|
||||
viewportWidth: 390,
|
||||
viewportHeight: 700,
|
||||
})).toEqual({ width: 0, height: 0 })
|
||||
})
|
||||
|
||||
it('pushes nothing when both panels are collapsed', () => {
|
||||
expect(layoutPushSize({
|
||||
narrow: false,
|
||||
panelOpen: false,
|
||||
bottomOpen: false,
|
||||
width: 400,
|
||||
bottomHeight: 400,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 800,
|
||||
})).toEqual({ width: 0, height: 0 })
|
||||
})
|
||||
|
||||
it('caps an oversize bottom panel so the conversation keeps PANEL_MIN', () => {
|
||||
const viewportHeight = 800
|
||||
const size = layoutPushSize({
|
||||
narrow: false,
|
||||
panelOpen: false,
|
||||
bottomOpen: true,
|
||||
width: 0,
|
||||
bottomHeight: 10_000,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight,
|
||||
})
|
||||
expect(size.height).toBe(viewportHeight - PANEL_MIN)
|
||||
expect(size.height).toBeGreaterThanOrEqual(BOTTOM_MIN)
|
||||
expect(viewportHeight - size.height).toBeGreaterThanOrEqual(PANEL_MIN)
|
||||
})
|
||||
|
||||
it('caps an oversize right panel at the viewport width', () => {
|
||||
expect(layoutPushSize({
|
||||
narrow: false,
|
||||
panelOpen: true,
|
||||
bottomOpen: false,
|
||||
width: 10_000,
|
||||
bottomHeight: 0,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 800,
|
||||
})).toEqual({ width: 1280, height: 0 })
|
||||
})
|
||||
|
||||
it('passes through in-range open sizes', () => {
|
||||
expect(layoutPushSize({
|
||||
narrow: false,
|
||||
panelOpen: true,
|
||||
bottomOpen: true,
|
||||
width: 400,
|
||||
bottomHeight: 220,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 800,
|
||||
})).toEqual({ width: 400, height: 220 })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user