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
|
@@ -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
|
+
```
|