Skip to content

Commit 8d6c2ac

Browse files
authored
fix(processing): make the Whitebox toolbox usable on a narrow panel (#1917)
* fix(processing): make the Whitebox toolbox usable on a narrow panel The panel's split was `grid-cols-[minmax(260px,320px)_minmax(0,1fr)]`. That 260px is a hard floor, so once the panel dropped under about 570px the tool list kept its 260px and the parameter form was left roughly 120px. Everything in the form then overflowed the panel: a label wrapped to one word per line, its input ran off the right edge, and the run-local toggle collided with the tool name. At a 482px viewport 24 elements laid out past the viewport; at 390px, 40. Breakpoints are container queries against the panel, not `sm:`/`md:`. The panel is draggable and resizable, and `style.width` overrides its width class, so a viewport media query would hold the two-column layout after the user dragged the panel down to 400px on a desktop -- the same squeeze, without a narrow viewport to signal it. - Split stacks into two equal, independently scrolling rows below @XL, and the divider follows the axis (bottom rule stacked, end rule side by side). - The tool-name row wraps, so the toggle and the two buttons drop to their own line. The name asks for 12rem so that wrap actually fires: with `flex-1` its basis is 0 and it shrank to "Build..." to keep everything on one line. The toggle no longer wraps to a three-line stack. - The three picker rows with a 150px label column stack below @sm, measured against the form rather than the panel, since the form is full width once the layout stacks. - Category and source filters share a row while stacked, so the two filters cost one row of height instead of two. Flex, not a 2-col grid, so category still fills the row when the source filter is absent. - The tool list's ScrollArea gets `[&>div>div]:!block`. Radix ships that wrapper as `display: table; min-width: 100%`, which sizes to content, so the rows' `truncate` never engaged. Local, not in the shared primitive: 129 call sites use ScrollArea and some want the content-sized, horizontally scrollable behaviour. Measured on the built app, counting elements laid out past the viewport, with a tool selected: 482px 24 -> 0 390px 40 -> 0 320px many -> 0 768px 0 -> 0 1024px 0 -> 0 1440px 0 -> 0 Desktop is not untouched, but the two differences are both fixes the ScrollArea change brought with it: the tool list no longer shows a spurious horizontal scrollbar, and its last row is no longer clipped by it. Checked at 1024px and 1440px in light and dark. * Address review feedback - LayerOrPathInput: give the layer picker `col-span-2 @sm/params:col-span-1`. The row has three children but the narrow template I added has only two columns, so grid auto-placement put the picker in column 1, squeezed the path Input into the 2.25rem browse-button track, and wrapped the button onto a row of its own. Measured with a probe carrying the production classes, at a 300px container: before, input 36px and button 256px on row 2; after, picker 300px on row 1 with input 256px and button 36px sharing row 2. At 500px all three sit on one row as before. The other two converted rows stack to a single column, where any child count is fine, so only this one needed it. (Claude review, CodeRabbit - same finding) My own check for this PR counted elements laid out past the viewport, which this could never trip: nothing overflowed, it was just squeezed into the wrong track. Both bots were right to flag it. * Address review feedback - ScrollArea override: move the important modifier to the trailing position, `[&>div>div]:block!`. This is Tailwind v4's syntax and matches the same Radix ScrollArea override already in LayerPanel.tsx, so the two now read alike. The premise that it might not have been compiling was not the case: the leading form generated `.\[\&\>div\>div\]\:\!block>div>div{display:block!important}` in the built stylesheet, v4 still accepting the v3 spelling, which is why the truncation fix did take effect and showed up in the pixel diff against main. Switched anyway, for the consistency reason rather than the correctness one - the legacy spelling is deprecated and could be dropped. Re-checked the built app after the change: the wrapper computes `display: block` and matches its viewport width exactly (301px at 1440px, 327px at 390px), the list has no horizontal scrollbar, and nothing escapes the viewport. (Claude review)
1 parent 80af16e commit 8d6c2ac

1 file changed

Lines changed: 106 additions & 39 deletions

File tree

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

Lines changed: 106 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,7 +1630,14 @@ export function ProcessingDialog({ mapControllerRef, onAddRaster }: ProcessingDi
16301630
// Height leaves room for the top offset (top-16) plus a bottom margin so
16311631
// the whole panel - including the bottom-right resize grip - stays on
16321632
// screen at small viewport heights.
1633-
"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",
1633+
//
1634+
// `@container/panel`: the responsive breakpoints inside measure this
1635+
// panel, not the viewport. They cannot use `sm:`/`md:` because the panel
1636+
// is draggable *and* resizable - `style.width` above overrides the
1637+
// w-[min(72rem,95vw)] class - so a viewport media query would keep the
1638+
// two-column layout after the user has dragged the panel down to 400px
1639+
// on a desktop, which is the same squeeze as a phone.
1640+
"@container/panel 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",
16341641
pos ? "" : "left-1/2 top-16 -translate-x-1/2",
16351642
)}
16361643
>
@@ -1701,8 +1708,17 @@ export function ProcessingDialog({ mapControllerRef, onAddRaster }: ProcessingDi
17011708
</button>
17021709
</div>
17031710

1704-
<div className="grid min-h-0 flex-1 grid-cols-[minmax(260px,320px)_minmax(0,1fr)] gap-4 overflow-hidden p-5">
1705-
<div className="flex min-h-0 flex-col gap-3 border-e pe-4">
1711+
{/* Tool list beside the parameter form, but only while the panel is wide
1712+
enough for both. The list column's 260px floor is a hard minimum, so
1713+
below roughly 576px it ate everything and left the form ~120px, where
1714+
the label/value rows overflowed the panel entirely (a parameter label
1715+
wrapped to one word per line and its input ran off the right edge).
1716+
Under @xl the two stack into equal-height rows instead, each scrolling
1717+
on its own. */}
1718+
<div className="grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)] grid-rows-[minmax(0,1fr)_minmax(0,1fr)] gap-4 overflow-hidden p-5 @xl/panel:grid-cols-[minmax(260px,320px)_minmax(0,1fr)] @xl/panel:grid-rows-[minmax(0,1fr)]">
1719+
{/* Divider follows the axis: a bottom rule between stacked rows, an end
1720+
rule between side-by-side columns. */}
1721+
<div className="flex min-h-0 flex-col gap-3 border-b pb-4 @xl/panel:border-b-0 @xl/panel:border-e @xl/panel:pb-0 @xl/panel:pe-4">
17061722
<div className="flex gap-2">
17071723
<div className="relative min-w-0 flex-1">
17081724
<Search className="pointer-events-none absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -1756,38 +1772,64 @@ export function ProcessingDialog({ mapControllerRef, onAddRaster }: ProcessingDi
17561772
</Button>
17571773
)}
17581774

