Skip to content

fix: semantic modal routing, remove win overlay, tighten mobile header#193

Merged
Hugo0 merged 12 commits into
mainfrom
fix/semantic-polish
Apr 11, 2026
Merged

fix: semantic modal routing, remove win overlay, tighten mobile header#193
Hugo0 merged 12 commits into
mainfrom
fix/semantic-polish

Conversation

@Hugo0
Copy link
Copy Markdown
Owner

@Hugo0 Hugo0 commented Apr 11, 2026

Summary

  • Fix: chart button opens wrong modal for semantic — PageShell now emits @results for noKeyboard modes instead of opening the generic GameStatsModal. Semantic page opens its own SemanticStatsModal. Generic modal suppressed with v-if="!noKeyboard".
  • Remove win overlay + green border — "Found bread" badge and green map-card.won border removed. These were inconsistent with other modes (which use notification toasts). ~70 lines of dead CSS removed.
  • Remove unused coderankColor function, celebrate-fade transitions, win-overlay/win-badge styles
  • Mobile header spacingpx-1.5 on mobile (→ px-3 at sm) gives more room for title text

Test plan

  • Semantic: chart button in header opens SemanticStatsModal (not generic)
  • Semantic: no green border on map when won
  • Semantic: no "Found bread" overlay badge on map
  • Classic/dordle/etc: chart button still opens generic StatsModal normally
  • Mobile: header icons slightly closer to edges, title less cramped

@coderabbitai review

Summary by CodeRabbit

  • Style

    • Adjusted header padding on smallest screens; added a new “reveal” transition animation and reveal wrapper for subtle entry animations.
  • Refactor

    • Removed win-state visual flair and map-card win styling.
    • Results handling now differs by mode: non-keyboard modes emit page events instead of auto-toggling the stats modal.
  • New Features

    • Centralized game lifecycle for consistent save/sync; day-rollover detection reloads when a new day is available.
    • Multi-board daily games attempt local restore and immediately render restored tiles.
  • Bug Fixes

    • Prevented mobile input scroll jump when the virtual keyboard opens.
    • Streamlined share action to show inline “Copied!” feedback.

- PageShell: noKeyboard modes (semantic) emit @results instead of
  opening generic GameStatsModal — semantic page opens its own
  SemanticStatsModal. Generic StatsModal suppressed with v-if.
- Remove win overlay ("Found bread" badge + green border) from
  semantic — inconsistent with other modes that use toasts
- Remove dead CSS (win-overlay, win-badge, celebrate-fade, map-card.won)
- Remove unused rankColor function from SemanticStatsModal
- Mobile header: tighter horizontal padding (px-1.5 → px-3 at sm)
  to give more room for title text
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Centralized game-over lifecycle into a new composable, wired semantic/page and PageShell to emit/use results differently, removed win-state UI and related styles, added mobile input focus handling, improved multi-board restoration and immediate tile rendering, many modal components switched to BaseModal, and added a reveal transition/component.

Changes

Cohort / File(s) Summary
Lifecycle & server sync
composables/useGameLifecycle.ts, composables/useSync.ts
Added useGameLifecycle (initStats, handleGameOver, syncGameResult, syncSpeedResult) and replaced direct per-game sync calls to use the composable; centralizes local save, stat recalculation, and server posting with retry hooks.
Page wiring / results flow
components/game/PageShell.vue, pages/[lang]/semantic.vue
PageShell now emits a results event (added to emits) and delegates results handling to a local onResults; semantic page uses useGameLifecycle() for init/handleGameOver and toggles/showing of stats modal adjusted.
Multi-board restoration & rendering
composables/useMultiBoardPage.ts, stores/game.ts
Multi-board init now attempts localStorage restore for daily games before starting new; loadMultiBoardFromLocalStorage now calls showTilesAllBoards() to immediately render restored tiles.
UI: Semantic pages & inputs
components/semantic/SemanticStatsModal.vue, components/semantic/SemanticInput.vue, pages/[lang]/semantic.vue
Semantic stats modal refactored to use BaseModal, share button consolidated, removed rankColor helper and related styles; SemanticInput adds touch focus handler to preserve scrollTop; semantic page had win-overlay UI and map-card win styling removed.
Modal component swaps
components/*/*Modal.vue, components/game/StatsModal.vue, components/game/SpeedResults.vue, ...
Many modal wrappers replaced SharedBaseModalBaseModal across app (LoginModal, BoardPickerModal, GameModePicker, LanguagePickerModal, HelpModal, CopyFallbackModal, SettingsModal, SpeedResults, StatsModal, etc.) with props/events preserved.
Component substitutions
components/game/BestStartingWordsPanel.vue, pages/[lang]/best-starting-words.vue, pages/profile.vue, components/game/StreakModal.vue
Replaced several Shared* child components with non-shared equivalents (SharedStartingWordsListStartingWordsList, SharedStreakCalendarStreakCalendar) and updated related template usage.
Reveal transition & UI changes
assets/css/main.css, components/shared/RevealTransition.vue, pages/index.vue, pages/profile.vue
Added .reveal transition CSS and RevealTransition wrapper component; wrapped various UI blocks in reveal transitions, adjusted avatar/fallback and continue-playing card icon layout.
Day rollover detection
composables/useDayRollover.ts, pages/[lang]/[mode].vue, pages/[lang]/index.vue, pages/[lang]/speed.vue
Added useDayRollover(lang, loadedDayIdx) to detect daily index changes on visibility change and reload when needed; invoked in daily pages to avoid stale daily word state.
Header tweak
components/app/AppHeader.vue
Adjusted smallest-breakpoint header padding from px-3 to px-1.5.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Page as Page (semantic.vue / PageShell)
participant Lifecycle as useGameLifecycle
participant Store as Local Stats Store
participant API as /api/user/game-result
Page->>Lifecycle: handleGameOver({won, attempts, isRestore})
alt isRestore == false
Lifecycle->>Store: saveResult(modeKey, result)
Store-->>Lifecycle: ack / error
Lifecycle->>Store: calculateStats(modeKey) + calculateTotalStats()
Store-->>Lifecycle: updated stats
opt user loggedIn
Lifecycle->>API: POST /api/user/game-result {dayIdx, game_mode, play_type, won, attempts}
API-->>Lifecycle: 200 / error
alt newBadges in response
Lifecycle->>Page: onNewBadges(...) callback
end
end
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through lifecycle and swap,
I caught saved boards before they flop,
Hid the win-badge, steadied the view,
Tuned touch focus so scrolls stay true,
Tiny padding stitched — the rabbit's hop!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary changes: semantic modal routing logic (PageShell emitting results event), removal of win overlay and green border styling, and mobile header padding adjustment (px-1.5).
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/semantic-polish

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pages/[lang]/semantic.vue (1)

413-413: Remove leftover win-overlay scoped CSS that is now unreachable.

After dropping the win overlay/card won state, the scoped mobile selectors at Line 742–Line 753 (.win-overlay, .win-badge, .win-word, .win-stat) look dead and can be removed to complete the cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/`[lang]/semantic.vue at line 413, Remove the now-unreachable scoped
mobile CSS selectors related to the removed win overlay/card by deleting the
rules for .win-overlay, .win-badge, .win-word, and .win-stat from the
component's scoped style block; search for these class selectors in the semantic
component and remove their entire rule sets (the leftover mobile selectors
previously at the bottom of the style) so the scoped CSS no longer contains dead
styles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pages/`[lang]/semantic.vue:
- Line 413: Remove the now-unreachable scoped mobile CSS selectors related to
the removed win overlay/card by deleting the rules for .win-overlay, .win-badge,
.win-word, and .win-stat from the component's scoped style block; search for
these class selectors in the semantic component and remove their entire rule
sets (the leftover mobile selectors previously at the bottom of the style) so
the scoped CSS no longer contains dead styles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a550a86f-3111-482d-83bb-8556606ec6c8

📥 Commits

Reviewing files that changed from the base of the PR and between 4a600d3 and edaf49c.

📒 Files selected for processing (4)
  • components/app/AppHeader.vue
  • components/game/PageShell.vue
  • components/semantic/SemanticStatsModal.vue
  • pages/[lang]/semantic.vue
💤 Files with no reviewable changes (1)
  • components/semantic/SemanticStatsModal.vue

Hugo0 added 2 commits April 11, 2026 11:03
Critical bugs fixed:
- Stats not showing on daily revisit: now calls calculateStats()
  with the semantic key after loading game results
- Duplicate saves on restore: game-over watcher returns early when
  restoring from localStorage (neighbours already present)
- Server sync missing: semantic bypasses game store's gameOver, so
  the sync plugin never fires. Now posts to /api/user/game-result
  directly from the semantic page
- Mobile keyboard scroll: overflow-y:hidden on semantic body at
  ≤520px prevents the browser from scrolling behind the fixed input
loadMultiBoardFromLocalStorage() existed but was never called.
Now useMultiBoardPage tries to restore daily games from localStorage
before creating fresh boards — matching classic daily's pattern.
Fixes: revisiting a completed dordle/quordle daily showed a fresh
game instead of the completed board.
@Hugo0
Copy link
Copy Markdown
Owner Author

Hugo0 commented Apr 11, 2026

Updated with multi-board daily restore fix + semantic state bugs. See updated test plan above. @coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

@Hugo0 Sure! I'll kick off a fresh review of the updated PR, including the multi-board daily restore fix and semantic state bug fixes. I'll check against the updated test plan as well.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Hugo0 added 5 commits April 11, 2026 11:17
DRY refactor:
- New useGameLifecycle() composable centralizes stats save, server
  sync, stats recalculation, and duplicate-save guards. Used by
  semantic; available for any future non-tile mode.
- Semantic page refactored to use lifecycle.handleGameOver() and
  lifecycle.initStats() instead of inline logic (~30 lines removed)

Multi-board daily restore:
- useMultiBoardPage now calls loadMultiBoardFromLocalStorage() for
  daily modes before creating fresh boards. Completed dordle/quordle
  dailies now show completed state on revisit.

Mobile keyboard scroll (improved):
- Reverted overflow-y:hidden (broke scrolling to leaderboard)
- Instead: save/restore scroll position on input focus/blur to
  counteract browser's auto-scroll on keyboard open
- Per-mode rows now show games played + avg attempts (not just count + win%)
- Speed Streak stats shown inline in overview (sessions, best score,
  best words, best combo) instead of a separate tab
- Removed Speed tab — all stats visible in overview
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 (1)
composables/useMultiBoardPage.ts (1)

90-96: ⚠️ Potential issue | 🟠 Major

Remove wordList.length as a hard gate for daily fallback initialization.

At Line 90, daily fallback is blocked when wordList is empty, even though startNewGame() can initialize from dailyWords alone. If restore fails in that state, boards won’t initialize.

💡 Proposed fix
-    if (!restored && wordList.length > 0) {
+    const hasDailyWords = dailyWords?.length === boardCount;
+    if (!restored && (hasDailyWords || wordList.length > 0)) {
         try {
             startNewGame();
         } catch {
             // Initialization failed silently
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useMultiBoardPage.ts` around lines 90 - 96, The current
conditional uses "if (!restored && wordList.length > 0) { startNewGame() }",
which prevents daily fallback initialization when wordList is empty; remove the
wordList.length check so that when restored is false you call startNewGame()
regardless (i.e., change the conditional to check only restored), keeping the
try/catch around startNewGame() so startNewGame can initialize from dailyWords
even when wordList is empty; update references in this block (the restored
variable and startNewGame function) accordingly.
🧹 Nitpick comments (3)
components/semantic/SemanticInput.vue (1)

51-67: Clarify the behavior comment and drop the no-op blur path.

Line 53 says “restore on blur,” but the restore currently happens on focus (Lines 60–62), and onBlur (Lines 65–67) is empty. This is a small readability/maintenance mismatch.

Proposed cleanup
-// on mobile — it doesn't need scrolling. Lock scroll position on focus, restore on blur.
+// on mobile — it doesn't need scrolling. Lock scroll position immediately on focus.
 let savedScrollTop = 0;
 function onFocus() {
@@
 }
-function onBlur() {
-    // No action needed — scroll is already at the saved position
-}
-                `@focus`="onFocus"
-                `@blur`="onBlur"
+                `@focus`="onFocus"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/semantic/SemanticInput.vue` around lines 51 - 67, Comment
incorrectly says “restore on blur” while the scroll restoration happens inside
onFocus; update the comment to accurately describe behavior (e.g., “prevent
browser auto-scroll on mobile by saving and reapplying the scrollTop in
onFocus”) and remove the no-op onBlur function; keep savedScrollTop, onFocus,
inputRef, isTouch and the '.semantic-body' closest lookup as-is and ensure the
requestAnimationFrame reset logic remains in onFocus.
components/semantic/SemanticStatsModal.vue (1)

57-57: Prefer top alignment for this stats/end-game modal.

BaseModal defaults to centered alignment; this modal is content-heavy and usually reads better with top alignment (per components/shared/BaseModal.vue:54-78 API intent).

Proposed tweak
-    <BaseModal :visible="visible" size="lg" label-id="semantic-stats-label" `@close`="emit('close')">
+    <BaseModal :visible="visible" size="lg" align="top" label-id="semantic-stats-label" `@close`="emit('close')">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/semantic/SemanticStatsModal.vue` at line 57, Update the BaseModal
usage in SemanticStatsModal.vue to request top alignment instead of the default
centered alignment by adding the alignment prop (e.g., align="top") to the
BaseModal component invocation; modify the tag <BaseModal :visible="visible"
size="lg" label-id="semantic-stats-label" `@close`="emit('close')"> to include the
alignment prop so the modal renders top-aligned per BaseModal's API.
composables/useGameLifecycle.ts (1)

62-64: Avoid fully silent server-sync failures

The empty catch makes production sync issues hard to diagnose. Consider at least lightweight logging (e.g., dev-only warning).

🔎 Proposed refinement
-            }).catch(() => {
-                /* non-fatal — will sync on next full sync */
-            });
+            }).catch((err) => {
+                if (import.meta.dev) {
+                    console.warn('[game-lifecycle] server sync failed', err);
+                }
+                /* non-fatal — will sync on next full sync */
+            });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useGameLifecycle.ts` around lines 62 - 64, The empty catch in
useGameLifecycle swallows server-sync failures; replace the silent handler with
a lightweight log that includes the caught error (e.g., .catch((err) => { /* log
err */ })), and only emit a warning in non-production builds (use NODE_ENV or
the existing app logger if available) so failures are visible during development
while remaining non-fatal in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@composables/useGameLifecycle.ts`:
- Around line 43-45: After saving and updating mode-specific stats
(stats.saveResult and stats.calculateStats) the aggregate totals remain stale
because totals are only recomputed in initStats; fix this by invoking the totals
recomputation after a successful save — call initStats() (or the function that
recalculates aggregate totals) right after stats.calculateStats(statsKey,
config.maxGuesses) inside the same try block so totals are immediately updated
for the UI.

