Skip to content

Dev - #23

Merged
Retsomm merged 7 commits into
mainfrom
dev
Jul 25, 2026
Merged

Dev#23
Retsomm merged 7 commits into
mainfrom
dev

Conversation

@Retsomm

@Retsomm Retsomm commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added an appearance theme selector in the profile settings.
    • Added six selectable visual themes with updated colors, borders, backgrounds, and styling.
    • Themes now persist locally for visitors and sync across signed-in sessions.
    • Applied saved themes before page load to prevent visual flicker.

Retsomm and others added 6 commits July 25, 2026 11:59
把 index.css 裡散落的顏色/切角字面值收斂成 CSS 變數(data-theme 覆寫用),
新增 useTheme hook(訪客走 localStorage、登入後同步資料庫)、/api/theme
route 與 Prisma theme 欄位,Profile 頁加上外觀主題切換器;目前只開放
「太空艙 HUD」一個選項,5 個新主題留待後續分批加入。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
紫粉星雲光暈玻璃卡片風格,對照 claude.ai/design 設計稿 1a 的色票/字體/
圓角規格,套用 data-theme="1a" 的 CSS 變數覆寫;同時把切換器點陣圖案的
background-size 抽成 --bg-pattern-size 變數,讓非平鋪型的星雲/極光背景
可以獨立於既有的星點平鋪效果。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
移除 1b 極簡星域主題(使用者測試後認為跟目前預設主題太相近),並依
claude.ai/design 設計稿 1c 的規格新增 data-theme="1c" 極光玻璃主題:
深藍夜空+薄玻璃卡片,極光漸層裝飾線條。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
依 claude.ai/design 設計稿 1e 的規格新增 data-theme="1e":靛藍夜空+
金色調配色、Cinzel 襯線字體,走古典優雅、儀式感路線。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
依 claude.ai/design 設計稿 1f 的規格新增 data-theme="1f":合成波地平線
掃描線+螢光洋紅/青配色、Orbitron 字體。這個主題沿用預設主題的切角卡片
(--card-clip/--btn-clip 用 :root 預設值,不覆寫成圓角),風格上比其餘
圓角玻璃系主題更接近預設的 HUD 語彙,但用色跟其餘主題明顯區隔。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
先前的極簡星域跟自製「駭客終端」都只透過改顏色/字型/圓角區分主題,使用者
認為不夠明顯。改用 claude.ai/design 重新提供的 1b 設計稿:把邊框寬度、
邊框樣式(實線/虛線)、發光強度抽成 --border-w/--border-style/--glow-a
三個新變數,回填進全站原本寫死 1px solid 與固定發光透明度的規則(預設
主題數值不變,行為零改變),讓 1b 用 3px 虛線邊框+壓低的發光強度呈現
跟其他主題明顯不同的視覺質感。同時移除已被取代的自製「駭客終端」主題,
主題數回到 5 個新主題(1a/1b/1c/1e/1f)+預設。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
easy-learn Ready Ready Preview, Comment Jul 25, 2026 6:11am

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 28 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 65a0ad28-048e-4667-974d-658912bf7c6c

📥 Commits

Reviewing files that changed from the base of the PR and between e645edb and 4aabd4f.

📒 Files selected for processing (6)
  • apps/web/src/app/api/theme/route.ts
  • apps/web/src/app/layout.tsx
  • apps/web/src/hooks/useTheme.ts
  • apps/web/src/index.css
  • apps/web/src/lib/themes.ts
  • apps/web/src/screens/Profile.tsx
📝 Walkthrough

Walkthrough

Adds selectable themes with CSS variable overrides, profile swatches, localStorage initialization, authenticated API synchronization, and a persisted User.theme database field.

Changes

Theme system

