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,552 @@
1
+ ---
2
+ name: editor-workspace-patterns
3
+ description: Multi-tab editors, workspace management, dirty state, real-time validation, and complex editor UIs
4
+ license: MIT
5
+ ---
6
+
7
+ # Editor & Workspace Patterns
8
+
9
+ Advanced patterns for building complex editor UIs with multi-tab workspaces, dirty state tracking, real-time validation, and professional workspace management. Designed for applications like code editors, customizers, and configuration tools.
10
+
11
+ ## 1. Multi-Tab Interface
12
+
13
+ ### Tab Structure
14
+ ```tsx
15
+ interface Tab {
16
+ id: string;
17
+ title: string;
18
+ icon?: React.ReactNode;
19
+ isDirty: boolean;
20
+ hasErrors: boolean;
21
+ errorCount: number;
22
+ isActive: boolean;
23
+ canClose: boolean;
24
+ }
25
+
26
+ // Visual representation
27
+ // Active tab: bg-background border-b-2 border-primary
28
+ // Inactive tab: bg-muted hover:bg-muted/80
29
+ <div className="flex items-center gap-1 px-3 py-2">
30
+ {tab.isDirty && <span className="text-primary">●</span>}
31
+ <span>{tab.title}</span>
32
+ {tab.errorCount > 0 && (
33
+ <span className="ml-2 px-1.5 py-0.5 text-xs bg-destructive text-destructive-foreground rounded">
34
+ {tab.errorCount}
35
+ </span>
36
+ )}
37
+ <button className="ml-2 hover:bg-background/50 rounded p-0.5">×</button>
38
+ </div>
39
+ ```
40
+
41
+ ### Tab Overflow Handling
42
+ ```tsx
43
+ const TabList = () => {
44
+ const [showOverflow, setShowOverflow] = useState(false);
45
+
46
+ return (
47
+ <div className="flex items-center max-w-full">
48
+ <div className="flex-1 overflow-x-auto scrollbar-thin">
49
+ {visibleTabs.map(tab => <Tab key={tab.id} {...tab} />)}
50
+ </div>
51
+ {hasOverflow && (
52
+ <DropdownMenu>
53
+ <DropdownMenuTrigger className="px-2 py-1 hover:bg-muted">
54
+
55
+ </DropdownMenuTrigger>
56
+ <DropdownMenuContent>
57
+ {hiddenTabs.map(tab => (
58
+ <DropdownMenuItem key={tab.id}>
59
+ {tab.isDirty && "● "}{tab.title}
60
+ </DropdownMenuItem>
61
+ ))}
62
+ </DropdownMenuContent>
63
+ </DropdownMenu>
64
+ )}
65
+ </div>
66
+ );
67
+ };
68
+ ```
69
+
70
+ ### Tab Reordering
71
+ Use `@dnd-kit/core` for drag-and-drop tab reordering with visual feedback during drag.
72
+
73
+ ## 2. Dirty State Management
74
+
75
+ ### DirtyStateTracker
76
+ ```tsx
77
+ interface DirtyStateTracker {
78
+ dirtyTabs: Set<string>;
79
+ markDirty(tabId: string): void;
80
+ markClean(tabId: string): void;
81
+ isDirty(tabId: string): boolean;
82
+ hasAnyDirty(): boolean;
83
+ }
84
+
85
+ class WorkspaceDirtyState implements DirtyStateTracker {
86
+ private dirtyTabs = new Set<string>();
87
+ private originalData = new Map<string, unknown>();
88
+
89
+ markDirty(tabId: string) {
90
+ this.dirtyTabs.add(tabId);
91
+ window.onbeforeunload = (e) => {
92
+ e.preventDefault();
93
+ return "Unsaved changes";
94
+ };
95
+ }
96
+
97
+ markClean(tabId: string) {
98
+ this.dirtyTabs.delete(tabId);
99
+ if (this.dirtyTabs.size === 0) {
100
+ window.onbeforeunload = null;
101
+ }
102
+ }
103
+
104
+ async promptSaveOnClose(tabId: string): Promise<'save' | 'discard' | 'cancel'> {
105
+ // Return user choice from dialog
106
+ }
107
+ }
108
+ ```
109
+
110
+ ### Close Tab Flow
111
+ ```tsx
112
+ const handleCloseTab = async (tabId: string) => {
113
+ if (dirtyState.isDirty(tabId)) {
114
+ const choice = await confirmDialog({
115
+ title: "Unsaved Changes",
116
+ description: "Save changes before closing?",
117
+ actions: [
118
+ { label: "Save", value: "save", variant: "default" },
119
+ { label: "Discard", value: "discard", variant: "destructive" },
120
+ { label: "Cancel", value: "cancel", variant: "ghost" }
121
+ ]
122
+ });
123
+
124
+ if (choice === 'cancel') return;
125
+ if (choice === 'save') await saveTab(tabId);
126
+ }
127
+
128
+ removeTab(tabId);
129
+ };
130
+ ```
131
+
132
+ ## 3. Real-Time Validation
133
+
134
+ ### Validation Architecture
135
+ ```tsx
136
+ interface ValidationResult {
137
+ field: string;
138
+ severity: 'error' | 'warning' | 'info';
139
+ message: string;
140
+ path?: string[];
141
+ }
142
+
143
+ interface ValidationSummary {
144
+ errors: ValidationResult[];
145
+ warnings: ValidationResult[];
146
+ isValid: boolean;
147
+ }
148
+
149
+ const useRealtimeValidation = (data: unknown, schema: ZodSchema) => {
150
+ const [validation, setValidation] = useState<ValidationSummary>({
151
+ errors: [],
152
+ warnings: [],
153
+ isValid: true
154
+ });
155
+
156
+ useEffect(() => {
157
+ const timer = setTimeout(() => {
158
+ const result = schema.safeParse(data);
159
+ if (!result.success) {
160
+ const errors = result.error.issues.map(issue => ({
161
+ field: issue.path.join('.'),
162
+ severity: 'error' as const,
163
+ message: issue.message,
164
+ path: issue.path as string[]
165
+ }));
166
+ setValidation({ errors, warnings: [], isValid: false });
167
+ } else {
168
+ setValidation({ errors: [], warnings: [], isValid: true });
169
+ }
170
+ }, 300); // Debounce
171
+
172
+ return () => clearTimeout(timer);
173
+ }, [data, schema]);
174
+
175
+ return validation;
176
+ };
177
+ ```
178
+
179
+ ### Inline Validation Display
180
+ ```tsx
181
+ <div className="space-y-1">
182
+ <Input
183
+ className={cn(
184
+ validation.errors.length > 0 && "border-destructive",
185
+ validation.warnings.length > 0 && "border-yellow-500"
186
+ )}
187
+ />
188
+ {validation.errors.map((err, i) => (
189
+ <p key={i} className="text-sm text-destructive">{err.message}</p>
190
+ ))}
191
+ {validation.warnings.map((warn, i) => (
192
+ <p key={i} className="text-sm text-yellow-600">{warn.message}</p>
193
+ ))}
194
+ </div>
195
+ ```
196
+
197
+ ## 4. Workspace Layout
198
+
199
+ ### Panel-Based Layout
200
+ ```tsx
201
+ const WorkspaceLayout = () => {
202
+ return (
203
+ <ResizablePanelGroup direction="horizontal">
204
+ <ResizablePanel defaultSize={20} minSize={15} collapsible>
205
+ <Sidebar />
206
+ </ResizablePanel>
207
+ <ResizableHandle />
208
+ <ResizablePanel defaultSize={80}>
209
+ <div className="flex flex-col h-full">
210
+ <TabBar />
211
+ <div className="flex-1 overflow-auto">
212
+ <EditorContent />
213
+ </div>
214
+ <StatusBar />
215
+ </div>
216
+ </ResizablePanel>
217
+ </ResizablePanelGroup>
218
+ );
219
+ };
220
+ ```
221
+
222
+ ### Collapsible Panels
223
+ Store panel states in localStorage for persistence across sessions.
224
+
225
+ ## 5. Undo/Redo
226
+
227
+ ### Command Pattern
228
+ ```tsx
229
+ interface Command {
230
+ execute(): void;
231
+ undo(): void;
232
+ redo(): void;
233
+ describe(): string;
234
+ }
235
+
236
+ class UndoStack {
237
+ private history: Command[] = [];
238
+ private currentIndex = -1;
239
+
240
+ execute(command: Command) {
241
+ command.execute();
242
+ this.history = this.history.slice(0, this.currentIndex + 1);
243
+ this.history.push(command);
244
+ this.currentIndex++;
245
+ }
246
+
247
+ undo() {
248
+ if (this.canUndo()) {
249
+ this.history[this.currentIndex].undo();
250
+ this.currentIndex--;
251
+ }
252
+ }
253
+
254
+ redo() {
255
+ if (this.canRedo()) {
256
+ this.currentIndex++;
257
+ this.history[this.currentIndex].redo();
258
+ }
259
+ }
260
+
261
+ canUndo() { return this.currentIndex >= 0; }
262
+ canRedo() { return this.currentIndex < this.history.length - 1; }
263
+ }
264
+ ```
265
+
266
+ ### Keyboard Shortcuts
267
+ ```tsx
268
+ useEffect(() => {
269
+ const handler = (e: KeyboardEvent) => {
270
+ if (e.ctrlKey || e.metaKey) {
271
+ if (e.key === 'z' && !e.shiftKey) {
272
+ e.preventDefault();
273
+ undoStack.undo();
274
+ } else if (e.key === 'z' && e.shiftKey || e.key === 'y') {
275
+ e.preventDefault();
276
+ undoStack.redo();
277
+ }
278
+ }
279
+ };
280
+
281
+ window.addEventListener('keydown', handler);
282
+ return () => window.removeEventListener('keydown', handler);
283
+ }, []);
284
+ ```
285
+
286
+ ## 6. Auto-Save Patterns
287
+
288
+ ### Debounced Auto-Save
289
+ ```tsx
290
+ const useAutoSave = (data: unknown, saveFunc: (data: unknown) => Promise<void>) => {
291
+ const [saving, setSaving] = useState(false);
292
+ const [lastSaved, setLastSaved] = useState<Date | null>(null);
293
+
294
+ useEffect(() => {
295
+ const timer = setTimeout(async () => {
296
+ setSaving(true);
297
+ try {
298
+ await saveFunc(data);
299
+ setLastSaved(new Date());
300
+ } catch (error) {
301
+ // Handle conflict or error
302
+ toast.error("Save failed. Please refresh.");
303
+ } finally {
304
+ setSaving(false);
305
+ }
306
+ }, 2000); // 2s debounce
307
+
308
+ return () => clearTimeout(timer);
309
+ }, [data, saveFunc]);
310
+
311
+ return { saving, lastSaved };
312
+ };
313
+ ```
314
+
315
+ ### Save Indicators
316
+ ```tsx
317
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
318
+ {saving && (
319
+ <>
320
+ <Loader2 className="w-3 h-3 animate-spin" />
321
+ <span>Saving...</span>
322
+ </>
323
+ )}
324
+ {!saving && lastSaved && (
325
+ <span>Saved {formatDistanceToNow(lastSaved)} ago</span>
326
+ )}
327
+ </div>
328
+ ```
329
+
330
+ ### Conflict Detection
331
+ ```tsx
332
+ const detectConflict = async (docId: string, localVersion: number) => {
333
+ const serverDoc = await fetchDocument(docId);
334
+ if (serverDoc.version > localVersion) {
335
+ // Show conflict resolution dialog
336
+ return true;
337
+ }
338
+ return false;
339
+ };
340
+ ```
341
+
342
+ ## 7. Configuration Tabs
343
+
344
+ ### URL-Synced Tabs
345
+ ```tsx
346
+ const useTabSync = () => {
347
+ const [searchParams, setSearchParams] = useSearchParams();
348
+ const activeTab = searchParams.get('tab') || 'general';
349
+
350
+ const setActiveTab = (tab: string) => {
351
+ setSearchParams({ tab });
352
+ };
353
+
354
+ return [activeTab, setActiveTab] as const;
355
+ };
356
+ ```
357
+
358
+ ### Lazy Loading Tab Content
359
+ ```tsx
360
+ const TabContent = ({ tabId }: { tabId: string }) => {
361
+ const [loaded, setLoaded] = useState(false);
362
+
363
+ useEffect(() => {
364
+ if (!loaded) {
365
+ // Load heavy content only when tab becomes active
366
+ loadTabContent(tabId).then(() => setLoaded(true));
367
+ }
368
+ }, [tabId, loaded]);
369
+
370
+ if (!loaded) return <Skeleton />;
371
+ return <ActualContent />;
372
+ };
373
+ ```
374
+
375
+ ### Tab Groups
376
+ ```tsx
377
+ <Tabs>
378
+ <TabsList>
379
+ <TabsGroup label="General">
380
+ <TabsTrigger value="profile">Profile</TabsTrigger>
381
+ <TabsTrigger value="settings">Settings</TabsTrigger>
382
+ </TabsGroup>
383
+ <Separator orientation="vertical" className="h-4" />
384
+ <TabsGroup label="Advanced">
385
+ <TabsTrigger value="api">API</TabsTrigger>
386
+ <TabsTrigger value="integrations">Integrations</TabsTrigger>
387
+ </TabsGroup>
388
+ </TabsList>
389
+ </Tabs>
390
+ ```
391
+
392
+ ## 8. Validation Display
393
+
394
+ ### Error vs Warning Colors
395
+ - **Errors (blocking)**: Red/destructive - `border-destructive`, `text-destructive`
396
+ - **Warnings (non-blocking)**: Yellow - `border-yellow-500`, `text-yellow-600`
397
+ - **Info**: Blue - `border-blue-500`, `text-blue-600`
398
+
399
+ ### Tab Badge with Error Count
400
+ ```tsx
401
+ {tab.errorCount > 0 && (
402
+ <Badge variant="destructive" className="ml-2">
403
+ {tab.errorCount}
404
+ </Badge>
405
+ )}
406
+ {tab.warningCount > 0 && tab.errorCount === 0 && (
407
+ <Badge variant="outline" className="ml-2 border-yellow-500 text-yellow-600">
408
+ {tab.warningCount}
409
+ </Badge>
410
+ )}
411
+ ```
412
+
413
+ ### Validation Summary Panel
414
+ ```tsx
415
+ <Card>
416
+ <CardHeader>
417
+ <CardTitle>Validation Results</CardTitle>
418
+ </CardHeader>
419
+ <CardContent>
420
+ {validation.errors.length > 0 && (
421
+ <div className="space-y-2">
422
+ <h4 className="text-sm font-medium text-destructive">
423
+ {validation.errors.length} Errors
424
+ </h4>
425
+ {validation.errors.map((err, i) => (
426
+ <button
427
+ key={i}
428
+ onClick={() => jumpToField(err.field)}
429
+ className="block w-full text-left text-sm p-2 hover:bg-muted rounded"
430
+ >
431
+ {err.field}: {err.message}
432
+ </button>
433
+ ))}
434
+ </div>
435
+ )}
436
+ </CardContent>
437
+ </Card>
438
+ ```
439
+
440
+ ## 9. WorkspaceProvider Pattern
441
+
442
+ ```tsx
443
+ interface WorkspaceContextValue {
444
+ tabs: Tab[];
445
+ activeTabId: string | null;
446
+ dirtyState: DirtyStateTracker;
447
+ validation: Map<string, ValidationSummary>;
448
+ addTab: (tab: Omit<Tab, 'id'>) => string;
449
+ removeTab: (id: string) => Promise<void>;
450
+ setActiveTab: (id: string) => void;
451
+ saveTab: (id: string) => Promise<void>;
452
+ saveAll: () => Promise<void>;
453
+ }
454
+
455
+ const WorkspaceContext = createContext<WorkspaceContextValue | null>(null);
456
+
457
+ export const WorkspaceProvider = ({ children }: PropsWithChildren) => {
458
+ const [tabs, setTabs] = useState<Tab[]>([]);
459
+ const [activeTabId, setActiveTabId] = useState<string | null>(null);
460
+ const dirtyState = useMemo(() => new WorkspaceDirtyState(), []);
461
+
462
+ const removeTab = async (id: string) => {
463
+ const tab = tabs.find(t => t.id === id);
464
+ if (!tab) return;
465
+
466
+ if (dirtyState.isDirty(id)) {
467
+ const choice = await dirtyState.promptSaveOnClose(id);
468
+ if (choice === 'cancel') return;
469
+ if (choice === 'save') await saveTab(id);
470
+ }
471
+
472
+ setTabs(prev => prev.filter(t => t.id !== id));
473
+ dirtyState.markClean(id);
474
+ };
475
+
476
+ const saveAll = async () => {
477
+ const dirtyTabIds = tabs.filter(t => dirtyState.isDirty(t.id)).map(t => t.id);
478
+ await Promise.all(dirtyTabIds.map(id => saveTab(id)));
479
+ };
480
+
481
+ return (
482
+ <WorkspaceContext.Provider value={{
483
+ tabs,
484
+ activeTabId,
485
+ dirtyState,
486
+ addTab,
487
+ removeTab,
488
+ setActiveTab,
489
+ saveTab,
490
+ saveAll
491
+ }}>
492
+ {children}
493
+ </WorkspaceContext.Provider>
494
+ );
495
+ };
496
+ ```
497
+
498
+ ## 10. Audit Checklist
499
+
500
+ ### Multi-Tab Interface
501
+ - [CRITICAL] Tab close button requires confirmation if dirty
502
+ - [CRITICAL] Dirty indicator (●) visible on unsaved tabs
503
+ - [MAJOR] Error count badge visible on tabs with validation errors
504
+ - [MAJOR] Tab overflow handled with dropdown or scroll
505
+ - [MINOR] Tab reordering supported via drag-and-drop
506
+ - [MINOR] Tab icons displayed for visual identification
507
+
508
+ ### Dirty State Management
509
+ - [CRITICAL] Browser unload warning when dirty tabs exist
510
+ - [CRITICAL] "Save/Discard/Cancel" dialog on close dirty tab
511
+ - [MAJOR] Dirty state persists across component remounts
512
+ - [MAJOR] "Save All" action available when multiple tabs dirty
513
+ - [MINOR] Dirty state cleared after successful save
514
+
515
+ ### Real-Time Validation
516
+ - [CRITICAL] Validation runs on user input (debounced)
517
+ - [CRITICAL] Errors prevent save/submit actions
518
+ - [MAJOR] Warnings shown but don't block actions
519
+ - [MAJOR] Validation messages appear inline near fields
520
+ - [MINOR] Validation summary panel shows all issues
521
+ - [MINOR] Click validation message jumps to field
522
+
523
+ ### Workspace Layout
524
+ - [MAJOR] Panels resizable with visible handles
525
+ - [MAJOR] Sidebar collapsible with toggle button
526
+ - [MINOR] Panel sizes persist in localStorage
527
+ - [MINOR] Responsive layout on smaller screens
528
+
529
+ ### Undo/Redo
530
+ - [CRITICAL] Ctrl+Z / Cmd+Z triggers undo
531
+ - [CRITICAL] Ctrl+Shift+Z / Cmd+Shift+Z triggers redo
532
+ - [MAJOR] Undo/redo buttons disabled when stack empty
533
+ - [MINOR] Undo history tooltip shows command description
534
+
535
+ ### Auto-Save
536
+ - [MAJOR] Auto-save debounced (1-3 seconds)
537
+ - [MAJOR] Save indicator shows "Saving..." and "Saved" states
538
+ - [CRITICAL] Conflict detection before auto-save
539
+ - [MINOR] Manual save option always available
540
+ - [MINOR] Last saved timestamp displayed
541
+
542
+ ### Performance
543
+ - [CRITICAL] Tab content lazy-loaded (not all at once)
544
+ - [MAJOR] Validation debounced to avoid excessive re-renders
545
+ - [MAJOR] Large lists virtualized in editor panels
546
+ - [MINOR] Tab switches feel instant (<100ms)
547
+
548
+ ### Accessibility
549
+ - [CRITICAL] Keyboard navigation works (Tab, Arrow keys)
550
+ - [MAJOR] Focus visible on all interactive elements
551
+ - [MAJOR] Screen reader announces tab changes and validation
552
+ - [MINOR] Tooltips on icon-only buttons