---

Outside diff comments:
In `@composables/useMultiBoardPage.ts`:
- Around line 90-96: The current conditional uses "if (!restored &&
wordList.length > 0) { startNewGame() }", which prevents daily fallback
initialization when wordList is empty; remove the wordList.length check so that
when restored is false you call startNewGame() regardless (i.e., change the
conditional to check only restored), keeping the try/catch around startNewGame()
so startNewGame can initialize from dailyWords even when wordList is empty;
update references in this block (the restored variable and startNewGame
function) accordingly.

---

Nitpick comments:
In `@components/semantic/SemanticInput.vue`:
- Around line 51-67: Comment incorrectly says “restore on blur” while the scroll
restoration happens inside onFocus; update the comment to accurately describe
behavior (e.g., “prevent browser auto-scroll on mobile by saving and reapplying
the scrollTop in onFocus”) and remove the no-op onBlur function; keep
savedScrollTop, onFocus, inputRef, isTouch and the '.semantic-body' closest
lookup as-is and ensure the requestAnimationFrame reset logic remains in
onFocus.

In `@components/semantic/SemanticStatsModal.vue`:
- Line 57: Update the BaseModal usage in SemanticStatsModal.vue to request top
alignment instead of the default centered alignment by adding the alignment prop
(e.g., align="top") to the BaseModal component invocation; modify the tag
<BaseModal :visible="visible" size="lg" label-id="semantic-stats-label"
`@close`="emit('close')"> to include the alignment prop so the modal renders
top-aligned per BaseModal's API.

