fix(ui): keep mobile unread dots from indenting task rows (#10691)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The Inbox helps operators scan task state on desktop and mobile screens > - Read and unread rows must keep the same title and status alignment > - The mobile unread control was a flex item, so unread rows gained an extra leading column > - Moving the control out of flex flow fixes the indent, but the tap target must stay inside clipped row containers > - This pull request reserves one mobile gutter for both states and overlays the unread control inside that gutter > - The benefit is stable mobile alignment without clipping the control or blocking the parent-row chevron ## Linked Issues or Issue Description Related merged work: Refs #9383, Refs #9685, and Refs #9767. **What happened?** On mobile screens, an unread Inbox row placed the mark-as-read control in the flex layout. The extra flex item moved the status and title to the right. A first overlay position could also place part of the control outside a row that clips overflow. **Expected behavior** Read and unread rows must use the same title and status positions. The unread control must stay tappable inside the row. A parent-row chevron must remain independently usable. **Steps to reproduce** 1. Open the Inbox on a mobile viewport. 2. Compare read and unread rows with the same nesting depth. 3. Include an unread parent row with a collapse chevron. 4. Observe that the unread row content starts farther right than the read row content. **Paperclip version or commit** The issue reproduced on the `master` parent of this pull request. **Deployment mode** Local dev (`pnpm dev`). **Installation method** Built from source. ## What Changed - Reserve a mobile leading gutter whenever an Inbox row participates in unread state. - Position the mark-as-read control absolutely inside that gutter so it does not add a flex column. - Keep read and unread rows on the same mobile padding path. - Keep the control inside overflow-clipping row containers and separate from the parent-row chevron. - Update the `IssueRow` regression test to verify absolute placement, the internal gutter, and the absence of `order-first` layout. ## Verification - `pnpm exec vitest run ui/src/components/IssueRow.test.tsx` — 16 tests passed. - `pnpm check:token-gates` — all token gates passed across 722 files. - `pnpm -r typecheck` — passed. - `pnpm build` — passed. - `pnpm test:run` — the general server shard passed 3,448 tests and the UI shard passed 3,362 tests. The CLI shard had one host-environment failure because inherited temporary AWS access-key variables changed an AWS doctor assertion from `pass` to `warn`. - `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN pnpm exec vitest run cli/src/__tests__/secrets.test.ts` — the affected file passed 8 tests in an isolated environment. - Mobile QA at 402 × 874 confirmed equal read/unread title positions, no shift after marking a row as read, and an independently usable parent chevron. ## Risks - Low risk. The change affects only `IssueRow` mobile presentation and its focused regression test. - The main risk is breakpoint-specific placement. The component test covers the responsive classes, and mobile QA covers the rendered interaction. - No API, schema, dependency, telemetry, or documentation contract changes. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex based on GPT-5, with reasoning, repository tools, command execution, and test execution. The runtime did not expose the exact deployment ID or context-window size. ## 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 (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] 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:
parent
2c90cf0f2c
commit
d5045d622c
|
|
@ -218,7 +218,7 @@ describe("IssueRow", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("puts the unread dot in the reserved far-left slot on desktop and in flow on mobile", () => {
|
||||
it("puts the unread dot in the reserved desktop slot and overlays it on mobile", () => {
|
||||
const root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(<IssueRow issue={createIssue()} unreadState="visible" />);
|
||||
|
|
@ -230,12 +230,16 @@ describe("IssueRow", () => {
|
|||
expect(slot).not.toBeNull();
|
||||
expect(slot?.querySelector('button[aria-label="Mark as read"]')).not.toBeNull();
|
||||
|
||||
// Mobile: a separate in-flow, order-first dot (mobile has no reserved slot).
|
||||
// Mobile: all inbox rows reserve a gutter and the dot stays fully inside
|
||||
// it, so the control cannot be clipped or indent the status/title.
|
||||
const mobileDot = container
|
||||
.querySelector('button[aria-label="Mark as read"].sm\\:hidden, span.sm\\:hidden button[aria-label="Mark as read"]')
|
||||
?.closest("span.sm\\:hidden");
|
||||
expect(mobileDot).not.toBeNull();
|
||||
expect(mobileDot?.className).toContain("order-first");
|
||||
expect(mobileDot?.className).toContain("absolute");
|
||||
expect(mobileDot?.className).toContain("left-0");
|
||||
expect(mobileDot?.className).not.toContain("order-first");
|
||||
expect(container.firstElementChild?.className).toContain("pl-4");
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
|
|
|
|||
|
|
@ -168,7 +168,8 @@ export function IssueRow({
|
|||
// No color transition on the row band: hover/selection must snap
|
||||
// instantly. A fade (transition-colors) leaves a trail of fading bands
|
||||
// when scrubbing the mouse fast across the list.
|
||||
"group relative flex items-start gap-2 rounded-lg py-2.5 pl-2 pr-3 text-sm no-underline text-inherit sm:items-center sm:py-2 sm:pl-1",
|
||||
"group relative flex items-start gap-2 rounded-lg py-2.5 pr-3 text-sm no-underline text-inherit sm:items-center sm:py-2 sm:pl-1",
|
||||
showUnreadSlot ? "pl-4" : "pl-2",
|
||||
"[&_button]:relative [&_button]:z-10",
|
||||
// Divider + hover/selected/checklist wash live on the ROOT row band so
|
||||
// the tint paints BEHIND the content and `last:border-b-0` matches the
|
||||
|
|
@ -323,10 +324,10 @@ export function IssueRow({
|
|||
</span>
|
||||
) : null}
|
||||
{showUnreadDot ? (
|
||||
// Mobile keeps the dot in flow as the leading item (mobile has no
|
||||
// reserved desktop dot gutter). Desktop renders the dot in the reserved
|
||||
// leading slot above instead, so this is mobile-only.
|
||||
<span className="order-first inline-flex h-4 w-4 shrink-0 items-center justify-center self-center sm:hidden">
|
||||
// Inbox rows reserve a mobile gutter on both read and unread rows. The
|
||||
// full control stays inside overflow-clipping row containers while its
|
||||
// absolute position avoids shifting or covering the leading control.
|
||||
<span className="absolute left-0 top-1/2 inline-flex h-4 w-4 -translate-y-1/2 items-center justify-center sm:hidden">
|
||||
{unreadDotButton}
|
||||
</span>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Reference in New Issue