fix(ui): use large mobile selector modals (#14250)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The new task dialog lets an operator assign work on a phone.
> - The mobile picker used fixed coordinates inside the Radix wrapper.
> - iOS Safari already makes fixed coordinates relative to the visual
viewport.
> - The old code added the visual viewport offset a second time.
> - This pull request removes the second offset and gives the mobile
wrapper full viewport geometry.
> - The benefit is a visible picker and search field when the iOS
keyboard opens.

## Linked Issues or Issue Description

**What happened?**

The assignee and project pickers could move outside the visible screen
in the new task dialog on iOS.

**What did you expect to happen?**

The picker and its search field must stay visible above the bottom edge
and the software keyboard.

**Steps to reproduce**

Open the new task dialog on an iPhone. Select Assignee or Project. The
picker can move outside the visual viewport when Safari pans the page.

**Paperclip version or commit**

The problem exists after the change in PR #13343.

**Deployment mode**

The problem affects the board UI in local and authenticated modes.

Refs #13343

## What Changed

- Use visual viewport local coordinates for the new task dialog.
- Give the mobile Radix popper wrapper full viewport geometry.
- Render both shared selector components as large, titled mobile modals.
- Keep desktop selectors as anchored popovers.
- Cover the new-task assignee, new-task project, composer assignee, and
generic searchable selector in Storybook.
- Add iPhone-sized Storybook states for the assignee and project
pickers.
- Update viewport tests for the corrected coordinate model.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/components/NewIssueDialog.test.tsx
src/components/InlineEntitySelector.test.tsx`
- `pnpm check:token-gates`
- Playwright used the iPhone 14 device profile. All mobile modals were
inside the 390 by 664 CSS viewport.
- Each mobile modal was `x=16, y=16, width=358, height=632`.
- A desktop check kept the anchored popover at `width=320, height=211`.

## Risks

- Low risk. The CSS only changes narrow mobile viewports.
- The full-screen popper wrapper does not receive pointer events. The
picker still receives pointer events.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. See `CONTRIBUTING.md`.

## Model Used

- OpenAI GPT-5.6, Codex agent with tool use and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal task
ID
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant Storybook documentation to reflect my
changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-09-27 11:33:24 +00:00
1 parent 74508357fa
commit d423fc4411
10 files changed
+226 -15

No files matched your search

+36 -7
View File
@@ -231,6 +231,7 @@
--new-issue-dialog-top-gap: max(1rem, env(safe-area-inset-top));
--new-issue-dialog-bottom-gap: max(1rem, env(safe-area-inset-bottom));
--mobile-entity-picker-visual-viewport-height: 100dvh;
--mobile-entity-picker-backdrop: color-mix(in oklab, var(--foreground) 50%, transparent);
/* ────────────────────────────────────────────────────────────────────────
Motion tokens — chat-style task thread (default; classic legacy view sits
@@ -3009,32 +3010,60 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] {
[data-radix-popper-content-wrapper]:has(> [data-mobile-entity-picker]) {
inset: 0 !important;
width: 100% !important;
height: 100% !important;
height: var(--mobile-entity-picker-visual-viewport-height) !important;
min-width: 0 !important;
transform: none !important;
will-change: auto !important;
pointer-events: none;
}
[data-radix-popper-content-wrapper]:has(> [data-mobile-entity-picker])::before {
content: "";
position: absolute;
inset: 0;
background: var(--mobile-entity-picker-backdrop);
pointer-events: none;
}
[data-mobile-entity-picker] {
position: fixed !important;
inset: auto calc(var(--spacing) * 4) calc(var(--spacing) * 4) !important;
inset:
max(calc(var(--spacing) * 4), env(safe-area-inset-top))
calc(var(--spacing) * 4)
auto !important;
width: auto !important;
max-width: none !important;
max-height: calc(var(--mobile-entity-picker-visual-viewport-height) - var(--spacing) * 8) !important;
height: calc(
var(--mobile-entity-picker-visual-viewport-height)
- max(calc(var(--spacing) * 4), env(safe-area-inset-top))
- max(calc(var(--spacing) * 4), env(safe-area-inset-bottom))
) !important;
max-height: none !important;
transform: none !important;
translate: none !important;
transform-origin: bottom center !important;
animation: none !important;
pointer-events: auto;
display: flex;
flex-direction: column;
padding: 0 !important;
overflow: hidden;
}
[data-mobile-entity-picker-header] {
display: flex;
}
[data-mobile-entity-picker] > [data-slot="command"] {
min-height: 0;
flex: 1;
}
[data-mobile-entity-picker] [data-slot="command-list"],
[data-mobile-entity-picker-list] {
max-height: min(
var(--sz-300px),
calc(var(--mobile-entity-picker-visual-viewport-height) - var(--spacing) * 24)
) !important;
max-height: none !important;
min-height: 0;
flex: 1;
}
}
:root {