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,608 @@
1
+ ---
2
+ name: form-patterns
3
+ description: Form UX patterns including validation timing, field layouts, error handling, and multi-step wizard forms
4
+ license: MIT
5
+ ---
6
+
7
+ # Form UX Patterns
8
+
9
+ Forms are the primary way users input data. Good form UX reduces friction and errors.
10
+
11
+ ## Form Field Component
12
+
13
+ ### Standard Field Structure
14
+ ```tsx
15
+ interface FormFieldProps {
16
+ label: string;
17
+ htmlFor: string;
18
+ error?: string;
19
+ hint?: string;
20
+ required?: boolean;
21
+ children: ReactNode;
22
+ }
23
+
24
+ function FormField({ label, htmlFor, error, hint, required, children }: FormFieldProps) {
25
+ return (
26
+ <div className="space-y-1.5">
27
+ <label
28
+ htmlFor={htmlFor}
29
+ className="block text-sm font-medium text-text-primary"
30
+ >
31
+ {label}
32
+ {required && <span className="text-red-400 ml-1">*</span>}
33
+ </label>
34
+
35
+ {children}
36
+
37
+ {hint && !error && (
38
+ <p className="text-xs text-text-muted">{hint}</p>
39
+ )}
40
+
41
+ {error && (
42
+ <p className="text-xs text-red-400 flex items-center gap-1" role="alert">
43
+ <AlertIcon className="w-3 h-3" />
44
+ {error}
45
+ </p>
46
+ )}
47
+ </div>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## Input Components
53
+
54
+ ### Text Input
55
+ ```tsx
56
+ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
57
+ error?: boolean;
58
+ icon?: ReactNode;
59
+ }
60
+
61
+ function Input({ error, icon, className, ...props }: InputProps) {
62
+ return (
63
+ <div className="relative">
64
+ {icon && (
65
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted">
66
+ {icon}
67
+ </div>
68
+ )}
69
+ <input
70
+ className={`
71
+ w-full px-3 py-2 bg-surface-deep border rounded-lg text-white text-sm
72
+ placeholder:text-text-muted
73
+ focus:outline-none focus:ring-2 focus:ring-accent/50 focus:border-accent
74
+ disabled:opacity-50 disabled:cursor-not-allowed
75
+ ${icon ? 'pl-10' : ''}
76
+ ${error ? 'border-red-500 focus:ring-red-500/50' : 'border-border'}
77
+ ${className || ''}
78
+ `}
79
+ {...props}
80
+ />
81
+ </div>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ### Textarea
87
+ ```tsx
88
+ function Textarea({ error, className, ...props }: TextareaProps) {
89
+ return (
90
+ <textarea
91
+ className={`
92
+ w-full px-3 py-2 bg-surface-deep border rounded-lg text-white text-sm
93
+ placeholder:text-text-muted resize-y min-h-[100px]
94
+ focus:outline-none focus:ring-2 focus:ring-accent/50 focus:border-accent
95
+ ${error ? 'border-red-500' : 'border-border'}
96
+ ${className || ''}
97
+ `}
98
+ {...props}
99
+ />
100
+ );
101
+ }
102
+ ```
103
+
104
+ ### Select
105
+ ```tsx
106
+ interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
107
+ options: { value: string; label: string }[];
108
+ placeholder?: string;
109
+ error?: boolean;
110
+ }
111
+
112
+ function Select({ options, placeholder, error, className, ...props }: SelectProps) {
113
+ return (
114
+ <select
115
+ className={`
116
+ w-full px-3 py-2 bg-surface-deep border rounded-lg text-white text-sm
117
+ focus:outline-none focus:ring-2 focus:ring-accent/50 focus:border-accent
118
+ ${error ? 'border-red-500' : 'border-border'}
119
+ ${className || ''}
120
+ `}
121
+ {...props}
122
+ >
123
+ {placeholder && (
124
+ <option value="" disabled>
125
+ {placeholder}
126
+ </option>
127
+ )}
128
+ {options.map((opt) => (
129
+ <option key={opt.value} value={opt.value}>
130
+ {opt.label}
131
+ </option>
132
+ ))}
133
+ </select>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ### Checkbox / Radio
139
+ ```tsx
140
+ function Checkbox({ label, checked, onChange, disabled }: CheckboxProps) {
141
+ return (
142
+ <label className={`
143
+ flex items-center gap-2 cursor-pointer
144
+ ${disabled ? 'opacity-50 cursor-not-allowed' : ''}
145
+ `}>
146
+ <input
147
+ type="checkbox"
148
+ checked={checked}
149
+ onChange={onChange}
150
+ disabled={disabled}
151
+ className="
152
+ w-4 h-4 rounded border-border bg-surface-deep
153
+ text-accent focus:ring-accent focus:ring-offset-0
154
+ checked:bg-accent checked:border-accent
155
+ "
156
+ />
157
+ <span className="text-sm text-text-primary">{label}</span>
158
+ </label>
159
+ );
160
+ }
161
+ ```
162
+
163
+ ## Validation Timing
164
+
165
+ ### When to Validate
166
+
167
+ | Timing | Use For | Example |
168
+ |--------|---------|---------|
169
+ | On blur | Most fields | Email format after leaving field |
170
+ | On change (debounced) | Real-time feedback | Password strength |
171
+ | On submit | Final validation | All fields before submit |
172
+ | Async | Server validation | Username availability |
173
+
174
+ ### Validation Pattern
175
+ ```tsx
176
+ function useFormValidation<T extends Record<string, any>>(
177
+ initialData: T,
178
+ validators: Record<keyof T, (value: any, data: T) => string | undefined>
179
+ ) {
180
+ const [data, setData] = useState<T>(initialData);
181
+ const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
182
+ const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
183
+
184
+ const validateField = useCallback((field: keyof T) => {
185
+ const validator = validators[field];
186
+ if (validator) {
187
+ const error = validator(data[field], data);
188
+ setErrors(prev => ({ ...prev, [field]: error }));
189
+ return !error;
190
+ }
191
+ return true;
192
+ }, [data, validators]);
193
+
194
+ const handleBlur = useCallback((field: keyof T) => {
195
+ setTouched(prev => ({ ...prev, [field]: true }));
196
+ validateField(field);
197
+ }, [validateField]);
198
+
199
+ const handleChange = useCallback((field: keyof T, value: any) => {
200
+ setData(prev => ({ ...prev, [field]: value }));
201
+ // Clear error on change if field was touched
202
+ if (touched[field]) {
203
+ setErrors(prev => ({ ...prev, [field]: undefined }));
204
+ }
205
+ }, [touched]);
206
+
207
+ const validateAll = useCallback(() => {
208
+ const newErrors: Partial<Record<keyof T, string>> = {};
209
+ let isValid = true;
210
+
211
+ for (const field of Object.keys(validators) as (keyof T)[]) {
212
+ const error = validators[field](data[field], data);
213
+ if (error) {
214
+ newErrors[field] = error;
215
+ isValid = false;
216
+ }
217
+ }
218
+
219
+ setErrors(newErrors);
220
+ setTouched(Object.keys(data).reduce((acc, key) => ({ ...acc, [key]: true }), {}));
221
+ return isValid;
222
+ }, [data, validators]);
223
+
224
+ return {
225
+ data,
226
+ errors,
227
+ touched,
228
+ handleChange,
229
+ handleBlur,
230
+ validateField,
231
+ validateAll,
232
+ setData,
233
+ reset: () => {
234
+ setData(initialData);
235
+ setErrors({});
236
+ setTouched({});
237
+ },
238
+ };
239
+ }
240
+
241
+ // Usage
242
+ const { data, errors, handleChange, handleBlur, validateAll } = useFormValidation(
243
+ { email: '', password: '' },
244
+ {
245
+ email: (value) => {
246
+ if (!value) return 'Email is required';
247
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';
248
+ },
249
+ password: (value) => {
250
+ if (!value) return 'Password is required';
251
+ if (value.length < 8) return 'Password must be at least 8 characters';
252
+ },
253
+ }
254
+ );
255
+ ```
256
+
257
+ ## Form Layout Patterns
258
+
259
+ ### Single Column Form
260
+ ```tsx
261
+ // Default for most forms - stack fields vertically
262
+ <form className="space-y-4 max-w-md">
263
+ <FormField label="Name" htmlFor="name" required>
264
+ <Input id="name" />
265
+ </FormField>
266
+
267
+ <FormField label="Email" htmlFor="email" required>
268
+ <Input id="email" type="email" />
269
+ </FormField>
270
+
271
+ <FormField label="Message" htmlFor="message">
272
+ <Textarea id="message" />
273
+ </FormField>
274
+ </form>
275
+ ```
276
+
277
+ ### Two Column Form
278
+ ```tsx
279
+ // For related fields that fit together
280
+ <form className="space-y-4">
281
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
282
+ <FormField label="First Name" htmlFor="firstName">
283
+ <Input id="firstName" />
284
+ </FormField>
285
+ <FormField label="Last Name" htmlFor="lastName">
286
+ <Input id="lastName" />
287
+ </FormField>
288
+ </div>
289
+
290
+ <FormField label="Email" htmlFor="email">
291
+ <Input id="email" type="email" />
292
+ </FormField>
293
+ </form>
294
+ ```
295
+
296
+ ### Sectioned Form
297
+ ```tsx
298
+ // For long forms with logical groupings
299
+ <form className="space-y-8">
300
+ <FormSection title="Personal Information" description="Your basic details">
301
+ <FormField label="Name" htmlFor="name">
302
+ <Input id="name" />
303
+ </FormField>
304
+ </FormSection>
305
+
306
+ <FormSection title="Contact Information" description="How we can reach you">
307
+ <FormField label="Email" htmlFor="email">
308
+ <Input id="email" type="email" />
309
+ </FormField>
310
+ </FormSection>
311
+ </form>
312
+
313
+ function FormSection({ title, description, children }) {
314
+ return (
315
+ <div className="border-b border-border pb-6">
316
+ <h3 className="text-lg font-semibold text-white mb-1">{title}</h3>
317
+ {description && (
318
+ <p className="text-sm text-text-secondary mb-4">{description}</p>
319
+ )}
320
+ <div className="space-y-4">{children}</div>
321
+ </div>
322
+ );
323
+ }
324
+ ```
325
+
326
+ ## Form Submission States
327
+
328
+ ### Submit Button States
329
+ ```tsx
330
+ interface SubmitButtonProps {
331
+ isLoading: boolean;
332
+ isDisabled: boolean;
333
+ loadingText?: string;
334
+ children: ReactNode;
335
+ }
336
+
337
+ function SubmitButton({ isLoading, isDisabled, loadingText = 'Saving...', children }: SubmitButtonProps) {
338
+ return (
339
+ <Button
340
+ type="submit"
341
+ variant="primary"
342
+ disabled={isLoading || isDisabled}
343
+ className="w-full sm:w-auto"
344
+ >
345
+ {isLoading ? (
346
+ <>
347
+ <Spinner className="w-4 h-4 mr-2" />
348
+ {loadingText}
349
+ </>
350
+ ) : (
351
+ children
352
+ )}
353
+ </Button>
354
+ );
355
+ }
356
+ ```
357
+
358
+ ### Form Action Bar
359
+ ```tsx
360
+ function FormActionBar({ onCancel, isLoading, isDirty, submitLabel = 'Save' }) {
361
+ return (
362
+ <div className="flex items-center justify-end gap-3 pt-6 border-t border-border mt-6">
363
+ <Button
364
+ type="button"
365
+ variant="ghost"
366
+ onClick={onCancel}
367
+ disabled={isLoading}
368
+ >
369
+ Cancel
370
+ </Button>
371
+ <SubmitButton isLoading={isLoading} isDisabled={!isDirty}>
372
+ {submitLabel}
373
+ </SubmitButton>
374
+ </div>
375
+ );
376
+ }
377
+ ```
378
+
379
+ ## Error Handling
380
+
381
+ ### Form-Level Error
382
+ ```tsx
383
+ {formError && (
384
+ <div className="p-4 bg-red-900/20 border border-red-600/30 rounded-lg" role="alert">
385
+ <div className="flex items-start gap-3">
386
+ <AlertCircleIcon className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
387
+ <div>
388
+ <p className="text-sm font-medium text-red-400">
389
+ There was a problem with your submission
390
+ </p>
391
+ <p className="text-sm text-red-400/80 mt-1">{formError}</p>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ )}
396
+ ```
397
+
398
+ ### Field Error Summary
399
+ ```tsx
400
+ // Show at top of form when there are errors
401
+ {Object.keys(errors).length > 0 && (
402
+ <div className="p-4 bg-red-900/20 border border-red-600/30 rounded-lg" role="alert">
403
+ <p className="text-sm font-medium text-red-400 mb-2">
404
+ Please fix the following errors:
405
+ </p>
406
+ <ul className="list-disc list-inside text-sm text-red-400/80 space-y-1">
407
+ {Object.entries(errors).map(([field, error]) => (
408
+ <li key={field}>{error}</li>
409
+ ))}
410
+ </ul>
411
+ </div>
412
+ )}
413
+ ```
414
+
415
+ ## Success States
416
+
417
+ ### Inline Success
418
+ ```tsx
419
+ {isSuccess && (
420
+ <div className="p-4 bg-emerald-900/20 border border-emerald-600/30 rounded-lg" role="status">
421
+ <div className="flex items-center gap-3">
422
+ <CheckCircleIcon className="w-5 h-5 text-emerald-400" />
423
+ <p className="text-sm text-emerald-400">
424
+ Changes saved successfully!
425
+ </p>
426
+ </div>
427
+ </div>
428
+ )}
429
+ ```
430
+
431
+ ## Multi-Step Form (Wizard)
432
+
433
+ ### Step Progress Indicator
434
+ ```tsx
435
+ function StepIndicator({ steps, currentStep }: { steps: string[]; currentStep: number }) {
436
+ return (
437
+ <div className="flex items-center justify-between mb-8">
438
+ {steps.map((step, index) => {
439
+ const status = index < currentStep ? 'complete' : index === currentStep ? 'current' : 'upcoming';
440
+ return (
441
+ <div key={step} className="flex items-center">
442
+ {/* Step circle */}
443
+ <div className={`
444
+ w-10 h-10 rounded-full flex items-center justify-center font-medium
445
+ ${status === 'complete' ? 'bg-accent text-white' :
446
+ status === 'current' ? 'bg-accent/20 border-2 border-accent text-accent' :
447
+ 'bg-surface-raised text-text-muted'}
448
+ `}>
449
+ {status === 'complete' ? <CheckIcon className="w-5 h-5" /> : index + 1}
450
+ </div>
451
+
452
+ {/* Step label */}
453
+ <span className={`ml-3 text-sm font-medium hidden sm:block
454
+ ${status === 'current' ? 'text-white' : 'text-text-secondary'}
455
+ `}>
456
+ {step}
457
+ </span>
458
+
459
+ {/* Connector line */}
460
+ {index < steps.length - 1 && (
461
+ <div className={`w-12 sm:w-24 h-0.5 mx-4
462
+ ${index < currentStep ? 'bg-accent' : 'bg-border'}
463
+ `} />
464
+ )}
465
+ </div>
466
+ );
467
+ })}
468
+ </div>
469
+ );
470
+ }
471
+ ```
472
+
473
+ ### Wizard Form Component
474
+ ```tsx
475
+ function WizardForm<T>({
476
+ steps,
477
+ initialData,
478
+ onComplete,
479
+ renderStep,
480
+ validateStep,
481
+ }: WizardFormProps<T>) {
482
+ const [currentStep, setCurrentStep] = useState(0);
483
+ const [data, setData] = useState<T>(initialData);
484
+ const [stepErrors, setStepErrors] = useState<Record<string, string>>({});
485
+ const [isSubmitting, setIsSubmitting] = useState(false);
486
+
487
+ const isFirstStep = currentStep === 0;
488
+ const isLastStep = currentStep === steps.length - 1;
489
+
490
+ const handleNext = async () => {
491
+ const errors = validateStep?.(currentStep, data);
492
+ if (errors && Object.keys(errors).length > 0) {
493
+ setStepErrors(errors);
494
+ return;
495
+ }
496
+ setStepErrors({});
497
+ setCurrentStep(prev => prev + 1);
498
+ };
499
+
500
+ const handleBack = () => {
501
+ setStepErrors({});
502
+ setCurrentStep(prev => prev - 1);
503
+ };
504
+
505
+ const handleSubmit = async () => {
506
+ const errors = validateStep?.(currentStep, data);
507
+ if (errors && Object.keys(errors).length > 0) {
508
+ setStepErrors(errors);
509
+ return;
510
+ }
511
+
512
+ setIsSubmitting(true);
513
+ try {
514
+ await onComplete(data);
515
+ } finally {
516
+ setIsSubmitting(false);
517
+ }
518
+ };
519
+
520
+ return (
521
+ <div>
522
+ <StepIndicator steps={steps.map(s => s.title)} currentStep={currentStep} />
523
+
524
+ <div className="min-h-[300px]">
525
+ {renderStep(currentStep, data, setData, stepErrors)}
526
+ </div>
527
+
528
+ <div className="flex items-center justify-between pt-6 border-t border-border mt-6">
529
+ <Button
530
+ variant="ghost"
531
+ onClick={handleBack}
532
+ disabled={isFirstStep || isSubmitting}
533
+ className={isFirstStep ? 'invisible' : ''}
534
+ >
535
+ Back
536
+ </Button>
537
+
538
+ {isLastStep ? (
539
+ <Button
540
+ variant="primary"
541
+ onClick={handleSubmit}
542
+ disabled={isSubmitting}
543
+ >
544
+ {isSubmitting ? 'Submitting...' : 'Complete'}
545
+ </Button>
546
+ ) : (
547
+ <Button variant="primary" onClick={handleNext}>
548
+ Continue
549
+ </Button>
550
+ )}
551
+ </div>
552
+ </div>
553
+ );
554
+ }
555
+ ```
556
+
557
+ ## Accessibility Requirements
558
+
559
+ ### Required Field Indication
560
+ ```tsx
561
+ // Announce required fields to screen readers
562
+ <label>
563
+ Email
564
+ <span className="text-red-400 ml-1" aria-label="required">*</span>
565
+ </label>
566
+ ```
567
+
568
+ ### Error Association
569
+ ```tsx
570
+ <input
571
+ id="email"
572
+ aria-invalid={!!error}
573
+ aria-describedby={error ? 'email-error' : hint ? 'email-hint' : undefined}
574
+ />
575
+ {error && <p id="email-error" role="alert">{error}</p>}
576
+ {hint && !error && <p id="email-hint">{hint}</p>}
577
+ ```
578
+
579
+ ### Form Instructions
580
+ ```tsx
581
+ // Announce form requirements at the top
582
+ <p className="text-sm text-text-secondary mb-4">
583
+ Fields marked with <span className="text-red-400">*</span> are required.
584
+ </p>
585
+ ```
586
+
587
+ ## Audit Checklist for Forms
588
+
589
+ ### Critical (Must Fix)
590
+ - [ ] All fields have visible labels - accessibility violation
591
+ - [ ] Error messages are clear and specific - users can't fix issues
592
+ - [ ] Errors appear next to relevant field - users can't find problems
593
+ - [ ] Keyboard navigation works (Tab, Enter) - accessibility violation
594
+ - [ ] Form can be submitted with Enter key - accessibility violation
595
+
596
+ ### Major (Should Fix)
597
+ - [ ] Required fields are marked - users submit incomplete forms
598
+ - [ ] Validation timing is appropriate - frustrating UX
599
+ - [ ] Form has loading state during submission - users double-submit
600
+ - [ ] Success/error feedback after submission - users don't know outcome
601
+ - [ ] Cancel button doesn't submit form - data loss risk
602
+ - [ ] Dirty state tracked for unsaved changes warning - data loss risk
603
+
604
+ ### Minor (Nice to Have)
605
+ - [ ] Long forms are sectioned or multi-step - cognitive load
606
+ - [ ] Password fields have visibility toggle - convenience
607
+ - [ ] Autofill attributes are correct - convenience
608
+ - [ ] Form resets properly after submission - edge case handling