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
@@ -333,3 +333,437 @@ function AnimatedComponent() {
333
333
  }
334
334
  }
335
335
  ```
336
+
337
+ ## Page Layout Patterns
338
+
339
+ ### Consistent Page Layout Component
340
+ ```tsx
341
+ interface PageLayoutProps {
342
+ title: string;
343
+ subtitle?: string;
344
+ backLink?: string | { href: string; label: string };
345
+ headerContent?: React.ReactNode; // Action buttons
346
+ maxWidth?: 'default' | 'narrow' | 'wide' | 'full';
347
+ children: React.ReactNode;
348
+ }
349
+
350
+ function PageLayout({ title, subtitle, backLink, headerContent, maxWidth = 'default', children }: PageLayoutProps) {
351
+ return (
352
+ <div className="min-h-screen bg-surface-deep p-6">
353
+ <div className={`mx-auto ${maxWidthClasses[maxWidth]}`}>
354
+ {/* Back navigation */}
355
+ {backLink && (
356
+ <Link href={typeof backLink === 'string' ? backLink : backLink.href}>
357
+ <span className="inline-flex items-center text-text-secondary hover:text-accent mb-6">
358
+ <ArrowLeftIcon className="w-4 h-4 mr-2" />
359
+ {typeof backLink === 'string' ? 'Back' : backLink.label}
360
+ </span>
361
+ </Link>
362
+ )}
363
+
364
+ {/* Header with title and actions */}
365
+ <div className="flex flex-wrap items-start justify-between gap-4 mb-8">
366
+ <div>
367
+ <h1 className="text-3xl font-bold text-white mb-2">{title}</h1>
368
+ {subtitle && <p className="text-text-secondary">{subtitle}</p>}
369
+ </div>
370
+ {headerContent}
371
+ </div>
372
+
373
+ {children}
374
+ </div>
375
+ </div>
376
+ );
377
+ }
378
+ ```
379
+
380
+ ### Page States Components
381
+ ```tsx
382
+ // Loading state
383
+ function PageLoading({ message = 'Loading...' }) {
384
+ return (
385
+ <div className="min-h-screen flex items-center justify-center">
386
+ <div className="text-center">
387
+ <Spinner className="w-12 h-12 mx-auto mb-4" />
388
+ <p className="text-text-secondary">{message}</p>
389
+ </div>
390
+ </div>
391
+ );
392
+ }
393
+
394
+ // Error state
395
+ function PageError({ title, message, backLink, backLabel }) {
396
+ return (
397
+ <div className="min-h-screen flex items-center justify-center p-8">
398
+ <div className="bg-red-900/20 border border-red-600/30 rounded-xl p-8 max-w-md text-center">
399
+ <h2 className="text-xl font-semibold text-red-400 mb-2">{title}</h2>
400
+ <p className="text-text-secondary mb-6">{message}</p>
401
+ {backLink && <Link href={backLink}>{backLabel}</Link>}
402
+ </div>
403
+ </div>
404
+ );
405
+ }
406
+
407
+ // Empty state
408
+ function EmptyState({ icon, title, message, action }) {
409
+ return (
410
+ <div className="bg-surface-raised/30 rounded-lg p-8 text-center border border-dashed border-border">
411
+ {icon && <div className="mb-3">{icon}</div>}
412
+ <p className="font-medium">{title}</p>
413
+ {message && <p className="text-sm mt-1 text-text-secondary">{message}</p>}
414
+ {action && <div className="mt-4">{action}</div>}
415
+ </div>
416
+ );
417
+ }
418
+ ```
419
+
420
+ ## Tab Navigation Patterns
421
+
422
+ ### URL-Synced Tabs
423
+ ```tsx
424
+ type TabId = 'overview' | 'career' | 'settings';
425
+
426
+ function TabbedPage() {
427
+ const router = useRouter();
428
+ const { id, tab: queryTab } = router.query;
429
+ const [activeTab, setActiveTab] = useState<TabId>('overview');
430
+
431
+ // Sync tab from URL
432
+ useEffect(() => {
433
+ if (queryTab && isValidTab(queryTab)) {
434
+ setActiveTab(queryTab as TabId);
435
+ }
436
+ }, [queryTab]);
437
+
438
+ // Update URL when tab changes (shallow routing)
439
+ const handleTabChange = useCallback((tab: TabId) => {
440
+ setActiveTab(tab);
441
+ const url = tab === 'overview'
442
+ ? `/items/${id}`
443
+ : `/items/${id}?tab=${tab}`;
444
+ router.replace(url, undefined, { shallow: true });
445
+ }, [id, router]);
446
+
447
+ return (
448
+ <div>
449
+ {/* Tab bar */}
450
+ <div className="flex gap-1 border-b border-border mb-6">
451
+ {['overview', 'career', 'settings'].map((tab) => (
452
+ <button
453
+ key={tab}
454
+ onClick={() => handleTabChange(tab as TabId)}
455
+ className={`px-4 py-2.5 text-sm font-medium relative ${
456
+ activeTab === tab ? 'text-accent' : 'text-text-secondary hover:text-white'
457
+ }`}
458
+ >
459
+ {tab.charAt(0).toUpperCase() + tab.slice(1)}
460
+ {activeTab === tab && (
461
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent" />
462
+ )}
463
+ </button>
464
+ ))}
465
+ </div>
466
+
467
+ {/* Tab content */}
468
+ {activeTab === 'overview' && <OverviewTab />}
469
+ {activeTab === 'career' && <CareerTab />}
470
+ {activeTab === 'settings' && <SettingsTab />}
471
+ </div>
472
+ );
473
+ }
474
+ ```
475
+
476
+ ### Multi-Unit Tab Manager (Complex)
477
+ ```tsx
478
+ // For workspaces with multiple editable items (like an IDE)
479
+ interface TabInfo {
480
+ id: string;
481
+ label: string;
482
+ isDirty: boolean;
483
+ }
484
+
485
+ function useTabManager() {
486
+ const [tabs, setTabs] = useState<TabInfo[]>([]);
487
+ const [activeTabId, setActiveTabId] = useState<string | null>(null);
488
+
489
+ const openTab = (id: string, label: string) => {
490
+ if (!tabs.find(t => t.id === id)) {
491
+ setTabs(prev => [...prev, { id, label, isDirty: false }]);
492
+ }
493
+ setActiveTabId(id);
494
+ };
495
+
496
+ const closeTab = (id: string) => {
497
+ const tab = tabs.find(t => t.id === id);
498
+ if (tab?.isDirty && !confirm('Unsaved changes. Close anyway?')) return;
499
+
500
+ setTabs(prev => prev.filter(t => t.id !== id));
501
+ if (activeTabId === id) {
502
+ setActiveTabId(tabs[0]?.id || null);
503
+ }
504
+ };
505
+
506
+ return { tabs, activeTabId, openTab, closeTab, setActiveTabId };
507
+ }
508
+ ```
509
+
510
+ ## Modal Patterns
511
+
512
+ ### Confirmation Modal with Loading
513
+ ```tsx
514
+ interface DeleteConfirmModalProps {
515
+ itemName: string;
516
+ isOpen: boolean;
517
+ isDeleting: boolean;
518
+ onConfirm: () => void;
519
+ onCancel: () => void;
520
+ }
521
+
522
+ function DeleteConfirmModal({ itemName, isOpen, isDeleting, onConfirm, onCancel }: DeleteConfirmModalProps) {
523
+ if (!isOpen) return null;
524
+
525
+ return (
526
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
527
+ {/* Backdrop - clicking closes (unless deleting) */}
528
+ <div
529
+ className="absolute inset-0 bg-black/70 backdrop-blur-sm"
530
+ onClick={!isDeleting ? onCancel : undefined}
531
+ />
532
+
533
+ {/* Modal content */}
534
+ <div className="relative bg-surface-base border border-border rounded-xl p-6 max-w-md w-full shadow-2xl">
535
+ <div className="text-center">
536
+ {/* Warning icon */}
537
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-red-900/30 flex items-center justify-center">
538
+ <WarningIcon className="w-8 h-8 text-red-400" />
539
+ </div>
540
+
541
+ <h3 className="text-xl font-bold text-white mb-2">Delete Item?</h3>
542
+ <p className="text-text-secondary mb-6">
543
+ Are you sure you want to permanently delete{' '}
544
+ <span className="text-accent font-semibold">{itemName}</span>?
545
+ This action cannot be undone.
546
+ </p>
547
+
548
+ <div className="flex items-center justify-center gap-3">
549
+ <Button variant="ghost" onClick={onCancel} disabled={isDeleting}>
550
+ Cancel
551
+ </Button>
552
+ <Button
553
+ variant="danger"
554
+ onClick={onConfirm}
555
+ isLoading={isDeleting}
556
+ >
557
+ Delete
558
+ </Button>
559
+ </div>
560
+ </div>
561
+ </div>
562
+ </div>
563
+ );
564
+ }
565
+ ```
566
+
567
+ ### Edit Modal with Form Reset
568
+ ```tsx
569
+ function EditModal({ item, isOpen, onSave, onCancel, isSaving }) {
570
+ const [formData, setFormData] = useState({ name: '', description: '' });
571
+
572
+ // Reset form when item changes
573
+ useEffect(() => {
574
+ if (item) {
575
+ setFormData({ name: item.name, description: item.description || '' });
576
+ }
577
+ }, [item]);
578
+
579
+ if (!isOpen) return null;
580
+
581
+ const handleSubmit = (e: React.FormEvent) => {
582
+ e.preventDefault();
583
+ if (formData.name.trim()) {
584
+ onSave(formData);
585
+ }
586
+ };
587
+
588
+ return (
589
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
590
+ <div className="absolute inset-0 bg-black/70" onClick={!isSaving ? onCancel : undefined} />
591
+ <div className="relative bg-surface-base rounded-xl p-6 max-w-md w-full">
592
+ <h3 className="text-xl font-bold mb-4">Edit Item</h3>
593
+
594
+ <form onSubmit={handleSubmit} className="space-y-4">
595
+ <div>
596
+ <label className="block text-sm font-medium mb-1.5">Name *</label>
597
+ <input
598
+ type="text"
599
+ value={formData.name}
600
+ onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
601
+ className="w-full px-4 py-2.5 bg-surface-raised border border-border rounded-lg"
602
+ required
603
+ autoFocus
604
+ />
605
+ </div>
606
+
607
+ <div className="flex justify-end gap-3 pt-4">
608
+ <Button type="button" variant="ghost" onClick={onCancel} disabled={isSaving}>
609
+ Cancel
610
+ </Button>
611
+ <Button type="submit" variant="primary" isLoading={isSaving}>
612
+ Save Changes
613
+ </Button>
614
+ </div>
615
+ </form>
616
+ </div>
617
+ </div>
618
+ );
619
+ }
620
+ ```
621
+
622
+ ## List Page Pattern
623
+
624
+ ### Standard List Page Structure
625
+ ```tsx
626
+ function ListPage() {
627
+ const [items, setItems] = useState([]);
628
+ const [filters, setFilters] = useState({ search: '', status: '' });
629
+ const [isLoading, setIsLoading] = useState(true);
630
+
631
+ // Filtered items (memoized)
632
+ const filteredItems = useMemo(() => {
633
+ return items.filter(item => {
634
+ if (filters.search && !item.name.toLowerCase().includes(filters.search.toLowerCase())) {
635
+ return false;
636
+ }
637
+ if (filters.status && item.status !== filters.status) {
638
+ return false;
639
+ }
640
+ return true;
641
+ });
642
+ }, [items, filters]);
643
+
644
+ if (isLoading) return <PageLoading message="Loading items..." />;
645
+
646
+ return (
647
+ <PageLayout
648
+ title="Items"
649
+ subtitle={`Manage your ${items.length} items`}
650
+ headerContent={
651
+ <Button variant="primary" onClick={handleCreate}>
652
+ <PlusIcon className="w-4 h-4 mr-2" />
653
+ Create Item
654
+ </Button>
655
+ }
656
+ >
657
+ {/* Filters Card */}
658
+ <Card className="mb-6">
659
+ <div className="flex flex-col sm:flex-row gap-4">
660
+ <div className="flex-1">
661
+ <Input
662
+ type="text"
663
+ placeholder="Search..."
664
+ value={filters.search}
665
+ onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
666
+ />
667
+ </div>
668
+ <Select
669
+ value={filters.status}
670
+ onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value }))}
671
+ options={statusOptions}
672
+ />
673
+ </div>
674
+
675
+ {/* Results count */}
676
+ <div className="mt-4 text-sm text-text-secondary">
677
+ Showing {filteredItems.length} of {items.length} items
678
+ {filters.search && <span className="text-accent ml-1">(filtered)</span>}
679
+ </div>
680
+ </Card>
681
+
682
+ {/* Items Grid or Table */}
683
+ {filteredItems.length === 0 ? (
684
+ <EmptyState
685
+ icon={<EmptyIcon />}
686
+ title={filters.search ? 'No items match your search' : 'No items yet'}
687
+ message={filters.search ? 'Try adjusting your filters' : 'Create your first item'}
688
+ action={!filters.search && <Button onClick={handleCreate}>Create Item</Button>}
689
+ />
690
+ ) : (
691
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
692
+ {filteredItems.map(item => (
693
+ <ItemCard key={item.id} item={item} onClick={() => handleClick(item)} />
694
+ ))}
695
+ </div>
696
+ )}
697
+ </PageLayout>
698
+ );
699
+ }
700
+ ```
701
+
702
+ ## Detail Page Pattern
703
+
704
+ ### Standard Detail Page Structure
705
+ ```tsx
706
+ function DetailPage() {
707
+ const router = useRouter();
708
+ const { id } = router.query;
709
+
710
+ const [item, setItem] = useState(null);
711
+ const [isLoading, setIsLoading] = useState(true);
712
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
713
+ const [isEditModalOpen, setIsEditModalOpen] = useState(false);
714
+
715
+ if (isLoading) return <PageLoading message="Loading..." />;
716
+ if (!item) return <PageError title="Not Found" message="Item not found" backLink="/items" />;
717
+
718
+ return (
719
+ <PageLayout
720
+ title={item.name}
721
+ subtitle={item.description}
722
+ backLink="/items"
723
+ backLabel="Back to Items"
724
+ headerContent={
725
+ <div className="flex items-center gap-3">
726
+ <Button variant="secondary" size="sm" onClick={() => setIsEditModalOpen(true)}>
727
+ <EditIcon className="w-4 h-4 mr-2" />
728
+ Edit
729
+ </Button>
730
+ <Button
731
+ variant="ghost"
732
+ size="sm"
733
+ onClick={() => setIsDeleteModalOpen(true)}
734
+ className="text-red-400 hover:bg-red-900/20"
735
+ >
736
+ <TrashIcon className="w-4 h-4 mr-2" />
737
+ Delete
738
+ </Button>
739
+ </div>
740
+ }
741
+ >
742
+ {/* Detail content - typically multi-column on desktop */}
743
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
744
+ <div className="lg:col-span-1 space-y-6">
745
+ <InfoCard item={item} />
746
+ <StatsCard item={item} />
747
+ </div>
748
+ <div className="lg:col-span-2">
749
+ <MainContentCard item={item} />
750
+ </div>
751
+ </div>
752
+
753
+ {/* Modals */}
754
+ <DeleteConfirmModal
755
+ itemName={item.name}
756
+ isOpen={isDeleteModalOpen}
757
+ onConfirm={handleDelete}
758
+ onCancel={() => setIsDeleteModalOpen(false)}
759
+ />
760
+ <EditModal
761
+ item={item}
762
+ isOpen={isEditModalOpen}
763
+ onSave={handleSave}
764
+ onCancel={() => setIsEditModalOpen(false)}
765
+ />
766
+ </PageLayout>
767
+ );
768
+ }
769
+ ```