ux-toolkit 0.1.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -7
- package/agents/card-reviewer.md +173 -0
- package/agents/comparison-reviewer.md +143 -0
- package/agents/density-reviewer.md +207 -0
- package/agents/detail-page-reviewer.md +143 -0
- package/agents/editor-reviewer.md +165 -0
- package/agents/form-reviewer.md +156 -0
- package/agents/game-ui-reviewer.md +181 -0
- package/agents/list-page-reviewer.md +132 -0
- package/agents/navigation-reviewer.md +145 -0
- package/agents/panel-reviewer.md +182 -0
- package/agents/replay-reviewer.md +174 -0
- package/agents/settings-reviewer.md +166 -0
- package/agents/ux-auditor.md +145 -45
- package/agents/ux-engineer.md +211 -38
- package/dist/cli.js +172 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +172 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -4
- package/dist/index.d.ts +128 -4
- package/dist/index.js +172 -5
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/skills/canvas-grid-patterns/SKILL.md +367 -0
- package/skills/comparison-patterns/SKILL.md +354 -0
- package/skills/data-density-patterns/SKILL.md +493 -0
- package/skills/detail-page-patterns/SKILL.md +522 -0
- package/skills/drag-drop-patterns/SKILL.md +406 -0
- package/skills/editor-workspace-patterns/SKILL.md +552 -0
- package/skills/event-timeline-patterns/SKILL.md +542 -0
- package/skills/form-patterns/SKILL.md +608 -0
- package/skills/info-card-patterns/SKILL.md +531 -0
- package/skills/keyboard-shortcuts-patterns/SKILL.md +365 -0
- package/skills/list-page-patterns/SKILL.md +351 -0
- package/skills/modal-patterns/SKILL.md +750 -0
- package/skills/navigation-patterns/SKILL.md +476 -0
- package/skills/page-structure-patterns/SKILL.md +271 -0
- package/skills/playback-replay-patterns/SKILL.md +695 -0
- package/skills/react-ux-patterns/SKILL.md +434 -0
- package/skills/split-panel-patterns/SKILL.md +609 -0
- package/skills/status-visualization-patterns/SKILL.md +635 -0
- package/skills/toast-notification-patterns/SKILL.md +207 -0
- package/skills/turn-based-ui-patterns/SKILL.md +506 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: toast-notification-patterns
|
|
3
|
+
description: Toast notifications, snackbars, alerts, and system feedback patterns
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Toast & Notification Patterns
|
|
8
|
+
|
|
9
|
+
## 1. Toast Types
|
|
10
|
+
|
|
11
|
+
**Success** - Confirmation of completed actions
|
|
12
|
+
**Error** - Critical failures requiring attention
|
|
13
|
+
**Warning** - Important non-critical issues
|
|
14
|
+
**Info** - Neutral system messages
|
|
15
|
+
**Loading** - In-progress operations with optional progress indicator
|
|
16
|
+
|
|
17
|
+
## 2. Toast Anatomy
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
interface Toast {
|
|
21
|
+
id: string;
|
|
22
|
+
type: 'success' | 'error' | 'warning' | 'info' | 'loading';
|
|
23
|
+
message: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
icon?: React.ReactNode;
|
|
26
|
+
action?: { label: string; onClick: () => void };
|
|
27
|
+
dismissible?: boolean;
|
|
28
|
+
duration?: number;
|
|
29
|
+
progress?: number; // 0-100 for loading toasts
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Core Elements:**
|
|
34
|
+
- Icon (type-specific, 16-20px)
|
|
35
|
+
- Message (bold, 14px)
|
|
36
|
+
- Description (optional, muted, 13px)
|
|
37
|
+
- Action button (optional, primary action)
|
|
38
|
+
- Dismiss button (X icon, always visible for errors)
|
|
39
|
+
- Progress bar (bottom edge, for timed auto-dismiss)
|
|
40
|
+
|
|
41
|
+
## 3. Positioning & Stacking
|
|
42
|
+
|
|
43
|
+
**Preferred positions:**
|
|
44
|
+
- `top-right` - Default, least intrusive
|
|
45
|
+
- `bottom-center` - Mobile-friendly, undo actions
|
|
46
|
+
- `top-center` - Critical system alerts
|
|
47
|
+
|
|
48
|
+
**Stacking behavior:**
|
|
49
|
+
```tsx
|
|
50
|
+
// Max 3 visible, newest on top
|
|
51
|
+
const MAX_VISIBLE = 3;
|
|
52
|
+
const STACK_OFFSET = 8; // px between toasts
|
|
53
|
+
const STACK_DIRECTION = 'down'; // or 'up' for bottom positioning
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Toasts stack vertically with 8px gap. When exceeding max, oldest dismissed first (FIFO).
|
|
57
|
+
|
|
58
|
+
## 4. Timing Rules
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
const DURATIONS = {
|
|
62
|
+
success: 3000, // 3s - Quick confirmation
|
|
63
|
+
info: 4000, // 4s - More reading time
|
|
64
|
+
warning: 6000, // 6s - Important message
|
|
65
|
+
error: Infinity, // Persist until user dismisses
|
|
66
|
+
loading: Infinity // Persist until operation completes
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Auto-dismiss logic:**
|
|
71
|
+
- Pause timer on hover/focus
|
|
72
|
+
- Resume on mouse leave
|
|
73
|
+
- Reset on content update
|
|
74
|
+
- Manual dismiss always available (except loading)
|
|
75
|
+
|
|
76
|
+
## 5. Accessibility
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<div
|
|
80
|
+
role="alert"
|
|
81
|
+
aria-live="polite" // 'assertive' for errors
|
|
82
|
+
aria-atomic="true"
|
|
83
|
+
className="toast"
|
|
84
|
+
>
|
|
85
|
+
<svg aria-hidden="true">{icon}</svg>
|
|
86
|
+
<div>
|
|
87
|
+
<p className="font-semibold">{message}</p>
|
|
88
|
+
{description && <p className="text-sm text-muted">{description}</p>}
|
|
89
|
+
</div>
|
|
90
|
+
<button aria-label="Dismiss notification">{dismissIcon}</button>
|
|
91
|
+
</div>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Focus management:**
|
|
95
|
+
- Errors: Move focus to toast on appear (if user not typing)
|
|
96
|
+
- Actions: Button must be keyboard accessible
|
|
97
|
+
- Dismiss: ESC key dismisses focused toast
|
|
98
|
+
|
|
99
|
+
## 6. Code Examples
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// Toast Provider
|
|
103
|
+
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
104
|
+
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
105
|
+
|
|
106
|
+
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
|
|
107
|
+
const id = crypto.randomUUID();
|
|
108
|
+
setToasts(prev => [{ ...toast, id }, ...prev].slice(0, MAX_VISIBLE));
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const removeToast = useCallback((id: string) => {
|
|
112
|
+
setToasts(prev => prev.filter(t => t.id !== id));
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<ToastContext.Provider value={{ addToast, removeToast }}>
|
|
117
|
+
{children}
|
|
118
|
+
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
|
|
119
|
+
{toasts.map((toast, idx) => (
|
|
120
|
+
<ToastItem key={toast.id} toast={toast} onDismiss={removeToast} />
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
</ToastContext.Provider>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Hook
|
|
128
|
+
export const useToast = () => useContext(ToastContext);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 7. Toast Queue Management
|
|
132
|
+
|
|
133
|
+
**Deduplication strategy:**
|
|
134
|
+
```tsx
|
|
135
|
+
const addToast = (toast: Omit<Toast, 'id'>) => {
|
|
136
|
+
setToasts(prev => {
|
|
137
|
+
// Remove duplicate messages within 5s window
|
|
138
|
+
const isDuplicate = prev.some(t =>
|
|
139
|
+
t.message === toast.message &&
|
|
140
|
+
Date.now() - t.createdAt < 5000
|
|
141
|
+
);
|
|
142
|
+
if (isDuplicate) return prev;
|
|
143
|
+
|
|
144
|
+
return [{ ...toast, id: uuid(), createdAt: Date.now() }, ...prev]
|
|
145
|
+
.slice(0, MAX_VISIBLE);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Priority system:**
|
|
151
|
+
Errors > Warnings > Loading > Success > Info
|
|
152
|
+
|
|
153
|
+
## 8. Action Toasts
|
|
154
|
+
|
|
155
|
+
**Undo pattern:**
|
|
156
|
+
```tsx
|
|
157
|
+
toast.success('Item deleted', {
|
|
158
|
+
action: {
|
|
159
|
+
label: 'Undo',
|
|
160
|
+
onClick: () => restoreItem(id)
|
|
161
|
+
},
|
|
162
|
+
duration: 8000 // Longer for user to react
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Retry pattern:**
|
|
167
|
+
```tsx
|
|
168
|
+
toast.error('Upload failed', {
|
|
169
|
+
action: {
|
|
170
|
+
label: 'Retry',
|
|
171
|
+
onClick: () => retryUpload()
|
|
172
|
+
},
|
|
173
|
+
duration: Infinity
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## 9. Persistent vs Transient
|
|
178
|
+
|
|
179
|
+
**Use persistent (no auto-dismiss) for:**
|
|
180
|
+
- Errors requiring user action
|
|
181
|
+
- Loading states
|
|
182
|
+
- Toasts with action buttons (undo, retry)
|
|
183
|
+
- Form validation summaries
|
|
184
|
+
|
|
185
|
+
**Use transient (auto-dismiss) for:**
|
|
186
|
+
- Success confirmations
|
|
187
|
+
- Informational updates
|
|
188
|
+
- Non-critical warnings
|
|
189
|
+
- Progress milestones
|
|
190
|
+
|
|
191
|
+
## 10. Audit Checklist
|
|
192
|
+
|
|
193
|
+
- [ ] **[CRITICAL]** Errors persist until manually dismissed
|
|
194
|
+
- [ ] **[CRITICAL]** All interactive elements keyboard accessible (Tab, Enter, ESC)
|
|
195
|
+
- [ ] **[CRITICAL]** role="alert" and aria-live present
|
|
196
|
+
- [ ] **[MAJOR]** Icons have aria-hidden="true"
|
|
197
|
+
- [ ] **[MAJOR]** Dismiss buttons have aria-label
|
|
198
|
+
- [ ] **[MAJOR]** Auto-dismiss pauses on hover/focus
|
|
199
|
+
- [ ] **[MAJOR]** Maximum 3 toasts visible simultaneously
|
|
200
|
+
- [ ] **[MAJOR]** Toast width constrained (max 400px)
|
|
201
|
+
- [ ] **[MINOR]** Success toasts auto-dismiss in 3-4s
|
|
202
|
+
- [ ] **[MINOR]** Toast animates in/out smoothly (200-300ms)
|
|
203
|
+
- [ ] **[MINOR]** Duplicate messages within 5s deduplicated
|
|
204
|
+
- [ ] **[MINOR]** Loading toasts show progress indicator
|
|
205
|
+
- [ ] **[MINOR]** Action toasts have 6-8s duration minimum
|
|
206
|
+
- [ ] **[MINOR]** Mobile: Bottom positioning preferred
|
|
207
|
+
- [ ] **[MINOR]** Color contrast meets WCAG AA (4.5:1)
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# Turn-Based & Phase UI Patterns
|
|
2
|
+
|
|
3
|
+
Patterns for turn-based games, phase-driven workflows, and sequential state machines. Applies to strategy games, board games, approval workflows, and any UI with distinct phases or turns.
|
|
4
|
+
|
|
5
|
+
## When to Use This Skill
|
|
6
|
+
|
|
7
|
+
- Turn-based strategy games (XCOM, Civilization, BattleTech)
|
|
8
|
+
- Board game implementations
|
|
9
|
+
- Multi-step approval workflows
|
|
10
|
+
- Wizard/stepper with dependent phases
|
|
11
|
+
- Auction/bidding systems
|
|
12
|
+
- Real-time strategy games with pause
|
|
13
|
+
- Collaborative editing with turn-taking
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Concepts
|
|
18
|
+
|
|
19
|
+
### Phase vs Turn vs Round
|
|
20
|
+
|
|
21
|
+
| Term | Definition | Example |
|
|
22
|
+
|------|------------|---------|
|
|
23
|
+
| **Phase** | Distinct action type within a turn | Movement Phase, Attack Phase, End Phase |
|
|
24
|
+
| **Turn** | One player's complete action sequence | Player 1's turn, Player 2's turn |
|
|
25
|
+
| **Round** | Complete cycle of all players' turns | Round 1 = All players take one turn |
|
|
26
|
+
|
|
27
|
+
### Phase Flow Types
|
|
28
|
+
|
|
29
|
+
| Type | Description | Use Case |
|
|
30
|
+
|------|-------------|----------|
|
|
31
|
+
| **Linear** | Phases always in same order | Most board games |
|
|
32
|
+
| **Conditional** | Some phases may be skipped | MTG: Skip combat if no creatures |
|
|
33
|
+
| **Parallel** | Multiple phases can be active | RTS games with simultaneous actions |
|
|
34
|
+
| **Interruptible** | Opponent can interrupt phases | Stack-based card games |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Audit Checklist
|
|
39
|
+
|
|
40
|
+
### Phase Banner/Indicator
|
|
41
|
+
- [ ] [CRITICAL] Current phase clearly displayed at all times
|
|
42
|
+
- [ ] [CRITICAL] Phase name readable and prominent
|
|
43
|
+
- [ ] [MAJOR] Visual distinction between phases (color/icon)
|
|
44
|
+
- [ ] [MAJOR] Phase transition is obvious (animation/sound)
|
|
45
|
+
- [ ] [MINOR] Phase description/tooltip explains what can be done
|
|
46
|
+
- [ ] [MINOR] Upcoming phase indicated (next phase preview)
|
|
47
|
+
|
|
48
|
+
### Turn Indicator
|
|
49
|
+
- [ ] [CRITICAL] Whose turn it is clearly shown
|
|
50
|
+
- [ ] [CRITICAL] Active player highlighted in player list
|
|
51
|
+
- [ ] [MAJOR] Turn number displayed
|
|
52
|
+
- [ ] [MAJOR] Different styling for "your turn" vs "opponent's turn"
|
|
53
|
+
- [ ] [MINOR] Turn timer (if applicable)
|
|
54
|
+
- [ ] [MINOR] Turn history accessible
|
|
55
|
+
|
|
56
|
+
### Action Availability
|
|
57
|
+
- [ ] [CRITICAL] Available actions clearly enabled
|
|
58
|
+
- [ ] [CRITICAL] Unavailable actions clearly disabled (grayed out)
|
|
59
|
+
- [ ] [MAJOR] Reason for unavailability shown (tooltip/message)
|
|
60
|
+
- [ ] [MAJOR] No clickable actions that do nothing
|
|
61
|
+
- [ ] [MINOR] Keyboard shortcuts for common actions
|
|
62
|
+
- [ ] [MINOR] Action cost displayed (if applicable)
|
|
63
|
+
|
|
64
|
+
### Phase-Specific Actions
|
|
65
|
+
- [ ] [CRITICAL] Action bar changes based on current phase
|
|
66
|
+
- [ ] [CRITICAL] Only phase-appropriate actions shown
|
|
67
|
+
- [ ] [MAJOR] Default/primary action emphasized
|
|
68
|
+
- [ ] [MAJOR] "End Phase" / "Pass" button always accessible
|
|
69
|
+
- [ ] [MINOR] Quick action buttons for common operations
|
|
70
|
+
- [ ] [MINOR] Action confirmation for irreversible actions
|
|
71
|
+
|
|
72
|
+
### State Feedback
|
|
73
|
+
- [ ] [CRITICAL] Game state updates immediately on action
|
|
74
|
+
- [ ] [CRITICAL] Animation indicates what changed
|
|
75
|
+
- [ ] [MAJOR] Sound feedback for actions (if applicable)
|
|
76
|
+
- [ ] [MAJOR] Undo available within phase (if allowed by rules)
|
|
77
|
+
- [ ] [MINOR] Action log shows what happened
|
|
78
|
+
- [ ] [MINOR] Pending actions queue visible
|
|
79
|
+
|
|
80
|
+
### Waiting States
|
|
81
|
+
- [ ] [CRITICAL] Clear indication when waiting for opponent
|
|
82
|
+
- [ ] [MAJOR] "Opponent is thinking..." or similar message
|
|
83
|
+
- [ ] [MAJOR] Estimated wait time or activity indicator
|
|
84
|
+
- [ ] [MINOR] Allow reviewing state while waiting
|
|
85
|
+
- [ ] [MINOR] Notification when it's your turn again
|
|
86
|
+
|
|
87
|
+
### Phase Transitions
|
|
88
|
+
- [ ] [CRITICAL] Clear visual/audio cue when phase changes
|
|
89
|
+
- [ ] [MAJOR] Brief pause between phases for readability
|
|
90
|
+
- [ ] [MAJOR] Auto-advance when phase has no actions
|
|
91
|
+
- [ ] [MINOR] Transition animation (slide, fade, etc.)
|
|
92
|
+
- [ ] [MINOR] Summary of phase results before moving on
|
|
93
|
+
|
|
94
|
+
### Error Prevention
|
|
95
|
+
- [ ] [CRITICAL] Cannot take actions out of turn
|
|
96
|
+
- [ ] [CRITICAL] Cannot perform invalid actions
|
|
97
|
+
- [ ] [MAJOR] Confirmation for game-ending actions
|
|
98
|
+
- [ ] [MAJOR] Warning for suboptimal moves (optional)
|
|
99
|
+
- [ ] [MINOR] "Are you sure?" for skipping phases with unused resources
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Implementation Patterns
|
|
104
|
+
|
|
105
|
+
### Phase State Machine
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
enum GamePhase {
|
|
109
|
+
Initiative = 'INITIATIVE',
|
|
110
|
+
Movement = 'MOVEMENT',
|
|
111
|
+
Attack = 'ATTACK',
|
|
112
|
+
PhysicalAttack = 'PHYSICAL_ATTACK',
|
|
113
|
+
Heat = 'HEAT',
|
|
114
|
+
End = 'END',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface PhaseConfig {
|
|
118
|
+
name: string;
|
|
119
|
+
description: string;
|
|
120
|
+
icon: string;
|
|
121
|
+
color: string;
|
|
122
|
+
allowedActions: ActionType[];
|
|
123
|
+
canSkip: boolean;
|
|
124
|
+
autoAdvance: boolean; // Auto-advance when no actions available
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const PHASE_CONFIG: Record<GamePhase, PhaseConfig> = {
|
|
128
|
+
[GamePhase.Movement]: {
|
|
129
|
+
name: 'Movement Phase',
|
|
130
|
+
description: 'Move your units across the battlefield',
|
|
131
|
+
icon: 'move',
|
|
132
|
+
color: 'blue',
|
|
133
|
+
allowedActions: ['move', 'stand', 'prone', 'sprint'],
|
|
134
|
+
canSkip: true,
|
|
135
|
+
autoAdvance: false,
|
|
136
|
+
},
|
|
137
|
+
[GamePhase.Attack]: {
|
|
138
|
+
name: 'Weapon Attack Phase',
|
|
139
|
+
description: 'Fire weapons at enemy targets',
|
|
140
|
+
icon: 'target',
|
|
141
|
+
color: 'red',
|
|
142
|
+
allowedActions: ['fire', 'aim', 'torso_twist'],
|
|
143
|
+
canSkip: true,
|
|
144
|
+
autoAdvance: false,
|
|
145
|
+
},
|
|
146
|
+
// ... more phases
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Phase Banner Component
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
interface PhaseBannerProps {
|
|
154
|
+
phase: GamePhase;
|
|
155
|
+
turn: number;
|
|
156
|
+
activeSide: 'player' | 'opponent';
|
|
157
|
+
isPlayerTurn: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function PhaseBanner({ phase, turn, activeSide, isPlayerTurn }: PhaseBannerProps) {
|
|
161
|
+
const config = PHASE_CONFIG[phase];
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
className={`
|
|
166
|
+
flex items-center justify-between px-6 py-3
|
|
167
|
+
${isPlayerTurn ? 'bg-blue-900' : 'bg-gray-800'}
|
|
168
|
+
border-b-2 ${isPlayerTurn ? 'border-blue-500' : 'border-gray-600'}
|
|
169
|
+
`}
|
|
170
|
+
role="status"
|
|
171
|
+
aria-live="polite"
|
|
172
|
+
>
|
|
173
|
+
{/* Turn Info */}
|
|
174
|
+
<div className="flex items-center gap-4">
|
|
175
|
+
<span className="text-sm text-gray-400">Turn {turn}</span>
|
|
176
|
+
<div className={`
|
|
177
|
+
px-3 py-1 rounded-full text-sm font-medium
|
|
178
|
+
${isPlayerTurn
|
|
179
|
+
? 'bg-emerald-500/20 text-emerald-400'
|
|
180
|
+
: 'bg-amber-500/20 text-amber-400'
|
|
181
|
+
}
|
|
182
|
+
`}>
|
|
183
|
+
{isPlayerTurn ? 'Your Turn' : "Opponent's Turn"}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Phase Info */}
|
|
188
|
+
<div className="flex items-center gap-3">
|
|
189
|
+
<PhaseIcon phase={phase} className="w-6 h-6" style={{ color: config.color }} />
|
|
190
|
+
<div>
|
|
191
|
+
<div className="font-semibold text-white">{config.name}</div>
|
|
192
|
+
<div className="text-xs text-gray-400">{config.description}</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Phase Progress */}
|
|
197
|
+
<PhaseProgressIndicator currentPhase={phase} />
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Phase Progress Indicator
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
function PhaseProgressIndicator({ currentPhase }: { currentPhase: GamePhase }) {
|
|
207
|
+
const phases = Object.values(GamePhase);
|
|
208
|
+
const currentIndex = phases.indexOf(currentPhase);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div className="flex items-center gap-2" aria-label="Phase progress">
|
|
212
|
+
{phases.map((phase, index) => {
|
|
213
|
+
const config = PHASE_CONFIG[phase];
|
|
214
|
+
const status = index < currentIndex ? 'completed'
|
|
215
|
+
: index === currentIndex ? 'current'
|
|
216
|
+
: 'upcoming';
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<React.Fragment key={phase}>
|
|
220
|
+
{index > 0 && (
|
|
221
|
+
<div className={`w-8 h-0.5 ${
|
|
222
|
+
status === 'completed' ? 'bg-emerald-500' : 'bg-gray-600'
|
|
223
|
+
}`} />
|
|
224
|
+
)}
|
|
225
|
+
<div
|
|
226
|
+
className={`
|
|
227
|
+
w-8 h-8 rounded-full flex items-center justify-center
|
|
228
|
+
${status === 'current' ? 'bg-accent ring-2 ring-accent/50' : ''}
|
|
229
|
+
${status === 'completed' ? 'bg-emerald-500' : ''}
|
|
230
|
+
${status === 'upcoming' ? 'bg-gray-700' : ''}
|
|
231
|
+
`}
|
|
232
|
+
title={config.name}
|
|
233
|
+
>
|
|
234
|
+
<PhaseIcon phase={phase} className="w-4 h-4" />
|
|
235
|
+
</div>
|
|
236
|
+
</React.Fragment>
|
|
237
|
+
);
|
|
238
|
+
})}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Action Bar Component
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
interface ActionBarProps {
|
|
248
|
+
phase: GamePhase;
|
|
249
|
+
canAct: boolean;
|
|
250
|
+
canUndo: boolean;
|
|
251
|
+
onAction: (action: string) => void;
|
|
252
|
+
onEndPhase: () => void;
|
|
253
|
+
onUndo: () => void;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function ActionBar({ phase, canAct, canUndo, onAction, onEndPhase, onUndo }: ActionBarProps) {
|
|
257
|
+
const config = PHASE_CONFIG[phase];
|
|
258
|
+
const actions = config.allowedActions;
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
className="flex items-center justify-between px-6 py-4 bg-gray-900 border-t border-gray-700"
|
|
263
|
+
role="toolbar"
|
|
264
|
+
aria-label="Phase actions"
|
|
265
|
+
>
|
|
266
|
+
{/* Phase-specific actions */}
|
|
267
|
+
<div className="flex items-center gap-2">
|
|
268
|
+
{actions.map(action => (
|
|
269
|
+
<ActionButton
|
|
270
|
+
key={action}
|
|
271
|
+
action={action}
|
|
272
|
+
disabled={!canAct}
|
|
273
|
+
onClick={() => onAction(action)}
|
|
274
|
+
/>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Universal actions */}
|
|
279
|
+
<div className="flex items-center gap-2">
|
|
280
|
+
{canUndo && (
|
|
281
|
+
<Button variant="ghost" onClick={onUndo}>
|
|
282
|
+
<UndoIcon className="w-4 h-4 mr-2" />
|
|
283
|
+
Undo
|
|
284
|
+
</Button>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
<Button
|
|
288
|
+
variant="primary"
|
|
289
|
+
onClick={onEndPhase}
|
|
290
|
+
disabled={!canAct}
|
|
291
|
+
>
|
|
292
|
+
{config.canSkip ? 'End Phase' : 'Continue'}
|
|
293
|
+
<ChevronRightIcon className="w-4 h-4 ml-2" />
|
|
294
|
+
</Button>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Waiting State Component
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
function WaitingForOpponent({ opponentName, lastAction }: WaitingProps) {
|
|
305
|
+
return (
|
|
306
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
307
|
+
<div className="bg-gray-800 rounded-lg p-8 text-center max-w-md">
|
|
308
|
+
<div className="animate-pulse mb-4">
|
|
309
|
+
<ClockIcon className="w-12 h-12 mx-auto text-amber-400" />
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<h2 className="text-xl font-bold text-white mb-2">
|
|
313
|
+
Waiting for {opponentName}
|
|
314
|
+
</h2>
|
|
315
|
+
|
|
316
|
+
<p className="text-gray-400 mb-4">
|
|
317
|
+
{lastAction
|
|
318
|
+
? `Last action: ${lastAction}`
|
|
319
|
+
: 'Opponent is thinking...'}
|
|
320
|
+
</p>
|
|
321
|
+
|
|
322
|
+
<div className="flex justify-center gap-2">
|
|
323
|
+
<span className="w-2 h-2 bg-amber-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
324
|
+
<span className="w-2 h-2 bg-amber-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
325
|
+
<span className="w-2 h-2 bg-amber-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Visual Hierarchy
|
|
336
|
+
|
|
337
|
+
### Phase Colors
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const PHASE_COLORS = {
|
|
341
|
+
movement: {
|
|
342
|
+
bg: 'bg-blue-900',
|
|
343
|
+
border: 'border-blue-500',
|
|
344
|
+
text: 'text-blue-400',
|
|
345
|
+
icon: '#3b82f6',
|
|
346
|
+
},
|
|
347
|
+
attack: {
|
|
348
|
+
bg: 'bg-red-900',
|
|
349
|
+
border: 'border-red-500',
|
|
350
|
+
text: 'text-red-400',
|
|
351
|
+
icon: '#ef4444',
|
|
352
|
+
},
|
|
353
|
+
defense: {
|
|
354
|
+
bg: 'bg-emerald-900',
|
|
355
|
+
border: 'border-emerald-500',
|
|
356
|
+
text: 'text-emerald-400',
|
|
357
|
+
icon: '#10b981',
|
|
358
|
+
},
|
|
359
|
+
utility: {
|
|
360
|
+
bg: 'bg-purple-900',
|
|
361
|
+
border: 'border-purple-500',
|
|
362
|
+
text: 'text-purple-400',
|
|
363
|
+
icon: '#8b5cf6',
|
|
364
|
+
},
|
|
365
|
+
end: {
|
|
366
|
+
bg: 'bg-gray-800',
|
|
367
|
+
border: 'border-gray-600',
|
|
368
|
+
text: 'text-gray-400',
|
|
369
|
+
icon: '#6b7280',
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Turn Indicator States
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
const TURN_STATES = {
|
|
378
|
+
yourTurn: {
|
|
379
|
+
label: 'Your Turn',
|
|
380
|
+
color: 'emerald',
|
|
381
|
+
pulse: true,
|
|
382
|
+
},
|
|
383
|
+
opponentTurn: {
|
|
384
|
+
label: "Opponent's Turn",
|
|
385
|
+
color: 'amber',
|
|
386
|
+
pulse: false,
|
|
387
|
+
},
|
|
388
|
+
waiting: {
|
|
389
|
+
label: 'Waiting...',
|
|
390
|
+
color: 'gray',
|
|
391
|
+
pulse: true,
|
|
392
|
+
},
|
|
393
|
+
gameOver: {
|
|
394
|
+
label: 'Game Over',
|
|
395
|
+
color: 'slate',
|
|
396
|
+
pulse: false,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Anti-Patterns
|
|
404
|
+
|
|
405
|
+
### DON'T: Hidden Phase Information
|
|
406
|
+
```tsx
|
|
407
|
+
// BAD - Phase only shown in small text
|
|
408
|
+
<span className="text-xs text-gray-500">{phase}</span>
|
|
409
|
+
|
|
410
|
+
// GOOD - Prominent, always-visible phase banner
|
|
411
|
+
<PhaseBanner phase={phase} className="sticky top-0 z-10" />
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### DON'T: Clickable Disabled Actions
|
|
415
|
+
```tsx
|
|
416
|
+
// BAD - Looks disabled but accepts clicks
|
|
417
|
+
<button className="opacity-50" onClick={handleAction}>Attack</button>
|
|
418
|
+
|
|
419
|
+
// GOOD - Actually disabled with explanation
|
|
420
|
+
<Tooltip content="Cannot attack: No valid targets">
|
|
421
|
+
<button disabled className="opacity-50 cursor-not-allowed">Attack</button>
|
|
422
|
+
</Tooltip>
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### DON'T: Silent Phase Transitions
|
|
426
|
+
```tsx
|
|
427
|
+
// BAD - Phase changes without notice
|
|
428
|
+
setPhase(nextPhase);
|
|
429
|
+
|
|
430
|
+
// GOOD - Announce transition
|
|
431
|
+
setPhase(nextPhase);
|
|
432
|
+
announceToScreenReader(`Entering ${PHASE_CONFIG[nextPhase].name}`);
|
|
433
|
+
playTransitionSound();
|
|
434
|
+
showTransitionAnimation();
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### DON'T: No Clear Turn Ownership
|
|
438
|
+
```tsx
|
|
439
|
+
// BAD - Ambiguous whose turn it is
|
|
440
|
+
<div>{currentPlayer} is playing</div>
|
|
441
|
+
|
|
442
|
+
// GOOD - Crystal clear with visual hierarchy
|
|
443
|
+
<div className={isYourTurn ? 'bg-emerald-500/20' : 'bg-gray-800'}>
|
|
444
|
+
{isYourTurn
|
|
445
|
+
? <span className="text-emerald-400 font-bold">YOUR TURN</span>
|
|
446
|
+
: <span className="text-amber-400">Opponent's Turn</span>
|
|
447
|
+
}
|
|
448
|
+
</div>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Accessibility
|
|
454
|
+
|
|
455
|
+
### Screen Reader Announcements
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
function usePhaseAnnouncer(phase: GamePhase, turn: number, isYourTurn: boolean) {
|
|
459
|
+
const prevPhase = useRef(phase);
|
|
460
|
+
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
if (phase !== prevPhase.current) {
|
|
463
|
+
const message = `${PHASE_CONFIG[phase].name}. ${
|
|
464
|
+
isYourTurn ? 'Your turn to act.' : 'Waiting for opponent.'
|
|
465
|
+
}`;
|
|
466
|
+
announceToScreenReader(message);
|
|
467
|
+
prevPhase.current = phase;
|
|
468
|
+
}
|
|
469
|
+
}, [phase, isYourTurn]);
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Keyboard Navigation
|
|
474
|
+
|
|
475
|
+
| Key | Action |
|
|
476
|
+
|-----|--------|
|
|
477
|
+
| `Space` / `Enter` | Confirm action / End phase |
|
|
478
|
+
| `Escape` | Cancel current action |
|
|
479
|
+
| `Tab` | Navigate between action buttons |
|
|
480
|
+
| `1-9` | Quick select action by number |
|
|
481
|
+
| `U` | Undo last action |
|
|
482
|
+
| `E` | End current phase |
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Testing Checklist
|
|
487
|
+
|
|
488
|
+
- [ ] Phase banner shows correct phase name
|
|
489
|
+
- [ ] Turn indicator shows correct player
|
|
490
|
+
- [ ] Actions disable when not your turn
|
|
491
|
+
- [ ] Phase transition animation plays
|
|
492
|
+
- [ ] End Phase advances to next phase
|
|
493
|
+
- [ ] Undo reverts last action (if allowed)
|
|
494
|
+
- [ ] Cannot take actions out of turn
|
|
495
|
+
- [ ] Phase-specific actions change correctly
|
|
496
|
+
- [ ] Waiting state shows during opponent's turn
|
|
497
|
+
- [ ] Screen reader announces phase changes
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Related Skills
|
|
502
|
+
|
|
503
|
+
- `canvas-grid-patterns` - For game board visualization
|
|
504
|
+
- `keyboard-shortcuts-patterns` - For action hotkeys
|
|
505
|
+
- `playback-replay-patterns` - For game replay functionality
|
|
506
|
+
- `event-timeline-patterns` - For turn history/log
|