@@ -83,6 +83,11 @@ type ParameterValues = Record<string, unknown>;
8383const LAYER_TOKEN_PREFIX = "layer:" ;
8484const RUNNING_JOB_STATUSES = new Set ( [ "pending" , "running" ] ) ;
8585
86+ // Smallest the floating panel can be resized to, so the two-column tool browser
87+ // stays usable (left list + a readable parameter form).
88+ const PANEL_MIN_W = 560 ;
89+ const PANEL_MIN_H = 400 ;
90+
8691function toolLabel ( tool : WhiteboxTool ) : string {
8792 return tool . display_name || humanize ( tool . id ) ;
8893}
@@ -458,6 +463,18 @@ export function ProcessingDialog({
458463 const panelRef = useRef < HTMLDivElement | null > ( null ) ;
459464 const [ pos , setPos ] = useState < { x : number ; y : number } | null > ( null ) ;
460465 const dragOffset = useRef < { x : number ; y : number } | null > ( null ) ;
466+ // Explicit size, null until first resized (the default responsive CSS size
467+ // applies). A drag from the bottom-right grip grows the panel from its pinned
468+ // top-left corner.
469+ const [ size , setSize ] = useState < { w : number ; h : number } | null > ( null ) ;
470+ const resizeStart = useRef < {
471+ x : number ;
472+ y : number ;
473+ w : number ;
474+ h : number ;
475+ left : number ;
476+ top : number ;
477+ } | null > ( null ) ;
461478
462479 const onDragStart = ( event : React . PointerEvent ) => {
463480 // Never begin a drag from an interactive control: the pointer capture would
@@ -498,6 +515,46 @@ export function ProcessingDialog({
498515 event . currentTarget . releasePointerCapture ?.( event . pointerId ) ;
499516 } ;
500517
518+ const onResizeStart = ( event : React . PointerEvent ) => {
519+ // Don't also start a header drag; the grip lives outside the header but stop
520+ // propagation defensively.
521+ event . stopPropagation ( ) ;
522+ const rect = panelRef . current ?. getBoundingClientRect ( ) ;
523+ if ( ! rect ) return ;
524+ // Pin the top-left so the panel grows toward the grip instead of staying
525+ // centered (the default placement uses a translate to center it).
526+ setPos ( { x : rect . left , y : rect . top } ) ;
527+ resizeStart . current = {
528+ x : event . clientX ,
529+ y : event . clientY ,
530+ w : rect . width ,
531+ h : rect . height ,
532+ left : rect . left ,
533+ top : rect . top ,
534+ } ;
535+ event . currentTarget . setPointerCapture ( event . pointerId ) ;
536+ } ;
537+
538+ const onResizeMove = ( event : React . PointerEvent ) => {
539+ const start = resizeStart . current ;
540+ if ( ! start ) return ;
541+ // Grow from the pinned top-left, clamped to a usable minimum and the viewport.
542+ const w = Math . max (
543+ PANEL_MIN_W ,
544+ Math . min ( start . w + ( event . clientX - start . x ) , window . innerWidth - start . left ) ,
545+ ) ;
546+ const h = Math . max (
547+ PANEL_MIN_H ,
548+ Math . min ( start . h + ( event . clientY - start . y ) , window . innerHeight - start . top ) ,
549+ ) ;
550+ setSize ( { w, h } ) ;
551+ } ;
552+
553+ const onResizeEnd = ( event : React . PointerEvent ) => {
554+ resizeStart . current = null ;
555+ event . currentTarget . releasePointerCapture ?.( event . pointerId ) ;
556+ } ;
557+
501558 // Escape closes the panel, preserving the affordance the Radix modal provided.
502559 // Guarded on `open` so it doesn't intercept Escape for the rest of the app.
503560 useEffect ( ( ) => {
@@ -511,13 +568,22 @@ export function ProcessingDialog({
511568 return ( ) => window . removeEventListener ( "keydown" , onKey ) ;
512569 } , [ open , setProcessingOpen ] ) ;
513570
514- // Re-clamp a dragged position when the viewport shrinks, so a panel dragged to
515- // an edge can't be left partly off-screen after a window resize. Only touches a
516- // pinned `pos` (functional update reads the latest), so the listener isn't
517- // re-subscribed on every drag frame.
571+ // Re-clamp an explicit size and dragged position when the viewport shrinks, so
572+ // a resized/moved panel can't be left oversized or partly off-screen after a
573+ // window resize. Functional updates read the latest values, so the listener
574+ // isn't re-subscribed on every drag/resize frame; a never-moved/never-resized
575+ // panel (null) keeps its responsive CSS placement.
518576 useEffect ( ( ) => {
519577 if ( ! open ) return ;
520578 const clamp = ( ) => {
579+ setSize ( ( current ) =>
580+ current
581+ ? {
582+ w : Math . max ( PANEL_MIN_W , Math . min ( current . w , window . innerWidth ) ) ,
583+ h : Math . max ( PANEL_MIN_H , Math . min ( current . h , window . innerHeight ) ) ,
584+ }
585+ : null ,
586+ ) ;
521587 const panel = panelRef . current ;
522588 if ( ! panel ) return ;
523589 setPos ( ( current ) =>
@@ -1265,9 +1331,16 @@ export function ProcessingDialog({
12651331 tabIndex = { - 1 }
12661332 aria-label = { t ( "processing.whitebox.toolbox" ) }
12671333 aria-modal = { false }
1268- style = { pos ? { left : pos . x , top : pos . y } : undefined }
1334+ style = { {
1335+ // Inline width/height (once resized) override the responsive w-/h- classes.
1336+ ...( pos ? { left : pos . x , top : pos . y } : null ) ,
1337+ ...( size ? { width : size . w , height : size . h } : null ) ,
1338+ } }
12691339 className = { cn (
1270- "fixed z-40 flex h-[min(760px,92vh)] w-[min(72rem,95vw)] flex-col overflow-hidden rounded-lg border bg-background shadow-xl" ,
1340+ // Height leaves room for the top offset (top-16) plus a bottom margin so
1341+ // the whole panel - including the bottom-right resize grip - stays on
1342+ // screen at small viewport heights.
1343+ "fixed z-40 flex h-[min(760px,calc(100vh-6rem))] w-[min(72rem,95vw)] flex-col overflow-hidden rounded-lg border bg-background shadow-xl" ,
12711344 pos ? "" : "left-1/2 top-16 -translate-x-1/2" ,
12721345 ) }
12731346 >
@@ -1569,6 +1642,31 @@ export function ProcessingDialog({
15691642 </ div >
15701643 </ div >
15711644 </ div >
1645+
1646+ { /* Resize grip (bottom-right). The diagonal lines hint the affordance.
1647+ Pointer-only, so it is presentational - there is no keyboard resize to
1648+ expose to assistive tech. */ }
1649+ < div
1650+ role = "presentation"
1651+ className = "absolute bottom-0 right-0 h-4 w-4 cursor-se-resize touch-none"
1652+ onPointerDown = { onResizeStart }
1653+ onPointerMove = { onResizeMove }
1654+ onPointerUp = { onResizeEnd }
1655+ onPointerCancel = { onResizeEnd }
1656+ >
1657+ < svg
1658+ viewBox = "0 0 10 10"
1659+ className = "h-full w-full text-muted-foreground"
1660+ aria-hidden = "true"
1661+ >
1662+ < path
1663+ d = "M9 1 L1 9 M9 5 L5 9"
1664+ stroke = "currentColor"
1665+ strokeWidth = { 1 }
1666+ fill = "none"
1667+ />
1668+ </ svg >
1669+ </ div >
15721670 </ div >
15731671 ) ;
15741672}
0 commit comments