Skip to content

fix(frontend): separate Upcoming Scheduled Purchases actions from the savings figure - #1868

Merged
cristim merged 1 commit into
mainfrom
fix/1776-upcoming-purchases-button-overlap
Aug 19, 2026
Merged

cristim merged 1 commit into
mainfrom
fix/1776-upcoming-purchases-button-overlap

Conversation

@cristim

@cristim cristim commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes #1776.

What the measurements actually show

Reproduced in Chromium against the production webpack bundle, sweeping 1600px down to 320px in 20px
steps, with fixture rows covering a 60-char plan name, the $0 figure from the report,
$1,234,567.89, and a short baseline row.

Width savings block -> actions block gap View Details -> Cancel gap
1600 179px (long row) / 321px 0px
1280 19px 0px
1024 0px, buttons stacked wrapped to a second line
960 0px (long row), 1.2px wrapped
900 0px wrapped
820 0px wrapped
800 0px (long row), 5.2px wrapped
780 0px on two of three rows wrapped
<=768 column layout, 16px vertical 0px

There are two defects behind one report, and they have different reach. The savings/actions
collision is width- and content-dependent: it appears from roughly 780px to 1024px with a realistic
plan name, and never on rows with short names. The button/button collision is content-independent
and present at every width including 1600px. The issue says "at all widths", which is true only
of the second one, so a fix aimed at the sentence would have addressed one and left the other.

Correction to the report's stated mechanism

There is no z-order overlap anywhere. Overlapping area is 0 at every width and every fixture
row. Verified per text node with Range.getClientRects() (text never escapes its own box), and
cross-checked that no global word-break / overflow-wrap rule defeats flexbox's min-width: auto
protection.

What users see is zero separation: bold green text sharing a pixel boundary with a solid-
background button reads as an overlap. Flagging this explicitly because a reviewer comparing the
issue's "render on top of" against a diff that adds gaps would otherwise reasonably think the wrong
thing was fixed. The spec still asserts overlap area is zero in addition to the minimum gap, so a
genuine overlap would also be caught.

Root cause

All three in frontend/src/styles/plans.css:

  1. .upcoming-card is display: flex; justify-content: space-between with no gap, so once
    the three blocks fill the row the distributed free space reaches zero and they abut.
  2. .upcoming-savings had no shrink guard, so it was squeezed until "Est. monthly savings" wrapped
    to two lines, extending the block right to the button edge.
  3. .upcoming-actions had no CSS rule at all, despite dashboard.ts:497 setting the class. Its
    buttons were bare inline-blocks with no separator, touching at every width, and the block shrank
    freely and wrapped the buttons into a two-row stack that pushed into the savings text.

The fix

Three declarations, no markup or TypeScript change:

.upcoming-card    { gap: var(--cudly-sp-4); }
.upcoming-savings { flex-shrink: 0; }
.upcoming-actions { display: flex; gap: var(--cudly-sp-2); }

gap fixes the collapse at its source rather than special-casing a width. flex-shrink: 0 on the
savings block makes the info block, which holds wrappable prose, absorb the squeeze instead.
display: flex is required for gap to apply to the button block at all. Sizes come from existing
spacing tokens.

Ablation, so nothing dead ships. Each declaration removed in turn, rebuilt, spec re-run:

Removed Specs failing
.upcoming-card { gap } 1
.upcoming-savings { flex-shrink: 0 } 1
.upcoming-actions { display: flex; gap } 1
.upcoming-actions { flex-shrink: 0 } (a 4th I had written) 0

The fourth changed nothing even under a dense sweep (1600px to 380px in 20px steps): with the
savings block pinned, the info block absorbs all shrink across the entire range where the card is a
flex row. It guarded an unreachable state, so it was removed before committing.

The @media (max-width: 768px) rule that sets gap: 1rem on .upcoming-card is untouched. It is
now redundant with the new default (--cudly-sp-4 is 1rem) but remains correct, and removing it
would be a drive-by change.

Regression spec

frontend/tests-e2e/upcoming-purchases-layout.spec.ts, following the #1864 precedent: real bundle
in Chromium, measuring getBoundingClientRect() geometry rather than class names or DOM structure,
because jsdom does not resolve stylesheets into layout and the jest suite stays green while the
collision is visible. Each card is measured in a single evaluate so all boxes come from one layout
pass; measuring locators one at a time produced a false failure when the Home charts reflowed
mid-assertion.

Reverting only plans.css to the parent commit, with the committed spec unchanged:

✓  every fixture row renders both action buttons
✘  the action buttons never overlap or touch the savings figure
     ... @ 1024px: savings/actions separation   Expected: >= 8   Received: 0
✘  the action buttons stay on one row, spaced apart from each other
     ... @ 1600px: gap between buttons          Expected: >= 8   Received: 0
✘  the savings label keeps one line without overflowing the card
     ... @ 1024px: savings label line count     Expected: 1      Received: 2
3 failed, 1 passed

Post-fix: 4 passed.

The one spec that passes in both states is a deliberate non-vacuity guard, not a weak test. It
asserts the fixture actually renders two buttons per row, so a fixture regression that dropped the
Cancel buttons could not make the spacing assertions pass trivially.

A vacuous assertion found in this spec and fixed

The label line-count assertion originally used Element.getClientRects().length. That returns a
single rect for a block box regardless of how many lines the text wraps to, so it passed on the
pre-fix stylesheet and proved nothing. It now uses a Range over the label's contents and fails
pre-fix as shown above.

Boundary cases

