Skip to content

feat(processing): make the Whitebox toolbox a floating panel - #1226

Merged
giswqs merged 8 commits into
mainfrom
feat/whitebox-floating-panel
Jul 14, 2026
Merged

feat(processing): make the Whitebox toolbox a floating panel#1226
giswqs merged 8 commits into
mainfrom
feat/whitebox-floating-panel

Conversation

@giswqs

@giswqs giswqs commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

  • Converts the Whitebox Processing dialog from a Radix modal to a non-modal, draggable floating panel, so the map stays fully interactive while a tool is open.
  • Directly benefits the COG/WMS/XYZ subset tools: their "Use map extent" button needs the user to pan/zoom the map, which a modal blocked.
  • Mirrors the existing RecordVideoDialog pattern: a fixed, draggable window with a grip title bar, a close button, and Escape-to-close, and no backdrop overlay. The two-column tool browser layout and all behavior are unchanged.

Test plan

  • tsc -b and pre-commit run --files (eslint + npm build) pass
  • Verified in the running app (Playwright, real UI): the panel renders with no modal overlay, the map canvas is directly hit-testable behind it, the header drags the panel, and the close button and Escape both dismiss it
  • Tool list, search, parameter fields, and Run still render and work

Summary by CodeRabbit

  • New Features

    • Processing information now appears in a movable, non-blocking floating panel with a draggable title bar and resizable handle.
    • Bounding box setup now supports “Use map extent” and a stateful “Draw on map” action with drawing/cancel hints.
  • Bug Fixes

    • Toggling/closing the panel now reliably stops in-progress map drawing and restores focus to the prior element.
    • Bounding box inputs are validated before applying map extent.
  • Documentation

    • Updated English UI text for toolbox/runtime availability and new bounding box drawing labels/hints.

Replace the Radix modal with a non-modal, draggable floating window so the
map stays interactive while a tool is open. This matters for the COG/WMS/XYZ
subset tools, whose "Use map extent" affordance needs the user to pan/zoom the
map without dismissing the dialog. Mirrors the RecordVideoDialog pattern: a
fixed, draggable panel with a grip title bar, close button, and Escape-to-close;
no backdrop overlay.
Copilot AI review requested due to automatic review settings July 13, 2026 22:12
@netlify

netlify Bot commented Jul 13, 2026

Copy link
Copy Markdown

Deploy Preview for geolibre-app ready!

Name Link
🔨 Latest commit 2921142
🔍 Latest deploy log https://app.netlify.com/projects/geolibre-app/deploys/6a5581c71fd9de00084155f1
😎 Deploy Preview https://deploy-preview-1226--geolibre-app.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@giswqs, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4f0771da-0dce-4871-a6c1-f1f0da934155

📥 Commits

Reviewing files that changed from the base of the PR and between a491f8d and 2921142.

📒 Files selected for processing (2)
  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx
  • apps/geolibre-desktop/src/lib/print-extent.ts
📝 Walkthrough

Walkthrough

ProcessingDialog now renders as a draggable, resizable non-modal panel with focus handling and Escape closing. Bbox controls validate map extents and support abortable map drawing, while Whitebox visibility and related localization are updated.

Changes

Processing panel and extent workflow

Layer / File(s) Summary
Panel interaction and rendering
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx
Radix dialog behavior is replaced with a bounded draggable/resizable panel featuring focus management, Escape closing, and accessible controls.
Bounding box extent workflow
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx
WGS84 bbox bounds are validated and applied centrally; map drawing is abortable and exposed through stateful parameter actions.
Runtime and extent labels
apps/geolibre-desktop/src/i18n/locales/en.json
English strings are added for runtime availability and bbox drawing states and hints.

Whitebox availability and menu visibility

Layer / File(s) Summary
Whitebox menu gating
apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx, apps/geolibre-desktop/src/lib/is-mobile.ts
Whitebox menu visibility and divider rendering use the UI profile flag, and mobile-tool documentation reflects the updated availability model.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ProcessingDialog
  participant PrintExtent
  User->>ProcessingDialog: Activate Draw on map
  ProcessingDialog->>PrintExtent: Start drawPrintExtent
  PrintExtent-->>ProcessingDialog: Return selected bounds
  ProcessingDialog->>ProcessingDialog: Validate and apply bbox extent
  User->>ProcessingDialog: Toggle drawing or close panel
  ProcessingDialog->>PrintExtent: Abort active extent drawing
Loading

Possibly related PRs

Suggested reviewers: baileysimrell

Poem

I nudge the panel across the screen,
And draw a box in bounds so green.
The map awaits my little line,
Then Escape says, “All fine!” 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: converting the Whitebox processing UI into a floating panel.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/whitebox-floating-panel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

⚡ Cloudflare Pages preview

