Skip to content

Commit dc8d8c1

Browse files
committed
feat(processing): make the Whitebox floating panel resizable
Add a bottom-right resize grip that grows the panel from its pinned top-left, clamped to a usable minimum (560x400) and the viewport. Size is re-clamped on window resize alongside position. Also cap the default height to leave room for the top offset and a bottom margin so the whole panel, including the grip, stays on screen at small viewport heights.
1 parent 8be4d28 commit dc8d8c1

1 file changed

Lines changed: 104 additions & 6 deletions

File tree

apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ type ParameterValues = Record<string, unknown>;
8383
const LAYER_TOKEN_PREFIX = "layer:";
8484
const 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+
8691
function 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

Comments
 (0)