Long savings figure ($1,234,567.89), zero figure ($0, the value in the report), 60-char plan
name, 320px (column layout, 16px vertical separation, no horizontal card overflow), 1600px, and the
768px breakpoint with 780px either side of it. Mobile touch targets are unaffected: the 44x44
minimum comes from @media (pointer: coarse) on the bare button selector in responsive.css,
which these buttons already match, and nothing here alters button box sizing.

Verification

Run from frontend/: npm run build exit 0; npm test exit 0 (90 suites, 2858 passed, 1 skipped);
npm run lint exit 0, 0 errors; npm run typecheck exit 0; full Playwright suite 36 passed, 0
failed. .github/workflows/frontend-e2e.yml runs npm run test:e2e, so the new spec gates on CI.

Not verified

  • docs/ui-ux-review.md, cited as the source in the issue, is not in the repo at any commit, so
    there is no screenshot or viewport to match against. The 780-1024px band is derived from this
    fixture's plan-name length and shifts with different content; the button/button 0px gap does not.
  • The single-button row variant. canCancelUpcomingPurchase() hides Cancel for rows the session did
    not create, but the e2e fixture session is an admin whose admin:* short-circuits the gate.
    Producing that row needs overriding both /api/auth/me/permissions and the group list on
    /api/auth/me. A one-button action block is strictly narrower than a two-button one and so cannot
    reintroduce the collision, so all three fixture rows render two buttons.
  • Chromium only, by the existing config's design (test(recommendations): end-to-end browser smoke for PR #160 (column filters + bottom action box) #167).

… savings figure

`.upcoming-card` is a flex row laid out with `justify-content: space-between`
and no `gap`, and `.upcoming-actions` had no CSS rule at all. Once a realistic
plan name fills the row the space-between free space reaches zero: the savings
block ends up pixel-adjacent to the button block, "Est. monthly savings" wraps
to two lines, and the two buttons stack. Measured in Chromium, the gap between
the savings block and the buttons is 0px from 780px to 1024px, and the gap
between View Details and Cancel is 0px at every width including 1600px. Text
flush against a solid-background button is what the report describes as the
buttons rendering on top of the figure.

Give the card a `gap`, pin the savings block at its natural width so a long
plan name squeezes the info block instead of the figure, and lay the action
block out as a flex row with its own gap.

The regression spec measures rendered bounding boxes in Chromium rather than
DOM structure, because jsdom does not resolve stylesheets into layout and the
jest suite stays green while the collision is visible. It fails on the
pre-fix stylesheet at 1024px (separation, label wrap) and 1600px (button gap).

Closes #1776.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 48 minutes

Limit details: You’ve used the included review currently available. Your 73 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

You’re in a promotional period — use the checkbox below to run this review for free:

  • Run review for free

On-demand reviews are free for the next 30 days. After that, they cost $0.25 per reviewed file.

How can I continue?

Run this review now using the option above, or comment @coderabbitai review --use-credits.

You can also wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5c4bba2-2a32-43c3-9630-749e05465ae1

📥 Commits

Reviewing files that changed from the base of the PR and between fffd2ea and e52d605.

📒 Files selected for processing (2)
  • frontend/src/styles/plans.css
  • frontend/tests-e2e/upcoming-purchases-layout.spec.ts

Comment @coderabbitai help to get the list of available commands.

@cristim cristim added priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/all-users Affects every user effort/xs Trivial / one-liner type/bug Defect triaged Item has been triaged labels Aug 19, 2026
@cristim

cristim commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

The push at 19:30Z hit the review limit before this PR was opened, so this head has never been reviewed. The stated countdown has elapsed. Requesting explicitly because the auto-review on push was consumed by the throttle rather than deferred.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

@cristim A full review is requested because the throttled automatic review did not inspect this PR head.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-08-06T08:28:24.968Z
Learning: In the LeanerCloud/CUDly repository, request CodeRabbit reviews at most once per hour across the repository because the adaptive quota is shared across open pull requests. When a PR head was pushed while automatic review was quota-exhausted and was not retried, use a full review rather than an incremental review so the missed commits are reviewed.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-08-05T05:27:02.254Z
Learning: For the LeanerCloud/CUDly repository, pace CodeRabbit review requests at one request per hour across the repository. The review quota is adaptive and shared per developer and organization; burst requests can exhaust the quota and tighten the limit. When a PR head was pushed while quota was exhausted, request a full review because an incremental review skips the unreviewed commits.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-26T17:23:32.317Z
Learning: In this repository, if a CodeRabbit review was throttled or hit a rate limit, the correct recovery is to request `coderabbitai full review` rather than `coderabbitai review`, because incremental review can silently skip the affected in-flight commit and report a false-clean result.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-26T17:21:01.385Z
Learning: For the LeanerCloud/CUDly repository review workflow, when a previous CodeRabbit review pass was skipped or failed to produce findings due to a rate-limit event, use a full review request on the pull request rather than the incremental review form.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-08-04T08:23:48.546Z
Learning: For the LeanerCloud/CUDly repository, CodeRabbit review quota is shared per developer across the organization and adapts to recent review volume. Space review requests to approximately one per hour. When an automatic review did not run because quota was exhausted, use a full review instead of an incremental review so CodeRabbit inspects the missed commits.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-08-04T22:26:24.782Z
Learning: For pull request `#1716`, when review quota exhaustion prevents automatic review of pushed commits, request a full review instead of an incremental review. An incremental review can omit those unreviewed commits.
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your current included review allowance is based on your included PR review attempts over the past 7 days. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 48 minutes.

@cristim
cristim merged commit edb353c into main Aug 19, 2026
26 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xs Trivial / one-liner impact/all-users Affects every user priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(frontend): Home "Upcoming Scheduled Purchases" action buttons overlap the estimated-savings figure

1 participant