Add mobile web responsive layout#5
Conversation
Hybrid approach: CSS min() for fluid maxWidths + useIsMobile() hook for structural layout changes. 600px breakpoint targets phones only. Key changes: - New useIsMobile hook (matchMedia-based, SSR-safe) - Mobile fixed bottom nav bar with backdrop blur + safe-area-inset - Fluid countdown font, progress bar, and block grid sizing - History view: hidden date column, wrapped stats, smaller margins - All component maxWidths use min() to prevent overflow on 375px screens - Added touchstart listener for mobile auto-hide controls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review infoConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (13)
📝 WalkthroughWalkthroughAdds a client-side Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/page.tsx`:
- Around line 226-236: The mobile flash happens because isMobile (from
useIsMobile) is false during SSR and flips on mount; to fix, either (A) move
positioning out of JS and into CSS media queries (replace the inline style logic
on the div with className toggles and create CSS rules that position the nav at
bottom for small screens and top-right for large screens), or (B) delay the
mobile/desktop switch until after hydration by adding a local hydrated flag
(e.g., const [hydrated, setHydrated] = useState(false); useEffect(() =>
setHydrated(true), [])) and only apply the isMobile-based styles when hydrated
(leave a neutral hidden/placeholder state or the CSS default during SSR). Update
the div that currently uses isMobile inline styles to use the chosen approach so
initial SSR output matches hydrated layout and avoids the layout jump.
In `@src/components/BlockTimer.tsx`:
- Around line 45-47: BlockTimer currently computes blockSize and blockLabelSize
using useIsMobile (isMobile) which is false during SSR, causing a hydration
flash from desktop to mobile sizes; remove the JS-driven sizing and move sizes
into CSS via custom properties and media queries instead: stop using
blockSize/blockLabelSize for layout, add CSS custom properties (e.g.,
--block-size, --block-label-size) with desktop defaults and override them in a
`@media` (max-width:...) rule, and have the BlockTimer markup (component using
useIsMobile) read sizes from CSS (or simply rely on CSS rules) so hydration uses
the same sizes as the client; update references to blockSize/blockLabelSize in
the BlockTimer component to use CSS-based sizing.
In `@src/components/PublicBoard.tsx`:
- Around line 30-31: PublicBoard duplicates the same inline wrapper style at two
places (around the loading and loaded layouts); extract that shared style into a
single constant (e.g., wrapperStyle or containerStyle) at the top of the
PublicBoard component file and replace both inline style occurrences with that
constant so the loading and loaded UIs stay aligned; update any JSX that
currently sets style={{ gap: "32px", animation: "fadeIn 0.6s ease", width:
"100%", maxWidth: "min(560px, calc(100vw - 24px))" }} to use the new shared
identifier.
In `@src/components/SessionView.tsx`:
- Around line 57-63: The touchstart listener in SessionView.tsx is added without
the passive option; update the window.addEventListener("touchstart", resetTimer)
call to use a passive listener (e.g., window.addEventListener("touchstart",
resetTimer, { passive: true })) and ensure the corresponding cleanup uses the
same options object when calling window.removeEventListener("touchstart",
resetTimer, { passive: true }) so the listener is removed correctly; locate
references to resetTimer and hideTimerRef.current in the file to apply this
change.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (13)
src/app/page.tsxsrc/components/AuthScreen.tsxsrc/components/BlockTimer.tsxsrc/components/CompletionScreen.tsxsrc/components/HistoryView.tsxsrc/components/HomeView.tsxsrc/components/MindStateBar.tsxsrc/components/PublicBoard.tsxsrc/components/SessionView.tsxsrc/components/SettingsView.tsxsrc/components/ThoughtCapture.tsxsrc/components/ThoughtJournal.tsxsrc/lib/useIsMobile.ts
| window.addEventListener("touchstart", resetTimer); | ||
| return () => { | ||
| window.removeEventListener("mousemove", resetTimer); | ||
| window.removeEventListener("mousedown", resetTimer); | ||
| window.removeEventListener("keydown", resetTimer); | ||
| window.removeEventListener("touchstart", resetTimer); | ||
| if (hideTimerRef.current) clearTimeout(hideTimerRef.current); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/components/SessionView.tsx | sed -n '50,70p'Repository: auerbachb/still-point
Length of output: 1078
🏁 Script executed:
cat -n src/components/SessionView.tsx | sed -n '1,100p'Repository: auerbachb/still-point
Length of output: 4673
🏁 Script executed:
rg "touchstart" --type=ts --type=tsx -A 2 -B 2Repository: auerbachb/still-point
Length of output: 92
🏁 Script executed:
web_search
removeEventListener options parameter typescriptRepository: auerbachb/still-point
Length of output: 173
🏁 Script executed:
rg "touchstart" -t ts -A 2 -B 2Repository: auerbachb/still-point
Length of output: 864
🌐 Web query:
removeEventListener options parameter removeEventListener with passive option
💡 Result:
removeEventListener() does not need (and does not match on) the passive flag. To remove a listener, browsers/spec only match on:
type- the same function reference (
listener) capture(akauseCapture) only (must match) [1][2]
So these are equivalent for removal (as long as capture: false was used when adding):
el.addEventListener("touchmove", onMove, { passive: true });
el.removeEventListener("touchmove", onMove); // OK
el.removeEventListener("touchmove", onMove, { passive: true }); // OK (passive ignored)
el.removeEventListener("touchmove", onMove, { capture: false }); // OK
el.removeEventListener("touchmove", onMove, { capture: true }); // NOT removedIf you added with capture: true, you must remove with capture: true [1][2].
Sources: MDN removeEventListener() (matching rules) [1]; WHATWG DOM Standard (removal uses only flattened capture) [2].
Add passive option to touchstart listener to prevent scroll-blocking warnings on mobile.
Line 57 adds a touchstart handler without the passive option. Since resetTimer doesn't call preventDefault(), this handler should be marked passive to avoid scroll-jank warnings and improve responsiveness on mobile devices.
🔧 Suggested fix
- window.addEventListener("touchstart", resetTimer);
+ window.addEventListener("touchstart", resetTimer, { passive: true });
return () => {
window.removeEventListener("mousemove", resetTimer);
window.removeEventListener("mousedown", resetTimer);
window.removeEventListener("keydown", resetTimer);
- window.removeEventListener("touchstart", resetTimer);
+ window.removeEventListener("touchstart", resetTimer, false);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| window.addEventListener("touchstart", resetTimer); | |
| return () => { | |
| window.removeEventListener("mousemove", resetTimer); | |
| window.removeEventListener("mousedown", resetTimer); | |
| window.removeEventListener("keydown", resetTimer); | |
| window.removeEventListener("touchstart", resetTimer); | |
| if (hideTimerRef.current) clearTimeout(hideTimerRef.current); | |
| window.addEventListener("touchstart", resetTimer, { passive: true }); | |
| return () => { | |
| window.removeEventListener("mousemove", resetTimer); | |
| window.removeEventListener("mousedown", resetTimer); | |
| window.removeEventListener("keydown", resetTimer); | |
| window.removeEventListener("touchstart", resetTimer, false); | |
| if (hideTimerRef.current) clearTimeout(hideTimerRef.current); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SessionView.tsx` around lines 57 - 63, The touchstart listener
in SessionView.tsx is added without the passive option; update the
window.addEventListener("touchstart", resetTimer) call to use a passive listener
(e.g., window.addEventListener("touchstart", resetTimer, { passive: true })) and
ensure the corresponding cleanup uses the same options object when calling
window.removeEventListener("touchstart", resetTimer, { passive: true }) so the
listener is removed correctly; locate references to resetTimer and
hideTimerRef.current in the file to apply this change.
- Add { passive: true } to touchstart listener in SessionView
- Extract shared containerStyle in PublicBoard to avoid drift
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/PublicBoard.tsx (1)
63-66: 🧹 Nitpick | 🔵 TrivialConsider responsive grid columns for very narrow viewports.
The grid's fixed column widths (
32px 1fr 64px 64px 64px+ 48px gaps + 24px padding ≈ 296px minimum) leave only ~55px for the username column on 375px screens. This works but may truncate longer usernames.If mobile usability testing reveals issues, consider using
useIsMobileto conditionally hide a column (e.g., streak) or reduce column widths on mobile, consistent with howHistoryViewhides the date column. Based on learnings, hook-based responsive logic is preferred for structural layout changes in this codebase.Also applies to: 83-86
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/PublicBoard.tsx` around lines 63 - 66, The grid in the PublicBoard component uses fixed column widths via the inline style gridTemplateColumns (currently "32px 1fr 64px 64px 64px"), which can squeeze the username column on small screens; update PublicBoard to use the useIsMobile hook to compute a responsive gridTemplateColumns string or conditionally hide the less-important column(s) (e.g., the streak column) when isMobile is true, and apply that computed value to the same div style where gridTemplateColumns is set (also update the corresponding row rendering code that shows the streak column so it is omitted when hidden), mirroring the pattern used in HistoryView for hiding the date column on mobile.src/components/HistoryView.tsx (1)
269-275: 🧹 Nitpick | 🔵 TrivialVerify expanded thoughts alignment when date column is hidden.
The left margin for expanded thoughts changes from 216px (desktop) to 44px (mobile). The desktop value (216px) accounts for the 160px date label + 32px day number + gaps, but the mobile value (44px) should align with just the 32px day number column plus the 8px gap (= 40px). Consider adjusting to 40px for pixel-perfect alignment, though the 4px difference is minor.
💅 Optional alignment fix
<div style={{ - marginLeft: isMobile ? "44px" : "216px", marginTop: "4px", marginBottom: "8px", + marginLeft: isMobile ? "40px" : "216px", marginTop: "4px", marginBottom: "8px", padding: "10px 14px", background: "rgba(232,228,222,0.02)",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/HistoryView.tsx` around lines 269 - 275, The expanded thoughts container uses marginLeft: isMobile ? "44px" : "216px" which misaligns the mobile view; update the mobile marginLeft to "40px" so the expanded thoughts align with the 32px day-number column plus the 8px gap. Locate the JSX block that checks expandedDay === entry.day and uses dayThoughts.length > 0 (the inline style object) and change the isMobile ? "44px" value to "40px" while keeping the desktop 216px value unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/SettingsView.tsx`:
- Line 49: The inline style for the SettingsView width uses maxWidth:
"min(400px, calc(100vw - 24px))" which creates 12px horizontal margins
inconsistent with the app's standard calc(100vw - 40px) (20px margins); update
the maxWidth expression in the SettingsView component to "min(400px, calc(100vw
- 40px))" so it matches the other components and maintains consistent horizontal
spacing.
---
Outside diff comments:
In `@src/components/HistoryView.tsx`:
- Around line 269-275: The expanded thoughts container uses marginLeft: isMobile
? "44px" : "216px" which misaligns the mobile view; update the mobile marginLeft
to "40px" so the expanded thoughts align with the 32px day-number column plus
the 8px gap. Locate the JSX block that checks expandedDay === entry.day and uses
dayThoughts.length > 0 (the inline style object) and change the isMobile ?
"44px" value to "40px" while keeping the desktop 216px value unchanged.
In `@src/components/PublicBoard.tsx`:
- Around line 63-66: The grid in the PublicBoard component uses fixed column
widths via the inline style gridTemplateColumns (currently "32px 1fr 64px 64px
64px"), which can squeeze the username column on small screens; update
PublicBoard to use the useIsMobile hook to compute a responsive
gridTemplateColumns string or conditionally hide the less-important column(s)
(e.g., the streak column) when isMobile is true, and apply that computed value
to the same div style where gridTemplateColumns is set (also update the
corresponding row rendering code that shows the streak column so it is omitted
when hidden), mirroring the pattern used in HistoryView for hiding the date
column on mobile.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (13)
src/app/page.tsxsrc/components/AuthScreen.tsxsrc/components/BlockTimer.tsxsrc/components/CompletionScreen.tsxsrc/components/HistoryView.tsxsrc/components/HomeView.tsxsrc/components/MindStateBar.tsxsrc/components/PublicBoard.tsxsrc/components/SessionView.tsxsrc/components/SettingsView.tsxsrc/components/ThoughtCapture.tsxsrc/components/ThoughtJournal.tsxsrc/lib/useIsMobile.ts
Align SettingsView with other narrow components using 40px margin pattern instead of 24px (which is reserved for wider layouts). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/HistoryView.tsx`:
- Line 94: The inline style in HistoryView.tsx uses maxWidth: "min(720px,
calc(100vw - 24px))" (seen in the style object around the HistoryView component)
which is inconsistent with the calc(100vw - 40px) used elsewhere; update both
occurrences (the one at the gap/width line and the second at line ~111) to use
calc(100vw - 40px) for consistent 20px side margins (or explicitly document why
HistoryView should be wider if you want to keep 24px).
In `@src/components/ThoughtJournal.tsx`:
- Line 36: The inline style for the ThoughtJournal component uses maxWidth:
"min(560px, calc(100vw - 24px))" which mismatches the app's common margin
pattern; update the maxWidth expression in the ThoughtJournal styles (look for
the style object in the ThoughtJournal component) to use calc(100vw - 40px) so
it reads maxWidth: "min(560px, calc(100vw - 40px))" for consistency with other
components, or if the 24px value is intentional add a brief comment in the
component explaining the different margin choice.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (13)
src/app/page.tsxsrc/components/AuthScreen.tsxsrc/components/BlockTimer.tsxsrc/components/CompletionScreen.tsxsrc/components/HistoryView.tsxsrc/components/HomeView.tsxsrc/components/MindStateBar.tsxsrc/components/PublicBoard.tsxsrc/components/SessionView.tsxsrc/components/SettingsView.tsxsrc/components/ThoughtCapture.tsxsrc/components/ThoughtJournal.tsxsrc/lib/useIsMobile.ts
| <div style={{ | ||
| display: "flex", flexDirection: "column", alignItems: "center", | ||
| gap: "32px", animation: "fadeIn 0.6s ease", width: "100%", maxWidth: "720px", | ||
| gap: "32px", animation: "fadeIn 0.6s ease", width: "100%", maxWidth: "min(720px, calc(100vw - 24px))", |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor margin inconsistency.
The calc(100vw - 24px) pattern (12px margins) differs from the calc(100vw - 40px) (20px margins) used in most other components. This appears on lines 94 and 111. Consider aligning for consistency, or document as intentional if wider content is desired for the history view.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/HistoryView.tsx` at line 94, The inline style in
HistoryView.tsx uses maxWidth: "min(720px, calc(100vw - 24px))" (seen in the
style object around the HistoryView component) which is inconsistent with the
calc(100vw - 40px) used elsewhere; update both occurrences (the one at the
gap/width line and the second at line ~111) to use calc(100vw - 40px) for
consistent 20px side margins (or explicitly document why HistoryView should be
wider if you want to keep 24px).
There was a problem hiding this comment.
Intentional — this is a two-tier margin system: wider layout components (HistoryView 720px, PublicBoard 560px, ThoughtJournal 560px, BlockTimer row 505px) all consistently use calc(100vw - 24px) (12px margins) because they have denser content that benefits from the extra horizontal space on small screens. Narrower components (≤400px) use calc(100vw - 40px) (20px margins) for more breathing room.
There was a problem hiding this comment.
@auerbachb, thank you for clarifying! That makes perfect sense—the two-tier margin system provides a more spacious layout for denser content on wider components while maintaining breathing room on narrower ones. I appreciate the detailed explanation of the design rationale.
✏️ Learnings added
Learnt from: auerbachb
Repo: auerbachb/still-point PR: 5
File: src/components/HistoryView.tsx:94-94
Timestamp: 2026-02-26T18:16:28.000Z
Learning: In the still-point repository, a two-tier responsive margin system is used intentionally: wider layout components (HistoryView 720px, PublicBoard 560px, ThoughtJournal 560px, BlockTimer row 505px) consistently use `calc(100vw - 24px)` (12px side margins) because their denser content benefits from extra horizontal space on small screens, while narrower components (≤400px) use `calc(100vw - 40px)` (20px side margins) for more breathing room. This is not an inconsistency but a deliberate design pattern.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: auerbachb
Repo: auerbachb/still-point PR: 5
File: src/app/page.tsx:226-236
Timestamp: 2026-02-26T17:56:03.752Z
Learning: In TypeScript React components (any .tsx file), prefer hook-based responsive logic (e.g., useIsMobile) over CSS media query classes to maintain pattern consistency and keep styling logic co-located with components. During reviews, look for inline styles or CSS class utilities driven by media queries and consider refactoring to component-scoped responsive hooks; avoid spreading responsive decisions across global styles. This guideline applies broadly to TSX files, not just the specific file mentioned.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/page.tsx`:
- Around line 226-233: The mobile bottom nav's inline style (the isMobile ? {
... } : { ... } block) currently sets layout but doesn't ensure touch-safe hit
areas; update the styles for the nav buttons inside that container (e.g., the
elements rendered as primary nav buttons within the isMobile branch) to
guarantee a minimum touch target of 44x44px—either by adding
padding/min-width/min-height or explicit width/height on the button elements and
centering their content (use minWidth: 44, minHeight: 44, padding: '10px',
boxSizing: 'content-box' or similar). Apply the same change to the other mobile
nav style block referenced in the file so both mobile toolbars enforce the 44×44
touch target.
In `@src/components/CompletionScreen.tsx`:
- Line 83: The inline style in CompletionScreen's container uses maxWidth:
"min(350px, calc(100vw - 48px))" which creates 24px side margins and breaks the
established pattern; update the style on the container (the object with
textAlign, maxWidth, margin) to use calc(100vw - 40px) for consistency with the
≤500px components (i.e., change the maxWidth expression from calc(100vw - 48px)
to calc(100vw - 40px)), unless the 48px value was intentionally chosen for
visual emphasis — in that case, leave a comment explaining the intent.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (13)
src/app/page.tsxsrc/components/AuthScreen.tsxsrc/components/BlockTimer.tsxsrc/components/CompletionScreen.tsxsrc/components/HistoryView.tsxsrc/components/HomeView.tsxsrc/components/MindStateBar.tsxsrc/components/PublicBoard.tsxsrc/components/SessionView.tsxsrc/components/SettingsView.tsxsrc/components/ThoughtCapture.tsxsrc/components/ThoughtJournal.tsxsrc/lib/useIsMobile.ts
- Add 44x44px minimum touch targets for mobile nav buttons - Fix CompletionScreen thoughts margin to match 40px pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/page.tsx`:
- Around line 222-223: The mobile root padding uses a fixed "80px" which can let
content be clipped behind the bottom nav that grows with
env(safe-area-inset-bottom); update the padding expression (where padding is
computed with isMobile) to add the safe-area inset, e.g. replace the fixed
bottom value with a calc using env(safe-area-inset-bottom, 0px) so bottom
padding becomes something like calc(80px + env(safe-area-inset-bottom, 0px)),
ensuring consistency with the nav that already uses env(safe-area-inset-bottom).
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (13)
src/app/page.tsxsrc/components/AuthScreen.tsxsrc/components/BlockTimer.tsxsrc/components/CompletionScreen.tsxsrc/components/HistoryView.tsxsrc/components/HomeView.tsxsrc/components/MindStateBar.tsxsrc/components/PublicBoard.tsxsrc/components/SessionView.tsxsrc/components/SettingsView.tsxsrc/components/ThoughtCapture.tsxsrc/components/ThoughtJournal.tsxsrc/lib/useIsMobile.ts
Use calc(80px + env(safe-area-inset-bottom)) so content doesn't clip behind the nav bar on devices with larger safe areas. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
Summary
min()for fluid maxWidths (~25 one-line fixes) +useIsMobile()hook for structural layout changes (nav, block grid, history rows)useIsMobile.ts)Key changes
useIsMobilehook:matchMedia-based, SSR-safe (defaultsfalse, hydrates client-side)backdrop-filter: blur,env(safe-area-inset-bottom),space-aroundlayout. Username badge hidden on mobile.min(120px, 18vw)), progress bar, and block grid (56px blocks on mobile vs 75px desktop)touchstartlistener added for mobile auto-hidemaxWidthvalues usemin()to prevent overflow on 375px screensTest plan
npm run buildpasses (verified locally)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements