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