In `@composables/useGameLifecycle.ts`:
- Around line 62-64: The empty catch in useGameLifecycle swallows server-sync
failures; replace the silent handler with a lightweight log that includes the
caught error (e.g., .catch((err) => { /* log err */ })), and only emit a warning
in non-production builds (use NODE_ENV or the existing app logger if available)
so failures are visible during development while remaining non-fatal in
production.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1b969dc-32f1-4c08-bf3f-1d9deb4ba882

📥 Commits

Reviewing files that changed from the base of the PR and between edaf49c and 2cd48e4.

📒 Files selected for processing (6)
  • components/semantic/SemanticInput.vue
  • components/semantic/SemanticStatsModal.vue
  • composables/useGameLifecycle.ts
  • composables/useMultiBoardPage.ts
  • pages/[lang]/semantic.vue
  • stores/game.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • pages/[lang]/semantic.vue

Comment on lines +43 to +45
stats.saveResult(statsKey, event.won, event.attempts);
stats.calculateStats(statsKey, config.maxGuesses);
} catch (e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Recalculate total stats after a successful save

On Line 44 you update mode stats, but aggregate totals are only recomputed in initStats (Line 73). This can leave totals stale right after a game ends.

💡 Proposed fix
         try {
             stats.saveResult(statsKey, event.won, event.attempts);
             stats.calculateStats(statsKey, config.maxGuesses);
+            stats.calculateTotalStats();
         } catch (e) {
             console.warn('[game-lifecycle] stats save failed', e);
         }
📝 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.

Suggested change
stats.saveResult(statsKey, event.won, event.attempts);
stats.calculateStats(statsKey, config.maxGuesses);
} catch (e) {
stats.saveResult(statsKey, event.won, event.attempts);
stats.calculateStats(statsKey, config.maxGuesses);
stats.calculateTotalStats();
} catch (e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useGameLifecycle.ts` around lines 43 - 45, After saving and
updating mode-specific stats (stats.saveResult and stats.calculateStats) the
aggregate totals remain stale because totals are only recomputed in initStats;
fix this by invoking the totals recomputation after a successful save — call
initStats() (or the function that recalculates aggregate totals) right after
stats.calculateStats(statsKey, config.maxGuesses) inside the same try block so
totals are immediately updated for the UI.

Hugo0 added 2 commits April 11, 2026 11:36
All game result syncing now flows through useGameLifecycle:
- syncGameResult(): handles auth, badge notifications, offline queue
- syncSpeedResult(): handles speed-specific endpoint
- useSync.ts refactored to call lifecycle methods instead of
  inline $fetch — removes ~40 lines of duplicated sync logic
- Semantic page already uses lifecycle.handleGameOver() which
  calls syncGameResult() internally

One composable, three entry points (tile gameOver watcher,
speed results watcher, semantic page watcher), zero duplication.
@Hugo0
Copy link
Copy Markdown
Owner Author

Hugo0 commented Apr 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Hugo0 added 2 commits April 11, 2026 11:57
- RevealTransition component: smooth fade+slide for auth-dependent content
  (homepage greeting, continue playing, profile sections, badges)
- Drop "Shared" prefix: nuxt.config pathPrefix:false for shared/ folder,
  BaseModal/ToggleSwitch/StreakCalendar etc no longer need Shared prefix
- Day rollover detection: useDayRollover composable reloads game pages
  when tab returns from background and the daily word has changed.
  Prevents stale word coloring bug (fixes #192)
@Hugo0 Hugo0 merged commit 5b240a1 into main Apr 11, 2026
4 of 5 checks passed
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.

1 participant