Mobile follow-ups to #7518: tool-list search, and drop the empty overflow menu (#7660)

# Description of Changes

Follow-up to #7518, picking up two mobile rough edges found while going
over that branch. Two changes, one commit each.

## 1. Tool search back in the tool list (mobile)

Tool search lives in the workbench bar's super search, which on mobile
sits on the Workspace slide. So searching for a tool meant swiping off
the tool list, typing, then swiping back. This puts a filter at the head
of the tool panel on mobile. Reuses the existing `ToolSearch` component
in `mode="filter"`, the same one the desktop fullscreen picker uses.
Drives `setSearchQuery` on `ToolWorkflowContext`, so the query,
filtering and grouped results are all existing paths. `ToolPanel` takes
a new `showSearch` prop; `RightSidebar` passes `showSearch={isMobile}`.
Desktop renders exactly as before.

**To test:**

- Open the editor at a phone-width viewport (under 1024px).
- A "Search tools..." field should sit above Favourites / Recommended in
the Tools pane.
- Typing filters into grouped results. Clearing goes back to the compact
list.
- It hides once a tool is open, and comes back on the way out.
- On desktop the field should not appear at all.

## 2. The mobile overflow menu opened with nothing in it

`WorkbenchBarMobileActions` rendered its kebab trigger unconditionally.
But every item inside is gated on `currentView === "viewer"` or
`!isCustomView`. In a `custom:*` workbench both are false, so the
dropdown was empty. `WorkbenchBarDesktopActions` renders nothing in that
case, so this only showed on phones. Now returns `null` when neither
group applies, with the two conditions named so the trigger and the
items can't drift apart again.

**To test:**

- Phone-width viewport, load a PDF.
- Open a tool with its own workbench view: Compare, Get Info report,
Show JS, Validate Signature, Edit Table of Contents, or PDF Text Editor.
- The kebab at the right of the workbench bar should be gone entirely,
rather than opening an empty menu.
- Back in the viewer or page editor it should still be there, with Print
/ Download / Save As / Close.
This commit is contained in:
EthanHealy01
2026-08-23 22:07:35 +00:00
committed by GitHub
parent 5f0fe06bbc
commit 1df372764f
4 changed files with 53 additions and 6 deletions
@@ -25,6 +25,13 @@ export default function WorkbenchBarMobileActions({
}: WorkbenchBarActionsProps) {
const { t } = useTranslation();
const exportDisabled = actionsDisabled || policyEnforcing;
const showPrint = currentView === "viewer";
const showFileActions = !isCustomView;
// Custom workbench views own their content, so none of these apply. The
// desktop cluster renders nothing at all in that case; without this the
// trigger would still be there, opening an empty dropdown.
if (!showPrint && !showFileActions) return null;
return (
<Menu shadow="md" width={230} position="bottom-end">
@@ -39,7 +46,7 @@ export default function WorkbenchBarMobileActions({
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{currentView === "viewer" && (
{showPrint && (
<Menu.Item
leftSection={<PrintIcon sx={{ fontSize: "1.1rem" }} />}
disabled={exportDisabled}
@@ -48,7 +55,7 @@ export default function WorkbenchBarMobileActions({
{t("workbenchBar.print", "Print PDF")}
</Menu.Item>
)}
{!isCustomView && (
{showFileActions && (
<Menu.Item
leftSection={
<LocalIcon
@@ -63,7 +70,7 @@ export default function WorkbenchBarMobileActions({
{downloadLabel}
</Menu.Item>
)}
{!isCustomView && saveAsIconName && (
{showFileActions && saveAsIconName && (
<Menu.Item
leftSection={
<LocalIcon icon={saveAsIconName} width="1.1rem" height="1.1rem" />
@@ -74,7 +81,7 @@ export default function WorkbenchBarMobileActions({
{t("workbenchBar.saveAs", "Save As")}
</Menu.Item>
)}
{!isCustomView && (
{showFileActions && (
<>
<Menu.Divider />
<Menu.Item
@@ -282,6 +282,9 @@ export default function RightSidebar() {
onShowAllTools={handleShowAllTools}
onToolSelect={handleToolSelectWithTransition}
compact={false}
/* Mobile keeps the workbench bar - and with it the super search -
on the other slide, so the list needs its own filter. */
showSearch={isMobile}
/>
</>
</div>
@@ -183,6 +183,17 @@
}
}
/* In-panel tool filter. Aligned with .tool-picker__compact's inline padding so
the field lines up with the tool rows underneath it. */
.tool-panel__search {
flex-shrink: 0;
padding: 0.5rem var(--mantine-spacing-sm) 0;
}
.tool-panel__search .search-input-container {
margin: 0;
}
.tool-panel__compact-header-actions {
display: flex;
align-items: center;
@@ -4,6 +4,7 @@ import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import ToolPicker from "@app/components/tools/ToolPicker";
import SearchResults from "@app/components/tools/SearchResults";
import ToolRenderer from "@app/components/tools/ToolRenderer";
import ToolSearch from "@app/components/tools/toolPicker/ToolSearch";
import { ToolPanelViewerBar } from "@app/components/tools/ToolPanelViewerBar";
import { ToolId } from "@app/types/toolId";
@@ -20,6 +21,11 @@ interface ToolPanelProps {
onToolSelect?: (id: ToolId) => void;
/** Whether to render the compact (favourites + recommended only) view. */
compact?: boolean;
/**
* Render a tool filter at the head of the panel. Set where the workbench
* bar's super search is out of reach, so the list stays searchable in place.
*/
showSearch?: boolean;
}
/** Tool list and renderer for the right rail; rail chrome lives in RightSidebar. */
@@ -28,24 +34,44 @@ export default function ToolPanel({
onShowAllTools,
onToolSelect,
compact: compactProp,
showSearch = false,
}: ToolPanelProps) {
const { t } = useTranslation();
const {
leftPanelView,
searchQuery,
setSearchQuery,
filteredTools,
toolRegistry,
selectedToolKey,
handleToolSelect,
setPreviewFile,
} = useToolWorkflow();
const selectTool = onToolSelect ?? handleToolSelect;
// Only offer the filter over the list itself; once a tool is open the panel
// belongs to that tool. Deriving the results branch from the same flag keeps
// the input and what it filters from drifting apart.
const panelSearch = showSearch && leftPanelView === "toolPicker";
const searching = searchQuery.trim().length > 0;
return (
<>
{/* Viewer mode tools — annotate, redact, form fill */}
<ToolPanelViewerBar />
{allToolsView && searchQuery.trim().length > 0 ? (
{panelSearch && (
<div className="tool-panel__search">
<ToolSearch
value={searchQuery}
onChange={setSearchQuery}
toolRegistry={toolRegistry}
mode="filter"
/>
</div>
)}
{searching && (allToolsView || panelSearch) ? (
<div className="flex-1 flex flex-col overflow-y-auto">
<SearchResults
filteredTools={filteredTools}
@@ -59,7 +85,7 @@ export default function ToolPanel({
selectedToolKey={selectedToolKey}
onSelect={(id) => selectTool(id as ToolId)}
filteredTools={filteredTools}
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
isSearching={searching}
compact={compactProp ?? !allToolsView}
onShowAllTools={onShowAllTools}
/>