実装計画書 - モダンタスク管理アプリ
目標
React と Vite を使用して、視覚的に魅力的でレスポンシブなタスク管理アプリケーションを作成します。「グラスモーフィズム(Glassmorphism)」を取り入れたモダンでプレミアムなデザイン、スムーズなアニメーション、洗練されたユーザーインターフェースを目指します。
ユーザーレビューが必要な事項
NOTE
Material UIやChakra UIなどの外部UIライブラリは使用しません。システム指示に従い、最大限のカスタマイズ性と独自性を確保するため、すべてのスタイリングは Vanilla CSS で行います。
提案する変更内容
技術スタック
- ビルドツール: Vite
- フレームワーク: React
- スタイリング: Vanilla CSS (CSS変数を使用したテーマ管理)
- フォント: Google Fonts (モダンな印象を与える Inter または Noto Sans JP)
[Design System]
[NEW] index.css
- カラーパレット用のCSS変数を定義 (ダークモダンテーマ):
- 背景: 深みのあるリッチなグラデーション
- 表面: 半透明のガラス効果 (backdrop-filter)
- プライマリ: 鮮やかなアクセントカラー (例: ネオンパープル/ブルー)
- テキスト: 高コントラストな白/グレー
- グローバルリセットとタイポグラフィ設定。
📄 index.css のソースコードを見る
css
:root {
/* Color Palette - Dark Modern Glassmorphism */
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--accent-primary: #8b5cf6;
/* Violet */
--accent-secondary: #ec4899;
/* Pink */
--accent-glow: rgba(139, 92, 246, 0.5);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--glass-bg: rgba(30, 41, 59, 0.7);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-highlight: rgba(255, 255, 255, 0.05);
--shadow-lg: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 20px var(--accent-glow);
--radius-lg: 16px;
--radius-md: 12px;
--radius-sm: 8px;
--font-main: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
--transition-fast: 0.2s ease;
--transition-smooth: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-main);
background-color: var(--bg-primary);
/* Rich gradient background */
background-image:
radial-gradient(circle at 10% 20%, rgba(139, 92, 246, 0.15) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(236, 72, 153, 0.15) 0%, transparent 40%);
background-attachment: fixed;
color: var(--text-primary);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
line-height: 1.6;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--bg-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
}
/* Utilities */
.container {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-lg);
border-radius: var(--radius-lg);
}
.btn {
border: none;
background: var(--accent-primary);
color: white;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-md);
font-weight: 600;
cursor: pointer;
transition: var(--transition-fast);
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 1rem;
}
.btn:hover {
background: #7c3aed;
box-shadow: var(--shadow-glow);
transform: translateY(-1px);
}
.input-field {
width: 100%;
padding: 1rem;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 1rem;
transition: var(--transition-fast);
outline: none;
}
.input-field:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
}
/* Typography */
h1 {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 2rem;
background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
letter-spacing: -0.02em;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn var(--transition-smooth) forwards;
}[Components]
[NEW] App.jsx
- 背景グラデーションを含むメインレイアウトコンテナ。
- アプリケーションの状態管理 (タスクのリスト)。
- ローカルストレージへの保存処理.
[NEW] components/TaskForm.jsx
- フローティングラベルまたはアニメーション付きフォーカス状態を持つスタイリッシュな入力フィールド。
- ホバー時に光る効果を持つ「タスク追加」ボタン。
[NEW] components/TaskList.jsx
- タスクリストの表示領域。
- タスクがない場合のフレンドリーなメッセージ/アイコン表示。
[NEW] components/TaskItem.jsx
- グラスモーフィズムスタイルの個別タスクカード。
- 完了チェックボックス (アニメーション付き)。
- 削除ボタン (ホバー時に表示、またはさりげなく常時表示)。
📦 全コンポーネントのソースコードをコードグループで見る
jsx
import { useState, useEffect } from 'react';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';
function App() {
const [tasks, setTasks] = useState(() => {
const saved = localStorage.getItem('tasks');
if (saved) {
try {
return JSON.parse(saved);
} catch (e) {
return [];
}
}
return [];
});
useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(tasks));
}, [tasks]);
const addTask = (text) => {
const newTask = {
id: Date.now(),
text,
completed: false
};
setTasks([newTask, ...tasks]);
};
const toggleTask = (id) => {
setTasks(tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
));
};
const deleteTask = (id) => {
setTasks(tasks.filter(task => task.id !== id));
};
return (
<div className="container">
<header style={{ textAlign: 'center', marginBottom: '3rem', paddingTop: '4rem' }}>
<h1>Task Manager</h1>
<p style={{ color: 'var(--text-secondary)', fontSize: '1.2rem' }}>
シンプルで美しいタスク管理
</p>
</header>
<main>
<TaskForm onAddTask={addTask} />
<div style={{ marginTop: '2rem' }}>
<h2 style={{
fontSize: '1.5rem',
marginBottom: '1rem',
color: 'var(--text-primary)',
fontWeight: 600
}}>
タスク一覧
</h2>
<TaskList
tasks={tasks}
onToggle={toggleTask}
onDelete={deleteTask}
/>
</div>
</main>
<footer style={{
textAlign: 'center',
marginTop: '4rem',
paddingBottom: '2rem',
color: 'var(--text-muted)'
}}>
<p>© {new Date().getFullYear()} Task Manager App</p>
</footer>
</div>
);
}
export default App;jsx
import { useState } from 'react';
function TaskForm({ onAddTask }) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
onAddTask(text);
setText('');
};
return (
<form onSubmit={handleSubmit} className="glass-panel" style={{ padding: '1.5rem', marginBottom: '2rem' }}>
<div style={{ display: 'flex', gap: '1rem' }}>
<input
type="text"
className="input-field"
placeholder="新しいタスクを入力..."
value={text}
onChange={(e) => setText(e.target.value)}
autoFocus
/>
<button type="submit" className="btn">
<span>追加</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
</form>
);
}
export default TaskForm;jsx
import TaskItem from './TaskItem';
function TaskList({ tasks, onToggle, onDelete }) {
if (tasks.length === 0) {
return (
<div
className="glass-panel"
style={{
padding: '3rem',
textAlign: 'center',
color: 'var(--text-muted)'
}}
>
<p style={{ fontSize: '1.1rem' }}>タスクがありません</p>
<p style={{ fontSize: '0.9rem', marginTop: '0.5rem' }}>新しいタスクを追加して生産性を上げましょう✨</p>
</div>
);
}
return (
<div className="animate-fade-in">
{tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</div>
);
}
export default TaskList;jsx
function TaskItem({ task, onToggle, onDelete }) {
return (
<div
className="glass-panel"
style={{
display: 'flex',
alignItems: 'center',
padding: '1rem 1.5rem',
marginBottom: '1rem',
transition: 'all 0.3s ease',
opacity: task.completed ? 0.7 : 1,
borderLeft: task.completed ? '4px solid var(--text-muted)' : '4px solid var(--accent-secondary)'
}}
>
<label style={{ display: 'flex', alignItems: 'center', flex: 1, cursor: 'pointer' }}>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggle(task.id)}
style={{
width: '1.2rem',
height: '1.2rem',
marginRight: '1rem',
accentColor: 'var(--accent-primary)',
cursor: 'pointer'
}}
/>
<span style={{
fontSize: '1.1rem',
textDecoration: task.completed ? 'line-through' : 'none',
color: task.completed ? 'var(--text-muted)' : 'var(--text-primary)',
transition: 'all 0.2s ease'
}}>
{task.text}
</span>
</label>
<button
onClick={() => onDelete(task.id)}
className="btn"
style={{
background: 'rgba(255,255,255,0.05)',
padding: '0.5rem',
color: '#ef4444',
marginLeft: '1rem'
}}
aria-label="Delete"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
);
}
export default TaskItem;🎓 練習ドリル:自力で作ってみよう!
このプロジェクトを自力で再現するためのステップバイステップ・ドリルです。
ステップ1: 環境構築
npm create vite@latest my-task-app -- --template reactでプロジェクトを作成。App.jsxとindex.cssを空にします。
ステップ2: デザインの基礎 (CSS)
index.cssに:root変数を定義し、深いグラデーションの背景を設定してください。- グラスモーフィズム(背景ぼかし)を実現するために
backdrop-filter: blur()を使ってみましょう。
ステップ3: 状態管理 (App.jsx)
useStateを使って、タスクの配列(tasks)を管理してください。- タスクを追加する関数
addTaskを作成してください。
ステップ4: コンポーネント分割
- TaskForm: 入力を受け取り、ボタンクリックで
addTaskを呼び出す。 - TaskList:
tasks.map()を使って複数のTaskItemを表示する。 - TaskItem: 完了/未完了の切り替えと、削除機能を実装する。
ステップ5: 永続化
useEffectを2つ使い、「初回読み込み時の取得」と「データ更新時の保存」をlocalStorageに対して実装してください。
検証計画
自動テスト
- ビルド検証:
npm run buildを実行し、エラーなくビルドできることを確認します。
手動検証
- 開発サーバーの起動:
npm run devを実行し、ブラウザで開きます。 - 表示確認:
- 背景グラデーションとグラスモーフィズム効果が正しく適用されているか確認します。
- 異なるウィンドウサイズでのレスポンスを確認します。
- 機能確認:
- 新しいタスクを追加 -> アニメーション付きでリストに追加されること。
- タスクを完了にする -> テキストに取り消し線が付き、表示が薄くなること。
- タスクを削除 -> リストから削除されること。
- ページをリロード -> タスクが保持されていること (ローカルストレージ)。