Skip to content

feat: redesign mobile key factor overlay as swipeable pager#4952

Open
aseckin wants to merge 10 commits into
mainfrom
key-factors-mobile
Open

feat: redesign mobile key factor overlay as swipeable pager#4952
aseckin wants to merge 10 commits into
mainfrom
key-factors-mobile

Conversation

@aseckin

@aseckin aseckin commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Closes #4839

What & why

The narrow-screen key factor overlay (tap a key factor on mobile) was poorly designed: navigation wasn't swipeable or obvious, prev/next arrows were small and faint, the overlay rendered behind the navbar (close button could be unreachable), and content didn't fit small viewports.

Changes

Mobile overlay → dvh-bounded horizontal pager (full rewrite)

  • Embla pager: horizontal swipe switches key factors; vertical drag scrolls the comment within a slide. Prominent bottom arrows + dot indicators; edge-to-edge full-bleed slides with peeking neighbors.
  • Sized to 100dvh: shared "KEY FACTORS FOR" + question title + CP header, key factor card, and a comment region filling the remaining height. Reply/upvote/changed-my-mind sit at the end of the comment (appear after scrolling).
  • Per-slide comments from the global comments feed; guards against a setState/re-subscribe loop.

Navbar-overlap fix (app-wide)

  • Added header/modal z-index tokens; lifted shared BaseModal + key factor overlays above the sticky top chrome.

Larger key-factor font

  • New large variant on KeyFactorItem (driver/base-rate/news), matching the comment username size.

i18n: new keyFactorsFor key in all six message files.

Verification

