Repository navigation
feat(ui): workbench polish: desktop file drops, compare waiting state, reference image rows, Upscale prompt tools, compact dropdowns - #10136
joshistoast wants to merge 34 commits into
Conversation
Field gains labelPlacement="inside"; Select, Combobox and ModelSelect render the label at the start of their box. Generate and canvas panels use inside labels for their dropdowns. Quick settings show selects as a labelled row with a value-width select.
Queue offers its job scope, Workflow its snap, minimap and connection style, and Image Map its cluster options.
The board list marks the destination board with the Saving to icon; the docs describe it. The boards toggle icon follows the gallery body's measured layout instead of its region. Picker board labels keep the list's text size, and the search field gets space below it.
SegmentTabs gains isScrollable: wheel scrolling, edge fades and the selected tab kept in view. Scroll-edge measurement moves from the shell's overflow edges to a shared platform module. The Queue widget's filter strip and lists share the gallery rail's inset.
Menus, popovers and hover cards draw an arrow kept clear of the content's rounded corners. Context menus anchored to the pointer and submenus show none; the layers keyboard menu anchors to a point.
The menu renders outside the item branches, so its leftover target no longer reopens it on the next item. Focus returns to the opener when it survives the deletion, otherwise to Preview, via a new returnFocus prop.
The dialog lists projects with the virtualized List, dims incompatible ones and recovers from load errors. List gains defaultFocusKey and ListItem isDisabled; the project switcher lazy-loads the dialog.
The chip resolves one literal translation key per state instead of a missing interpolated key. Adds the loadingModel and loadingModelUnnamed strings, the latter for loads without a model name.
In Fit mode the tile border, selection and focus ring wrap the image's own rect inside the square cell; hover tints the cell and badges stay on its corners. Generate, canvas, upscale and video progress tiles use the job's target ratio; ratios clamp to 1:3–3:1. Clicks beside the image select and focus the tile, and mouse drags still start from anywhere in the cell.
lstein
left a comment
There was a problem hiding this comment.
Reviewed at 7a09ad9. The gallery, performance baselines, Open project dialog, quick-settings cogs, queue-status strings and the converted fields all held up. I found four low-severity issues, three of them confirmed in the browser, plus one focus issue I could not reproduce.
Confirmed in the browser
1. Hint cards on inside-label dropdowns open the dropdown.
FieldInsideLabel (src/platform/ui/Field.tsx:105) renders the FeatureHint inside the Select/ModelSelect trigger <button>. The hint card is portalled, but React synthetic events still bubble through the portal to the trigger. Clicking "Don't show me these" or "Learn more" in the card also opens the dropdown. Pressing Enter on them is caught by the trigger's keydown handler, so the dropdown opens and the link or button does nothing. This affects VAE, VAE precision, PID mode, encoder quantization, DyPE preset and Scheduler. With the outside label the bubble stopped at a plain <label>, so this is new.
Suggested fix: stop click/keydown/pointerdown from the hint card's content reaching the trigger, and add a test that interacts with the card.
2. Clicking a popover or menu arrow tip dismisses it.
arrowClearOfCorners (src/platform/ui/theme/recipes.ts:51) sets pointerEvents: 'none' on the arrow box. The hover-card arrowTip restores auto (:133); the popover (:159) and menu (:648) arrowTip recipes don't, so the tip inherits none. A click on the visible tip falls through, Zag treats it as an outside interaction, and the popover closes (or toggles, if it lands on the trigger). Before this PR the tip was part of the content's hit area.
Suggested fix: pointerEvents: 'auto' on the popover and menu arrowTip; the clip-path already limits the hit area to the visible half.
Separately, the tip is very hard to see in the dark theme: its fill matches the popover background and only a 1px outline shows.
3. Clicking the Scheduler's inside label while its list is open closes and reopens it, and the typed filter is lost.
The inside <label> (src/platform/ui/Combobox.tsx:231) sits inside Combobox.Control, but Zag's dismiss layer only excludes the input, trigger and clear trigger. Pointerdown on the label counts as outside: handleOpenChange(false) clears the query, then the label activates the input and openOnClick reopens an unfiltered list. To reproduce, open Scheduler, type dpm, click the word "Scheduler". The existing test only clicks the label while the list is closed.
Suggested fix: preventDefault the label's pointerdown while open, or add the label to the dismiss layer's exclusions.
From code reading, not verified
4. Hovering an invalid Combobox with an inside label tints the input part twice.
INSIDE_INPUT_CSS sets bg: 'transparent' on the input, but that doesn't cancel the recipe's nested _hover._invalid background from formControlInteraction (combobox.input in recipes.ts). The Control also gets bg.error/60 on hover, so the input region shows two stacked tints and the label region one, with a seam between them. I couldn't produce an error state on Scheduler to check it on screen.
Could not reproduce, may depend on environment
Preview: deleting without confirmation from a filmstrip thumbnail or the header ⋮ may drop focus to <body>.
closeContextMenu (src/workbench/widgets/preview/PreviewWidgetView.tsx:500) resolves the return target through getContextMenuReturnFocus (:485) synchronously when the menu closes. With confirmation off, deleteItems applies the optimistic removal in the same click, but React hasn't committed yet, so opener.isConnected is still true and focus goes to the thumb or ⋮. The commit then removes that element. I tested both paths on Linux (Cinnamon, click-to-focus) and focus stayed in Preview, so something in the real ordering saves it there. Because the code relies on timing rather than resolving the target after the commit, it may still fail in other browsers or under different load. The tests only cover the thumb and ⋮ paths with confirmation on; a {asks: false} variant of those would settle it.
…card Hint cards stop pressed, clicked and keyed events from reaching the field that holds their label. An open Combobox no longer dismisses, and loses its filter, when its inside label is pressed.
The visible tip takes the pointer; a menu ignores outside presses on its own arrow and keeps focus.
…tion The menu's return target is resolved after the removal commits, and a removed filmstrip thumb hands focus to Preview.
Pfannkuchensack
left a comment
There was a problem hiding this comment.
Reviewed at 31bafcd, with lstein's review at 7a09ad9 as the starting point. No blockers. Findings come from reading the code and tests; nothing was run, so "fixed" below rests on the code and the new tests as written.
Status of lstein's points
- Hint card reaching the trigger: fixed.
CARD_EVENT_BOUNDARYinFeatureHint.tsx, with pointer and Enter tests for both dropdown types. - Arrow tip: the click is fixed (
pointerEvents: 'auto'inrecipes.ts:73, plus theMenu.tsxguard). The dark-theme visibility is unchanged:recipes.tshas no colour or contrast change since 7a09ad9. - Combobox inside label while open: fixed in
Combobox.tsxhandlePointerDownOutside. The test asserts the list, filter,aria-expandedand focus survive. - Double tint on invalid hover: fixed through
INSIDE_INPUT_CSS, with a computed-style test. - Preview delete without confirmation: fixed for the tested paths, with
{asks: true/false}variants for stage, thumb and header ⋮. Those tests delete synchronously. Removal is optimistic inuseImageActions, so the real path should match, but that is not confirmed.
Low
PreviewWidgetView.tsx:486-534: a later removal of the header ⋮ has no focus handoff of its own, while thumbs havereleaseFocusOnRemoval. Only a removal landing in the same click is covered.PreviewWidgetView.tsx,closeContextMenu(about:521): ifonCloseis ever reached from an effect,flushSynclogs and does not flush. The possible route is theuseRegisterWidgetOverlaymount effect callingsetOpen(false). Not verified; checking whethercloseWidgetOverlayscan fire while this menu is open would settle it.CARD_EVENT_BOUNDARYstopsonKeyDown,onFocusandonBlurfor all React ancestors of every hint card. It is documented and Zag's capture listeners are unaffected, so this is acceptable. It is worth keeping in mind for any Escape or focus handler above a hint label.
Sound: the lazy Open project dialog, the baseline edits (they match the lazy split), the Select and ModelSelect inside-label wiring, the new en.json keys, and the accessibility script locator. Gallery tiles, SegmentTabs and the queue changes are unchanged since lstein's review, and I did not review them in depth.
Thumbs, the header ⋮ and the video return focus to Preview when removed while focused, so closing the context menu no longer flushes React.
…us regions Moves releaseFocusOnRemoval into the eagerly loaded focusRegions module instead of a new startup source.
Pfannkuchensack
left a comment
There was a problem hiding this comment.
Follow-up at 175dee8, covering the changes since 31bafcd. Both Low focus points from my last review are fixed: the header ⋮ and the video now hand focus to Preview when removed, and flushSync is gone from closeContextMenu. There is one thing to do before this can be judged. Findings come from reading the code and CI logs; nothing was run locally.
This head has no real CI, and the next one will go red without a test fix.
- The PR now conflicts with main in
PreviewActionStrip.tsx, on the import block that #10114 changed. GitHub does not startpull_requestworkflows on a conflicted PR, so175dee8810has only CodeQL and the labeler. ThecloseContextMenuchange is therefore untested at this head. frontend testsfailed onf10b93eb57, which still merged cleanly.PreviewNavigation.browser.test.tsxfailed at import withdoes not provide an export named 'VideoUseGroups'. The test now renders the realPreviewActionStrip, which importsVideoUseGroupsafter the merge with main, and thevi.mock('@workbench/image-actions', …)factory at:467does not list it.175dee8810does not touch that file, so the same failure should return once the import conflict is resolved. AddingVideoUseGroups: () => nullto the mock should fix it.
Please rebase on main and add that, then the whole Preview focus suite gets its first run against these changes, including the {asks: true/false} delete variants that the flushSync removal affects.
The dark-theme arrow visibility from lstein's point 2 is still unchanged, as recipes.ts only moved pointerEvents since 7a09ad9.
Sound, from reading: releaseFocusOnRemoval in focusRegions.tsx resolves its target before the node goes and only acts if focus really fell to the body. Moving the closest lookup to cleanup time is fine, because refs detach before the node is removed. The video ref cleanup keeps the old behaviour of clearing videoRef and publishing false.
# Conflicts: # invokeai/frontend/webv2/src/workbench/widgets/preview/PreviewActionStrip.tsx
Pfannkuchensack
left a comment
There was a problem hiding this comment.
Follow-up at 197ad69. The conflict is resolved, VideoUseGroups is in the mock, and CI is green here, including frontend-webv2-tests, so the Preview focus suite has now run against these changes. Nothing left from my reviews.
The merge resolution in PreviewActionStrip.tsx keeps both sides' imports, and the only other merge-side change is the mock line. The PR's own diff is otherwise unchanged. Findings come from reading the code and CI status; nothing was run locally.
Still open is the dark-theme arrow visibility from lstein's review: recipes.ts has not changed since my last look. I am leaving the decision on that, and on the requested changes, to lstein. As said on 05.11., I did not review the gallery tiles, SegmentTabs or the queue changes in depth, so this is not an approval.
# Conflicts: # invokeai/frontend/webv2/src/features/queue/ui/QueueWidgetView.tsx # invokeai/frontend/webv2/src/workbench/widgets/preview/PreviewWidgetView.tsx
# Conflicts: # invokeai/frontend/webv2/src/features/generation/ui/GenerateComponentsSection.tsx # invokeai/frontend/webv2/src/workbench/image-actions/ImageContextMenu.tsx
|
@Pfannkuchensack I'm adding more commits onto this branch here shortly, but am unable to replicate whatever dark arrow behavior is being reported here. |
lucide-react 1.69 adds the Opacity and Blend icons. The menu patch keeps a submenu's placement when the pointer enters it, so a submenu that opens to the left no longer starts to close on the way in.
… border Menu item icons default to the subtle colour; checks and icon-only items stay bright. Arrow tips no longer show the content border through them. The reset size menu gets shorter labels and loses its double indent.
Status bar chips align their labels and take segment-style fills. The top bar always shows Cancel, opens a folder icon with the project menu, gives layout presets tooltips and puts an issue's Show at the end of its row. Notifications use the list component, widget menus name the panels they fill, and delete dialogs are centred.
Workflows and Canvas share one pair of header history buttons with shortcut tooltips. A narrow Workflows header moves them into its overflow menu.
Opacity is a typed field with a slider behind a chevron, replacing the scrubber. The Overview viewport box is outlined to read on any image. Control weight and begin/end sliders keep their value after a drag.
…view Files dragged in from outside the app upload to the current board, then land on the Canvas drop zone they were released over or open in Preview. A stray drop no longer navigates the page away.
…ble on any background A readout at the top right gives the frame size and the size it renders at. The frame outline is cased dark and light around the accent line. Masks stay hidden while a staged result is toggled off.
Select for compare shows a hint, a pressed toggle and a gallery badge until a second image is picked, and can be cancelled from each or with Esc. The compare toolbar sits in a dock bar like the filmstrip, and differently sized images zoom together.
Upscale runs the prompt it shares with Generate through the selected template and dynamic prompts. Its sections remember whether they were open.
Reference images are list rows with rename, an add row, Undo after removal and issues in a popover by the toggle. Concept rows show the model type under the title with triggers beside it.
Editor route scripts grow about 1%; request counts are unchanged. Startup owner sets follow the new modules.
This comment has been minimized.
This comment has been minimized.
Whether the description or the widest label sets the menu width depends on the font, so the check allows a few pixels instead of none.
Control layers hide with masks and regions under a progress image or staged result, as in the legacy canvas. Painting, moving or transforming a hidden layer shows them for the edit. The Original/Map/Overlay switch is not offered meanwhile.
Layer rows sit 3px from the panel edge instead of 6px.
Summary
A broad UI pass over the workbench: forms, menus, top bar, status bar, Canvas, Preview, Gallery, Generate, Upscale, Layers, Workflows, Queue and Projects.
New
Listwith hover and dividers, rename by double-click, an add row, Undo after removal, and issues in an icon popover by the toggle. Concept rows show the model type under the title with triggers beside it.OpacityandBlendicons.FieldgainslabelPlacement="inside". Select, Combobox and ModelSelect show the label at the start of the box and the value right-aligned. Converted across Generate, Canvas and widget quick settings.List; incompatible projects show dimmed with their reason.Improved
Listwith a row menu.DatabaseZapicon.Fixed
@zag-js/menuthat keeps the submenu's placement on pointer entry.Related Issues / Discussions
None.
QA Instructions
Commands run from
invokeai/frontend/webv2with Node 22.14 and pnpm 10, on the pushed head:lint:code,architecture:check,test(13,254 unit tests),test:fixtures,architecture:inventory,test:performance:architecture, the project-file journey and the browser smoke.test:browser: one failure,ConfirmDialog.browser.test.tsx"reconciles a confirmation closed while its Activity is hidden". It fails the same way locally without these changes.test:accessibility:browserfails locally at the same hidden-dialog step. I have not proven on a clean checkout that this is the same local cause.test:browseralso had one failure inreferenceImageRow.browser.test.tsx("offers Choose model…") in a full run; it passed 3 of 3 when rerun alone.LayerContextMenu.browser.test.tsx; the assertion now allows a few pixels. The macOS Python job also failed there; its log was not available when I looked, and this PR changes no Python.Manual checks against the mock backend (
pnpm dev:mock):Not checked: real generation and staging against a live backend, real OS drags outside the test harness, Firefox and WebKit, the light theme.
Screenshots aren't attached yet.
Review
Material findings resolved:
droplistener unmounted the zones before React'sonDropran, so drops did nothing outside tests. Fixed, with tests that drive real drag events. Files dropped on text fields were also swallowed; fixed.Remaining limits:
Compatibility / Rollout
@zag-js/menu@1.43.3is patched through pnpmpatchedDependencies. A bump of that package needs the patch re-applied or dropped.lucide-reactmoves from 1.61 to 1.69.Checklist
What's Newcopy (if doing a release after this PR)Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.