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.
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,609 @@
1
+ # Split Panel UI Patterns
2
+
3
+ Patterns for multi-panel layouts with resizable dividers, synchronized views, and responsive behavior. Applies to IDEs, email clients, comparison views, master-detail layouts, and any application with side-by-side panels.
4
+
5
+ ## When to Use This Skill
6
+
7
+ - IDE/code editors (file tree + editor + terminal)
8
+ - Email clients (folders + list + preview)
9
+ - Master-detail layouts
10
+ - Side-by-side comparison
11
+ - Property inspectors
12
+ - Debug layouts (code + console + variables)
13
+ - Dashboard panels
14
+
15
+ ---
16
+
17
+ ## Core Concepts
18
+
19
+ ### Panel Types
20
+
21
+ | Type | Description | Example |
22
+ |------|-------------|---------|
23
+ | **Primary** | Main content area | Editor, email body |
24
+ | **Secondary** | Supporting content | Sidebar, preview |
25
+ | **Tertiary** | Optional/collapsible | Properties, console |
26
+
27
+ ### Split Orientations
28
+
29
+ | Orientation | Use Case |
30
+ |-------------|----------|
31
+ | **Horizontal** | Left/right panels (most common) |
32
+ | **Vertical** | Top/bottom panels (IDE terminals) |
33
+ | **Grid** | 2x2 or more complex layouts |
34
+
35
+ ### Resize Modes
36
+
37
+ | Mode | Behavior |
38
+ |------|----------|
39
+ | **Drag** | User drags divider |
40
+ | **Double-click** | Reset to default or toggle collapse |
41
+ | **Keyboard** | Arrow keys adjust size |
42
+ | **Preset** | Jump to predefined ratios (33/66, 50/50) |
43
+
44
+ ---
45
+
46
+ ## Audit Checklist
47
+
48
+ ### Panel Dividers
49
+ - [ ] [CRITICAL] Divider visually distinct from panels
50
+ - [ ] [CRITICAL] Divider cursor indicates draggability
51
+ - [ ] [MAJOR] Hover state on divider
52
+ - [ ] [MAJOR] Drag feedback (line follows mouse)
53
+ - [ ] [MINOR] Double-click to reset/toggle
54
+ - [ ] [MINOR] Keyboard control for resize
55
+
56
+ ### Resize Behavior
57
+ - [ ] [CRITICAL] Panels resize smoothly during drag
58
+ - [ ] [CRITICAL] Minimum sizes enforced (content not crushed)
59
+ - [ ] [MAJOR] Maximum sizes enforced (if applicable)
60
+ - [ ] [MAJOR] Content reflows appropriately
61
+ - [ ] [MINOR] Snap to preset ratios
62
+ - [ ] [MINOR] Size persistence across sessions
63
+
64
+ ### Collapse/Expand
65
+ - [ ] [MAJOR] Panel can collapse to minimize
66
+ - [ ] [MAJOR] Clear toggle button for collapse
67
+ - [ ] [MAJOR] Smooth collapse animation
68
+ - [ ] [MINOR] Keyboard shortcut for toggle
69
+ - [ ] [MINOR] Collapsed state shows hint of content
70
+
71
+ ### Content Scrolling
72
+ - [ ] [CRITICAL] Each panel scrolls independently
73
+ - [ ] [MAJOR] Scroll position preserved on resize
74
+ - [ ] [MINOR] Synchronized scrolling option (for diff views)
75
+ - [ ] [MINOR] Scroll indicators visible
76
+
77
+ ### Responsive Behavior
78
+ - [ ] [CRITICAL] Layout adapts to narrow viewports
79
+ - [ ] [MAJOR] Panels stack vertically on mobile
80
+ - [ ] [MAJOR] One panel prioritized on small screens
81
+ - [ ] [MINOR] Touch-friendly resize handles
82
+ - [ ] [MINOR] Swipe to switch panels on mobile
83
+
84
+ ### Accessibility
85
+ - [ ] [CRITICAL] Panels are keyboard navigable
86
+ - [ ] [CRITICAL] Divider is keyboard accessible
87
+ - [ ] [MAJOR] Focus management between panels
88
+ - [ ] [MAJOR] ARIA labels for panels and dividers
89
+ - [ ] [MINOR] Screen reader announces panel sizes
90
+
91
+ ### Performance
92
+ - [ ] [MAJOR] Resize doesn't cause layout thrashing
93
+ - [ ] [MAJOR] Content doesn't flicker during resize
94
+ - [ ] [MINOR] Debounced resize callbacks
95
+ - [ ] [MINOR] CSS resize where possible
96
+
97
+ ---
98
+
99
+ ## Implementation Patterns
100
+
101
+ ### Basic Split Panel Component
102
+
103
+ ```tsx
104
+ interface SplitPanelProps {
105
+ left: React.ReactNode;
106
+ right: React.ReactNode;
107
+ defaultLeftWidth?: number; // Percentage
108
+ minLeftWidth?: number;
109
+ maxLeftWidth?: number;
110
+ onResize?: (leftWidth: number) => void;
111
+ className?: string;
112
+ }
113
+
114
+ function SplitPanel({
115
+ left,
116
+ right,
117
+ defaultLeftWidth = 50,
118
+ minLeftWidth = 20,
119
+ maxLeftWidth = 80,
120
+ onResize,
121
+ className = '',
122
+ }: SplitPanelProps) {
123
+ const containerRef = useRef<HTMLDivElement>(null);
124
+ const [leftWidth, setLeftWidth] = useState(defaultLeftWidth);
125
+ const [isDragging, setIsDragging] = useState(false);
126
+
127
+ const handleMouseDown = useCallback(() => {
128
+ setIsDragging(true);
129
+ }, []);
130
+
131
+ const handleMouseMove = useCallback((e: MouseEvent) => {
132
+ if (!isDragging || !containerRef.current) return;
133
+
134
+ const rect = containerRef.current.getBoundingClientRect();
135
+ const x = e.clientX - rect.left;
136
+ const percentage = (x / rect.width) * 100;
137
+ const clamped = Math.max(minLeftWidth, Math.min(maxLeftWidth, percentage));
138
+
139
+ setLeftWidth(clamped);
140
+ onResize?.(clamped);
141
+ }, [isDragging, minLeftWidth, maxLeftWidth, onResize]);
142
+
143
+ const handleMouseUp = useCallback(() => {
144
+ setIsDragging(false);
145
+ }, []);
146
+
147
+ // Add/remove global listeners when dragging
148
+ useEffect(() => {
149
+ if (isDragging) {
150
+ window.addEventListener('mousemove', handleMouseMove);
151
+ window.addEventListener('mouseup', handleMouseUp);
152
+ document.body.style.cursor = 'col-resize';
153
+ document.body.style.userSelect = 'none';
154
+
155
+ return () => {
156
+ window.removeEventListener('mousemove', handleMouseMove);
157
+ window.removeEventListener('mouseup', handleMouseUp);
158
+ document.body.style.cursor = '';
159
+ document.body.style.userSelect = '';
160
+ };
161
+ }
162
+ }, [isDragging, handleMouseMove, handleMouseUp]);
163
+
164
+ // Double-click to reset
165
+ const handleDoubleClick = useCallback(() => {
166
+ setLeftWidth(defaultLeftWidth);
167
+ onResize?.(defaultLeftWidth);
168
+ }, [defaultLeftWidth, onResize]);
169
+
170
+ return (
171
+ <div
172
+ ref={containerRef}
173
+ className={`flex h-full ${className}`}
174
+ >
175
+ {/* Left panel */}
176
+ <div
177
+ className="overflow-hidden"
178
+ style={{ width: `${leftWidth}%` }}
179
+ >
180
+ {left}
181
+ </div>
182
+
183
+ {/* Divider */}
184
+ <div
185
+ className={`
186
+ w-1 flex-shrink-0 cursor-col-resize
187
+ bg-border-theme-subtle hover:bg-accent
188
+ transition-colors
189
+ ${isDragging ? 'bg-accent' : ''}
190
+ `}
191
+ onMouseDown={handleMouseDown}
192
+ onDoubleClick={handleDoubleClick}
193
+ role="separator"
194
+ aria-orientation="vertical"
195
+ aria-label="Resize panels"
196
+ tabIndex={0}
197
+ onKeyDown={(e) => {
198
+ if (e.key === 'ArrowLeft') {
199
+ setLeftWidth(prev => Math.max(minLeftWidth, prev - 5));
200
+ } else if (e.key === 'ArrowRight') {
201
+ setLeftWidth(prev => Math.min(maxLeftWidth, prev + 5));
202
+ }
203
+ }}
204
+ />
205
+
206
+ {/* Right panel */}
207
+ <div
208
+ className="flex-1 overflow-hidden"
209
+ style={{ width: `${100 - leftWidth}%` }}
210
+ >
211
+ {right}
212
+ </div>
213
+ </div>
214
+ );
215
+ }
216
+ ```
217
+
218
+ ### Collapsible Panel Component
219
+
220
+ ```tsx
221
+ interface CollapsiblePanelProps {
222
+ children: React.ReactNode;
223
+ title: string;
224
+ side: 'left' | 'right';
225
+ defaultExpanded?: boolean;
226
+ defaultWidth?: number;
227
+ minWidth?: number;
228
+ onExpandedChange?: (expanded: boolean) => void;
229
+ }
230
+
231
+ function CollapsiblePanel({
232
+ children,
233
+ title,
234
+ side,
235
+ defaultExpanded = true,
236
+ defaultWidth = 300,
237
+ minWidth = 200,
238
+ onExpandedChange,
239
+ }: CollapsiblePanelProps) {
240
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded);
241
+ const [width, setWidth] = useState(defaultWidth);
242
+
243
+ const toggle = useCallback(() => {
244
+ setIsExpanded(prev => {
245
+ const newValue = !prev;
246
+ onExpandedChange?.(newValue);
247
+ return newValue;
248
+ });
249
+ }, [onExpandedChange]);
250
+
251
+ return (
252
+ <div
253
+ className={`
254
+ flex-shrink-0 border-${side === 'left' ? 'r' : 'l'} border-border-theme-subtle
255
+ transition-all duration-300 ease-in-out overflow-hidden
256
+ `}
257
+ style={{ width: isExpanded ? width : 40 }}
258
+ >
259
+ {isExpanded ? (
260
+ <div className="h-full flex flex-col" style={{ width }}>
261
+ {/* Header */}
262
+ <div className="flex items-center justify-between px-3 py-2 border-b border-border-theme-subtle">
263
+ <h3 className="font-medium text-sm text-text-theme-primary">{title}</h3>
264
+ <button
265
+ onClick={toggle}
266
+ className="p-1 hover:bg-surface-raised rounded"
267
+ aria-label={`Collapse ${title}`}
268
+ >
269
+ {side === 'left' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
270
+ </button>
271
+ </div>
272
+
273
+ {/* Content */}
274
+ <div className="flex-1 overflow-auto">
275
+ {children}
276
+ </div>
277
+ </div>
278
+ ) : (
279
+ <button
280
+ onClick={toggle}
281
+ className="w-full h-full flex flex-col items-center justify-center gap-2 hover:bg-surface-raised"
282
+ aria-label={`Expand ${title}`}
283
+ >
284
+ {side === 'left' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
285
+ <span className="writing-mode-vertical text-xs text-text-theme-secondary">
286
+ {title}
287
+ </span>
288
+ </button>
289
+ )}
290
+ </div>
291
+ );
292
+ }
293
+ ```
294
+
295
+ ### Three-Panel Layout
296
+
297
+ ```tsx
298
+ interface ThreePanelLayoutProps {
299
+ left: React.ReactNode;
300
+ center: React.ReactNode;
301
+ right: React.ReactNode;
302
+ leftTitle?: string;
303
+ rightTitle?: string;
304
+ defaultLeftWidth?: number;
305
+ defaultRightWidth?: number;
306
+ }
307
+
308
+ function ThreePanelLayout({
309
+ left,
310
+ center,
311
+ right,
312
+ leftTitle = 'Navigator',
313
+ rightTitle = 'Properties',
314
+ defaultLeftWidth = 250,
315
+ defaultRightWidth = 300,
316
+ }: ThreePanelLayoutProps) {
317
+ const [leftExpanded, setLeftExpanded] = useState(true);
318
+ const [rightExpanded, setRightExpanded] = useState(true);
319
+
320
+ return (
321
+ <div className="flex h-full">
322
+ {/* Left panel */}
323
+ <CollapsiblePanel
324
+ title={leftTitle}
325
+ side="left"
326
+ defaultWidth={defaultLeftWidth}
327
+ defaultExpanded={leftExpanded}
328
+ onExpandedChange={setLeftExpanded}
329
+ >
330
+ {left}
331
+ </CollapsiblePanel>
332
+
333
+ {/* Center panel (main content) */}
334
+ <div className="flex-1 overflow-hidden">
335
+ {center}
336
+ </div>
337
+
338
+ {/* Right panel */}
339
+ <CollapsiblePanel
340
+ title={rightTitle}
341
+ side="right"
342
+ defaultWidth={defaultRightWidth}
343
+ defaultExpanded={rightExpanded}
344
+ onExpandedChange={setRightExpanded}
345
+ >
346
+ {right}
347
+ </CollapsiblePanel>
348
+ </div>
349
+ );
350
+ }
351
+ ```
352
+
353
+ ### Synchronized Scroll Panels
354
+
355
+ ```tsx
356
+ interface SyncScrollPanelsProps {
357
+ leftContent: React.ReactNode;
358
+ rightContent: React.ReactNode;
359
+ syncEnabled?: boolean;
360
+ }
361
+
362
+ function SyncScrollPanels({
363
+ leftContent,
364
+ rightContent,
365
+ syncEnabled = true
366
+ }: SyncScrollPanelsProps) {
367
+ const leftRef = useRef<HTMLDivElement>(null);
368
+ const rightRef = useRef<HTMLDivElement>(null);
369
+ const isScrolling = useRef(false);
370
+
371
+ const syncScroll = useCallback((source: 'left' | 'right') => {
372
+ if (!syncEnabled || isScrolling.current) return;
373
+
374
+ isScrolling.current = true;
375
+ const sourceEl = source === 'left' ? leftRef.current : rightRef.current;
376
+ const targetEl = source === 'left' ? rightRef.current : leftRef.current;
377
+
378
+ if (sourceEl && targetEl) {
379
+ // Sync by percentage
380
+ const scrollPercentage = sourceEl.scrollTop /
381
+ (sourceEl.scrollHeight - sourceEl.clientHeight);
382
+ targetEl.scrollTop = scrollPercentage *
383
+ (targetEl.scrollHeight - targetEl.clientHeight);
384
+ }
385
+
386
+ requestAnimationFrame(() => {
387
+ isScrolling.current = false;
388
+ });
389
+ }, [syncEnabled]);
390
+
391
+ return (
392
+ <SplitPanel
393
+ left={
394
+ <div
395
+ ref={leftRef}
396
+ className="h-full overflow-auto"
397
+ onScroll={() => syncScroll('left')}
398
+ >
399
+ {leftContent}
400
+ </div>
401
+ }
402
+ right={
403
+ <div
404
+ ref={rightRef}
405
+ className="h-full overflow-auto"
406
+ onScroll={() => syncScroll('right')}
407
+ >
408
+ {rightContent}
409
+ </div>
410
+ }
411
+ />
412
+ );
413
+ }
414
+ ```
415
+
416
+ ### Responsive Split Layout
417
+
418
+ ```tsx
419
+ interface ResponsiveSplitProps {
420
+ primary: React.ReactNode;
421
+ secondary: React.ReactNode;
422
+ breakpoint?: number;
423
+ primaryLabel: string;
424
+ secondaryLabel: string;
425
+ }
426
+
427
+ function ResponsiveSplit({
428
+ primary,
429
+ secondary,
430
+ breakpoint = 768,
431
+ primaryLabel,
432
+ secondaryLabel,
433
+ }: ResponsiveSplitProps) {
434
+ const [isMobile, setIsMobile] = useState(false);
435
+ const [activePanel, setActivePanel] = useState<'primary' | 'secondary'>('primary');
436
+
437
+ useEffect(() => {
438
+ const checkBreakpoint = () => {
439
+ setIsMobile(window.innerWidth < breakpoint);
440
+ };
441
+
442
+ checkBreakpoint();
443
+ window.addEventListener('resize', checkBreakpoint);
444
+ return () => window.removeEventListener('resize', checkBreakpoint);
445
+ }, [breakpoint]);
446
+
447
+ if (isMobile) {
448
+ return (
449
+ <div className="h-full flex flex-col">
450
+ {/* Mobile tabs */}
451
+ <div className="flex border-b border-border-theme-subtle">
452
+ <button
453
+ onClick={() => setActivePanel('primary')}
454
+ className={`flex-1 px-4 py-3 text-sm font-medium ${
455
+ activePanel === 'primary'
456
+ ? 'text-accent border-b-2 border-accent'
457
+ : 'text-text-theme-secondary'
458
+ }`}
459
+ >
460
+ {primaryLabel}
461
+ </button>
462
+ <button
463
+ onClick={() => setActivePanel('secondary')}
464
+ className={`flex-1 px-4 py-3 text-sm font-medium ${
465
+ activePanel === 'secondary'
466
+ ? 'text-accent border-b-2 border-accent'
467
+ : 'text-text-theme-secondary'
468
+ }`}
469
+ >
470
+ {secondaryLabel}
471
+ </button>
472
+ </div>
473
+
474
+ {/* Mobile content */}
475
+ <div className="flex-1 overflow-hidden">
476
+ {activePanel === 'primary' ? primary : secondary}
477
+ </div>
478
+ </div>
479
+ );
480
+ }
481
+
482
+ return (
483
+ <SplitPanel
484
+ left={primary}
485
+ right={secondary}
486
+ defaultLeftWidth={50}
487
+ />
488
+ );
489
+ }
490
+ ```
491
+
492
+ ---
493
+
494
+ ## Layout Presets
495
+
496
+ ```typescript
497
+ const SPLIT_PRESETS = {
498
+ // Common ratios
499
+ equal: { left: 50, right: 50 },
500
+ goldenLeft: { left: 62, right: 38 },
501
+ goldenRight: { left: 38, right: 62 },
502
+ sidebarLeft: { left: 25, right: 75 },
503
+ sidebarRight: { left: 75, right: 25 },
504
+
505
+ // IDE-style
506
+ fileTree: { left: 20, right: 80 },
507
+ properties: { left: 70, right: 30 },
508
+ };
509
+ ```
510
+
511
+ ---
512
+
513
+ ## Anti-Patterns
514
+
515
+ ### DON'T: Content Overflow During Resize
516
+ ```tsx
517
+ // BAD - Content overflows during resize
518
+ <div style={{ width: `${width}px` }}>
519
+ {content}
520
+ </div>
521
+
522
+ // GOOD - Hidden overflow with proper containment
523
+ <div
524
+ style={{ width: `${width}px` }}
525
+ className="overflow-hidden"
526
+ >
527
+ <div className="w-full h-full overflow-auto">
528
+ {content}
529
+ </div>
530
+ </div>
531
+ ```
532
+
533
+ ### DON'T: No Minimum Size
534
+ ```tsx
535
+ // BAD - Panel can be crushed to 0
536
+ const newWidth = (e.clientX / containerWidth) * 100;
537
+ setWidth(newWidth);
538
+
539
+ // GOOD - Enforce minimum
540
+ const newWidth = Math.max(MIN_WIDTH, (e.clientX / containerWidth) * 100);
541
+ setWidth(newWidth);
542
+ ```
543
+
544
+ ### DON'T: No Visual Divider Feedback
545
+ ```tsx
546
+ // BAD - Divider looks static
547
+ <div className="w-1 bg-gray-300" />
548
+
549
+ // GOOD - Clear interaction feedback
550
+ <div className={`
551
+ w-1 bg-gray-300
552
+ cursor-col-resize
553
+ hover:bg-accent
554
+ transition-colors
555
+ ${isDragging ? 'bg-accent' : ''}
556
+ `} />
557
+ ```
558
+
559
+ ---
560
+
561
+ ## Accessibility
562
+
563
+ ### ARIA for Separators
564
+
565
+ ```tsx
566
+ <div
567
+ role="separator"
568
+ aria-orientation="vertical"
569
+ aria-valuenow={leftWidthPercent}
570
+ aria-valuemin={minWidth}
571
+ aria-valuemax={maxWidth}
572
+ aria-label="Resize panels. Use left and right arrow keys."
573
+ tabIndex={0}
574
+ onKeyDown={handleKeyboard}
575
+ />
576
+ ```
577
+
578
+ ### Keyboard Navigation
579
+
580
+ | Key | Action |
581
+ |-----|--------|
582
+ | `ArrowLeft` | Decrease left panel width |
583
+ | `ArrowRight` | Increase left panel width |
584
+ | `Home` | Set to minimum |
585
+ | `End` | Set to maximum |
586
+ | `Enter` | Toggle collapse |
587
+
588
+ ---
589
+
590
+ ## Testing Checklist
591
+
592
+ - [ ] Divider draggable with mouse
593
+ - [ ] Resize respects min/max constraints
594
+ - [ ] Double-click resets to default
595
+ - [ ] Keyboard resize works
596
+ - [ ] Collapse/expand animates smoothly
597
+ - [ ] Panels scroll independently
598
+ - [ ] Responsive layout stacks on mobile
599
+ - [ ] Panel size persists across sessions
600
+ - [ ] Content doesn't flicker during resize
601
+ - [ ] Screen reader announces panel changes
602
+
603
+ ---
604
+
605
+ ## Related Skills
606
+
607
+ - `comparison-patterns` - For side-by-side views
608
+ - `editor-workspace-patterns` - For IDE layouts
609
+ - `mobile-responsive-ux` - For mobile adaptation