1759-
<Select value={category} onChange={(e) => setCategory(e.target.value)}>
1760-
{categories.map((item) => (
1761-
<option key={item.value} value={item.value}>
1762-
{item.label}
1763-
</option>
1764-
))}
1765-
</Select>
1766-
1767-
{hasGeolibreTools && (
1775+
{/* Side by side while the panel is stacked, so the two filters cost
1776+
one row of height instead of two and the tool list keeps most of
1777+
its half. Back to a column once the list has a column of its own,
1778+
which is narrower than the panel. Flex, not a 2-col grid, so the
1779+
category select still fills the row when the source filter is
1780+
absent (no GeoLibre-authored tools in the catalog). */}
1781+
<div className="flex gap-2 @xl/panel:flex-col @xl/panel:gap-3">
17681782
<Select
1769-
value={source}
1770-
// Reset the category too: a category with no tools in the newly
1771-
// chosen source would otherwise leave the list empty.
1772-
onChange={(e) => {
1773-
setSource(e.target.value);
1774-
setCategory("All");
1775-
}}
1776-
aria-label={t("processing.whitebox.filterBySource")}
1783+
className="min-w-0 flex-1"
1784+
value={category}
1785+
onChange={(e) => setCategory(e.target.value)}
17771786
>
1778-
<option value="All">
1779-
{t("processing.whitebox.allSources")} ({sourceCounts.all})
1780-
</option>
1781-
<option value="geolibre">
1782-
{t("processing.whitebox.geolibreTools")} ({sourceCounts.geolibre})
1783-
</option>
1784-
<option value="whitebox">
1785-
{t("processing.whitebox.whiteboxTools")} ({sourceCounts.whitebox})
1786-
</option>
1787+
{categories.map((item) => (
1788+
<option key={item.value} value={item.value}>
1789+
{item.label}
1790+
</option>
1791+
))}
17871792
</Select>
1788-
)}
17891793

