Skip to content

refactor(ui): open the remaining option menus from PickerSurface - #205

Merged
kipavy merged 2 commits into
devfrom
refactor/picker-surface-unify
Sep 1, 2026
Merged

refactor(ui): open the remaining option menus from PickerSurface#205
kipavy merged 2 commits into
devfrom
refactor/picker-surface-unify

Conversation

@kipavy

@kipavy kipavy commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #203. Four more dropdowns positioned themselves: each measured its
anchor with its own getBoundingClientRect block and painted a
surface-float fixed z-9999 div. Each therefore also missed what PickerSurface
already does — re-measuring while the form panel scrolls, flipping above the
trigger when there is no room below, and becoming a bottom sheet on Android.

keychain/IdentityForm's KeySelector was the furthest gone. Its local
DropdownItem was a copy of PickerOption down to the class list and the accent
check, DropdownDivider a copy of PickerDivider, and its trigger a copy of
PickerTrigger including the 150ms chevron rotation. It also carried a
hand-rolled flip that guessed the menu height from a row count. All of that is
deleted in favour of the shared parts.

KeychainToolbar, VaultPicker and PortInput already used the
canonical rows, so they only lose their positioning code.

PickerSurface grows what those callers needed:

  • width: "content" — a menu that sizes to its rows rather than to a narrow
    trigger. Pinned by right when right-aligned, since there is no width to
    subtract from the anchor's edge.
  • minWidth — the floor for that mode, so VaultPicker keeps its min-w-52 off
    a text-sized trigger.
  • maxHeightPortInput's list keeps its shorter 240px cap.

Left alone deliberately

SyncDropdown and ShareMenu are anchored panels — header row, close button,
fixed w-64/280px, animate-fadeIn with transformOrigin math — not option
lists; routing them through PickerSurface would be a redesign.
SidebarAccountButton compensates for the UI scale with its own
transform: scale(uiScale), which PickerSurface has no handling for.

connections/KeySelector is intentionally still separate from the keychain one:
that one picks between an inline key and the keychain and ends in a "manage in
keychain" action, this one has a third "no key" state and no key-type badges.
Unifying them would take more props than the two compositions cost lines, and
there is now a comment saying so.

Verification

tsc --noEmit clean; 132 tests across shared, connections and keychain
pass (266 including settings).

Driven in the headless build and read back from screenshots:

  • KeychainToolbar — right-aligned, content-width, rows unchanged;
  • IdentityForm's key selector — sits at the bottom of the form panel and now
    flips above the trigger, with the accent check on "No key";
  • VaultPicker — content width at its 208px floor, left-aligned to the small
    text trigger;
  • PortInput — against two emulated ptys, the list opens under the input,
    picking one fills /dev/ttyUSB99 and dismisses the surface.

Four dropdowns positioned themselves. Each measured its anchor with its own
getBoundingClientRect block and painted a `surface-float fixed z-9999` div, so
each also missed what PickerSurface already does: re-measuring while the form
panel scrolls, flipping above the trigger when there is no room below, and
becoming a bottom sheet on Android.

The keychain's KeySelector was the furthest gone. Its local DropdownItem was a
copy of PickerOption down to the class list and the accent check, DropdownDivider
a copy of PickerDivider, and its trigger a copy of PickerTrigger including the
150ms chevron rotation. It also carried a hand-rolled flip that guessed the menu
height from a row count. All of it is deleted in favour of the shared parts.

KeychainToolbar, VaultPicker and PortInput already used the canonical rows, so
they only lose their positioning code.

PickerSurface grows what those callers needed: `width: "content"` for a menu that
sizes to its rows rather than to a narrow trigger (pinned by `right` when
right-aligned, since there is no width to subtract from the anchor's edge),
`minWidth` as the floor for that mode, and `maxHeight` for the port list's
shorter cap.

Left alone deliberately: SyncDropdown and ShareMenu are anchored panels with a
header, a fixed width and an entry animation rather than option lists, and
SidebarAccountButton compensates for the UI scale with its own transform.
`min-w-52` on the vault picker's old surface was 13rem, and rem tracks the UI
scale: at the default scale the root is 15px, so the floor was 195px, not the
208px the port count assumed. A before/after capture put the converted menu 13px
wider than the one it replaced.

`minWidth` now takes any CSS length and the caller passes `13rem`, which restores
the old width exactly and keeps it correct at every UI scale.
@kipavy
kipavy merged commit 9a1a128 into dev Sep 1, 2026
4 checks passed
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.

1 participant