@@ -49,18 +49,28 @@ export interface PopoutSize {
4949 width : number
5050}
5151
52+ /** Viewport-space rect the floating composer is confined to. Defaults to the
53+ * whole window; pass the thread area so the box can't slide under a pinned
54+ * sidebar or behind the header. */
55+ export interface PopoutBounds {
56+ bottom : number
57+ left : number
58+ right : number
59+ top : number
60+ }
61+
5262interface SetPositionOptions {
63+ /** Thread-area rect to confine the box to; falls back to the full window. */
64+ area ?: PopoutBounds
5365 persist ?: boolean
5466 /** Measured box size; falls back to the compact width + a min height so the
5567 * box stays grabbable even when the caller can't measure it. */
5668 size ?: PopoutSize
5769}
5870
59- // Keep at least this much of every edge between the box and the viewport , so the
71+ // Keep at least this much between the box and every edge of its bounds , so the
6072// floating composer can never be dragged (or restored) out of reach.
6173const EDGE_MARGIN = 8
62- const TITLEBAR_HEIGHT_FALLBACK = 34
63- const TITLEBAR_CLEARANCE_REM = 0.75
6474// Height floor used when the real box height is unknown (init / load / peel-off).
6575export const POPOUT_ESTIMATED_HEIGHT = 56
6676const MIN_VISIBLE_HEIGHT = POPOUT_ESTIMATED_HEIGHT
@@ -69,24 +79,32 @@ const clampRange = (value: number, lo: number, hi: number) => Math.min(Math.max(
6979
7080const rootFontSize = ( ) => parseFloat ( getComputedStyle ( document . documentElement ) . fontSize ) || 16
7181
72- function titlebarTopMargin ( ) {
73- const raw = getComputedStyle ( document . documentElement ) . getPropertyValue ( '--titlebar-height' ) . trim ( )
74- const titlebarHeight = Number . parseFloat ( raw )
75- const breathingRoom = TITLEBAR_CLEARANCE_REM * rootFontSize ( )
82+ /** The thread area's viewport rect (excludes a pinned sidebar + the header), or
83+ * undefined before it mounts — callers then fall back to the full window. */
84+ export function readPopoutBounds ( composer : Element | null ) : PopoutBounds | undefined {
85+ const el = ( composer ?. parentElement ?? document ) . querySelector ( '[data-slot="composer-bounds"]' )
86+
87+ if ( ! el ) {
88+ return undefined
89+ }
90+
91+ const { bottom, left, right, top } = el . getBoundingClientRect ( )
7692
77- return Math . max ( EDGE_MARGIN , ( Number . isFinite ( titlebarHeight ) ? titlebarHeight : TITLEBAR_HEIGHT_FALLBACK ) + breathingRoom )
93+ return { bottom , left , right , top }
7894}
7995
80- // Bound the bottom-right inset so the WHOLE box stays on-screen — the corner
81- // anchor alone would let the box's width/height push it past the left/top edges.
82- function clampPosition ( { bottom, right } : PopoutPosition , size ?: PopoutSize ) : PopoutPosition {
96+ // Bound the bottom/right inset so the WHOLE box stays inside `area` (the thread
97+ // region, or the window by default) — the corner anchor alone would let the
98+ // box's width/height push it past the opposite edges.
99+ function clampPosition ( { bottom, right } : PopoutPosition , size ?: PopoutSize , area ?: PopoutBounds ) : PopoutPosition {
83100 const width = size ?. width || POPOUT_WIDTH_REM * rootFontSize ( )
84101 const height = size ?. height || MIN_VISIBLE_HEIGHT
85- const topMargin = titlebarTopMargin ( )
102+ const { innerHeight : vh , innerWidth : vw } = window
103+ const a = area ?? { bottom : vh , left : 0 , right : vw , top : 0 }
86104
87105 return {
88- bottom : clampRange ( bottom , EDGE_MARGIN , window . innerHeight - height - topMargin ) ,
89- right : clampRange ( right , EDGE_MARGIN , window . innerWidth - width - EDGE_MARGIN )
106+ bottom : clampRange ( bottom , vh - a . bottom + EDGE_MARGIN , vh - a . top - height - EDGE_MARGIN ) ,
107+ right : clampRange ( right , vw - a . right + EDGE_MARGIN , vw - a . left - width - EDGE_MARGIN )
90108 }
91109}
92110
@@ -102,8 +120,8 @@ export function setComposerPoppedOut(value: boolean) {
102120 * unless `persist`. Returns the clamped position so callers can sync their live
103121 * ref. Pass the measured `size` for exact bounds; otherwise a fallback keeps it
104122 * on-screen. */
105- export function setComposerPopoutPosition ( position : PopoutPosition , { persist, size } : SetPositionOptions = { } ) : PopoutPosition {
106- const next = clampPosition ( position , size )
123+ export function setComposerPopoutPosition ( position : PopoutPosition , { area , persist, size } : SetPositionOptions = { } ) : PopoutPosition {
124+ const next = clampPosition ( position , size , area )
107125 $composerPopoutPosition . set ( next )
108126
109127 if ( persist ) {
0 commit comments