ux-toolkit 0.1.0 → 0.4.0

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.
Files changed (44) hide show
  1. package/README.md +113 -7
  2. package/agents/card-reviewer.md +173 -0
  3. package/agents/comparison-reviewer.md +143 -0
  4. package/agents/density-reviewer.md +207 -0
  5. package/agents/detail-page-reviewer.md +143 -0
  6. package/agents/editor-reviewer.md +165 -0
  7. package/agents/form-reviewer.md +156 -0
  8. package/agents/game-ui-reviewer.md +181 -0
  9. package/agents/list-page-reviewer.md +132 -0
  10. package/agents/navigation-reviewer.md +145 -0
  11. package/agents/panel-reviewer.md +182 -0
  12. package/agents/replay-reviewer.md +174 -0
  13. package/agents/settings-reviewer.md +166 -0
  14. package/agents/ux-auditor.md +145 -45
  15. package/agents/ux-engineer.md +211 -38
  16. package/dist/cli.js +172 -5
  17. package/dist/cli.js.map +1 -1
  18. package/dist/index.cjs +172 -5
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +128 -4
  21. package/dist/index.d.ts +128 -4
  22. package/dist/index.js +172 -5
  23. package/dist/index.js.map +1 -1
  24. package/package.json +6 -4
  25. package/skills/canvas-grid-patterns/SKILL.md +367 -0
  26. package/skills/comparison-patterns/SKILL.md +354 -0
  27. package/skills/data-density-patterns/SKILL.md +493 -0
  28. package/skills/detail-page-patterns/SKILL.md +522 -0
  29. package/skills/drag-drop-patterns/SKILL.md +406 -0
  30. package/skills/editor-workspace-patterns/SKILL.md +552 -0
  31. package/skills/event-timeline-patterns/SKILL.md +542 -0
  32. package/skills/form-patterns/SKILL.md +608 -0
  33. package/skills/info-card-patterns/SKILL.md +531 -0
  34. package/skills/keyboard-shortcuts-patterns/SKILL.md +365 -0
  35. package/skills/list-page-patterns/SKILL.md +351 -0
  36. package/skills/modal-patterns/SKILL.md +750 -0
  37. package/skills/navigation-patterns/SKILL.md +476 -0
  38. package/skills/page-structure-patterns/SKILL.md +271 -0
  39. package/skills/playback-replay-patterns/SKILL.md +695 -0
  40. package/skills/react-ux-patterns/SKILL.md +434 -0
  41. package/skills/split-panel-patterns/SKILL.md +609 -0
  42. package/skills/status-visualization-patterns/SKILL.md +635 -0
  43. package/skills/toast-notification-patterns/SKILL.md +207 -0
  44. package/skills/turn-based-ui-patterns/SKILL.md +506 -0
