Skip to content

feat(ui): アニメーション・ローディングコンポーネント追加 (Issue #67 PR B)#76

Merged
hiiragi17 merged 4 commits into
mainfrom
claude/implement-issue-67-animations-b
Apr 19, 2026
Merged

feat(ui): アニメーション・ローディングコンポーネント追加 (Issue #67 PR B)#76
hiiragi17 merged 4 commits into
mainfrom
claude/implement-issue-67-animations-b

Conversation

@hiiragi17

@hiiragi17 hiiragi17 commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

Issue #67 (Phase 2.3 UI/UX 改善) の PR B: アニメーション & ローディング です。
PR A (レスポンシブ強化) に続く 2 本目の分割 PR で、独立してマージ可能です。

追加コンポーネント

コンポーネント 用途
FadeIn マウント時にふわっと表示。duration / delay / as をサポート
SlideIn top/bottom/left/right 方向のスライド。距離・遅延もカスタマイズ可
SkeletonLoader text/rectangle/circle バリアント。count で複数行サポート
Loading Loader2 + メッセージの統一スピナー。fullScreen オーバーレイあり

既存ページへの適用

  • dashboard/page.tsx: ウェルカム/Quick Actions/Getting Started を段階的にフェード&スライドイン
  • history/page.tsx: 統計カード・カレンダー領域をアニメ化、詳細パネルを右からスライドイン (日付切替時に再生)
  • profile/page.tsx: プロフィールカードをフェードイン

アクセシビリティ

  • 全アニメーションは motion-reduce: ユーティリティで prefers-reduced-motion を尊重
  • SkeletonLoader / Loadingrole="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 テスト pass
  • フルテスト: 306 passed / 6 skipped (既存の authStore タイムアウト 1 件失敗は本 PR 無関係)
  • npx eslint 変更ファイル全て warnings/errors なし
  • dev サーバーで dashboard / history / profile の表示確認
  • OS レベルで「視差効果を減らす」を有効にしアニメーションが抑制されることを確認

https://claude.ai/code/session_01789bk14oLFGxu5bwWAj9Gv

Summary by CodeRabbit

  • New Features

    • UI sections now animate on entry (fade/slide) across Dashboard, History, and Profile; animations respect reduced-motion preferences.
    • New reusable FadeIn and SlideIn animation components and a useReducedMotion hook.
    • New Loading component (sizes, accessible status, optional full-screen) and SkeletonLoader (text/rectangle/circle, count/size).
  • Tests

    • Added comprehensive tests covering animations, loading UI, skeletons, and reduced-motion behavior.

- FadeIn / SlideIn: prefers-reduced-motion を尊重したマウント時アニメーション
- SkeletonLoader: text/rectangle/circle バリアント、複数行サポート
- Loading: Loader2 ベースの統一スピナー、fullScreen オーバーレイ対応
- dashboard/history/profile ページに段階的なフェード・スライド適用
- 各コンポーネントの単体テスト26件追加

https://claude.ai/code/session_01789bk14oLFGxu5bwWAj9Gv
@vercel

vercel Bot commented Apr 18, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
reflecthub Ready Ready Preview, Comment Apr 19, 2026 7:36am

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f2430b7e-4495-4990-8070-79be14e3c915

📥 Commits

Reviewing files that changed from the base of the PR and between 2699e51 and 7440586.

📒 Files selected for processing (2)
  • src/app/dashboard/page.tsx
  • src/app/history/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/dashboard/page.tsx

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Animation Components
src/components/animations/FadeIn.tsx, src/components/animations/FadeIn.test.tsx, src/components/animations/SlideIn.tsx, src/components/animations/SlideIn.test.tsx
Added polymorphic FadeIn and SlideIn components with duration/delay (and distance/direction for SlideIn), reduced-motion support, exported types/props, and comprehensive tests verifying initial/offscreen states, timed transitions, delays/durations, reduced-motion behavior, and polymorphism/className preservation.
Feedback Components
src/components/feedback/Loading.tsx, src/components/feedback/Loading.test.tsx, src/components/feedback/SkeletonLoader.tsx, src/components/feedback/SkeletonLoader.test.tsx
Added Loading (spinner with sizes, optional full-screen overlay, ARIA attributes) and SkeletonLoader (variants: `text
Hook
src/hooks/useReducedMotion.ts, src/hooks/useReducedMotion.test.ts
New useReducedMotion hook that reads (prefers-reduced-motion: reduce), sets initial value safely, subscribes to change events, and returns a boolean. Tests mock matchMedia and verify initial value and listener registration.
Page Integrations
src/app/dashboard/page.tsx, src/app/history/page.tsx, src/app/profile/page.tsx
Integrated animation wrappers into pages: dashboard wraps welcome/quick-actions/getting-started sections with FadeIn/SlideIn; history replaces static containers with animated wrappers, removes unused date-fns imports, and adds a key on the detail panel to force remount; profile wraps ProfileCard in FadeIn.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

I'm a rabbit in the code, light and spry,
I fade in gently, then slide on by,
Spinners whirl and skeletons play,
Motion mindful, clear the way,
Hoppity-hop — the UI says hi! 🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: adding animation and loading components with reference to Issue #67 PR B.
Description check ✅ Passed The PR description comprehensively covers all required sections: overview, specific changes with component details, motivation/background, test results with checkboxes, and includes detailed test plan information.

✏️ 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 claude/implement-issue-67-animations-b

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between cc5648b and 691f252.

📒 Files selected for processing (11)
  • src/app/dashboard/page.tsx
  • src/app/history/page.tsx
  • src/app/profile/page.tsx
  • src/components/animations/FadeIn.test.tsx
  • src/components/animations/FadeIn.tsx
  • src/components/animations/SlideIn.test.tsx
  • src/components/animations/SlideIn.tsx
  • src/components/feedback/Loading.test.tsx
  • src/components/feedback/Loading.tsx
  • src/components/feedback/SkeletonLoader.test.tsx
  • src/components/feedback/SkeletonLoader.tsx

Comment thread src/app/history/page.tsx
Comment thread src/components/animations/FadeIn.tsx
Comment thread src/components/animations/SlideIn.tsx Outdated
Comment thread src/components/feedback/SkeletonLoader.tsx Outdated
- 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

@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: 2

🧹 Nitpick comments (1)
src/hooks/useReducedMotion.ts (1)

24-25: Consider if a legacy addListener fallback is needed for your support window.

The suggestion to support older addListener/removeListener APIs is technically valid—these are required on iOS 13 and earlier. However, as of 2026, iOS 14+ (with full addEventListener('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

📥 Commits

Reviewing files that changed from the base of the PR and between 691f252 and 638ca74.

📒 Files selected for processing (9)
  • src/app/history/page.tsx
  • src/components/animations/FadeIn.test.tsx
  • src/components/animations/FadeIn.tsx
  • src/components/animations/SlideIn.test.tsx
  • src/components/animations/SlideIn.tsx
  • src/components/feedback/SkeletonLoader.test.tsx
  • src/components/feedback/SkeletonLoader.tsx
  • src/hooks/useReducedMotion.test.ts
  • src/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

Comment thread src/components/animations/FadeIn.test.tsx
Comment thread src/hooks/useReducedMotion.test.ts Outdated
FadeIn / SlideIn / useReducedMotion テストで window.matchMedia と
fake timer の状態が他テストにリークしていた問題を修正:

- afterEach で originalMatchMedia へ復元
- vi.runOnlyPendingTimers() → vi.useRealTimers() の順で fake timer をクリア
- vi.restoreAllMocks() でモックを完全リセット

@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.

🧹 Nitpick comments (1)
src/components/animations/SlideIn.test.tsx (1)

96-106: Make the reduced-motion assertion more explicit.

Line 104 checks opacity-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

📥 Commits

Reviewing files that changed from the base of the PR and between 638ca74 and 2699e51.

📒 Files selected for processing (3)
  • src/components/animations/FadeIn.test.tsx
  • src/components/animations/SlideIn.test.tsx
  • src/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
@hiiragi17 hiiragi17 merged commit 9f46f9d into main Apr 19, 2026
3 checks passed
@hiiragi17 hiiragi17 deleted the claude/implement-issue-67-animations-b branch April 19, 2026 07:38
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.

2 participants