Layer / File(s) Summary
Theme contract and persistence
apps/web/src/lib/themes.ts, apps/web/prisma/..., apps/web/src/app/api/theme/route.ts
Defines enabled themes, adds the persisted User.theme field, and implements authenticated theme GET/POST handlers.
Client theme synchronization
apps/web/src/hooks/useTheme.ts, apps/web/src/app/layout.tsx
Loads and applies themes from localStorage, initializes the document before hydration, and synchronizes signed-in preferences with the API.
Profile theme selection
apps/web/src/App.tsx, apps/web/src/screens/Profile.tsx, apps/web/src/index.css
Passes theme state into Profile and renders selectable enabled-theme swatches with active styling.
Theme-aware application styling
apps/web/src/index.css
Replaces fixed colors, borders, glows, patterns, and clipping with shared variables and adds five themed variable overrides.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Profile
  participant useTheme
  participant document
  participant ThemeAPI
  participant User
  Profile->>useTheme: select theme id
  useTheme->>document: set data-theme
  useTheme->>ThemeAPI: POST selected theme
  ThemeAPI->>User: upsert User.theme
  User-->>ThemeAPI: persisted theme
  ThemeAPI-->>useTheme: updated theme
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too generic and does not describe the feature changes in this PR. Use a concise title that names the main change, such as adding theme support and persistence.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
apps/web/src/hooks/useTheme.ts (1)

54-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

In-flight sync can clobber a newer selection.

Nothing cancels the async IIFE: if the user picks a theme (or signs out) while /api/theme is pending, the late response still calls setThemeState/applyToDocument and overrides it. A cancellation flag in the effect cleanup avoids the flip-back.

♻️ Suggested guard
     if (syncedRef.current) return
+    let cancelled = false
     ;(async () => {
       try {
         const res = await fetch('/api/theme')
@@
+        if (cancelled) return
         setThemeState(resolved)
         applyToDocument(resolved)
         syncedRef.current = true
       } catch (err) {
         console.error('theme load/sync failed', err)
       }
     })()
+    return () => {
+      cancelled = true
+    }
   }, [isLoaded, isSignedIn, hydrated])
🤖 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 `@apps/web/src/hooks/useTheme.ts` around lines 54 - 81, Update the useTheme
effect’s async synchronization flow to track cancellation through its cleanup
function. Check the cancellation flag before applying the fetched theme or
marking syncedRef, and ensure cleanup sets it so a pending /api/theme response
cannot overwrite a newer selection or signed-out state.
apps/web/src/app/layout.tsx (1)

33-38: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate the stored id in the init script.

The script applies whatever is in localStorage, while load() in useTheme.ts rejects unknown/disabled ids. A stale value (e.g. a theme later disabled) is painted first and then corrected after hydration — the exact flash this script exists to prevent. An inline allow-list keeps both sides consistent.

Note on the static-analysis hint for dangerouslySetInnerHTML (line 52): the injected string is a module constant with no interpolation, so there is no injection vector here.

♻️ Suggested tightening
 const themeInitScript = `
 try {
   var t = localStorage.getItem('easylearn-theme-v1');
-  if (t) document.documentElement.dataset.theme = t;
+  var allowed = ${JSON.stringify(THEMES.filter((x) => x.enabled).map((x) => x.id))};
+  if (t && allowed.indexOf(t) !== -1) document.documentElement.dataset.theme = t;
 } catch (e) {}
 `

Requires import { THEMES } from '@/lib/themes' in this file.

🤖 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 `@apps/web/src/app/layout.tsx` around lines 33 - 38, Update themeInitScript in
the layout module to import THEMES and validate the stored theme id against the
enabled theme allow-list before assigning
document.documentElement.dataset.theme. Ignore unknown or disabled values so the
inline initialization matches useTheme.load() and prevents a flash of an invalid
theme.

Source: Linters/SAST tools

apps/web/src/lib/themes.ts (1)

1-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider narrowing theme ids to a literal union.

id: string plus a boolean-returning isValidThemeId gives no compile-time safety; consumers (useTheme, Profile) pass raw strings. A ThemeId union plus a type predicate would let TS catch typos and let theme state be typed.

♻️ Optional typing tightening
+export type ThemeId = 'default' | '1a' | '1b' | '1c' | '1e' | '1f'
+
 export interface ThemeOption {
-  id: string
+  id: ThemeId
   label: string
   swatch: string
   enabled: boolean
 }