Item Value
Preview https://f034eb66.geolibre-preview.pages.dev
Demo app https://f034eb66.geolibre-preview.pages.dev/demo/
Commit 09d7363

Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
Comment on lines +477 to +494
const onDragMove = (event: React.PointerEvent) => {
if (!dragOffset.current) return;
const width = panelRef.current?.offsetWidth ?? 0;
const height = panelRef.current?.offsetHeight ?? 0;
// Keep the panel within the viewport so it can't be dragged off-screen.
const x = Math.max(
0,
Math.min(event.clientX - dragOffset.current.x, window.innerWidth - width),
);
const y = Math.max(
0,
Math.min(
event.clientY - dragOffset.current.y,
window.innerHeight - height,
),
);
setPos({ x, y });
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: drag now re-renders the full (unvirtualized) tool list on every pointer-move frame

onDragMove calls setPos(...) on every pointermove event while dragging. pos is state on the top-level ProcessingDialog component, so each move re-renders the whole component tree — including the tool list at line ~1367 (filteredTools.map(...)), which is a plain, non-virtualized map that can hold up to the "~700 Whitebox tools" mentioned elsewhere in this file (line 721). Before this PR there was no drag interaction, so this expensive subtree only re-rendered on meaningful actions (typing in search, selecting a tool); now it re-renders at pointer-move frequency for the entire duration of every drag.

In practice this is likely fine at typical list sizes/reconciliation speed, but with the full catalog loaded it could show visible jank while dragging. Consider isolating the list (e.g. React.memo on the row/list, or moving pos into a ref + directly mutating the panel's inline style via useLayoutEffect/direct DOM write instead of triggering a React re-render per frame) if this turns out to be perceptible.

Confidence: medium — a plausible perf regression under real-world list sizes, but not verified with profiling, and the same pattern (pos as top-level state) is already used in RecordVideoDialog/RecordTourDialog, just without a large list behind it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving as-is for now (open for your call). This mirrors the accepted pos-as-state pattern in RecordVideoDialog/RecordTourDialog; the difference here is the ~700-item unvirtualized tool list behind it. It is unverified (no profiling shows jank), and the clean fixes (memoizing/extracting the list, or moving pos to a ref + direct style writes) are a non-trivial refactor I would rather not do speculatively. Happy to optimize if profiling with the full catalog shows perceptible drag jank.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still deferring (leaving open for a maintainer call). Unchanged reasoning: this mirrors the accepted pos-as-state pattern in RecordVideoDialog/RecordTourDialog, and the perf hit is unverified (no profiling shows jank). The clean fixes (memoizing/extracting the list or ref + direct DOM writes) are a non-trivial refactor I would rather not do speculatively for a floating-panel drag. Happy to optimize if profiling with the full ~700-tool catalog shows perceptible jank.

Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

I reviewed the diff (gh pr diff/gh pr view) that converts the Whitebox ProcessingDialog from a Radix modal to a draggable, non-modal floating panel mirroring RecordVideoDialog, and read the full resulting file plus RecordVideoDialog.tsx and the other floating panels (RecordTourDialog, RasterSubsetPanel, PixelTimeSeriesControl, FieldCollectionDialog) for convention comparison.

Bugs: None found. The drag/pointer-capture logic, viewport clamping, Escape-to-close, and the if (!open) return null early-return (placed after all hooks) all faithfully mirror the already-shipped RecordVideoDialog pattern. JSX nesting closes correctly despite the indentation issue noted below.

Security: None found. No new user input handling, no injected HTML, no secrets.

Performance:

  • onDragMove drives pos state on the top-level component, so every pointermove frame during a drag re-renders the whole panel, including the non-virtualized tool list (up to ~700 Whitebox tools per an existing comment in the file). Previously there was no drag interaction, so this expensive subtree only re-rendered on deliberate actions; now it re-renders at pointer-move frequency for the drag's duration. Likely fine in practice but could jank with the full catalog loaded. (Medium confidence)

Quality:

  • The new aria-label="Whitebox toolbox" hardcodes a literal that duplicates the visible h2 text, and neither is run through t() — unlike the close button two lines below (t("common.close")) and unlike RecordVideoDialog, which the PR explicitly says it mirrors and which uses the same t() key for both its title and its aria-label. Minor i18n debt, easily fixed. (Medium confidence)
  • The moved content block (~lines 1260–1517: the two-column tool browser, parameter form, footer) wasn't re-indented after losing a nesting level (Dialog/DialogContent → a single div), so indentation is now off by ~4 spaces relative to its container. Compiles fine (JSX ignores whitespid), but harder to read and will produce noisy diffs on the next formatting pass. (High confidence, low severity)

CLAUDE.md: No violations beyond the i18n nit above (grouped under Quality). The new floating-panel pattern correctly reuses the cn/Tailwind conventions and z-index (z-40) already established by RecordVideoDialog/RecordTourDialog, and no map/tile-host or CSP files were touched.

@github-actions

Copy link
Copy Markdown
Contributor

(typo fix in my review comment above: "whitespid" should read "whitespace" — no change to the findings.)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx`:
- Around line 1225-1230: Update the title-bar drag handle around onDragStart,
onDragMove, and onDragEnd to be keyboard accessible: make it focusable and
handle arrow-key movement, or add explicit move/reset controls. Ensure keyboard
movement repositions the panel consistently with pointer dragging and preserves
access to the map beneath.
- Around line 1209-1216: Update the ProcessingDialog component’s open/close
lifecycle around the panelRef element to move focus to the panel or its first
control when the dialog opens, then restore focus to the previously active
element when it closes. Preserve the existing conditional rendering and ensure
the focus behavior handles repeated open/close cycles without targeting an
unmounted element.
- Line 1215: Update the new Whitebox panel title, its aria-label, and the
runtime fallback messages in the processing dialog to use the existing
react-i18next `t()` translation function instead of hard-coded strings. Add the
corresponding translation keys and preserve the current displayed text as their
default values.
- Around line 477-494: The saved panel position is not re-clamped when the
viewport changes. In the ProcessingDialog open lifecycle, add a resize handler
that reads the current panel dimensions, clamps the existing pos coordinates to
the viewport bounds, and updates state; register it while open and remove it on
cleanup so reopening after a resize also restores an on-screen position.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 596ca57c-7a4d-4141-a994-181948c606f0

📥 Commits

Reviewing files that changed from the base of the PR and between 124da49 and 30788f0.

📒 Files selected for processing (1)
  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx

Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
- Route the new panel title, aria-label, and runtime fallback strings through
  t() (new i18n keys) so they are translatable and the title/aria-label share
  one source instead of two hardcoded copies.
- Move focus into the panel on open (deferred two frames past the Radix menu's
  own focus restore) and best-effort restore it on close, replacing the focus
  management lost when the modal was removed.
- Re-clamp a dragged position on window resize so the panel can't be left
  off-screen after the viewport shrinks.
- Re-indent the content block one level shallower now that it is no longer
  wrapped in Dialog/DialogContent.
Comment thread apps/geolibre-desktop/src/i18n/locales/en.json Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the conversion of the Whitebox Processing dialog from a Radix modal to a draggable, non-modal floating panel (ProcessingDialog.tsx + new en.json strings). The drag/focus/Escape scaffolding mirrors the existing RecordVideoDialog pattern; the tool-browsing logic itself is unchanged (just reindented).

Bugs

  • Medium confidence: the resize-clamp useEffect only re-clamps pos on a resize event while the panel is open — it never clamps immediately when the panel (re)opens. Drag the panel toward an edge, close it, shrink the browser window while it's closed, then reopen: the panel renders at the stale, now out-of-bounds position and stays there until the next resize fires (since pos isn't reset on close). Left an inline suggestion that calls clamp() once per open transition. (ProcessingDialog.tsx)

Security

  • None found. No new user input handling, network calls, or injection surface in this diff — it's purely presentational (positioning, focus, keyboard) changes to an existing component.

Performance

  • Low confidence: onDragMove calls setPos on every raw pointermove event (no rAF throttling), which re-renders the whole panel — including the unvirtualized tool list (up to ~700 <button> rows when "All" categories are shown) — on every pixel of drag movement. filteredTools is memoized so list identity doesn't change, but this could still be a source of jank while dragging with a large/unfiltered catalog. This mirrors the existing RecordVideoDialog/RecordTourDialog drag pattern (no throttling there either), so it's an existing codebase characteristic being reapplied to a heavier component rather than a new anti-pattern.

Quality

  • Medium-high confidence: the new processing.whitebox.toolsAvailable i18n key doesn't follow this repo's documented pluralization convention (docs/i18n.md, and dozens of existing key_one/key_other pairs elsewhere in en.json). As written it will render "1 tools available." for a single-tool catalog. Left an inline suggestion — the call site already passes { count: tools.length }, so only the catalog needs the _one/_other split.
  • Low confidence: the panel's aria-label duplicates the visible <h2> heading text verbatim; aria-labelledby pointing at the heading would avoid a screen reader announcing the title twice.
  • Low confidence: no automated test (unit or e2e) exercises the new drag/Escape/focus/resize-clamp behavior — the PR relies on manual Playwright verification described in the description. The file wasn't under test before either, so this isn't a regression, just a coverage gap worth noting given the amount of new interactive logic.

CLAUDE.md

  • The pluralization-convention miss above is technically a docs/i18n.md adherence issue (see Quality). Everything else checked out: no leftover Radix Dialog imports/usages, new strings correctly route through t(), only en.json was touched (consistent with "other locales may be partial and fall back to en" per the i18n catalog test), and the i18next.d.ts typing (sourced directly from en.json) needs no separate update.

Also verified: no rules-of-hooks violation from the late if (!open) return null (all hooks run unconditionally before it, same pattern as the prior Radix-gated render), the drag-exclusion .closest("button, a, input, [role='button']") correctly lets the close button's click through even though event.target is the inner SVG icon, and the global Escape-closes-panel listener follows the same !event.defaultPrevented-guarded window.addEventListener("keydown", …) convention used by several other floating panels in this codebase (RasterSubsetPanel, PixelTimeSeriesControl, etc.), so it isn't a new pattern.

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.
Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/i18n/locales/en.json Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the conversion of the Whitebox Processing dialog from a Radix modal to a draggable, resizable, non-modal floating panel (ProcessingDialog.tsx, en.json).

Bugs

  • Medium confidence: The Escape-to-close keydown listener is attached to window and fires regardless of whether the panel has focus, so pressing Escape anywhere in the app (e.g. to cancel a map draw interaction, or while another floating panel is focused) will unexpectedly close the Whitebox panel too — unlike Radix's Dialog, which only intercepts Escape when it owns focus. (ProcessingDialog.tsx:558-569)
  • Low-medium confidence: In onResizeMove, Math.max(PANEL_MIN_W, Math.min(...)) lets the minimum size win over the viewport-based cap. If the panel was previously dragged near the right/bottom edge (or the window is narrow), shrinking via the resize grip can force the panel back up to 560×400 and push it (including the grip itself) off-screen. (ProcessingDialog.tsx:538-551)

Security

  • None found. This is a pure client-side UI change (drag/resize/focus handling); no new user input parsing, network calls, or injection surface.

Performance

  • None found. The new pointer/keyboard/resize listeners are cheap, properly scoped with early returns, and cleaned up on unmount/close.

Quality

  • Medium-high confidence: The new processing.whitebox.toolsAvailable key ("{{count}} tools available.") doesn't follow this catalog's established _one/_other pluralization convention (used pervasively elsewhere in en.json), so it will read "1 tools available." for a single tool. (en.json:2553)
  • Low confidence: Dropping Radix's DialogDescription removes the automatic aria-describedby link between the panel and its runtime-status text; the panel now only carries an aria-label for its name, with no programmatic association to the status paragraph for assistive tech.
  • Nice catch converting the previously-hardcoded "Whitebox toolbox" title and status strings to t() calls — that was a pre-existing i18n gap this PR incidentally fixes.

CLAUDE.md

  • Only en.json was updated for the new strings, which matches the documented convention (English is the source of truth; other locale catalogs fall back at runtime and are checked only for coverage/placeholder parity, not full parity).
  • No Whitebox WASM catalog / menu regeneration needed here — this PR doesn't touch geolibre-wasm or the tool catalog, just the dialog chrome.

Alongside "Use map extent", the COG/WMS/XYZ subset tools now offer "Draw on
map": rubber-band a box on the map to fill the bbox (and bbox_crs 4326). This is
only workable now that the panel is non-modal. Reuses the print-extent draw
helper (pan/zoom suspended during the drag, box preview, Escape/blur handling);
the drawn box is cleared afterward. Escape during a draw cancels the draw instead
of closing the panel, and closing the panel or toggling the button aborts an
in-flight draw. The fill/validation is shared with "Use map extent".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx`:
- Around line 1044-1085: Update the bbox context handlers around
handleUseMapExtent, handleDrawBbox, and tool selection so any active draw is
aborted before applying the map extent or switching tools. Ensure the pending
draw cannot later call applyBboxExtent or update the newly selected tool’s
values, while preserving normal drawing behavior when the context remains
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 18e6357e-497a-4b25-b31d-6651be81d207

📥 Commits

Reviewing files that changed from the base of the PR and between 30788f0 and 89ced12.

📒 Files selected for processing (2)
  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json

Comment thread apps/geolibre-desktop/src/i18n/locales/en.json Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the conversion of the Whitebox Processing dialog from a Radix modal to a draggable/resizable non-modal floating panel, plus the new "Draw on map" bbox tool for the COG/WMS/XYZ subset parameters. Compared the new drag/resize code against the existing RecordVideoDialog pattern it mirrors, and the reused drawPrintExtent/clearPrintExtent helper from lib/print-extent.ts.

Bugs

  • Draw-bbox not scoped to the tool that started it (medium confidence): switching the selected tool while a "Draw on map" drag is in-flight doesn't abort it. values resets per-tool on selectedTool?.id change, so the eventual applyBboxExtent/updateValue call lands in whatever tool is selected when the draw finishes, not the tool the user actually drew for, and the global drawing flag makes an unrelated tool's button show "Drawing...". (ProcessingDialog.tsx:1079-1085)
  • Resize floor can exceed the viewport (medium-low confidence): PANEL_MIN_W/PANEL_MIN_H (560x400) are enforced as a hard minimum with no clamp against window.innerWidth/innerHeight, so a narrow/short viewport (e.g. a small embedded width) can leave the panel, and its own resize grip, partly unreachable off-screen. (ProcessingDialog.tsx:543-556, also the resize-on-window-resize effect just below)

Security

  • None found. No new user input reaches injection-sensitive sinks; the new code is all client-side drag/resize/draw state and bbox math over trusted map view data.

Performance

  • None found. Pointer-move handlers for drag/resize update React state per event without RAF batching, but this exactly mirrors the pre-existing, already-shipped RecordVideoDialog drag pattern, so it isn't a regression introduced here.

Quality

  • Missing i18next plural forms for the new toolsAvailable key (low-medium confidence): every other count-based string in en.json uses _one/_other suffixes; this one doesn't, so a singular tool count reads "1 tools available." (en.json:2553)
  • Resize grip has no keyboard equivalent (low confidence, self-acknowledged in the code's own comment): pointer-only resize means keyboard/screen-reader users can't resize the panel. Pre-existing limitation for drag-to-reposition too (shared with RecordVideoDialog), but the resize affordance is new here.
  • Focus-steal race on open (low confidence): the deferred double-requestAnimationFrame call to panelRef.current?.focus() on open isn't guarded against the user already having moved focus elsewhere (e.g. started typing in the tool search box) within those ~2 frames; unlikely in practice but has no defensive check.

CLAUDE.md

  • New user-facing strings are correctly routed through t() with keys added to en.json (the source-of-truth catalog), consistent with the i18n convention, aside from the plural-forms nit above.
  • No hand-edits to the auto-generated Whitebox menu catalog or other generated files.
  • No CSP/tile-host or node_modules changes involved.

Everything else, the drag/pin/clamp logic, the shared applyBboxExtent refactor, the abort-on-close wiring for the draw helper, and the focus-restore-on-close behavior, checked out correctly against print-extent.ts and the RecordVideoDialog precedent it mirrors.

giswqs added 2 commits July 13, 2026 20:01
- Abort an in-flight "Draw on map" when the selected tool changes or "Use map
  extent" is clicked, so a late-resolving box can't fill the wrong tool's bbox.
- Scope the panel's Escape-to-close to when focus is inside the panel, so
  Escaping an unrelated map/panel interaction doesn't also close it.
- Cap the resize minimum to the room available to the viewport edge (and clamp
  the re-clamp effect's minimum to the viewport), so a panel pinned near an edge
  or a small viewport shrinks to fit instead of being pushed off-screen.
- Clamp size/position once on open (not only on resize), fixing a stale
  off-screen position when the window shrank while the panel was closed.
- Split toolsAvailable into _one/_other plural forms per the i18next convention.
The Whitebox toolbox and its WASI/GeoLibre tool catalog run entirely in the
browser via WebAssembly (the dialog defaults to the WASM runner off-desktop), so
unlike the sidecar-backed tools they don't need a Python process. Drop the
mobile gate on the Whitebox menu item and its tool categories so they're
available on Android/iOS; Conversion, Raster tools, and AI Segmentation stay
hidden on mobile since they still require the sidecar.
Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the full diff (ProcessingDialog.tsx + en.json) converting the Whitebox toolbox from a Radix modal to a draggable/resizable, non-modal floating panel, plus the new "Draw on map" bbox affordance. The PR's own commit history shows five rounds of self-addressed review feedback (i18n, focus management, resize clamping, draw-abort races), and that diligence shows — I could not find a high-confidence functional bug. Findings below are lower-severity/nits, posted inline where line-specific.

Bugs

  • None found with high confidence. I traced the drag/resize/escape/focus effects, the abort-on-close / abort-on-tool-change / abort-on-toggle logic for "Draw on map", and the interaction between the panel's own scoped Escape handler and print-extent.ts's own Escape/blur handling — all hold up, including the trickier races (double-click toggling a draw, switching tools mid-draw, closing the panel mid-draw). (Confidence: high that these are correct.)
  • Minor/speculative: the viewport re-clamp effect's setPos clamp reads panel.offsetWidth/offsetHeight before the sibling setSize update for the same tick has committed to the DOM, so position clamping can lag one tick behind a fresh size clamp. This only makes the resulting position more conservative, never off-screen, so it's cosmetic at worst. See inline comment. (Confidence: low-medium)

Security

  • None found. The change is UI-only (drag/resize/focus/keyboard handling in the client), adds no new network calls, file-path handling, or unsanitized string interpolation. All user-facing text goes through t()/plain text nodes, not dangerouslySetInnerHTML. (Confidence: high)

Performance

  • The window-resize re-clamp effect isn't throttled — a live OS window-resize drag fires resize continuously, and each tick runs two setState calls, re-rendering the panel tree every tick. Coalescing via requestAnimationFrame (as the drag/resize pointer-move handlers already do) would avoid the re-render storm. See inline comment. (Confidence: medium)

Quality

  • aria-label on the panel duplicates the visible <h2> heading text via a second t() call; aria-labelledby pointing at the heading's id would share one source more directly than two independent t() lookups. See inline comment. (Confidence: low)
  • The resize grip is intentionally pointer-only with no keyboard equivalent — already called out in the code's own comment as a known a11y gap rather than an oversight, so not re-flagging as a new issue. (Confidence: n/a, informational)

CLAUDE.md

  • New user-facing strings are routed through t() with keys added to en.json (the source of truth), including correctly split _one/_other plural forms per the i18next convention — matches the documented convention. (Confidence: high)
  • No edits to node_modules, no hand-edits to the auto-generated Whitebox menu catalog, no new external tile/map hosts requiring CSP changes — none of those apply to this diff. (Confidence: high)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx (1)

600-639: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

pos re-clamp reads stale panel dimensions in the same pass that resizes it.

setSize(...) (Lines 603-615) queues the viewport-capped width/height, but the very next lines read panel.offsetWidth/offsetHeight (Lines 625, 629) to clamp pos — since React defers the DOM commit until after this synchronous callback returns, those reads still reflect the pre-clamp layout. If the panel was oversized before this run, pos gets clamped against the old (larger) dimensions instead of the new ones just computed, so the position can end up more/less clamped than the just-resized panel actually needs. It self-corrects on the next resize event, but is inconsistent for this pass.

♻️ Proposed fix: derive the clamped width/height instead of reading the DOM
     const clampToViewport = () => {
-      setSize((current) =>
-        current
-          ? {
-              w: clamp(
-                current.w,
-                Math.min(PANEL_MIN_W, window.innerWidth),
-                window.innerWidth,
-              ),
-              h: clamp(
-                current.h,
-                Math.min(PANEL_MIN_H, window.innerHeight),
-                window.innerHeight,
-              ),
-            }
-          : null,
-      );
-      const panel = panelRef.current;
-      if (!panel) return;
+      const panel = panelRef.current;
+      let nextW = panel?.offsetWidth;
+      let nextH = panel?.offsetHeight;
+      setSize((current) => {
+        if (!current) return null;
+        nextW = clamp(
+          current.w,
+          Math.min(PANEL_MIN_W, window.innerWidth),
+          window.innerWidth,
+        );
+        nextH = clamp(
+          current.h,
+          Math.min(PANEL_MIN_H, window.innerHeight),
+          window.innerHeight,
+        );
+        return { w: nextW, h: nextH };
+      });
+      if (!panel || nextW === undefined || nextH === undefined) return;
       setPos((current) =>
         current
           ? {
-              x: Math.max(
-                0,
-                Math.min(current.x, window.innerWidth - panel.offsetWidth),
-              ),
-              y: Math.max(
-                0,
-                Math.min(current.y, window.innerHeight - panel.offsetHeight),
-              ),
+              x: Math.max(0, Math.min(current.x, window.innerWidth - nextW)),
+              y: Math.max(0, Math.min(current.y, window.innerHeight - nextH)),
             }
           : null,
       );
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx` around
lines 600 - 639, Update the clampToViewport callback in the open viewport effect
to derive the viewport-capped width and height once, then use those dimensions
when clamping pos instead of reading panelRef.current.offsetWidth/offsetHeight.
Preserve the existing setSize behavior and ensure both size and position are
calculated from the same clamped dimensions during a single pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx`:
- Around line 600-639: Update the clampToViewport callback in the open viewport
effect to derive the viewport-capped width and height once, then use those
dimensions when clamping pos instead of reading
panelRef.current.offsetWidth/offsetHeight. Preserve the existing setSize
behavior and ensure both size and position are calculated from the same clamped
dimensions during a single pass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 73ca22b1-65c1-46c6-a8c3-1f7bc4f1ee7c

📥 Commits

Reviewing files that changed from the base of the PR and between 89ced12 and ce3684f.

📒 Files selected for processing (2)
  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json

- Coalesce the window-resize clamp with requestAnimationFrame so a live OS
  window-resize drag re-renders the tool browser once per frame instead of on
  every resize event, matching the rAF pattern used by the pointer handlers.
- Label the panel via aria-labelledby pointing at the h2 title's id, so the
  accessible name shares the single visible text node instead of a second t()
  lookup that could drift from the title.
if (!rect) return;
// Pin the top-left so the panel grows toward the grip instead of staying
// centered (the default placement uses a translate to center it).
setPos({ x: rect.left, y: rect.top });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium confidence): onResizeStart fires on every pointerdown on the grip, including a plain click with no subsequent drag, and unconditionally calls setPos({ x: rect.left, y: rect.top }). That permanently switches the panel from the responsive, CSS-centered placement (left-1/2 top-16 -translate-x-1/2) to an explicitly pinned position — even though nothing was actually resized (onResizeEnd only clears resizeStart.current, never pos).

Net effect: merely touching the resize grip (e.g. an accidental click) un-centers the panel for the rest of the session, and afterwards a browser-window resize no longer re-centers it (only re-clamps it to stay on-screen via the viewport effect).

Consider only committing to an explicit pos once real movement is observed (e.g. set it lazily on the first onResizeMove, or track a small drag-distance threshold) rather than unconditionally on pointerdown.

Comment on lines +1737 to +1747
}}
/>
) : (
error && (
<p className="flex items-center gap-2 text-sm text-destructive">
<AlertCircle className="h-4 w-4" />
{error}
</p>
)
)}
{job && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessibility (medium confidence): the resize grip is pointer-only (onPointerDown/onPointerMove/onPointerUp on a role="presentation" div) with no keyboard equivalent — a keyboard-only user has no way to enlarge the panel beyond its default/last-dragged size. This is called out honestly in the comment above, but it's still a real WCAG 2.1.1 (keyboard operable) gap worth tracking, e.g. a follow-up that lets arrow keys resize the panel when the grip (or the panel itself) has focus.

>
<span className="block truncate font-medium">
{tool.locked ? "[Locked] " : ""}
{toolLabel(tool)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality / CLAUDE.md i18n convention (low-medium confidence): this PR wraps the neighboring runtime-status strings in the same panel with t() (processing.whitebox.checkingRuntime, .toolsAvailable, .runtimeUnavailable, .toolbox), but these sibling strings in the same touched block are still hardcoded English and bypass t():

  • line 1615: "No tool selected"
  • line 1664: "This tool is locked."
  • line 1673: "This tool has no parameters."

They're pre-existing (just re-indented here), so this is a minor nit rather than a regression, but since the PR is already touching/reformatting this exact code and per CLAUDE.md "Use t() for new user-facing strings" for the surrounding additions, it'd be a small, low-risk win to fold these into the same i18n pass while the file is already open.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the diff for feat(processing): make the Whitebox toolbox a floating panel (ProcessingMenu.tsx, ProcessingDialog.tsx, en.json, is-mobile.ts). This converts the Whitebox dialog from a Radix modal to a draggable/resizable, non-modal floating panel, and adds Draw on map as a second way to fill a bbox parameter alongside Use map extent.

Bugs

  • Medium confidence: onResizeStart unconditionally pins the panel position (setPos) on every pointerdown on the resize grip, even a plain click with no drag. This permanently converts the panel from its responsive, CSS-centered placement to a fixed pinned position, and afterwards a browser-window resize only re-clamps it to the viewport instead of re-centering it. (see inline comment)
  • Low confidence: the shared mapExtentInvalid error message (Adjust the view, then try again) is now also shown for the new Draw on map path, where the instruction doesn't quite fit (the user drew a box, they didn't move the map view). In practice this branch is very hard to hit for a drawn box (longitude is normalized and lat is bounded by the Mercator projection before the check runs), so this is mostly a copy nit rather than a functional bug.

Security

  • Nothing found. No new external input parsing, no injected HTML/URLs, no secrets in the diff.

Performance

  • Nothing notable. The drag/resize handlers are plain pointer-event math with no unnecessary re-renders beyond what dragging/resizing requires, consistent with the existing RecordVideoDialog pattern this mirrors.

Quality

  • Medium confidence: the bottom-right resize grip is pointer-only (no keyboard equivalent), which is called out honestly in a code comment but is still a real keyboard-operability gap. (see inline comment)
  • Low-medium confidence: the double-requestAnimationFrame focus-restore hack (to avoid Radix's own menu-close focus restoration stealing focus back) is a bit fragile - it depends on undocumented timing of Radix's internal behavior and could silently regress if that timing changes in a future Radix update. Not a bug today, just a maintainability risk worth a short comment/test if feasible.
  • Low-medium confidence: a few sibling strings in the same touched block remain hardcoded English (No tool selected, This tool has no parameters., This tool is locked.) even though this PR wraps the neighboring runtime-status strings in t(). Pre-existing, but flagged since the PR is already doing an i18n pass over this exact code. (see inline comment)

CLAUDE.md adherence

  • New user-facing strings (title, runtime status, draw-bbox labels/hint) are correctly added to en.json and consumed via t(), following the en.json-is-the-source-of-truth / other-locales-fall-back convention - good adherence here, aside from the pre-existing hardcoded strings noted above.
  • No Whitebox menu/catalog files were hand-edited, and no geolibre-wasm version bump is in this diff, so the regenerate-the-menu-catalog rule doesn't apply.
  • No new external tile/map hosts, so no Tauri CSP changes were needed.

Overall this is a well-considered, carefully-commented change that mirrors an existing pattern (RecordVideoDialog) and adds thoughtful extras (viewport re-clamping, escape-to-close scoped to focus-within, abort-safe draw handling) beyond what it mirrors. The findings above are edge cases and polish items, not blockers.

// The Whitebox toolbox (and its WASI/GeoLibre tool catalog) runs entirely in
// the browser via WebAssembly, so unlike the sidecar-backed tools it stays
// available on mobile.
const showWhitebox = show("processing.whitebox");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium-high confidence): Making the Whitebox toolbox reachable on mobile exposes a case ProcessingDialog.tsx doesn't handle: its desktop flag is just isTauri() (ProcessingDialog.tsx:446), which is also true inside a Tauri Android/iOS webview — per is-mobile.ts's own comment, "the Tauri Android webview reports an Android UA."

Every other sidecar-backed dialog (Conversion, Raster, Segmentation) avoids this because it stays behind !mobile && show(...) here, so it's never opened on mobile at all. Whitebox is now the one exception, but ProcessingDialog's runLocal still defaults to !desktop (i.e. false, sidecar mode) on a mobile Tauri build, and its "Start server" button is gated only on desktop, not !mobile — so a mobile Tauri user opening this dialog lands in sidecar mode by default with a "Start server" button that calls startGeoLibreSidecar(), which cannot succeed on Android/iOS. They'd have to discover and manually check "Run locally" to get a working WASM tool.

Worth defaulting runLocal/gating the server buttons off desktop && !mobile instead of desktop alone.

</button>
</div>

<div className="grid min-h-0 flex-1 grid-cols-[minmax(260px,320px)_minmax(0,1fr)] gap-4 overflow-hidden p-5">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality/UX (medium confidence): This two-column layout (grid-cols-[minmax(260px,320px)_minmax(0,1fr)], overflow-hidden) is unchanged from the modal version and was never responsive for narrow viewports — but that was fine while Whitebox stayed hidden on mobile (ProcessingMenu.tsx's old !mobile && show(...) gate). Now that ProcessingMenu.tsx shows Whitebox on mobile (including a plain mobile browser, not just Tauri), the panel's own width can be as little as 95vw, and the tool-list column alone needs 260–320px minimum — on a ~360–400px-wide phone that leaves little to no room for the parameter form on the right, with overflow-hidden clipping rather than scrolling it into view.

Since this PR is what makes the panel reachable on phone-width screens for the first time, it may be worth a narrow-viewport fallback (e.g. stack the columns, or drop to a single-column list at small widths) rather than relying on the desktop-oriented two-column grid unconditionally.

Comment on lines +500 to +544
const onDragMove = (event: React.PointerEvent) => {
if (!dragOffset.current) return;
const width = panelRef.current?.offsetWidth ?? 0;
const height = panelRef.current?.offsetHeight ?? 0;
// Keep the panel within the viewport so it can't be dragged off-screen.
const x = Math.max(
0,
Math.min(event.clientX - dragOffset.current.x, window.innerWidth - width),
);
const y = Math.max(
0,
Math.min(
event.clientY - dragOffset.current.y,
window.innerHeight - height,
),
);
setPos({ x, y });
};

const onDragEnd = (event: React.PointerEvent) => {
dragOffset.current = null;
event.currentTarget.releasePointerCapture?.(event.pointerId);
};

const onResizeStart = (event: React.PointerEvent) => {
// Don't also start a header drag; the grip lives outside the header but stop
// propagation defensively.
event.stopPropagation();
const rect = panelRef.current?.getBoundingClientRect();
if (!rect) return;
// Pin the top-left so the panel grows toward the grip instead of staying
// centered (the default placement uses a translate to center it).
setPos({ x: rect.left, y: rect.top });
resizeStart.current = {
x: event.clientX,
y: event.clientY,
w: rect.width,
h: rect.height,
left: rect.left,
top: rect.top,
};
event.currentTarget.setPointerCapture(event.pointerId);
};

const onResizeMove = (event: React.PointerEvent) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance (medium confidence): onDragMove/onResizeMove call setPos/setSize directly on every raw pointermove event, with no per-frame coalescing — unlike drawPrintExtent's queueMove/rAF-batching pattern used elsewhere in this very PR (lib/print-extent.ts). Each call re-renders the whole panel, including the (potentially ~700-row) tool list and the parameter form.

This mirrors RecordVideoDialog's existing drag handler, so it's a pre-existing pattern in the codebase, but RecordVideoDialog's tree is much lighter than this dialog's — dragging/resizing the Whitebox panel while a large tool list/param form is mounted is more likely to visibly jank on slower machines. Consider coalescing to one setPos/setSize per animation frame, the way drawPrintExtent already does for the rubber-band preview.

const bounds = mapControllerRef.current?.readView().bbox;
if (!bounds) {
setError(t("processing.whitebox.mapExtentUnavailable"));
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality (low-medium confidence): applyBboxExtent is now shared between "Use map extent" and the new "Draw on map" (below), but the error copy it shows on an invalid/unavailable box — mapExtentInvalid: "The current map view can't be used as a bounding box…" and mapExtentUnavailable: "The map view is not available yet…" — still only makes sense for the map-extent path. A drawn box that happens to fail this same validation (e.g. spans the antimeridian, per drawPrintExtent's documented limitation) would show a message about "the current map view" even though the user just hand-drew a box, which reads as confusing/wrong for that caller.

Might be worth caller-agnostic copy (or a second pair of strings for the draw path) now that this validation is shared between the two entry points.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the conversion of the Whitebox Processing dialog from a Radix modal to a draggable, resizable non-modal floating panel, plus the new "Draw on map" bbox affordance and the mobile-visibility change in ProcessingMenu.tsx/is-mobile.ts.

Bugs

  • ProcessingDialog.tsx's desktop/runLocal defaulting doesn't account for mobile Tauri. desktop is just isTauri(), which is also true inside a Tauri Android/iOS webview. Every other sidecar-backed dialog avoids this by staying hidden on mobile (ProcessingMenu.tsx's !mobile && show(...) gate), but this PR now shows Whitebox on mobile too, on the premise it's fully WASM-backed. In practice, on mobile Tauri, runLocal still defaults to false (sidecar mode) and the "Start server" button (gated only on desktop) is shown despite the sidecar being unable to run there. (Medium-high confidence)

Performance

  • Unthrottled pointermove handlers for drag/resize. onDragMove/onResizeMove call setPos/setSize on every raw pointer event with no per-frame coalescing, unlike the rAF-batched queueMove pattern the same PR introduces in lib/print-extent.ts. This mirrors an existing RecordVideoDialog pattern, but this dialog's tree (tool list + parameter form) is much heavier, so dragging/resizing is more likely to visibly jank. (Medium confidence)

Quality

  • Two-column grid layout has no mobile-responsive fallback. grid-cols-[minmax(260px,320px)_minmax(0,1fr)] with overflow-hidden was fine while Whitebox was hidden on mobile, but this PR makes the panel reachable on phone-width screens (including plain mobile browsers) for the first time, where the tool-list column alone can consume most of the panel's width. (Medium confidence)
  • Shared applyBboxExtent error copy is map-view-specific. The mapExtentInvalid/mapExtentUnavailable strings ("the current map view can't be used…") are now also shown for the new "Draw on map" path, where they read as a confusing mismatch since the user drew a box rather than using the current view. (Low-medium confidence)

Security

  • None found. No new input parsing from untrusted sources, no injection surface, and the drag/resize/focus code only touches DOM geometry and local component state.

CLAUDE.md adherence

  • New user-facing strings are correctly added via en.json and t() (translatable UI strings convention), including proper _one/_other pluralization consistent with existing keys.
  • No unrelated refactors or scope creep; changes are scoped to the floating-panel conversion and the related mobile-visibility/i18n updates.

The "Draw on map" preview reused drawPrintExtent's MapLibre fill/line box, which
is occluded by the interleaved deck.gl raster overlay (WebGPU) — the same reason
RasterSubsetPanel draws its box as an SVG. Add an optional onPreview callback and
a drawBox toggle to drawPrintExtent so the caller can render its own preview; the
subset dialog now draws the box as a viewport-space SVG portaled to <body> at a
higher z-index, so it stays visible above the raster. Print Layout is unchanged
(drawBox defaults on, onPreview unset).
// The Whitebox toolbox (and its WASI/GeoLibre tool catalog) runs entirely in
// the browser via WebAssembly, so unlike the sidecar-backed tools it stays
// available on mobile.
const showWhitebox = show("processing.whitebox");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium confidence): Removing the !mobile gate here means the Whitebox toolbox is now shown on Tauri Android/iOS too, but ProcessingDialog.tsx still defaults runLocal to useState(!desktop), and desktop = isTauri() is true on Tauri mobile builds (per the isMobile() doc comment: "the Tauri Android webview reports an "Android" UA"). So on Tauri Android/iOS, runLocal defaults to false (sidecar mode) — but the sidecar can never run on mobile, so runtimeAvailable stays false and the Run button (disabled={... (!runLocal && runtimeAvailable !== true)}) stays disabled until the user manually checks "Run locally". The toolbox becomes visible-but-non-functional by default on that platform, which cuts against this PR's stated rationale ("unlike the sidecar-backed tools it stays available on mobile").

Suggest defaulting runLocal to something like !desktop || isMobile() so Tauri-mobile also opens straight into WASM mode.

const handleUseMapExtent = () => {
// Clear any stale banner (e.g. a prior "map not ready") so a later success
// doesn't leave it lingering, mirroring RasterSubsetPanel.handleUseView.
// Validate a WGS84 box and write it into the `bbox`/`bbox_crs` fields. Shared

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality (low confidence): This now stacks two comment blocks that explain the same thing. The lines just above (1054-1057) are the original comment written for handleUseMapExtent (references GeoLibre#1213) but now sit above the renamed/shared applyBboxExtent, immediately followed by this new block covering the same validation logic from a slightly different angle. The actual handleUseMapExtent a few lines below (now a 3-line wrapper) is left with no comment of its own. Consider merging these into one comment on applyBboxExtent and dropping the stale block, or moving the GH#1213 context down to handleUseMapExtent.

@giswqs
giswqs merged commit f4a2fe7 into main Jul 14, 2026
20 checks passed
@giswqs
giswqs deleted the feat/whitebox-floating-panel branch July 14, 2026 00:32
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the diff (ProcessingMenu.tsx, ProcessingDialog.tsx, print-extent.ts, is-mobile.ts, en.json) for correctness, security, performance, quality, and CLAUDE.md adherence. This is a well-scoped UI change that mirrors the existing RecordVideoDialog draggable-panel pattern closely, and the bbox validation / draw-abort bookkeeping is careful and well-commented.

Bugs

  • Medium confidence - ProcessingMenu.tsx:69: dropping the !mobile gate on the Whitebox menu item exposes it on Tauri Android/iOS, but ProcessingDialog.tsx runLocal state still defaults to !desktop (desktop = isTauri(), which is true on Tauri mobile). That means Tauri-mobile users open the toolbox defaulted to sidecar mode, which can never succeed on mobile; the Run button stays disabled until the user manually toggles Run locally. The toolbox becomes visible-but-non-functional by default on that platform, undercutting the stated rationale that Whitebox stays available on mobile. (Inline comment posted.)

Security

  • None found. No new input handling, network calls, or injection surface - this is a client-side DOM/drag-resize UI change plus i18n string additions.

Performance

  • Low confidence / informational: onDragMove/onResizeMove call setPos/setSize synchronously on every pointermove without the requestAnimationFrame coalescing used by the new window resize listener. This mirrors the pre-existing RecordVideoDialog drag pattern exactly, so it is consistent with precedent rather than a new regression, but could still cause visible re-render jank while dragging/resizing on lower-end devices.

Quality

  • Low confidence - ProcessingDialog.tsx:1054-1063: two comment blocks now sit back-to-back describing the same validation logic (the original handleUseMapExtent comment referencing GeoLibre#1213, followed by a new block for the extracted applyBboxExtent), while the actual handleUseMapExtent a few lines down is left uncommented. Worth consolidating. (Inline comment posted.)
  • Low confidence - the comment on the window resize clamp effect (ProcessingDialog.tsx:644) says it matches the rAF pattern used by the pointer drag/resize handlers, but the pointer handlers (onDragMove/onResizeMove) do not actually use requestAnimationFrame - only the window-resize listener does. Minor documentation-accuracy nit, not functional.
  • onResizeStart stopPropagation() is commented as needed to avoid also starting a header drag, but the resize grip is a sibling of the header (not nested inside it), so a pointerdown there would not bubble into the header handler regardless - the guard is harmless but the comment overstates the risk.

CLAUDE.md adherence

  • New user-facing strings correctly go through t() and are added to en.json as the source of truth (other locale catalogs are intentionally left to fall back to English per docs/i18n.md) - good adherence here, and this diff actually improves i18n coverage for strings that were previously hard-coded (Whitebox toolbox, Checking runtime, etc.).
  • No other CLAUDE.md-governed files (Whitebox menu catalog, Tauri CSP allowlist, MapLibre control CSS overrides) were touched, and none needed to be for this change.

No security, correctness-blocking, or CLAUDE.md-violation issues beyond what is listed above; the two items above (mobile runLocal default, stale comment) are the only points worth a second look before merge.

giswqs added a commit that referenced this pull request Jul 18, 2026
Replace the Radix modals with draggable, non-modal floating windows
(mirroring RecordVideoDialog's pattern, as the Whitebox toolbox did in
#1226) so the map and attribute table stay interactive while selecting:
run a selection, pan to inspect the highlight, refine, and re-run without
reopening. Both panels can be open at once.

Since the camera can now move while the panel is open, Select by
Expression re-snapshots ["zoom"] and the @map_* variables at each run
instead of at open (the modal Expression Builder keeps its open-time
snapshot for stable prop identities). The redundant footer Close button
is dropped in favor of the title-bar X.
giswqs added a commit that referenced this pull request Jul 18, 2026
* feat(selection): Select by Expression and Select by Location (#1314)

Interactive feature selection, QGIS style: two new dialogs turn expression
matches or spatial-predicate matches into the live selection (highlighted on
the map and in the attribute table), with four combine modes (new/add/remove/
intersect) plus selection actions (zoom to, invert, clear, export as layer)
in the Edit menu and the vector layer context menu.

- core: selection helpers (featureSelectionId, applySelectionMode,
  invertSelection) and matchFeaturesByExpression (compile-once batch
  boolean evaluation with per-feature runtime-error counting)
- processing: extract matchFeaturesByLocation from the Select by location
  tool and share it with the new dialog; export MAX_CLIENT_PAIRS and the
  predicate types
- app: SelectByExpressionDialog (reuses the shared Expression Builder),
  SelectByLocationDialog, selection actions lib, Edit menu + layer context
  menu entries with ui-profile toggles, i18n strings
- StylePanel: extract getAttributePropertyNames/standardExpressionVariables
  into lib/expression-inputs so all expression entry points share them
- tests: tests/feature-selection.test.ts (21 cases)

Verified end-to-end with Playwright against real data: 9/1000 US cities
match population > 1M; cities vs states intersects/disjoint = 992/8
(matches shapely ground truth); modes, invert, clear, export all correct.

* Address review feedback

- Gate "Select by Location..." on two selectable layers in both the Edit
  menu and the layer context menu, so it can't open straight into the
  "needs two vector layers" dead end (Claude review)
- Move applyMatchedSelection into lib/selection-actions.ts and add four
  store-integration tests covering the layer-id guard and the
  selectLayer-before-selectFeatures ordering (Claude review)
- Compute the MAX_CLIENT_PAIRS check from geometry-bearing features only,
  matching the Select by location processing tool's accounting
  (Claude review)
- Deduplicate remove/intersect results in applySelectionMode to honor the
  documented dedup guarantee, with tests (CodeRabbit)
- Reset the selection dialogs' open state and preselected layer ids in
  newProject/loadProject so a stale dialog can't survive a project switch
  (CodeRabbit)
- Add a directional "contains" predicate test (CodeRabbit)

* Address Claude review feedback

- Force mode "new" (and disable add/remove/intersect in the mode dropdown)
  when the dialog's target layer does not hold the live selection, so
  remove/intersect can no longer silently produce an always-empty
  selection across layers; document the cross-layer fallback accurately
  in applyMatchedSelection's docstring

* feat(selection): render Select by Expression/Location as floating panels

Replace the Radix modals with draggable, non-modal floating windows
(mirroring RecordVideoDialog's pattern, as the Whitebox toolbox did in
#1226) so the map and attribute table stay interactive while selecting:
run a selection, pan to inspect the highlight, refine, and re-run without
reopening. Both panels can be open at once.

Since the camera can now move while the panel is open, Select by
Expression re-snapshots ["zoom"] and the @map_* variables at each run
instead of at open (the modal Expression Builder keeps its open-time
snapshot for stable prop identities). The redundant footer Close button
is dropped in favor of the title-bar X.

* feat(selection): anchor the floating panels to the map canvas top-left

Mount the selection panels inside the map area (next to the plugin
FloatingPanels overlay) and switch the shell from viewport-fixed to an
absolute inset-0 pass-through layer, so the panels open in the map
canvas's top-left corner and drag-clamp to the map area instead of the
whole window — they can no longer cover the sidebars.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants