fix(desktop): narrow edge overlay honours the dragged pane width
The narrow edge overlay sized itself from the pane's declared data.width (260px for bots), discarding the persisted widthOverride the sash drag wrote, so every chat-switch reveal snapped the panel back to its declared width. Size from the same fixedTrackSize resolution the pane's docked zone uses, extracted as a pure narrowOverlayWidth helper so the resolution itself is testable under jsdom. Co-authored-by: kokhlo <47825603+kokhlo@users.noreply.github.com>
This commit is contained in:
committed by
brooklyn!
parent
a1dcbc20ad
commit
351d59be8f
@@ -3,13 +3,14 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vite
|
||||
|
||||
import { PANE_TOGGLE_REVEAL_EVENT } from '@/components/pane-shell'
|
||||
import { registry } from '@/contrib/registry'
|
||||
import { $paneStates, setPaneWidthOverride } from '@/store/panes'
|
||||
import { $connection } from '@/store/session'
|
||||
import { stubResizeObserver } from '@/test/jsdom'
|
||||
|
||||
import { group, split } from '../model'
|
||||
import { $hiddenTreePanes, $layoutTree, $narrowViewport, declareDefaultTree } from '../store'
|
||||
|
||||
import { NarrowOverlays } from './narrow-overlays'
|
||||
import { NarrowOverlays, narrowOverlayWidth } from './narrow-overlays'
|
||||
|
||||
// Ground truth for "the Bots tab is still visible when the sessions sidebar
|
||||
// collapses on a narrow window". A collapsible pane DOCKED into the sessions
|
||||
@@ -128,4 +129,44 @@ describe('narrow overlay of a stacked zone', () => {
|
||||
expect(dragSpacer).not.toBeNull()
|
||||
expect(dragSpacer?.style.height).toBe('34px')
|
||||
})
|
||||
|
||||
it('honors the user drag width, not the declared width, when the zone collapsed', () => {
|
||||
// The sash writes a widthOverride per shown pane of the zone; the overlay
|
||||
// must size from the same resolution the docked zone uses (fixedTrackSize
|
||||
// = declared max() refined by overrides), not from data.width alone.
|
||||
// jsdom's CSSOM drops the min() wrapper from style.width, so the rendered
|
||||
// width is unobservable here — assert the overlay's own resolution
|
||||
// (narrowOverlayWidth, the pure helper the component styles from) against
|
||||
// the live tree + store the mounted overlay saw.
|
||||
setPaneWidthOverride('sessions', 170)
|
||||
setPaneWidthOverride('bots', 170)
|
||||
|
||||
const { container } = render(<NarrowOverlays />)
|
||||
|
||||
revealPane('bots')
|
||||
|
||||
const overlay = container.querySelector<HTMLElement>('[data-narrow-overlay]')
|
||||
expect(overlay).toBeTruthy()
|
||||
|
||||
const tree = $layoutTree.get()!
|
||||
const bots = registry.getArea('panes').find(p => p.id === 'bots')!
|
||||
|
||||
const width = narrowOverlayWidth(
|
||||
{
|
||||
paneFor: id => registry.getArea('panes').find(p => p.id === id),
|
||||
paneGone: () => false,
|
||||
overrides: $paneStates.get()
|
||||
},
|
||||
tree,
|
||||
bots
|
||||
)
|
||||
|
||||
// The user dragged the zone to 170px; the declared 260px must lose.
|
||||
expect(width).toBe('170px')
|
||||
expect(width).not.toBe('260px')
|
||||
|
||||
// And the seamless fallback: without a tree the declared width still
|
||||
// sizes the overlay.
|
||||
expect(narrowOverlayWidth({ paneFor: () => undefined, paneGone: () => false, overrides: {} }, null, bots)).toBe('260px')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,21 +16,45 @@ import { useContributions } from '@/contrib/react/use-contributions'
|
||||
import type { Contribution } from '@/contrib/types'
|
||||
import { ESCAPE_PRIORITY, isTopEscapeLayer, pushEscapeLayer } from '@/lib/escape-layers'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $paneStates } from '@/store/panes'
|
||||
|
||||
import { PANE_TOGGLE_REVEAL_EVENT } from '../..'
|
||||
import { useWindowControlsOverlap } from '../../geometry'
|
||||
import { NO_PANE_GROUP } from '../../pane-visibility'
|
||||
import { allPaneIds, findGroupOfPane } from '../model'
|
||||
import { allPaneIds, findGroupOfPane, type LayoutNode } from '../model'
|
||||
import { $hiddenTreePanes, $layoutTree, $narrowViewport } from '../store'
|
||||
|
||||
import { KeepAlivePaneSlot, useStablePaneHosts } from './keep-alive-panes'
|
||||
import { paneChrome } from './track-model'
|
||||
import { fixedTrackSize, paneChrome, type TrackContext } from './track-model'
|
||||
|
||||
/** The width a revealed narrow overlay sizes itself to: the SAME resolution
|
||||
* the pane's zone uses while docked — declared max() refined by the live
|
||||
* widthOverride of the zone's shown panes (fixedTrackSize) — so the overlay
|
||||
* and the docked zone can never disagree. Falls back to the pane's declared
|
||||
* `data.width` (then 18rem) when no zone claims the pane. Pure, so the
|
||||
* regression test asserts the resolution itself (jsdom's CSSOM drops the
|
||||
* `min()` wrapper from style.width, hiding the rendered result). */
|
||||
export function narrowOverlayWidth(
|
||||
ctx: TrackContext,
|
||||
tree: LayoutNode | null,
|
||||
revealed: Contribution
|
||||
): string {
|
||||
if (!tree) {
|
||||
return paneChrome(revealed).width ?? '18rem'
|
||||
}
|
||||
|
||||
const zone = findGroupOfPane(tree, revealed.id)
|
||||
const track = zone ? fixedTrackSize(zone, 'row', ctx) : null
|
||||
|
||||
return track ?? paneChrome(revealed).width ?? '18rem'
|
||||
}
|
||||
|
||||
export function NarrowOverlays() {
|
||||
const narrow = useStore($narrowViewport)
|
||||
const solo = useStore($chatOnboardingSolo)
|
||||
const tree = useStore($layoutTree)
|
||||
const panes = useContributions('panes')
|
||||
const paneStates = useStore($paneStates)
|
||||
const stableHosts = useStablePaneHosts()
|
||||
const hiddenPanes = useStore($hiddenTreePanes)
|
||||
const [reveal, setReveal] = useState<{ id: string; pinned: boolean } | null>(null)
|
||||
@@ -137,6 +161,14 @@ export function NarrowOverlays() {
|
||||
const revealed = reveal ? collapsibles.find(p => p.id === reveal.id) : undefined
|
||||
const sides = [...new Set(collapsibles.map(sideOf))]
|
||||
|
||||
// Size the overlay the way the pane's zone is sized while docked: declared
|
||||
// width refined by the user's drag override (fixedTrackSize), so a pane the
|
||||
// user narrowed stays narrowed here too — reading only data.width would
|
||||
// reset the overlay to the declared size on every reveal.
|
||||
const overlayWidth = revealed
|
||||
? narrowOverlayWidth({ paneFor: id => panes.find(p => p.id === id), paneGone: () => false, overrides: paneStates }, tree, revealed)
|
||||
: null
|
||||
|
||||
// The revealed pane's ZONE-mates that also left the grid (the sessions zone
|
||||
// stacks SESSIONS | BOTS): the overlay mirrors the zone's tab strip so a
|
||||
// pane docked into a collapsed zone stays reachable on narrow viewports —
|
||||
@@ -192,7 +224,7 @@ export function NarrowOverlays() {
|
||||
// draggable, mirroring TreeGroup's reservation.
|
||||
style={{
|
||||
paddingTop: wcOverlap ? wcOverlap.y + wcOverlap.height : undefined,
|
||||
width: `min(${(revealed.data as { width?: string } | undefined)?.width ?? '18rem'}, 85vw)`
|
||||
width: `min(${overlayWidth}, 85vw)`
|
||||
}}
|
||||
>
|
||||
{wcOverlap && (
|
||||
|
||||
Reference in New Issue
Block a user