feat(ui): アニメーション・ローディングコンポーネント追加 (Issue #67 PR B)#76
Conversation
- FadeIn / SlideIn: prefers-reduced-motion を尊重したマウント時アニメーション - SkeletonLoader: text/rectangle/circle バリアント、複数行サポート - Loading: Loader2 ベースの統一スピナー、fullScreen オーバーレイ対応 - dashboard/history/profile ページに段階的なフェード・スライド適用 - 各コンポーネントの単体テスト26件追加 https://claude.ai/code/session_01789bk14oLFGxu5bwWAj9Gv
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds two new animation components (FadeIn, SlideIn), two feedback components (Loading, SkeletonLoader), a useReducedMotion hook with tests, and integrates the animations into dashboard, history, and profile pages (including a detail-panel remount key and removal of unused date-fns imports). Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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/history/page.tsx`:
- Around line 215-217: selectedDetail.date.toISOString() can throw for invalid
Date objects; change the key generation used on the SlideIn component to a
non-throwing source such as String(selectedDetail.date?.getTime() ??
selectedDetail.id ?? 'detail') or otherwise guard selectedDetail.date before
calling toISOString. Update the key expression where SlideIn is rendered (the
line using selectedDetail.date.toISOString()) to use getTime() with a fallback
(or another stable id) so rendering cannot crash on invalid dates.
In `@src/components/animations/FadeIn.tsx`:
- Around line 23-33: The component currently always waits the timeout and starts
hidden which still hides content for users with reduced motion; update the logic
in FadeIn (the useEffect, visible state initialization, and any delay handling)
to detect prefers-reduced-motion via window.matchMedia('(prefers-reduced-motion:
reduce)').matches and, when true, set visible immediately (skip setTimeout) and
avoid applying the delay/hidden phase to Component so content renders with
opacity-100 instantly; ensure the existing cleanup that clears the timer is
preserved for the non-reduced-motion path.
In `@src/components/animations/SlideIn.tsx`:
- Around line 50-57: The inline style setting transform in the SlideIn component
(style.transform) overrides the motion-reduce utility and must be removed;
instead, apply transform states via Tailwind-style class names computed from
visible, direction, distance and getOffset (use className merging where the
current transition classes are built) so motion-reduce:transform-none can take
effect, and detect prefers-reduced-motion (e.g., via
window.matchMedia('(prefers-reduced-motion: reduce)') or a useReducedMotion
hook) to bypass any visibility delay/timeout so hidden content is revealed
immediately for reduced-motion users—update the logic around visible, the
timeout that sets visibility, and the style object to rely on classes rather
than inline transform.
In `@src/components/feedback/SkeletonLoader.tsx`:
- Around line 32-64: The code uses count directly to build safeCount and then
Array.from({ length: safeCount }) which will throw for non-finite values like
Infinity; change the calculation of safeCount to first coerce and validate count
(e.g., const normalized = Number.isFinite(count) ? Math.max(0,
Math.floor(count)) : 1), then clamp it to a reasonable maximum (e.g., const
MAX_SKELETONS = 50; const safeCount = Math.max(1, Math.min(MAX_SKELETONS,
normalized))). Update any references to safeCount (used in the single-item
branch and Array.from({ length: safeCount })) so only a finite, bounded integer
is passed to Array.from.
🪄 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: 8c47ae26-1eb7-48f0-8768-6a83cd231a33
📒 Files selected for processing (11)
src/app/dashboard/page.tsxsrc/app/history/page.tsxsrc/app/profile/page.tsxsrc/components/animations/FadeIn.test.tsxsrc/components/animations/FadeIn.tsxsrc/components/animations/SlideIn.test.tsxsrc/components/animations/SlideIn.tsxsrc/components/feedback/Loading.test.tsxsrc/components/feedback/Loading.tsxsrc/components/feedback/SkeletonLoader.test.tsxsrc/components/feedback/SkeletonLoader.tsx
- useReducedMotion フック追加: prefers-reduced-motion を JS で検出 - FadeIn / SlideIn: reduced-motion 時は遅延・hidden 状態をスキップして即時表示 (CSS の motion-reduce ユーティリティだけでは inline style を上書きできない問題を修正) - SkeletonLoader: count を Number.isFinite でガードし、最大 50 件にクランプ - history/page.tsx: 無効な Date でも throw しない key 生成 (getTime + フォールバック) - 関連テスト追加: reduced-motion 経路、Infinity count、各 motion-reduce class https://claude.ai/code/session_01789bk14oLFGxu5bwWAj9Gv
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/hooks/useReducedMotion.ts (1)
24-25: Consider if a legacyaddListenerfallback is needed for your support window.The suggestion to support older
addListener/removeListenerAPIs is technically valid—these are required on iOS 13 and earlier. However, as of 2026, iOS 14+ (with fulladdEventListener('change')support) represents 99%+ of the market, and typical support windows cover iOS 18-26. If your project only targets current/recent iOS versions, the fallback is unnecessary. Add it only if you explicitly need to support older iOS versions (13 or earlier).Optional compatibility patch if needed
useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return; } const mql = window.matchMedia(QUERY); const handler = (event: MediaQueryListEvent) => setPrefersReduced(event.matches); setPrefersReduced(mql.matches); - mql.addEventListener('change', handler); - return () => mql.removeEventListener('change', handler); + if ('addEventListener' in mql) { + mql.addEventListener('change', handler); + return () => mql.removeEventListener('change', handler); + } + mql.addListener(handler); + return () => mql.removeListener(handler); }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useReducedMotion.ts` around lines 24 - 25, The current useReducedMotion hook uses mql.addEventListener('change', handler) and mql.removeEventListener('change', handler); if you need legacy iOS <=13 support, add a fallback: check for mql.addEventListener existence and otherwise call mql.addListener(handler), and similarly check for mql.removeEventListener and otherwise call mql.removeListener(handler). Update the cleanup and setup in the useReducedMotion effect to branch on those APIs (referencing mql, handler, addEventListener/addListener and removeEventListener/removeListener) so older WebKit builds are supported only when required.
🤖 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/animations/FadeIn.test.tsx`:
- Around line 5-16: Tests patch window.matchMedia via mockMatchMedia and use
fake timers; add an afterEach cleanup that restores timers, resets mocks, and
restores the original window.matchMedia to avoid leaks. Save the original
window.matchMedia in a variable (e.g., originalMatchMedia) before any
beforeEach/mockMatchMedia runs, then in afterEach call vi.useRealTimers(),
vi.restoreAllMocks() (or vi.resetAllMocks()/vi.clearAllMocks() as appropriate
for your setup), and set window.matchMedia = originalMatchMedia to restore
global state; reference the mockMatchMedia helper and the test lifecycle hooks
(beforeEach/afterEach) when locating where to add this.
In `@src/hooks/useReducedMotion.test.ts`:
- Around line 1-3: The tests overwrite global window.matchMedia in beforeEach
and per-test cases but never restore it; add an afterEach teardown that restores
the original matchMedia and clears mocks to avoid cross-test pollution: capture
the original (e.g., originalMatchMedia) before you overwrite it (or store it at
module scope), then in afterEach set window.matchMedia back to
originalMatchMedia and call vi.restoreAllMocks() (or
vi.resetAllMocks()/vi.resetModules() as appropriate) so the mutations made in
the tests around useReducedMotion are undone after each test.
---
Nitpick comments:
In `@src/hooks/useReducedMotion.ts`:
- Around line 24-25: The current useReducedMotion hook uses
mql.addEventListener('change', handler) and mql.removeEventListener('change',
handler); if you need legacy iOS <=13 support, add a fallback: check for
mql.addEventListener existence and otherwise call mql.addListener(handler), and
similarly check for mql.removeEventListener and otherwise call
mql.removeListener(handler). Update the cleanup and setup in the
useReducedMotion effect to branch on those APIs (referencing mql, handler,
addEventListener/addListener and removeEventListener/removeListener) so older
WebKit builds are supported only when required.
🪄 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: 903b7c5d-ed35-4c93-8a97-9aa4aaede021
📒 Files selected for processing (9)
src/app/history/page.tsxsrc/components/animations/FadeIn.test.tsxsrc/components/animations/FadeIn.tsxsrc/components/animations/SlideIn.test.tsxsrc/components/animations/SlideIn.tsxsrc/components/feedback/SkeletonLoader.test.tsxsrc/components/feedback/SkeletonLoader.tsxsrc/hooks/useReducedMotion.test.tssrc/hooks/useReducedMotion.ts
✅ Files skipped from review due to trivial changes (2)
- src/components/feedback/SkeletonLoader.test.tsx
- src/components/animations/SlideIn.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- src/components/animations/SlideIn.test.tsx
- src/app/history/page.tsx
- src/components/feedback/SkeletonLoader.tsx
- src/components/animations/FadeIn.tsx
FadeIn / SlideIn / useReducedMotion テストで window.matchMedia と fake timer の状態が他テストにリークしていた問題を修正: - afterEach で originalMatchMedia へ復元 - vi.runOnlyPendingTimers() → vi.useRealTimers() の順で fake timer をクリア - vi.restoreAllMocks() でモックを完全リセット
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/components/animations/SlideIn.test.tsx (1)
96-106: Make the reduced-motion assertion more explicit.
Line 104checksopacity-100, but that substring can be present via utility fallback classes. Prefer asserting the reduced-motion style contract directly (transitionDuration: 0ms) for clearer intent.Suggested test tweak
it('skips animation entirely when prefers-reduced-motion is set', () => { mockMatchMedia(true); render( <SlideIn direction="bottom" distance={24}> <span data-testid="child">child</span> </SlideIn>, ); const wrapper = screen.getByTestId('child').parentElement!; - expect(wrapper.className).toContain('opacity-100'); + expect(wrapper.style.transitionDuration).toBe('0ms'); expect(wrapper.style.transform).toBe(''); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/animations/SlideIn.test.tsx` around lines 96 - 106, The test for reduced-motion should assert the explicit reduced-motion contract rather than relying on utility classes: after calling mockMatchMedia(true) and rendering SlideIn, get the wrapper (parent of data-testid "child") and replace the className assertion with an assertion on the computed style transition duration (e.g., expect(getComputedStyle(wrapper).transitionDuration).toBe('0ms')) and keep the transform check; reference mockMatchMedia, SlideIn and the wrapper element when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/components/animations/SlideIn.test.tsx`:
- Around line 96-106: The test for reduced-motion should assert the explicit
reduced-motion contract rather than relying on utility classes: after calling
mockMatchMedia(true) and rendering SlideIn, get the wrapper (parent of
data-testid "child") and replace the className assertion with an assertion on
the computed style transition duration (e.g.,
expect(getComputedStyle(wrapper).transitionDuration).toBe('0ms')) and keep the
transform check; reference mockMatchMedia, SlideIn and the wrapper element when
making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a323985d-6381-4cb4-b683-122e2fc8be02
📒 Files selected for processing (3)
src/components/animations/FadeIn.test.tsxsrc/components/animations/SlideIn.test.tsxsrc/hooks/useReducedMotion.test.ts
✅ Files skipped from review due to trivial changes (2)
- src/hooks/useReducedMotion.test.ts
- src/components/animations/FadeIn.test.tsx
…e-67-animations-b # Conflicts: # src/app/history/page.tsx
Summary
Issue #67 (Phase 2.3 UI/UX 改善) の PR B: アニメーション & ローディング です。
PR A (レスポンシブ強化) に続く 2 本目の分割 PR で、独立してマージ可能です。
追加コンポーネント
FadeInduration/delay/asをサポートSlideIntop/bottom/left/right方向のスライド。距離・遅延もカスタマイズ可SkeletonLoadertext/rectangle/circleバリアント。countで複数行サポートLoadingLoader2+ メッセージの統一スピナー。fullScreenオーバーレイあり既存ページへの適用
dashboard/page.tsx: ウェルカム/Quick Actions/Getting Started を段階的にフェード&スライドインhistory/page.tsx: 統計カード・カレンダー領域をアニメ化、詳細パネルを右からスライドイン (日付切替時に再生)profile/page.tsx: プロフィールカードをフェードインアクセシビリティ
motion-reduce:ユーティリティでprefers-reduced-motionを尊重SkeletonLoader/Loadingはrole="status"+aria-busy+aria-live="polite"を付与補足
src/app/history/page.tsxの未使用 import (format/parseISO/ja) を削除 (lint 0 warnings 維持)Test plan
npx vitest run src/components/animations src/components/feedbackで新規 26 テスト passauthStoreタイムアウト 1 件失敗は本 PR 無関係)npx eslint変更ファイル全て warnings/errors なしdashboard/history/profileの表示確認https://claude.ai/code/session_01789bk14oLFGxu5bwWAj9Gv
Summary by CodeRabbit
New Features
Tests