ESLint + tsc clean. Not verified in-browser (local frontend can't reach the backend API here) — worth a manual pass on a real question with multiple key factors at a small mobile viewport.

Summary by CodeRabbit

  • New Features
    • Added “key factors” navigation/CTA text across multiple languages.
    • Introduced an optional larger typography mode for key-factor cards and items.
    • Improved mobile key-factor browsing with carousel navigation, dots, swipe support, and selection-driven updates.
  • Bug Fixes
    • Updated stacking order for sticky headers and key-factor modals to improve overlay display.
  • Style
    • Added active-state highlighting for key-factor cards.
    • Refined key-factor text sizing and enhanced shared modal styling; added related Tailwind animation utilities.

Rebuild the narrow-screen key factor experience as a clean, dvh-bounded
horizontal pager:

- Embla-based pager: horizontal swipe (and prominent bottom arrows + dot
  indicators) switches between key factors; vertical drag scrolls the
  comment within a slide. Slides run edge-to-edge full-bleed.
- Layout fits the dynamic viewport: shared "KEY FACTORS FOR" + question
  title + CP header, key factor card, and a comment region that fills the
  remaining height. Reply/upvote/changed-my-mind sit at the end of the
  comment so they appear only after scrolling.
- Key factor text rendered larger via a new `large` variant on
  KeyFactorItem (driver, base rate, news), matching the comment username.

Also fixes overlays rendering behind the navbar at the source: add
`header`/`modal` z-index tokens and lift the shared BaseModal + key factor
overlays above the sticky top chrome so every modal covers the navbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@aseckin, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4ea0b4b4-d200-41b4-9c18-d3d355c25f77

📥 Commits

Reviewing files that changed from the base of the PR and between 2ffc1d2 and 6f767dc.

📒 Files selected for processing (4)
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/key_factor_card_container.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/mobile_key_factor_overlay.tsx
  • front_end/tailwind.config.ts
📝 Walkthrough

Walkthrough

Replaces the manual carousel in MobileKeyFactorOverlay with an Embla-based carousel, internalizes comment/vote state and handlers, adds a large typography prop to key-factor item subcomponents, introduces z-header/z-modal Tailwind tokens replacing hardcoded z-index values, and adds keyFactorsFor/goToKeyFactor i18n keys across six locales.

Changes

Key Factor Carousel Refactor and Visual Updates

Layer / File(s) Summary
Named z-index Tailwind tokens
front_end/tailwind.config.ts, front_end/src/app/(main)/components/top_chrome_client.tsx, front_end/src/components/base_modal.tsx, front_end/src/app/(main)/questions/[id]/components/key_factors/key_factor_detail_overlay.tsx
Adds z-header (210) and z-modal (220) to Tailwind config; replaces z-[210], z-[201] hardcoded values across TopChromeClient, BaseModal, and both dialog wrappers in the detail overlay.
large prop propagation through key-factor item subcomponents
...key_factors/item_view/index.tsx, ...item_view/driver/key_factor_driver.tsx, ...item_view/base_rate/key_factor_base_rate.tsx, ...item_view/news/key_factor_news.tsx, ...item_view/news/key_factor_news_item.tsx
Adds optional large?: boolean to KeyFactorItem Props and forwards it through KeyFactorDriver, KeyFactorBaseRate, KeyFactorNews, and KeyFactorNewsItem, each switching to larger typography classes when large is true.
MobileKeyFactorOverlay Embla carousel rewrite and detail overlay wiring
...key_factors/mobile_key_factor_overlay.tsx, ...key_factors/key_factor_detail_overlay.tsx
Rewrites MobileKeyFactorOverlay to use Embla carousel starting at initialIndex, tracks selectedIndex/canPrev/canNext via Embla events, sources comments from useCommentsFeed, and internalizes scroll/reply/vote/mind handlers. KeyFactorDetailOverlay drops hasPrev/hasNext/hasComment and passes the simplified prop set.
i18n keyFactorsFor and goToKeyFactor strings
front_end/messages/*.json
Adds keyFactorsFor and goToKeyFactor (with {number} placeholder) translation keys across en, cs, es, pt, zh, and zh-TW locale files.

Sequence Diagram

sequenceDiagram
  participant KeyFactorDetailOverlay
  participant MobileKeyFactorOverlay
  participant EmblaCarousel
  participant useCommentsFeed

  KeyFactorDetailOverlay->>MobileKeyFactorOverlay: allPostKeyFactors, initialIndex, onSelectKeyFactor
  MobileKeyFactorOverlay->>EmblaCarousel: init with startIndex
  EmblaCarousel->>MobileKeyFactorOverlay: select/reInit → selectedIndex, canPrev, canNext
  MobileKeyFactorOverlay->>useCommentsFeed: ensureCommentLoaded(kf.comment_id)
  useCommentsFeed-->>MobileKeyFactorOverlay: comment data
  MobileKeyFactorOverlay->>KeyFactorDetailOverlay: onSelectKeyFactor(keyFactor)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Metaculus/metaculus#4718: Modifies top_chrome_client.tsx for sticky positioning; this PR further changes its z-index to use the new z-header token.

Suggested reviewers

  • ncarazon
  • hlbmtc
  • cemreinanc

🐇 Hop hop, the carousel glides,
Embla now steers the mobile rides!
z-modal stacks tidy and neat,
large text makes the factors complete.
New i18n strings in every tongue —
the key factors fix has finally sprung! 🎉

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: redesigning the mobile key factor overlay into a swipeable pager.
Linked Issues check ✅ Passed The changes address #4839 by adding swipe paging, clearer navigation controls, and z-index fixes so the overlay sits above the navbar.
Out of Scope Changes check ✅ Passed The modified files stay aligned with the mobile key factors redesign and related modal/navigation styling changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch key-factors-mobile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4952-key-factors-mobile-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:key-factors-mobile-6f767dc
🗄️ PostgreSQL NeonDB branch preview/pr-4952-key-factors-mobile
Redis Fly Redis mtc-redis-pr-4952-key-factors-mobile

Details

  • Commit: a1318aa04efd8035147c8e324ad0039bdaef2cb4
  • Branch: key-factors-mobile
  • Fly App: metaculus-pr-4952-key-factors-mobile

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
front_end/src/app/(main)/questions/[id]/components/key_factors/key_factor_detail_overlay.tsx (1)

138-141: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the desktop loading state out of the “simple” branch.

isSimple now treats comment === null the same as “loaded empty comment”, so a driver key factor with no siblings renders the card-only modal first and only switches to CommentDetailPanel after ensureCommentLoaded() resolves. That drops the existing loading/skeleton path during slow fetches.

💡 Suggested fix
+  const hasComment = !!(comment?.text?.trim() || !comment);
   const isSimple =
     questionLink ||
     !keyFactor?.driver ||
-    (relatedKeyFactors.length === 0 && !comment?.text?.trim());
+    (relatedKeyFactors.length === 0 && !hasComment);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/key_factor_detail_overlay.tsx
around lines 138 - 141, The `isSimple` condition in `KeyFactorDetailOverlay` is
too broad because it treats an unloaded `comment` the same as an empty comment,
which forces the card-only modal while `ensureCommentLoaded()` is still pending.
Update the branching so the “simple” path only applies when the comment has
actually loaded and is empty, and keep the existing loading/skeleton state for
unresolved comments. Use the `isSimple` logic and the
`CommentDetailPanel`/loading branch in `key_factor_detail_overlay.tsx` to
preserve the desktop loading behavior during slow fetches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/mobile_key_factor_overlay.tsx:
- Around line 135-145: The mobile Reply action is incorrectly reusing
handleScrollToComment, so it only navigates to the comment thread instead of
starting the reply flow. Update the mobile key factor overlay handlers in
mobile_key_factor_overlay.tsx so onReply uses a dedicated reply handler,
matching the desktop behavior, and call
questionLayout?.requestReplyToComment(kf.comment_id) after ensureCommentLoaded
rather than requestScrollToComment. Keep the existing view-comment behavior in
handleScrollToComment unchanged.
- Around line 372-376: The carousel navigation accessibility labels in
mobile_key_factor_overlay are hardcoded English, so they bypass the existing
i18n flow. Update the button aria-labels in the carousel/key factor navigation
(including the repeated controls around the current key factor list) to use the
component’s existing useTranslations() helper instead of string literals, and
add the corresponding locale keys for the “Go to key factor …” and related
navigation labels.

---

Outside diff comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/key_factor_detail_overlay.tsx:
- Around line 138-141: The `isSimple` condition in `KeyFactorDetailOverlay` is
too broad because it treats an unloaded `comment` the same as an empty comment,
which forces the card-only modal while `ensureCommentLoaded()` is still pending.
Update the branching so the “simple” path only applies when the comment has
actually loaded and is empty, and keep the existing loading/skeleton state for
unresolved comments. Use the `isSimple` logic and the
`CommentDetailPanel`/loading branch in `key_factor_detail_overlay.tsx` to
preserve the desktop loading behavior during slow fetches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bb09de59-781c-45f4-8e66-c6357d8de80a

📥 Commits

Reviewing files that changed from the base of the PR and between f99a940 and 14d9353.

📒 Files selected for processing (16)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/components/top_chrome_client.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/base_rate/key_factor_base_rate.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/driver/key_factor_driver.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/news/key_factor_news.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/news/key_factor_news_item.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/key_factor_detail_overlay.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/mobile_key_factor_overlay.tsx
  • front_end/src/components/base_modal.tsx
  • front_end/tailwind.config.ts

- Footer: single space-efficient row with prev arrow on the left edge,
  dot indicators centered, next arrow on the right edge.
- First/last slides keep a screen-edge gap equal to the header padding
  (16px) instead of touching the edge; middle peeks still run edge to edge.
- Comment header stacks username over timestamp (like the feed's mobile
  comment) so long usernames no longer break the layout; reuse
  formatUsername.
- Scroll the whole key factor + comment slide as one unit, so swiping up
  scrolls the key factor away too, instead of only scrolling the comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace hardcoded "Previous"/"Next"/"Go to key factor N" aria-labels with
the existing useTranslations() helper. Reuse the existing `previous`/`next`
keys and add a new `goToKeyFactor` ({number}) key across all locale files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mobile overlay's Reply action reused handleScrollToComment, so it only
navigated to the comment thread. Add a dedicated handleReplyToComment that
calls questionLayout.requestReplyToComment after ensureCommentLoaded
(matching desktop), and wire onReply to it. View-comment behavior
(handleScrollToComment / onScrollToLink) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The slide scroll container used overflow-y-auto with overflow-x left
visible, which CSS computes to auto — letting the content drift
horizontally during a vertical swipe. Explicitly clip the x-axis with
overflow-x-hidden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aseckin aseckin marked this pull request as ready for review June 29, 2026 15:54
@aseckin aseckin marked this pull request as draft June 29, 2026 16:40
Multiple key factors can share a comment, and the list is sorted by vote
score, so swiping re-showed the same comment repeatedly. Lift the comment
out of the horizontal carousel into a single shared panel below the
key-factor pager:

- The carousel now holds only the key factor cards; its height tracks the
  active card (manual embla auto-height) so the comment re-anchors below it.
- The comment renders once for the active key factor. It is keyed by
  comment_id, so swiping between key factors of the SAME comment reuses it
  in place (static), while a different comment remounts and cross-fades
  (new animate-fade-in utility).
- Pager + comment share one vertical scroll column, so swiping up still
  scrolls the card away.
- Remove the in-comment "Key Factors" chips (siblings are reached by
  swiping the cards).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
front_end/src/app/(main)/questions/[id]/components/key_factors/mobile_key_factor_overlay.tsx (1)

320-340: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give the dialog an accessible title. The visible <h2> isn’t associated with the Dialog, so the modal can be announced without a name. Wire it with aria-labelledby or use DialogTitle.

♿ Proposed fix
-      <Dialog as="div" className="relative z-modal" onClose={onClose}>
+      <Dialog
+        as="div"
+        className="relative z-modal"
+        onClose={onClose}
+        aria-labelledby="mobile-key-factor-overlay-title"
+      >
@@
-            <h2 className="mt-0 min-w-0 flex-1 text-base font-semibold leading-5 tracking-tight text-gray-800 dark:text-gray-800-dark">
+            <h2
+              id="mobile-key-factor-overlay-title"
+              className="mt-0 min-w-0 flex-1 text-base font-semibold leading-5 tracking-tight text-gray-800 dark:text-gray-800-dark"
+            >
               {post.title}
             </h2>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/mobile_key_factor_overlay.tsx
around lines 320 - 340, The mobile key factor modal in
mobile_key_factor_overlay.tsx lacks an accessible name because the visible post
title is not linked to the Dialog. Update the Dialog in the overlay component to
expose a title by connecting the existing heading (the h2 that renders
post.title) with aria-labelledby, or replace it with DialogTitle so assistive
technologies can announce the modal properly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/mobile_key_factor_overlay.tsx:
- Around line 320-340: The mobile key factor modal in
mobile_key_factor_overlay.tsx lacks an accessible name because the visible post
title is not linked to the Dialog. Update the Dialog in the overlay component to
expose a title by connecting the existing heading (the h2 that renders
post.title) with aria-labelledby, or replace it with DialogTitle so assistive
technologies can announce the modal properly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b79c2021-c4b7-44ff-8243-56d1a377ce2e

📥 Commits

Reviewing files that changed from the base of the PR and between b363bf4 and 2ffc1d2.

📒 Files selected for processing (2)
  • front_end/src/app/(main)/questions/[id]/components/key_factors/mobile_key_factor_overlay.tsx
  • front_end/tailwind.config.ts

- Swipe to navigate: a horizontal swipe on the comment (not just the cards)
  now pages between key factors.
- Edge gap: give the first/last cards a 16px screen-edge gap via per-slide
  first:pl-4/last:pr-4 (reliably measured by embla) instead of container
  padding, so the last card no longer hugs the right edge.
- Active vs hover: disable card hover in the overlay (no sticky hover on
  touch) and instead highlight the centered card with an active blue-500
  border (transition-colors, timed with the slide). The attached comment
  gets the same border, masked to fade out after 50px.
- Scroll fades: add ~10px top/bottom gradient fade overlays on the inner
  scroll area, shown based on scroll position.
- Directional comment transition: replace the flat crossfade with a
  slower (280ms) directional slide-in keyed off swipe direction
  (animate-comment-in-right / -left).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The comment panel is keyed by comment_id, so same-comment swipes keep it
mounted. But the entrance animation class was derived from the live swipe
direction, so reversing direction flipped the animation-name on the still
-mounted panel and re-triggered the animation for a comment that hadn't
changed. Freeze the entrance direction at the moment comment_id changes
(adjust-state-during-render), so the class is stable while the same comment
stays mounted and only changes on a real comment switch.

Also give the overlay an accessible name: render the post title via
Headless UI DialogTitle so the modal is announced by assistive tech.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

Fix mobile Key Factors experience

1 participant