@@ -0,0 +1,365 @@
1
+ ---
2
+ name: keyboard-shortcuts-patterns
3
+ description: Keyboard shortcuts, command palette (Cmd+K), and power user navigation patterns
4
+ license: MIT
5
+ ---
6
+
7
+ # Keyboard Shortcuts & Command Palette Patterns
8
+
9
+ ## 1. Command Palette (Cmd+K)
10
+
11
+ ### Search UI Pattern
12
+ ```tsx
13
+ interface CommandPaletteProps {
14
+ items: CommandItem[];
15
+ onSelect: (item: CommandItem) => void;
16
+ placeholder?: string;
17
+ recentItems?: CommandItem[];
18
+ }
19
+
20
+ interface CommandItem {
21
+ id: string;
22
+ label: string;
23
+ category?: string;
24
+ keywords?: string[];
25
+ icon?: React.ReactNode;
26
+ shortcut?: string;
27
+ }
28
+
29
+ // Fuzzy search with ranking
30
+ const fuzzyMatch = (search: string, item: CommandItem): number => {
31
+ const text = `${item.label} ${item.keywords?.join(' ')}`.toLowerCase();
32
+ const query = search.toLowerCase();
33
+ let score = 0;
34
+ let lastIndex = -1;
35
+
36
+ for (const char of query) {
37
+ const index = text.indexOf(char, lastIndex + 1);
38
+ if (index === -1) return 0;
39
+ score += 1 / (index - lastIndex);
40
+ lastIndex = index;
41
+ }
42
+ return score;
43
+ };
44
+ ```
45
+
46
+ ### Categories & Recent Items
47
+ ```tsx
48
+ const CommandPalette = ({ items, recentItems }: CommandPaletteProps) => {
49
+ const [search, setSearch] = useState('');
50
+ const filtered = search ? items.filter(i => fuzzyMatch(search, i) > 0) : items;
51
+ const grouped = groupBy(filtered, 'category');
52
+
53
+ return (
54
+ <div className="fixed inset-0 z-50 bg-black/50" role="dialog">
55
+ <div className="mx-auto mt-20 max-w-xl rounded-lg bg-white shadow-2xl">
56
+ <input
57
+ type="text"
58
+ value={search}
59
+ onChange={(e) => setSearch(e.target.value)}
60
+ placeholder="Search commands..."
61
+ className="w-full border-b px-4 py-3 text-lg outline-none"
62
+ />
63
+ <div className="max-h-96 overflow-y-auto">
64
+ {!search && recentItems && (
65
+ <CommandGroup title="Recent" items={recentItems} />
66
+ )}
67
+ {Object.entries(grouped).map(([category, items]) => (
68
+ <CommandGroup key={category} title={category} items={items} />
69
+ ))}
70
+ </div>
71
+ </div>
72
+ </div>
73
+ );
74
+ };
75
+ ```
76
+
77
+ ## 2. Global Shortcuts
78
+
79
+ ### Common Pattern Registry
80
+ ```tsx
81
+ const GLOBAL_SHORTCUTS = {
82
+ save: { key: 's', modifiers: ['meta'] },
83
+ undo: { key: 'z', modifiers: ['meta'] },
84
+ redo: { key: 'z', modifiers: ['meta', 'shift'] },
85
+ find: { key: 'f', modifiers: ['meta'] },
86
+ close: { key: 'Escape', modifiers: [] },
87
+ commandPalette: { key: 'k', modifiers: ['meta'] },
88
+ } as const;
89
+
90
+ // Platform-aware modifier
91
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
92
+ const metaKey = isMac ? 'Cmd' : 'Ctrl';
93
+ ```
94
+
95
+ ### useKeyboardShortcut Hook
96
+ ```tsx
97
+ interface ShortcutConfig {
98
+ key: string;
99
+ modifiers?: ('meta' | 'ctrl' | 'shift' | 'alt')[];
100
+ enabled?: boolean;
101
+ preventDefault?: boolean;
102
+ }
103
+
104
+ const useKeyboardShortcut = (
105
+ config: ShortcutConfig,
106
+ callback: () => void
107
+ ) => {
108
+ useEffect(() => {
109
+ if (!config.enabled ?? true) return;
110
+
111
+ const handler = (e: KeyboardEvent) => {
112
+ const modMatch =
113
+ (!config.modifiers?.includes('meta') || e.metaKey) &&
114
+ (!config.modifiers?.includes('ctrl') || e.ctrlKey) &&
115
+ (!config.modifiers?.includes('shift') || e.shiftKey) &&
116
+ (!config.modifiers?.includes('alt') || e.altKey);
117
+
118
+ if (e.key === config.key && modMatch) {
119
+ if (config.preventDefault) e.preventDefault();
120
+ callback();
121
+ }
122
+ };
123
+
124
+ document.addEventListener('keydown', handler);
125
+ return () => document.removeEventListener('keydown', handler);
126
+ }, [config, callback]);
127
+ };
128
+ ```
129
+
130
+ ## 3. Contextual Shortcuts
131
+
132
+ ### Focus-Based Shortcuts
133
+ ```tsx
134
+ const useContextualShortcuts = (context: 'editor' | 'sidebar' | 'modal') => {
135
+ const shortcuts = {
136
+ editor: {
137
+ 'Cmd+B': 'Toggle bold',
138
+ 'Cmd+I': 'Toggle italic',
139
+ 'Cmd+/': 'Toggle comment',
140
+ },
141
+ sidebar: {
142
+ 'ArrowUp/Down': 'Navigate items',
143
+ 'Enter': 'Open item',
144
+ '/': 'Focus search',
145
+ },
146
+ modal: {
147
+ 'Escape': 'Close modal',
148
+ 'Tab': 'Next field',
149
+ },
150
+ }[context];
151
+
152
+ return shortcuts;
153
+ };
154
+ ```
155
+
156
+ ## 4. Shortcut Discovery
157
+
158
+ ### Tooltip Hints
159
+ ```tsx
160
+ const Button = ({ shortcut, children }: { shortcut?: string }) => (
161
+ <button className="group relative px-4 py-2">
162
+ {children}
163
+ {shortcut && (
164
+ <span className="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-gray-500 opacity-0 group-hover:opacity-100">
165
+ {shortcut}
166
+ </span>
167
+ )}
168
+ </button>
169
+ );
170
+
171
+ // Usage: <Button shortcut="⌘S">Save</Button>
172
+ ```
173
+
174
+ ### Keyboard Shortcut Modal
175
+ ```tsx
176
+ const ShortcutHelpModal = ({ shortcuts }: { shortcuts: Record<string, string> }) => (
177
+ <div className="rounded-lg bg-white p-6 shadow-xl">
178
+ <h2 className="mb-4 text-xl font-bold">Keyboard Shortcuts</h2>
179
+ <div className="grid gap-2">
180
+ {Object.entries(shortcuts).map(([key, description]) => (
181
+ <div key={key} className="flex justify-between border-b pb-2">
182
+ <span className="text-gray-700">{description}</span>
183
+ <kbd className="rounded bg-gray-100 px-2 py-1 font-mono text-sm">{key}</kbd>
184
+ </div>
185
+ ))}
186
+ </div>
187
+ </div>
188
+ );
189
+ ```
190
+
191
+ ## 5. Shortcut Conflicts
192
+
193
+ ### Browser Defaults to Avoid [CRITICAL]
194
+ - **Cmd+W** (close tab), **Cmd+T** (new tab), **Cmd+R** (reload)
195
+ - **Cmd+L** (address bar), **Cmd+D** (bookmark)
196
+ - Use `e.preventDefault()` cautiously
197
+
198
+ ### Platform Differences
199
+ ```tsx
200
+ const formatShortcut = (shortcut: string): string => {
201
+ const isMac = navigator.platform.includes('Mac');
202
+ return shortcut
203
+ .replace('Cmd', isMac ? '⌘' : 'Ctrl')
204
+ .replace('Alt', isMac ? '⌥' : 'Alt')
205
+ .replace('Shift', '⇧');
206
+ };
207
+ ```
208
+
209
+ ## 6. Focus Management
210
+
211
+ ### Focus Trap in Modals [CRITICAL]
212
+ ```tsx
213
+ const useFocusTrap = (containerRef: RefObject<HTMLElement>) => {
214
+ useEffect(() => {
215
+ const container = containerRef.current;
216
+ if (!container) return;
217
+
218
+ const focusable = container.querySelectorAll(
219
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
220
+ );
221
+ const first = focusable[0] as HTMLElement;
222
+ const last = focusable[focusable.length - 1] as HTMLElement;
223
+
224
+ const handler = (e: KeyboardEvent) => {
225
+ if (e.key === 'Tab') {
226
+ if (e.shiftKey && document.activeElement === first) {
227
+ e.preventDefault();
228
+ last.focus();
229
+ } else if (!e.shiftKey && document.activeElement === last) {
230
+ e.preventDefault();
231
+ first.focus();
232
+ }
233
+ }
234
+ };
235
+
236
+ container.addEventListener('keydown', handler);
237
+ first.focus();
238
+ return () => container.removeEventListener('keydown', handler);
239
+ }, [containerRef]);
240
+ };
241
+ ```
242
+
243
+ ### Focus Restoration [MAJOR]
244
+ ```tsx
245
+ const Modal = ({ onClose }: { onClose: () => void }) => {
246
+ const previousFocus = useRef<HTMLElement | null>(null);
247
+
248
+ useEffect(() => {
249
+ previousFocus.current = document.activeElement as HTMLElement;
250
+ return () => previousFocus.current?.focus();
251
+ }, []);
252
+
253
+ return <div role="dialog">...</div>;
254
+ };
255
+ ```
256
+
257
+ ## 7. Accessibility
258
+
259
+ ### Screen Reader Announcements [CRITICAL]
260
+ ```tsx
261
+ const useLiveRegion = () => {
262
+ const announce = (message: string, priority: 'polite' | 'assertive' = 'polite') => {
263
+ const region = document.getElementById('live-region');
264
+ if (region) {
265
+ region.setAttribute('aria-live', priority);
266
+ region.textContent = message;
267
+ }
268
+ };
269
+ return announce;
270
+ };
271
+
272
+ // In root: <div id="live-region" className="sr-only" aria-live="polite" />
273
+ ```
274
+
275
+ ### Visible Focus Indicators
276
+ ```tsx
277
+ // globals.css
278
+ .focus-visible:focus {
279
+ @apply outline-none ring-2 ring-blue-500 ring-offset-2;
280
+ }
281
+ ```
282
+
283
+ ## 8. Shortcut Registry
284
+
285
+ ### Centralized Management
286
+ ```tsx
287
+ class ShortcutRegistry {
288
+ private shortcuts = new Map<string, () => void>();
289
+
290
+ register(key: string, callback: () => void, context?: string) {
291
+ const id = context ? `${context}:${key}` : key;
292
+ this.shortcuts.set(id, callback);
293
+ }
294
+
295
+ unregister(key: string, context?: string) {
296
+ const id = context ? `${context}:${key}` : key;
297
+ this.shortcuts.delete(id);
298
+ }
299
+
300
+ handle(e: KeyboardEvent, context?: string) {
301
+ const key = this.formatKey(e);
302
+ const contextKey = context ? `${context}:${key}` : key;
303
+ const handler = this.shortcuts.get(contextKey) || this.shortcuts.get(key);
304
+ if (handler) {
305
+ e.preventDefault();
306
+ handler();
307
+ }
308
+ }
309
+
310
+ private formatKey(e: KeyboardEvent): string {
311
+ const mods = [
312
+ e.metaKey && 'Cmd',
313
+ e.ctrlKey && 'Ctrl',
314
+ e.shiftKey && 'Shift',
315
+ e.altKey && 'Alt',
316
+ ].filter(Boolean);
317
+ return [...mods, e.key].join('+');
318
+ }
319
+ }
320
+
321
+ export const shortcuts = new ShortcutRegistry();
322
+ ```
323
+
324
+ ## 9. Audit Checklist
325
+
326
+ ### Command Palette [CRITICAL]
327
+ - [ ] Cmd+K opens palette
328
+ - [ ] Fuzzy search works
329
+ - [ ] Recent items shown when empty
330
+ - [ ] Categories clearly separated
331
+ - [ ] Keyboard navigation (arrows, Enter, Escape)
332
+
333
+ ### Global Shortcuts [MAJOR]
334
+ - [ ] Platform detection (Mac vs Windows)
335
+ - [ ] Shortcuts displayed in UI (tooltips, help modal)
336
+ - [ ] No conflicts with browser defaults
337
+ - [ ] preventDefault used appropriately
338
+
339
+ ### Focus Management [CRITICAL]
340
+ - [ ] Focus trapped in modals
341
+ - [ ] Focus restored after modal close
342
+ - [ ] Visible focus indicators on all interactive elements
343
+ - [ ] Tab order is logical
344
+
345
+ ### Accessibility [CRITICAL]
346
+ - [ ] Screen reader announces shortcut actions
347
+ - [ ] Skip to content link available
348
+ - [ ] All shortcuts documented in help
349
+ - [ ] Alternative non-keyboard methods available
350
+
351
+ ### Contextual Shortcuts [MAJOR]
352
+ - [ ] Shortcuts change based on focus
353
+ - [ ] Conflicts resolved between contexts
354
+ - [ ] Help shows context-specific shortcuts
355
+
356
+ ### Discovery [MINOR]
357
+ - [ ] Shortcut hints in tooltips
358
+ - [ ] Help modal accessible (Cmd+? or ?)
359
+ - [ ] Shortcuts shown in command palette
360
+ - [ ] Onboarding highlights key shortcuts
361
+
362
+ ### Performance [MINOR]
363
+ - [ ] Debounced search in command palette
364
+ - [ ] Keyboard handlers cleaned up properly
365
+ - [ ] No memory leaks from event listeners
@@ -0,0 +1,351 @@
1
+ ---
2
+ name: list-page-patterns
3
+ description: UX patterns specific to list/browse pages including filters, sorting, pagination, and grid/table displays
4
+ license: MIT
5
+ ---
6
+
7
+ # List Page UX Patterns
8
+
9
+ List pages display collections of items with filtering, sorting, and navigation capabilities.
10
+
11
+ ## Required Components
12
+
13
+ ### 1. Filter Card
14
+ Every list page MUST have a filter section.
15
+
16
+ ```tsx
17
+ <Card className="mb-6">
18
+ <div className="flex flex-col sm:flex-row gap-4">
19
+ {/* Search - always first */}
20
+ <div className="flex-1">
21
+ <Input
22
+ type="text"
23
+ placeholder="Search by name..."
24
+ value={filters.search}
25
+ onChange={(e) => handleFilterChange('search', e.target.value)}
26
+ aria-label="Search items"
27
+ />
28
+ </div>
29
+
30
+ {/* Filter dropdowns */}
31
+ <Select
32
+ value={filters.status}
33
+ onChange={(e) => handleFilterChange('status', e.target.value)}
34
+ options={statusOptions}
35
+ aria-label="Filter by status"
36
+ />
37
+
38
+ {/* Clear filters button */}
39
+ <Button variant="secondary" onClick={clearFilters} className="text-xs">
40
+ Clear
41
+ </Button>
42
+ </div>
43
+
44
+ {/* Results count - REQUIRED */}
45
+ <div className="mt-4 text-sm text-text-secondary">
46
+ Showing {filteredItems.length} of {items.length} results
47
+ {hasActiveFilters && (
48
+ <span className="text-accent ml-1">(filtered)</span>
49
+ )}
50
+ </div>
51
+ </Card>
52
+ ```
53
+
54
+ ### 2. Results Display
55
+
56
+ #### Grid Layout (Cards)
57
+ ```tsx
58
+ // For rich items with multiple data points
59
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
60
+ {items.map(item => (
61
+ <ItemCard key={item.id} item={item} onClick={() => handleClick(item)} />
62
+ ))}
63
+ </div>
64
+ ```
65
+
66
+ #### Table Layout (Data-dense)
67
+ ```tsx
68
+ // For data-heavy lists needing sorting
69
+ <Card variant="dark" className="overflow-hidden">
70
+ <div className="overflow-x-auto">
71
+ <table className="w-full min-w-[800px]">
72
+ <thead className="bg-surface-base">
73
+ <tr className="text-left text-text-secondary text-xs uppercase">
74
+ <SortableHeader column="name" />
75
+ <SortableHeader column="status" />
76
+ <SortableHeader column="date" />
77
+ </tr>
78
+ </thead>
79
+ <tbody className="divide-y divide-border/50">
80
+ {items.map(item => (
81
+ <tr key={item.id} className="hover:bg-surface-raised/20">
82
+ <td className="px-4 py-3">{item.name}</td>
83
+ </tr>
84
+ ))}
85
+ </tbody>
86
+ </table>
87
+ </div>
88
+ </Card>
89
+ ```
90
+
91
+ ### 3. Sortable Table Header
92
+ ```tsx
93
+ interface SortableHeaderProps {
94
+ label: string;
95
+ column: string;
96
+ currentColumn: string;
97
+ direction: 'asc' | 'desc';
98
+ onSort: (column: string) => void;
99
+ }
100
+
101
+ function SortableHeader({ label, column, currentColumn, direction, onSort }: SortableHeaderProps) {
102
+ const isActive = column === currentColumn;
103
+
104
+ return (
105
+ <th
106
+ className="px-4 py-3 font-medium cursor-pointer hover:text-white select-none"
107
+ onClick={() => onSort(column)}
108
+ >
109
+ <span className="flex items-center gap-1">
110
+ {label}
111
+ {isActive && (
112
+ <span className="text-accent text-xs">
113
+ {direction === 'asc' ? '▲' : '▼'}
114
+ </span>
115
+ )}
116
+ </span>
117
+ </th>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ### 4. Pagination
123
+ ```tsx
124
+ // Only show if totalPages > 1
125
+ {totalPages > 1 && (
126
+ <div className="flex justify-center mt-6">
127
+ <PaginationButtons
128
+ currentPage={currentPage}
129
+ totalPages={totalPages}
130
+ onPageChange={setCurrentPage}
131
+ />
132
+ </div>
133
+ )}
134
+
135
+ function PaginationButtons({ currentPage, totalPages, onPageChange }) {
136
+ return (
137
+ <div className="flex items-center gap-2">
138
+ <button
139
+ onClick={() => onPageChange(currentPage - 1)}
140
+ disabled={currentPage === 1}
141
+ className="px-3 py-1.5 rounded bg-surface-raised disabled:opacity-50"
142
+ >
143
+ Previous
144
+ </button>
145
+
146
+ <span className="text-sm text-text-secondary">
147
+ Page {currentPage} of {totalPages}
148
+ </span>
149
+
150
+ <button
151
+ onClick={() => onPageChange(currentPage + 1)}
152
+ disabled={currentPage === totalPages}
153
+ className="px-3 py-1.5 rounded bg-surface-raised disabled:opacity-50"
154
+ >
155
+ Next
156
+ </button>
157
+ </div>
158
+ );
159
+ }
160
+ ```
161
+
162
+ ### 5. Empty State
163
+ ```tsx
164
+ {filteredItems.length === 0 && (
165
+ <EmptyState
166
+ icon={<EmptyIcon />}
167
+ title={hasFilters ? 'No items match your filters' : 'No items yet'}
168
+ message={hasFilters ? 'Try adjusting your search or filters' : 'Create your first item to get started'}
169
+ action={!hasFilters && (
170
+ <Button variant="primary" onClick={handleCreate}>
171
+ Create First Item
172
+ </Button>
173
+ )}
174
+ />
175
+ )}
176
+ ```
177
+
178
+ ## Item Card Pattern
179
+
180
+ ### Standard Card Structure
181
+ ```tsx
182
+ function ItemCard({ item, onClick }) {
183
+ return (
184
+ <div
185
+ onClick={onClick}
186
+ className="
187
+ group relative p-5 rounded-xl border-2 transition-all cursor-pointer
188
+ bg-gradient-to-br from-surface-raised/60 to-surface-raised/30
189
+ border-border hover:border-accent/50 hover:shadow-lg
190
+ hover:scale-[1.02] hover:-translate-y-0.5
191
+ "
192
+ >
193
+ {/* Accent line at top */}
194
+ <div className="absolute top-0 left-4 right-4 h-0.5 bg-accent/30 group-hover:bg-accent rounded-full" />
195
+
196
+ {/* Header with title and badge */}
197
+ <div className="flex items-start justify-between gap-3 mb-4">
198
+ <div className="flex-1 min-w-0">
199
+ <h3 className="text-lg font-bold text-white truncate group-hover:text-accent">
200
+ {item.name}
201
+ </h3>
202
+ {item.subtitle && (
203
+ <p className="text-accent/80 text-sm truncate">{item.subtitle}</p>
204
+ )}
205
+ </div>
206
+ <StatusBadge status={item.status} />
207
+ </div>
208
+
209
+ {/* Stats or metadata */}
210
+ <div className="flex items-center justify-between text-sm text-text-secondary">
211
+ <span>{item.count} items</span>
212
+ <span>{item.date}</span>
213
+ </div>
214
+
215
+ {/* Hover arrow indicator */}
216
+ <div className="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
217
+ <ChevronRightIcon className="w-5 h-5 text-accent" />
218
+ </div>
219
+ </div>
220
+ );
221
+ }
222
+ ```
223
+
224
+ ## Filter State Management
225
+
226
+ ### Standard Filter Pattern
227
+ ```tsx
228
+ interface FilterState {
229
+ search: string;
230
+ status: string;
231
+ category: string;
232
+ }
233
+
234
+ function useFilters<T>(items: T[], filterFn: (item: T, filters: FilterState) => boolean) {
235
+ const [filters, setFilters] = useState<FilterState>({
236
+ search: '',
237
+ status: '',
238
+ category: '',
239
+ });
240
+
241
+ const filteredItems = useMemo(() => {
242
+ return items.filter(item => filterFn(item, filters));
243
+ }, [items, filters, filterFn]);
244
+
245
+ const hasActiveFilters = Object.values(filters).some(v => v !== '');
246
+
247
+ const handleFilterChange = (key: keyof FilterState, value: string) => {
248
+ setFilters(prev => ({ ...prev, [key]: value }));
249
+ };
250
+
251
+ const clearFilters = () => {
252
+ setFilters({ search: '', status: '', category: '' });
253
+ };
254
+
255
+ return { filters, filteredItems, hasActiveFilters, handleFilterChange, clearFilters };
256
+ }
257
+ ```
258
+
259
+ ## Sort State Management
260
+
261
+ ### Standard Sort Pattern
262
+ ```tsx
263
+ type SortDirection = 'asc' | 'desc';
264
+
265
+ interface SortState<T extends string> {
266
+ column: T;
267
+ direction: SortDirection;
268
+ }
269
+
270
+ function useSort<T, C extends string>(items: T[], sortFn: (a: T, b: T, column: C) => number) {
271
+ const [sort, setSort] = useState<SortState<C>>({
272
+ column: 'name' as C,
273
+ direction: 'asc',
274
+ });
275
+
276
+ const sortedItems = useMemo(() => {
277
+ const sorted = [...items];
278
+ sorted.sort((a, b) => {
279
+ const result = sortFn(a, b, sort.column);
280
+ return sort.direction === 'asc' ? result : -result;
281
+ });
282
+ return sorted;
283
+ }, [items, sort, sortFn]);
284
+
285
+ const handleSort = (column: C) => {
286
+ setSort(prev => ({
287
+ column,
288
+ direction: prev.column === column && prev.direction === 'asc' ? 'desc' : 'asc',
289
+ }));
290
+ };
291
+
292
+ return { sort, sortedItems, handleSort };
293
+ }
294
+ ```
295
+
296
+ ## Advanced Filters Panel
297
+
298
+ ### Expandable Advanced Filters
299
+ ```tsx
300
+ const [showAdvanced, setShowAdvanced] = useState(false);
301
+ const hasAdvancedFilters = filters.dateMin || filters.dateMax || filters.valueMin;
302
+
303
+ <Button
304
+ variant="ghost"
305
+ onClick={() => setShowAdvanced(!showAdvanced)}
306
+ className={hasAdvancedFilters ? 'text-accent' : ''}
307
+ >
308
+ {showAdvanced ? '▼' : '▶'} Advanced
309
+ {hasAdvancedFilters && ' •'}
310
+ </Button>
311
+
312
+ {showAdvanced && (
313
+ <div className="mt-4 pt-4 border-t border-border">
314
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
315
+ {/* Range filters */}
316
+ <div>
317
+ <label className="block text-xs text-text-secondary mb-1.5 uppercase">
318
+ Date Range
319
+ </label>
320
+ <div className="flex items-center gap-2">
321
+ <Input type="date" value={filters.dateMin} onChange={...} />
322
+ <span className="text-text-muted">–</span>
323
+ <Input type="date" value={filters.dateMax} onChange={...} />
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ )}
329
+ ```
330
+
331
+ ## Audit Checklist for List Pages
332
+
333
+ ### Critical (Must Fix)
334
+ - [ ] Has search input with aria-label - accessibility violation
335
+ - [ ] Handles empty initial state - users see nothing, think app is broken
336
+ - [ ] Loading state while fetching - users think app froze
337
+ - [ ] Cards link to detail pages - dead end without navigation
338
+
339
+ ### Major (Should Fix)
340
+ - [ ] Has relevant filter dropdowns - can't find items efficiently
341
+ - [ ] Shows results count - users don't know if search worked
342
+ - [ ] Handles empty filtered results - confusing when no matches
343
+ - [ ] Grid is responsive (1-4 columns) - mobile users blocked
344
+ - [ ] Pagination works correctly - can't access all data
345
+ - [ ] Create button in header - no clear path to add items
346
+
347
+ ### Minor (Nice to Have)
348
+ - [ ] Indicates when filters are active - visual clarity
349
+ - [ ] Has clear filters button - convenience
350
+ - [ ] Cards have hover states - interaction feedback
351
+ - [ ] Table has sortable headers (if using table) - power user feature