@@
-export const isValidThemeId = (id: string): boolean => THEMES.some((t) => t.id === id && t.enabled)
+export const isValidThemeId = (id: string): id is ThemeId =>
+  THEMES.some((t) => t.id === id && t.enabled)
🤖 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 `@apps/web/src/lib/themes.ts` around lines 1 - 20, Define a literal-union
ThemeId from the supported theme identifiers and use it for ThemeOption.id and
DEFAULT_THEME_ID. Update isValidThemeId to accept a raw string while returning a
ThemeId type predicate, then propagate ThemeId typing to theme state and
consumers such as useTheme and Profile so validated values gain compile-time
safety without changing runtime validation.
apps/web/src/screens/Profile.tsx (1)

153-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose selection state to assistive tech.

Active state is communicated only via the is-active class.

♿ Suggested change
           <button
             key={t.id}
+            type="button"
+            aria-pressed={theme === t.id}
             className={`theme-swatch${theme === t.id ? ' is-active' : ''}`}
             onClick={() => onThemeChange(t.id)}
           >
🤖 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 `@apps/web/src/screens/Profile.tsx` around lines 153 - 157, Update the theme
swatch button rendering in Profile so the selected theme is exposed to assistive
technologies through an appropriate selection-state ARIA attribute, using the
same theme === t.id condition as the is-active class while preserving the
existing click behavior.
🤖 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 `@apps/web/src/app/api/theme/route.ts`:
- Around line 25-28: Update the request body parsing in the theme route so
errors from request.json() are caught and treated as invalid input, returning
the existing 400 invalid-theme response for empty or malformed bodies. Preserve
the current validation through isValidThemeId for successfully parsed bodies.

In `@apps/web/src/hooks/useTheme.ts`:
- Around line 83-90: Update setTheme to persist every valid selection to
localStorage under easylearn-theme-v1 before applying the theme, including
signed-in users. Preserve the existing signed-in API POST and guest behavior
while ensuring the pre-hydration script reads the newly selected theme.
- Around line 20-28: Update postJson to validate the fetch response before
reading data.theme: reject or throw for non-OK responses, and ensure the parsed
successful response contains a valid theme string. Keep callers from receiving
undefined so the theme state and document dataset are only updated with a valid
theme.

In `@apps/web/src/index.css`:
- Line 58: Update the font-family declarations in index.css, including the
--font-mono stack and the additional reported locations, to satisfy Stylelint’s
value-keyword-case rule by consistently lowercasing unquoted font keywords or
quoting names such as Menlo and Consolas. Also normalize currentColor at the
referenced color declaration, preserving the existing styling and font order.
- Around line 1958-1963: Update the `--ink-rgb` token in theme `1a` to match the
RGB components of `--ink: `#f1e9ff``, using `241, 233, 255`; leave the other theme
tokens unchanged.

In `@apps/web/src/screens/Profile.tsx`:
- Around line 150-162: Make the theme picker reachable for guests by moving the
THEME swatch grid using onThemeChange above the !isSignedIn early return in
Profile, or rendering the same picker in the login branch. Preserve the existing
active-theme styling, enabled-theme filtering, and theme selection behavior.

---

Nitpick comments:
In `@apps/web/src/app/layout.tsx`:
- Around line 33-38: Update themeInitScript in the layout module to import
THEMES and validate the stored theme id against the enabled theme allow-list
before assigning document.documentElement.dataset.theme. Ignore unknown or
disabled values so the inline initialization matches useTheme.load() and
prevents a flash of an invalid theme.

In `@apps/web/src/hooks/useTheme.ts`:
- Around line 54-81: Update the useTheme effect’s async synchronization flow to
track cancellation through its cleanup function. Check the cancellation flag
before applying the fetched theme or marking syncedRef, and ensure cleanup sets
it so a pending /api/theme response cannot overwrite a newer selection or
signed-out state.

In `@apps/web/src/lib/themes.ts`:
- Around line 1-20: Define a literal-union ThemeId from the supported theme
identifiers and use it for ThemeOption.id and DEFAULT_THEME_ID. Update
isValidThemeId to accept a raw string while returning a ThemeId type predicate,
then propagate ThemeId typing to theme state and consumers such as useTheme and
Profile so validated values gain compile-time safety without changing runtime
validation.

