Conversation
把 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 28 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughAdds selectable themes with CSS variable overrides, profile swatches, localStorage initialization, authenticated API synchronization, and a persisted ChangesTheme system
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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
apps/web/src/hooks/useTheme.ts (1)
54-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winIn-flight sync can clobber a newer selection.
Nothing cancels the async IIFE: if the user picks a theme (or signs out) while
/api/themeis pending, the late response still callssetThemeState/applyToDocumentand 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 winValidate the stored id in the init script.
The script applies whatever is in
localStorage, whileload()inuseTheme.tsrejects 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 valueConsider narrowing theme ids to a literal union.
id: stringplus a boolean-returningisValidThemeIdgives no compile-time safety; consumers (useTheme,Profile) pass raw strings. AThemeIdunion plus a type predicate would let TS catch typos and letthemestate 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 winExpose selection state to assistive tech.
Active state is communicated only via the
is-activeclass.♿ 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
📒 Files selected for processing (9)
apps/web/prisma/migrations/20260725034631_add_user_theme/migration.sqlapps/web/prisma/schema.prismaapps/web/src/App.tsxapps/web/src/app/api/theme/route.tsapps/web/src/app/layout.tsxapps/web/src/hooks/useTheme.tsapps/web/src/index.cssapps/web/src/lib/themes.tsapps/web/src/screens/Profile.tsx
| ); | ||
| --shadow: none; | ||
| --font-sans: 'Noto Sans TC', system-ui, -apple-system, sans-serif; | ||
| --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; |
There was a problem hiding this comment.
📐 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
| <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> |
There was a problem hiding this comment.
🎯 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>
Summary by CodeRabbit