1790-
<ScrollArea className="min-h-0 flex-1 rounded-md border">
1794+
{hasGeolibreTools && (
1795+
<Select
1796+
className="min-w-0 flex-1"
1797+
value={source}
1798+
// Reset the category too: a category with no tools in the newly
1799+
// chosen source would otherwise leave the list empty.
1800+
onChange={(e) => {
1801+
setSource(e.target.value);
1802+
setCategory("All");
1803+
}}
1804+
aria-label={t("processing.whitebox.filterBySource")}
1805+
>
1806+
<option value="All">
1807+
{t("processing.whitebox.allSources")} ({sourceCounts.all})
1808+
</option>
1809+
<option value="geolibre">
1810+
{t("processing.whitebox.geolibreTools")} ({sourceCounts.geolibre})
1811+
</option>
1812+
<option value="whitebox">
1813+
{t("processing.whitebox.whiteboxTools")} ({sourceCounts.whitebox})
1814+
</option>
1815+
</Select>
1816+
)}
1817+
</div>
1818+
1819+
{/* `[&>div>div]:block!` targets the wrapper Radix puts inside the
1820+
ScrollArea viewport, which ships as `display: table; min-width:
1821+
100%`. Table layout sizes to content, so the widest tool name set
1822+
the list's width and the rows' `truncate` never engaged - at a
1823+
260px column, "Build Object Hierarchy Multiscale" laid out 330px
1824+
wide and only the scrollport's clip hid it. As a block it fills the
1825+
viewport instead and the names ellipsize as intended. The bang is
1826+
load-bearing (Radix sets that display inline) and trails the
1827+
utility, which is Tailwind v4's syntax and matches the same
1828+
override in LayerPanel. Applied here rather than in the shared
1829+
primitive: 129 call sites use ScrollArea and some legitimately want
1830+
the content-sized, horizontally scrollable behaviour (the log pane
1831+
below is one). */}
1832+
<ScrollArea className="min-h-0 flex-1 rounded-md border [&>div>div]:block!">
17911833
<div className="divide-y">
17921834
{loadingTools ? (
17931835
<div className="flex items-center gap-2 p-3 text-sm text-muted-foreground">
@@ -1827,8 +1869,16 @@ export function ProcessingDialog({ mapControllerRef, onAddRaster }: ProcessingDi
18271869

18281870
<div className="grid min-h-0 grid-rows-[auto_minmax(0,1fr)_auto] gap-3">
18291871
<div className="min-w-0 border-b pb-3">
1830-
<div className="flex items-start justify-between gap-3">
1831-
<div className="min-w-0">
1872+
{/* Wraps rather than overflowing: the run-local toggle and the two
1873+
buttons need ~250px between them, so on a narrow panel they drop
1874+
to their own line under the tool name instead of running past
1875+
the panel edge. The name asks for 12rem (`basis-48`) so that
1876+
wrap actually triggers - with `flex-1` its basis is 0, and it
1877+
would shrink to "Build..." to keep everything on one line rather
1878+
than let the controls move down. `grow` still lets it take the
1879+
slack on a wide panel. */}
1880+
<div className="flex flex-wrap items-start justify-between gap-x-3 gap-y-2">
1881+
<div className="min-w-0 grow basis-48">
18321882
<h3 className="truncate text-base font-semibold">
18331883
{selectedTool ? toolLabel(selectedTool) : t("processing.whitebox.noToolSelected")}
18341884
</h3>
@@ -1841,7 +1891,10 @@ export function ProcessingDialog({ mapControllerRef, onAddRaster }: ProcessingDi
18411891
local/server toggle is dropped (WASM is the only runtime). */}
18421892
{!IS_MAS_BUILD && (
18431893
<label
1844-
className="flex items-center gap-1.5 text-xs text-muted-foreground"
1894+
// whitespace-nowrap: the label is short enough to keep on one
1895+
// line, and letting it wrap turned "Run locally (WASM)" into a
1896+
// three-line stack squeezed against the buttons.
1897+
className="flex shrink-0 items-center gap-1.5 whitespace-nowrap text-xs text-muted-foreground"
18451898
title={t("processing.whitebox.runLocalHint")}
18461899
>
18471900
<input
@@ -1896,7 +1949,11 @@ export function ProcessingDialog({ mapControllerRef, onAddRaster }: ProcessingDi
18961949
</div>
18971950

18981951
<ScrollArea className="min-h-0">
1899-
<div className="grid gap-4 pb-2 pe-5">
1952+
{/* A second container, because the picker/input rows below care
1953+
about the width of this form, not of the whole panel: once the
1954+
layout stacks, the form is full width even though the panel is
1955+
narrow. */}
1956+
<div className="@container/params grid gap-4 pb-2 pe-5">
19001957
{/* The chosen layers are WGS84 and this tool takes a ground
19011958
distance, so its distance fields carry a unit picker
19021959
(GeoLibre#1540). Say once, up front, that the layers are
@@ -2329,7 +2386,7 @@ function ParameterField({
23292386
// above) so a `url` description that happens to contain a word
23302387
// isPathParameter matches (path/file/…) can't shadow this picker into a
23312388
// local file-browse control.
2332-
<div className="grid grid-cols-[minmax(150px,200px)_minmax(0,1fr)] gap-2">
2389+
<div className="grid grid-cols-[minmax(0,1fr)] gap-2 @sm/params:grid-cols-[minmax(150px,200px)_minmax(0,1fr)]">
23332390
<Select
23342391
aria-label={t("processing.whitebox.fromLayer")}
23352392
value=""
@@ -2388,7 +2445,7 @@ function ParameterField({
23882445
// that layer's attribute names so the column need not be typed from
23892446
// memory (GeoLibre#1459). The text box stays editable alongside the
23902447
// picker, so a column the property sample missed can still be typed.
2391-
<div className="grid grid-cols-[minmax(150px,200px)_minmax(0,1fr)] gap-2">
2448+
<div className="grid grid-cols-[minmax(0,1fr)] gap-2 @sm/params:grid-cols-[minmax(150px,200px)_minmax(0,1fr)]">
23922449
<Select
23932450
aria-label={t("processing.whitebox.selectField")}
23942451
value={fieldOptions.includes(valueText) ? valueText : ""}
@@ -2690,8 +2747,18 @@ function LayerOrPathInput({
26902747
const { t } = useTranslation();
26912748
const usingLayer = value.startsWith(LAYER_TOKEN_PREFIX);
26922749
return (
2693-
<div className="grid grid-cols-[minmax(150px,200px)_minmax(0,1fr)_2.25rem] gap-2">
2694-
<Select value={usingLayer ? value : ""} onChange={(event) => onChange(event.target.value)}>
2750+
<div className="grid grid-cols-[minmax(0,1fr)_2.25rem] gap-2 @sm/params:grid-cols-[minmax(150px,200px)_minmax(0,1fr)_2.25rem]">
2751+
{/* Three children, but the narrow template has only two columns, so the
2752+
picker has to claim the whole first row explicitly. Left to
2753+
auto-placement it lands in column 1, the path Input gets squeezed into
2754+
the 2.25rem browse-button track, and the button wraps to a row of its
2755+
own. The other two converted rows stack to a single column, where any
2756+
child count is fine; this is the one that needs saying. */}
2757+
<Select
2758+
className="col-span-2 @sm/params:col-span-1"
2759+
value={usingLayer ? value : ""}
2760+
onChange={(event) => onChange(event.target.value)}
2761+
>
26952762
<option value="">{t("processing.whitebox.optionPath")}</option>
26962763
{layers.map((layer) => (
26972764
<option key={layer.id} value={`${LAYER_TOKEN_PREFIX}${layer.id}`}>

0 commit comments

Comments
 (0)