In `@apps/web/src/screens/Profile.tsx`:
- Around line 153-157: Update the theme swatch button rendering in Profile so
the selected theme is exposed to assistive technologies through an appropriate
selection-state ARIA attribute, using the same theme === t.id condition as the
is-active class while preserving the existing click behavior.
🪄 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 Plus

Run ID: cebe3c3d-fb4c-409c-95af-cf1a7f5893d6

📥 Commits

Reviewing files that changed from the base of the PR and between a55724e and e645edb.

📒 Files selected for processing (9)
  • apps/web/prisma/migrations/20260725034631_add_user_theme/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/theme/route.ts
  • apps/web/src/app/layout.tsx
  • apps/web/src/hooks/useTheme.ts
  • apps/web/src/index.css
  • apps/web/src/lib/themes.ts
  • apps/web/src/screens/Profile.tsx

Comment thread apps/web/src/app/api/theme/route.ts Outdated
Comment thread apps/web/src/hooks/useTheme.ts Outdated
Comment thread apps/web/src/hooks/useTheme.ts
Comment thread apps/web/src/index.css
);
--shadow: none;
--font-sans: 'Noto Sans TC', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stylelint value-keyword-case failures will break the lint job.

Reported on the new font stacks (Menlo, Consolas) and currentColor at line 1688. Either lowercase these values, quote the font names ('Menlo', 'Consolas'), or relax the rule for font families in the Stylelint config.

Also applies to: 1688-1688, 1980-1980, 2018-2018, 2064-2064, 2097-2097, 2129-2129

🧰 Tools
🪛 Stylelint (17.14.0)

[error] 58-58: Expected "Menlo" to be "menlo" (value-keyword-case)

(value-keyword-case)


[error] 58-58: Expected "Consolas" to be "consolas" (value-keyword-case)

(value-keyword-case)

🤖 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 `@apps/web/src/index.css` at line 58, Update the font-family declarations in
index.css, including the --font-mono stack and the additional reported
locations, to satisfy Stylelint’s value-keyword-case rule by consistently
lowercasing unquoted font keywords or quoting names such as Menlo and Consolas.
Also normalize currentColor at the referenced color declaration, preserving the
existing styling and font order.

Source: Linters/SAST tools

Comment thread apps/web/src/index.css Outdated
Comment thread apps/web/src/screens/Profile.tsx Outdated
Comment on lines +150 to +162
<h3 className="section-title">外觀主題</h3>
<div className="theme-swatch-grid">
{THEMES.filter((t) => t.enabled).map((t) => (
<button
key={t.id}
className={`theme-swatch${theme === t.id ? ' is-active' : ''}`}
onClick={() => onThemeChange(t.id)}
>
<span className="theme-swatch-dot" style={{ background: t.swatch, color: t.swatch }} />
{t.label}
</button>
))}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guests can never reach the theme picker.

useTheme fully supports guest mode (localStorage persistence), but this block sits after the !isSignedIn early return at line 63, so the only entry point to theme selection is behind sign-in. If guest theming is intended, render the swatch grid in the login branch too (or hoist it above the return).

🤖 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 `@apps/web/src/screens/Profile.tsx` around lines 150 - 162, Make the theme
picker reachable for guests by moving the THEME swatch grid using onThemeChange
above the !isSignedIn early return in Profile, or rendering the same picker in
the login branch. Preserve the existing active-theme styling, enabled-theme
filtering, and theme selection behavior.

- API route:request.json() 加 try/catch,避免空 body 造成未捕捉例外
- useTheme:postJson 驗證回應狀態與內容;signed-in 使用者切換主題也寫回 localStorage;
  同步 effect 加上取消旗標避免競態
- layout.tsx:pre-hydration script 套用主題前先核對啟用主題清單
- index.css:修正主題 1a 的 --ink-rgb 與 --ink 色碼不一致
- themes.ts:ThemeId 改為字面量聯合型別,isValidThemeId 改為型別守衛
- Profile.tsx:主題切換的 aria-pressed 無障礙屬性;主題選單維持僅登入後可見

Co-Authored-By: Claude Sonnet 5 <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.

1 participant