diff --git a/HOOK_AND_COMPONENT_SUMMARY.md b/HOOK_AND_COMPONENT_SUMMARY.md index 6ecd5b6..b6701ef 100644 --- a/HOOK_AND_COMPONENT_SUMMARY.md +++ b/HOOK_AND_COMPONENT_SUMMARY.md @@ -6,70 +6,107 @@ A comprehensive custom hook library and extended UI component collection has bee ## What Was Built -### ๐ŸŽฃ Custom Hooks Library (22 Hooks) +### ๐ŸŽฃ Custom Hooks Library (32 Hooks) -#### State Management +#### State Management (7 hooks) 1. **useToggle** - Boolean state management with toggle function 2. **usePrevious** - Access previous value of any state 3. **useLocalStorage** - Persist state in browser localStorage +4. **useArray** - Advanced array manipulation (push, filter, update, remove, move, swap) +5. **useMap** - Map data structure with reactive updates +6. **useSet** - Set data structure with reactive updates +7. **useUndo** - Undo/redo functionality with history management -#### Async & Performance -4. **useAsync** - Async operation handling with loading/error states -5. **useDebounce** - Delay rapid value changes (search optimization) -6. **useThrottle** - Limit function execution frequency +#### Async & Performance (3 hooks) +8. **useAsync** - Async operation handling with loading/error states +9. **useDebounce** - Delay rapid value changes (search optimization) +10. **useThrottle** - Limit function execution frequency -#### UI & Interaction -7. **useMediaQuery** - Responsive breakpoint detection -8. **useIsMobile** - Mobile device detection (existing, documented) -9. **useWindowSize** - Window dimension tracking -10. **useScrollPosition** - Scroll position monitoring -11. **useOnClickOutside** - Outside click detection for dropdowns/modals -12. **useIntersectionObserver** - Element visibility detection (lazy loading) -13. **useKeyboardShortcut** - Global keyboard shortcut handling -14. **useIdleTimer** - User idle state detection -15. **useCopyToClipboard** - Copy text to clipboard with feedback +#### UI & Interaction (10 hooks) +11. **useMediaQuery** - Responsive breakpoint detection +12. **useIsMobile** - Mobile device detection +13. **useWindowSize** - Window dimension tracking +14. **useScrollPosition** - Scroll position monitoring +15. **useOnClickOutside** - Outside click detection for dropdowns/modals +16. **useIntersectionObserver** - Element visibility detection (lazy loading) +17. **useKeyboardShortcut** - Global keyboard shortcut handling +18. **useIdleTimer** - User idle state detection +19. **useDisclosure** - Open/close state management for modals/drawers +20. **useFocusTrap** - Focus management within elements -#### Data Management -16. **useFilter** - Array filtering with automatic debouncing -17. **useSort** - Array sorting with direction control -18. **usePagination** - Complete pagination logic with navigation -19. **useSelection** - Multi-item selection with bulk operations +#### Data Management (5 hooks) +21. **useFilter** - Array filtering with automatic debouncing +22. **useSort** - Array sorting with direction control +23. **usePagination** - Complete pagination logic with navigation +24. **useSelection** - Multi-item selection with bulk operations +25. **useTable** - Complete data table state management -#### Forms & Workflows -20. **useFormValidation** - Form validation with error handling -21. **useWizard** - Multi-step form/wizard state management +#### Forms & Workflows (5 hooks) +26. **useFormValidation** - Form validation with error handling +27. **useFormState** - Form state management +28. **useWizard** - Multi-step form/wizard state management +29. **useSteps** - Step-by-step navigation +30. **useMultiStepForm** - Multi-step form with validation -#### Application-Specific -22. **useNotifications** - Notification system (existing, documented) +#### Utilities (7 hooks) +31. **useCopyToClipboard** - Copy text to clipboard with feedback +32. **useClipboard** - Enhanced clipboard operations +33. **useConfirmation** - Confirmation dialog state management +34. **useDownload** - File download utilities (CSV, JSON, TXT) +35. **useQueryParams** - URL query parameter management +36. **useInterval** - Interval timer with controls +37. **useCountdown** - Countdown timer with start/pause/reset +38. **useTimeout** - Timeout with cleanup -### ๐ŸŽจ Extended UI Components (17 New Components) +#### Application-Specific (2 hooks) +39. **useNotifications** - Notification system +40. **useSampleData** - Sample data generation -#### Display Components +### ๐ŸŽจ Extended UI Components (27 New Components) + +#### Display Components (7) 1. **EmptyState** - Empty state placeholder with icon, title, description, action -2. **StatusBadge** - Status indicator with 6 variants (success, error, warning, info, pending, neutral) -3. **StatCard** - Metric display card with optional trend indicator and icon -4. **DataList** - Key-value pair display (vertical/horizontal orientations) -5. **Timeline** - Chronological event timeline with completion states +2. **StatusBadge** - Status indicator with 6 variants +3. **StatCard** - Metric display card with trend indicator +4. **MetricCard** - Enhanced metric card with icon and change tracking +5. **DataList** - Key-value pair display +6. **Timeline** - Chronological event timeline +7. **Tag** - Tag/label component with variants and removable option -#### Input Components -6. **SearchInput** - Search field with clear button and debounce support -7. **FileUpload** - Drag-and-drop file upload with validation +#### Layout Components (4) +8. **Grid** - Responsive grid layout with GridItem +9. **Stack** - Flexible stack layout (horizontal/vertical) +10. **Section** - Page section with header and action area +11. **PageHeader** - Page header with title, description, breadcrumbs, actions -#### Navigation Components -8. **Stepper** - Multi-step progress indicator with click navigation +#### Input Components (2) +12. **SearchInput** - Search field with clear button +13. **FileUpload** - Drag-and-drop file upload -#### Feedback Components -9. **LoadingSpinner** - Animated spinner (sm, md, lg, xl sizes) -10. **LoadingOverlay** - Full overlay loading state with optional text -11. **InfoBox** - Contextual information box (info, warning, success, error variants) +#### Navigation Components (3) +14. **Stepper** - Multi-step progress indicator +15. **QuickPagination** - Simplified pagination controls +16. **FilterBar** - Active filter display with remove actions -#### Utility Components -12. **Chip** - Tag/chip component with remove capability -13. **CopyButton** - Copy-to-clipboard button with success feedback -14. **CodeBlock** - Code display block with language indicator -15. **Divider** - Section divider (horizontal/vertical with optional label) -16. **Kbd** - Keyboard shortcut display (e.g., Ctrl+K) -17. **SortableHeader** - Table header with sort direction indicators +#### Feedback Components (3) +17. **LoadingSpinner** - Animated spinner +18. **LoadingOverlay** - Full overlay loading state +19. **InfoBox** - Contextual information box + +#### Utility Components (5) +20. **Chip** - Tag/chip component with remove +21. **CopyButton** - Copy-to-clipboard button +22. **CodeBlock** - Code display block +23. **Divider** - Section divider +24. **Kbd** - Keyboard shortcut display +25. **SortableHeader** - Table header with sort indicators + +#### Dialog/Modal Components (2) +26. **Modal** - Customizable modal dialog +27. **ConfirmModal** - Confirmation dialog + +#### Complex Components (1) +28. **DataTable** - Full-featured data table with sorting, filtering, selection, pagination ### ๐Ÿ“š Documentation @@ -80,13 +117,7 @@ A comprehensive custom hook library and extended UI component collection has bee ### ๐ŸŽฏ Live Demonstration -**ComponentShowcase** - Interactive demonstration page accessible via sidebar showing: -- All new hooks in action (debounce, pagination, selection, wizard) -- All new UI components with variants -- Real-world usage patterns -- Integration examples - -Access via: **Navigation Menu โ†’ Component Library** +**ComponentShowcase** - Interactive demonstration page accessible via sidebar ## Key Features @@ -111,236 +142,75 @@ Access via: **Navigation Menu โ†’ Component Library** - Screen reader friendly ### Composability -Hooks designed to work together: -```tsx -// Example: Full-featured data table -const debouncedSearch = useDebounce(searchQuery, 300) -const filtered = useFilter(items, debouncedSearch, filterFn) -const sorted = useSort(filtered, sortKey, sortDirection) -const { paginatedItems, ...pagination } = usePagination(sorted, 10) -const { selectedIds, ...selection } = useSelection(paginatedItems) -``` - -## Usage Examples - -### Quick Search with Debouncing -```tsx -import { useDebounce } from '@/hooks' - -const [search, setSearch] = useState('') -const debouncedSearch = useDebounce(search, 500) - -useEffect(() => { - fetchResults(debouncedSearch) -}, [debouncedSearch]) -``` - -### Data Table with Pagination -```tsx -import { usePagination, SearchInput, EmptyState } from '@/hooks' - -const { paginatedItems, currentPage, totalPages, nextPage, previousPage } = - usePagination(items, 10) - -return ( -
- setSearch(e.target.value)} /> - {paginatedItems.length === 0 ? ( - - ) : ( - - )} - -) -``` - -### Multi-Step Wizard -```tsx -import { useWizard, Stepper } from '@/hooks' - -const steps = [ - { id: '1', title: 'Personal Info' }, - { id: '2', title: 'Review' }, - { id: '3', title: 'Complete' } -] - -const { currentStep, goToNextStep, isLastStep } = useWizard(steps) - -return ( -
- - {/* Step content */} - -
-) -``` - -### Status Display -```tsx -import { StatusBadge } from '@/components/ui/status-badge' - - - - -``` - -### Form Validation -```tsx -import { useFormValidation } from '@/hooks' - -const { values, errors, handleChange, validateAll } = useFormValidation( - { email: '', password: '' }, - { - email: val => !val.includes('@') ? 'Invalid email' : undefined, - password: val => val.length < 8 ? 'Too short' : undefined - } -) -``` - -## Integration with Existing Code - -All hooks and components are: -- โœ… Compatible with existing codebase -- โœ… Follow established patterns -- โœ… Use existing theme variables -- โœ… Work with shadcn components -- โœ… Support Tailwind styling - -## File Structure - -``` -src/ -โ”œโ”€โ”€ hooks/ -โ”‚ โ”œโ”€โ”€ index.ts # Hook exports -โ”‚ โ”œโ”€โ”€ README.md # Hook documentation -โ”‚ โ”œโ”€โ”€ use-async.ts -โ”‚ โ”œโ”€โ”€ use-copy-to-clipboard.ts -โ”‚ โ”œโ”€โ”€ use-debounce.ts -โ”‚ โ”œโ”€โ”€ use-filter.ts -โ”‚ โ”œโ”€โ”€ use-form-validation.ts -โ”‚ โ”œโ”€โ”€ use-idle-timer.ts -โ”‚ โ”œโ”€โ”€ use-intersection-observer.ts -โ”‚ โ”œโ”€โ”€ use-keyboard-shortcut.ts -โ”‚ โ”œโ”€โ”€ use-local-storage.ts -โ”‚ โ”œโ”€โ”€ use-media-query.ts -โ”‚ โ”œโ”€โ”€ use-mobile.ts # Existing -โ”‚ โ”œโ”€โ”€ use-notifications.ts # Existing -โ”‚ โ”œโ”€โ”€ use-on-click-outside.ts -โ”‚ โ”œโ”€โ”€ use-pagination.ts -โ”‚ โ”œโ”€โ”€ use-previous.ts -โ”‚ โ”œโ”€โ”€ use-sample-data.ts # Existing -โ”‚ โ”œโ”€โ”€ use-scroll-position.ts -โ”‚ โ”œโ”€โ”€ use-selection.ts -โ”‚ โ”œโ”€โ”€ use-sort.ts -โ”‚ โ”œโ”€โ”€ use-throttle.ts -โ”‚ โ”œโ”€โ”€ use-toggle.ts -โ”‚ โ”œโ”€โ”€ use-window-size.ts -โ”‚ โ””โ”€โ”€ use-wizard.ts -โ”œโ”€โ”€ components/ -โ”‚ โ”œโ”€โ”€ ComponentShowcase.tsx # Live demo -โ”‚ โ””โ”€โ”€ ui/ -โ”‚ โ”œโ”€โ”€ README.md # Component docs -โ”‚ โ”œโ”€โ”€ chip.tsx -โ”‚ โ”œโ”€โ”€ code-block.tsx -โ”‚ โ”œโ”€โ”€ copy-button.tsx -โ”‚ โ”œโ”€โ”€ data-list.tsx -โ”‚ โ”œโ”€โ”€ divider.tsx -โ”‚ โ”œโ”€โ”€ empty-state.tsx -โ”‚ โ”œโ”€โ”€ file-upload.tsx -โ”‚ โ”œโ”€โ”€ info-box.tsx -โ”‚ โ”œโ”€โ”€ kbd.tsx -โ”‚ โ”œโ”€โ”€ loading-overlay.tsx -โ”‚ โ”œโ”€โ”€ loading-spinner.tsx -โ”‚ โ”œโ”€โ”€ search-input.tsx -โ”‚ โ”œโ”€โ”€ sortable-header.tsx -โ”‚ โ”œโ”€โ”€ stat-card.tsx -โ”‚ โ”œโ”€โ”€ status-badge.tsx -โ”‚ โ”œโ”€โ”€ stepper.tsx -โ”‚ โ””โ”€โ”€ timeline.tsx -โ””โ”€โ”€ COMPONENT_LIBRARY.md # This file -``` - -## Benefits - -### For Developers -- ๐Ÿš€ Faster feature development -- ๐Ÿ”„ Reusable logic and UI patterns -- ๐Ÿ“ Less boilerplate code -- ๐ŸŽฏ Consistent behavior across app -- ๐Ÿ“š Comprehensive documentation - -### For Users -- โšก Better performance (debouncing, throttling) -- ๐ŸŽจ Consistent UI/UX -- โ™ฟ Improved accessibility -- ๐Ÿ“ฑ Responsive design -- โŒจ๏ธ Keyboard shortcuts - -### For Codebase -- ๐Ÿ“ฆ Modular architecture -- ๐Ÿงช Easier testing -- ๐Ÿ› ๏ธ Maintainable code -- ๐Ÿ“ˆ Scalable patterns -- ๐ŸŽจ Themeable components - -## Next Steps - -### Recommended Usage -1. Browse ComponentShowcase for live examples -2. Check hook/component READMEs for detailed docs -3. Import and use in your components -4. Extend/customize as needed - -### Future Enhancements -- Add unit tests for all hooks -- Add Storybook for component documentation -- Create more specialized hooks (useAPI, useWebSocket, etc.) -- Add more complex components (DataGrid, Calendar, etc.) -- Performance benchmarking +Hooks designed to work together for complex features like data tables with full filtering, sorting, pagination, and selection. ## Total Additions -- **22 Custom Hooks** (20 new, 2 documented existing) -- **17 New UI Components** +- **40 Custom Hooks** (30+ new, 2 documented existing) +- **27 New UI Components** - **4 Documentation Files** - **1 Interactive Showcase** -- **63 Total UI Components** (17 new + 46 existing shadcn) +- **73+ Total UI Components** (27 new + 46 existing shadcn) ## Import Reference ```tsx // Hooks - all from single import import { + useArray, useAsync, + useClipboard, + useConfirmation, useCopyToClipboard, + useCountdown, useDebounce, + useDisclosure, + useDownload, useFilter, + useFocusTrap, + useFormState, useFormValidation, useIdleTimer, useIntersectionObserver, + useInterval, useKeyboardShortcut, useLocalStorage, + useMap, useMediaQuery, useIsMobile, + useMultiStepForm, useNotifications, useOnClickOutside, usePagination, usePrevious, + useQueryParams, useSampleData, useScrollPosition, useSelection, + useSet, useSort, + useSteps, + useTable, useThrottle, + useTimeout, useToggle, + useUndo, useWindowSize, useWizard } from '@/hooks' // UI Components - individual imports +import { DataTable } from '@/components/ui/data-table' import { EmptyState } from '@/components/ui/empty-state' +import { FilterBar } from '@/components/ui/filter-bar' +import { Grid, GridItem } from '@/components/ui/grid' +import { MetricCard } from '@/components/ui/metric-card' +import { Modal, ConfirmModal } from '@/components/ui/modal' +import { PageHeader } from '@/components/ui/page-header' +import { QuickPagination } from '@/components/ui/quick-pagination' +import { Section } from '@/components/ui/section' +import { Stack } from '@/components/ui/stack' import { StatusBadge } from '@/components/ui/status-badge' -import { SearchInput } from '@/components/ui/search-input' +import { Tag } from '@/components/ui/tag' // ... etc ``` diff --git a/src/components/ui/README.md b/src/components/ui/README.md index c511bc1..8f4b033 100644 --- a/src/components/ui/README.md +++ b/src/components/ui/README.md @@ -365,6 +365,196 @@ Table header with sort indicators. ## Component Props +### New Advanced Components + +#### Grid & GridItem +Responsive grid layout system. + +```tsx + + Main content + Sidebar + Footer + +``` + +#### Stack +Flexible stack layout (horizontal/vertical). + +```tsx + +
Item 1
+
Item 2
+
Item 3
+
+``` + +#### Section +Page section with header and action area. + +```tsx +
Create New} +> + +
+``` + +#### PageHeader +Full-featured page header. + +```tsx +} + backButton={} + actions={ + <> + + + + } +/> +``` + +#### MetricCard (Enhanced) +Metric card with change tracking. + +```tsx +} + description="Compared to last month" + loading={false} +/> +``` + +#### FilterBar (Enhanced) +Active filter display with removal. + +```tsx + removeFilter(key)} + onClearAll={() => clearFilters()} + onOpenFilters={() => setShowFilters(true)} + showFilterButton +/> +``` + +#### QuickPagination (Enhanced) +Pagination with item count display. + +```tsx + +``` + +#### Modal & ConfirmModal +Dialog components with proper structure. + +```tsx + + + + + } +> + + + + +``` + +#### Tag +Enhanced tag component with variants. + +```tsx +Featured + removeTag('active')}>Active +Urgent +``` + +#### DataTable (Full-Featured) +Complete data table with all features. + +```tsx + item.name + }, + { + key: 'status', + header: 'Status', + render: (item) => + }, + { + key: 'actions', + header: 'Actions', + render: (item) => ( + + ) + } + ]} + loading={isLoading} + emptyMessage="No data found" + emptyIcon={} + onSort={(key, direction) => handleSort(key, direction)} + sortKey={sortKey} + sortDirection={sortDirection} + selectable + selectedIds={selectedIds} + onSelectionChange={setSelectedIds} + getRowId={(item) => item.id} + onRowClick={(item) => viewDetails(item)} + pagination={{ + currentPage: page, + totalPages: totalPages, + onPageChange: setPage, + itemsPerPage: 10, + totalItems: totalItems + }} +/> +``` + +## Component Props + All components support standard HTML attributes and can be styled using Tailwind classes via the `className` prop. ## Accessibility @@ -374,3 +564,13 @@ All components are built with accessibility in mind: - ARIA labels where appropriate - Keyboard navigation support - Focus management + +## Usage Tips + +1. **Layout Components**: Use `Grid`, `Stack`, and `Section` for consistent layouts +2. **Data Display**: Combine `DataTable` with `FilterBar` and `QuickPagination` for full-featured tables +3. **Metrics**: Use `MetricCard` for dashboard KPIs +4. **Navigation**: Use `PageHeader` with breadcrumbs and actions on all main pages +5. **Modals**: Use `ConfirmModal` for destructive actions, `Modal` for forms +6. **Tags**: Use `Tag` component for filters, categories, and status indicators +7. **Feedback**: Always provide `EmptyState` when data is unavailable diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index 09caafc..1c881e6 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -1,77 +1,205 @@ import * as React from 'react' import { cn } from '@/lib/utils' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from './table' +import { Button } from './button' +import { EmptyState } from './empty-state' +import { LoadingSpinner } from './loading-spinner' +import { QuickPagination } from './quick-pagination' +import { SortableHeader } from './sortable-header' +import { Checkbox } from './checkbox' -export interface DataTableProps extends React.HTMLAttributes { - columns: Array<{ - key: keyof T - header: string - width?: string - sortable?: boolean - render?: (value: any, row: T) => React.ReactNode - }> - data: T[] - onRowClick?: (row: T) => void - emptyMessage?: string +export interface Column { + key: string + header: string + sortable?: boolean + render?: (item: T) => React.ReactNode + accessor?: (item: T) => any + className?: string + headerClassName?: string } -export function DataTable>({ - columns, +export interface DataTableProps { + data: T[] + columns: Column[] + loading?: boolean + emptyMessage?: string + emptyIcon?: React.ReactNode + onSort?: (key: string, direction: 'asc' | 'desc') => void + sortKey?: string + sortDirection?: 'asc' | 'desc' + selectable?: boolean + selectedIds?: string[] + onSelectionChange?: (ids: string[]) => void + getRowId?: (item: T) => string + onRowClick?: (item: T) => void + pagination?: { + currentPage: number + totalPages: number + onPageChange: (page: number) => void + itemsPerPage?: number + totalItems?: number + } + className?: string +} + +function DataTable({ data, - onRowClick, + columns, + loading = false, emptyMessage = 'No data available', - className, - ...props + emptyIcon, + onSort, + sortKey, + sortDirection, + selectable = false, + selectedIds = [], + onSelectionChange, + getRowId, + onRowClick, + pagination, + className }: DataTableProps) { - return ( -
-
-
- - - {columns.map((column) => ( - - ))} - - - - {data.length === 0 ? ( - - - - ) : ( - data.map((row, rowIndex) => ( - onRowClick?.(row)} - className={cn( - 'bg-card hover:bg-muted/50 transition-colors', - onRowClick && 'cursor-pointer' - )} - > - {columns.map((column) => ( - - ))} - - )) - )} - -
- {column.header} -
- {emptyMessage} -
- {column.render - ? column.render(row[column.key], row) - : String(row[column.key] ?? '')} -
+ const allSelected = data.length > 0 && selectedIds.length === data.length + const someSelected = selectedIds.length > 0 && selectedIds.length < data.length + + const handleSelectAll = () => { + if (!onSelectionChange || !getRowId) return + + if (allSelected) { + onSelectionChange([]) + } else { + onSelectionChange(data.map(getRowId)) + } + } + + const handleSelectRow = (id: string) => { + if (!onSelectionChange) return + + if (selectedIds.includes(id)) { + onSelectionChange(selectedIds.filter(selectedId => selectedId !== id)) + } else { + onSelectionChange([...selectedIds, id]) + } + } + + if (loading) { + return ( +
+
+ ) + } + + if (data.length === 0) { + return ( + + ) + } + + return ( +
+
+ + + + {selectable && ( + + + + )} + {columns.map((column) => ( + + {column.sortable && onSort ? ( + { + const newDirection = sortKey === column.key && sortDirection === 'asc' ? 'desc' : 'asc' + onSort(column.key, newDirection) + }} + /> + ) : ( + column.header + )} + + ))} + + + + {data.map((item, index) => { + const rowId = getRowId ? getRowId(item) : String(index) + const isSelected = selectedIds.includes(rowId) + + return ( + onRowClick?.(item)} + > + {selectable && ( + + handleSelectRow(rowId)} + aria-label={`Select row ${rowId}`} + onClick={(e) => e.stopPropagation()} + /> + + )} + {columns.map((column) => ( + + {column.render + ? column.render(item) + : column.accessor + ? column.accessor(item) + : (item as any)[column.key]} + + ))} + + ) + })} + +
+
+ + {pagination && ( + + )}
) } + +export { DataTable } diff --git a/src/components/ui/filter-bar.tsx b/src/components/ui/filter-bar.tsx index ba5500d..80ff9dc 100644 --- a/src/components/ui/filter-bar.tsx +++ b/src/components/ui/filter-bar.tsx @@ -1,38 +1,95 @@ import * as React from 'react' import { cn } from '@/lib/utils' +import { Button } from './button' +import { Funnel, X } from '@phosphor-icons/react' +import { Badge } from './badge' + +export interface FilterOption { + key: string + label: string + value: any +} export interface FilterBarProps extends React.HTMLAttributes { - children: React.ReactNode + activeFilters: FilterOption[] + onRemoveFilter: (key: string) => void + onClearAll: () => void + onOpenFilters?: () => void + showFilterButton?: boolean } -export function FilterBar({ children, className, ...props }: FilterBarProps) { - return ( -
- {children} -
- ) -} +const FilterBar = React.forwardRef( + ({ + className, + activeFilters, + onRemoveFilter, + onClearAll, + onOpenFilters, + showFilterButton = true, + ...props + }, ref) => { + if (activeFilters.length === 0 && !showFilterButton) { + return null + } -export interface FilterGroupProps extends React.HTMLAttributes { - label?: string - children: React.ReactNode -} + return ( +
+ {showFilterButton && onOpenFilters && ( + + )} -export function FilterGroup({ label, children, className, ...props }: FilterGroupProps) { - return ( -
- {label && ( - - )} - {children} -
- ) -} + {activeFilters.map((filter) => ( + + + {filter.label}: {filter.value} + + + + ))} + + {activeFilters.length > 0 && ( + + )} +
+ ) + } +) +FilterBar.displayName = 'FilterBar' + +export { FilterBar } diff --git a/src/components/ui/grid.tsx b/src/components/ui/grid.tsx index a6e8bf7..6751428 100644 --- a/src/components/ui/grid.tsx +++ b/src/components/ui/grid.tsx @@ -2,38 +2,57 @@ import * as React from 'react' import { cn } from '@/lib/utils' export interface GridProps extends React.HTMLAttributes { - children: React.ReactNode - cols?: 1 | 2 | 3 | 4 | 5 | 6 - gap?: 'none' | 'sm' | 'md' | 'lg' | 'xl' + cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12 + gap?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 + responsive?: boolean } -export function Grid({ - children, - cols = 1, - gap = 'md', - className, - ...props -}: GridProps) { - return ( -
- {children} -
- ) +const Grid = React.forwardRef( + ({ className, cols = 1, gap = 4, responsive = true, children, ...props }, ref) => { + const gridColsClass = responsive + ? `grid-cols-1 ${cols >= 2 ? 'sm:grid-cols-2' : ''} ${cols >= 3 ? 'md:grid-cols-3' : ''} ${cols === 4 ? 'lg:grid-cols-4' : ''} ${cols >= 5 ? `lg:grid-cols-${cols}` : ''}` + : `grid-cols-${cols}` + + return ( +
+ {children} +
+ ) + } +) +Grid.displayName = 'Grid' + +export interface GridItemProps extends React.HTMLAttributes { + colSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 12 | 'full' + rowSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 'full' } + +const GridItem = React.forwardRef( + ({ className, colSpan, rowSpan, children, ...props }, ref) => { + return ( +
+ {children} +
+ ) + } +) +GridItem.displayName = 'GridItem' + +export { Grid, GridItem } diff --git a/src/components/ui/metric-card.tsx b/src/components/ui/metric-card.tsx index dcfb69b..3546a52 100644 --- a/src/components/ui/metric-card.tsx +++ b/src/components/ui/metric-card.tsx @@ -1,114 +1,64 @@ import * as React from 'react' import { cn } from '@/lib/utils' +import { Card } from './card' -const MetricCard = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) +export interface MetricCardProps extends React.HTMLAttributes { + label: string + value: string | number + change?: { + value: number + trend: 'up' | 'down' | 'neutral' + } + icon?: React.ReactNode + description?: string + loading?: boolean +} + +const MetricCard = React.forwardRef( + ({ className, label, value, change, icon, description, loading = false, ...props }, ref) => { + const trendColor = change?.trend === 'up' + ? 'text-success' + : change?.trend === 'down' + ? 'text-destructive' + : 'text-muted-foreground' + + const trendSign = change?.trend === 'up' ? '+' : change?.trend === 'down' ? '-' : '' + + return ( + +
+
+

+ {label} +

+ {loading ? ( +
+ ) : ( +

+ {value} +

+ )} + {change && ( +

+ {trendSign}{Math.abs(change.value)}% +

+ )} + {description && ( +

+ {description} +

+ )} +
+ {icon && ( +
+ {icon} +
+ )} +
+ + ) + } +) MetricCard.displayName = 'MetricCard' -const MetricCardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -MetricCardHeader.displayName = 'MetricCardHeader' - -const MetricCardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -MetricCardTitle.displayName = 'MetricCardTitle' - -const MetricCardIcon = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -MetricCardIcon.displayName = 'MetricCardIcon' - -const MetricCardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -MetricCardContent.displayName = 'MetricCardContent' - -const MetricCardValue = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -MetricCardValue.displayName = 'MetricCardValue' - -const MetricCardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -MetricCardDescription.displayName = 'MetricCardDescription' - -const MetricCardTrend = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & { trend?: 'up' | 'down' | 'neutral' } ->(({ className, trend = 'neutral', ...props }, ref) => ( -

-)) -MetricCardTrend.displayName = 'MetricCardTrend' - -export { - MetricCard, - MetricCardHeader, - MetricCardTitle, - MetricCardIcon, - MetricCardContent, - MetricCardValue, - MetricCardDescription, - MetricCardTrend -} +export { MetricCard } diff --git a/src/components/ui/modal.tsx b/src/components/ui/modal.tsx index c949d98..257f294 100644 --- a/src/components/ui/modal.tsx +++ b/src/components/ui/modal.tsx @@ -1,110 +1,128 @@ import * as React from 'react' -import { cn } from '@/lib/utils' +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './dialog' import { Button } from './button' -import { X } from '@phosphor-icons/react' export interface ModalProps { - isOpen: boolean - onClose: () => void + open: boolean + onOpenChange: (open: boolean) => void + title: string + description?: string children: React.ReactNode + footer?: React.ReactNode size?: 'sm' | 'md' | 'lg' | 'xl' | 'full' - className?: string + showCloseButton?: boolean } -export function Modal({ - isOpen, - onClose, +const Modal: React.FC = ({ + open, + onOpenChange, + title, + description, children, + footer, size = 'md', - className -}: ModalProps) { - if (!isOpen) return null + showCloseButton = true +}) => { + const sizeClasses = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + full: 'max-w-7xl' + } return ( -
-
-
+ + + {title} + {description && {description}} + +
+ {children} +
+ {footer && ( + + {footer} + )} - > - {children} -
-
+ + ) } +Modal.displayName = 'Modal' -export interface ModalHeaderProps extends React.HTMLAttributes { - children: React.ReactNode - onClose?: () => void +export interface ConfirmModalProps { + open: boolean + onOpenChange: (open: boolean) => void + title: string + description?: string + confirmLabel?: string + cancelLabel?: string + onConfirm: () => void + onCancel?: () => void + variant?: 'default' | 'destructive' + loading?: boolean } -export function ModalHeader({ children, onClose, className, ...props }: ModalHeaderProps) { +const ConfirmModal: React.FC = ({ + open, + onOpenChange, + title, + description, + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', + onConfirm, + onCancel, + variant = 'default', + loading = false +}) => { + const handleCancel = () => { + onOpenChange(false) + onCancel?.() + } + + const handleConfirm = () => { + onConfirm() + if (!loading) { + onOpenChange(false) + } + } + return ( -
+ + + + } > -
{children}
- {onClose && ( - + {description && ( +

+ {description} +

)} -
+ ) } +ConfirmModal.displayName = 'ConfirmModal' -export interface ModalTitleProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function ModalTitle({ children, className, ...props }: ModalTitleProps) { - return ( -

- {children} -

- ) -} - -export interface ModalBodyProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function ModalBody({ children, className, ...props }: ModalBodyProps) { - return ( -
- {children} -
- ) -} - -export interface ModalFooterProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function ModalFooter({ children, className, ...props }: ModalFooterProps) { - return ( -
- {children} -
- ) -} +export { Modal, ConfirmModal } diff --git a/src/components/ui/page-header.tsx b/src/components/ui/page-header.tsx index 824efc0..e76c6cc 100644 --- a/src/components/ui/page-header.tsx +++ b/src/components/ui/page-header.tsx @@ -2,61 +2,50 @@ import * as React from 'react' import { cn } from '@/lib/utils' export interface PageHeaderProps extends React.HTMLAttributes { - children: React.ReactNode + title: string + description?: string + actions?: React.ReactNode + breadcrumbs?: React.ReactNode + backButton?: React.ReactNode } -export function PageHeader({ children, className, ...props }: PageHeaderProps) { - return ( -
- {children} -
- ) -} +const PageHeader = React.forwardRef( + ({ className, title, description, actions, breadcrumbs, backButton, ...props }, ref) => { + return ( +
+ {breadcrumbs && ( +
+ {breadcrumbs} +
+ )} +
+
+
+ {backButton} +

+ {title} +

+
+ {description && ( +

+ {description} +

+ )} +
+ {actions && ( +
+ {actions} +
+ )} +
+
+ ) + } +) +PageHeader.displayName = 'PageHeader' -export interface PageTitleProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function PageTitle({ children, className, ...props }: PageTitleProps) { - return ( -

- {children} -

- ) -} - -export interface PageDescriptionProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function PageDescription({ children, className, ...props }: PageDescriptionProps) { - return ( -

- {children} -

- ) -} - -export interface PageActionsProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function PageActions({ children, className, ...props }: PageActionsProps) { - return ( -
- {children} -
- ) -} - -export interface PageHeaderRowProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function PageHeaderRow({ children, className, ...props }: PageHeaderRowProps) { - return ( -
- {children} -
- ) -} +export { PageHeader } diff --git a/src/components/ui/quick-pagination.tsx b/src/components/ui/quick-pagination.tsx index 1ff8610..fb6af82 100644 --- a/src/components/ui/quick-pagination.tsx +++ b/src/components/ui/quick-pagination.tsx @@ -7,43 +7,76 @@ export interface QuickPaginationProps { currentPage: number totalPages: number onPageChange: (page: number) => void + itemsPerPage?: number + totalItems?: number + showInfo?: boolean className?: string } -export function QuickPagination({ - currentPage, - totalPages, - onPageChange, - className -}: QuickPaginationProps) { - const canGoPrevious = currentPage > 1 - const canGoNext = currentPage < totalPages +const QuickPagination = React.forwardRef( + ({ + currentPage, + totalPages, + onPageChange, + itemsPerPage, + totalItems, + showInfo = false, + className + }, ref) => { + const canGoPrevious = currentPage > 1 + const canGoNext = currentPage < totalPages - return ( -
- + {showInfo && startItem && endItem && totalItems ? ( +

+ Showing {startItem} to{' '} + {endItem} of{' '} + {totalItems} results +

+ ) : ( +
+ )} - - Page {currentPage} of {totalPages} - +
+ - -
- ) -} +
+ {currentPage} + of + {totalPages} +
+ + +
+
+ ) + } +) +QuickPagination.displayName = 'QuickPagination' + +export { QuickPagination } diff --git a/src/components/ui/section.tsx b/src/components/ui/section.tsx index 8b41fe4..c58babe 100644 --- a/src/components/ui/section.tsx +++ b/src/components/ui/section.tsx @@ -2,61 +2,46 @@ import * as React from 'react' import { cn } from '@/lib/utils' export interface SectionProps extends React.HTMLAttributes { - children: React.ReactNode + title?: string + description?: string + action?: React.ReactNode + noPadding?: boolean } -export function Section({ children, className, ...props }: SectionProps) { - return ( -
- {children} -
- ) -} +const Section = React.forwardRef( + ({ className, title, description, action, noPadding = false, children, ...props }, ref) => { + return ( +
+ {(title || description || action) && ( +
+
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ {action && ( +
+ {action} +
+ )} +
+ )} + {children} +
+ ) + } +) +Section.displayName = 'Section' -export interface SectionHeaderProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function SectionHeader({ children, className, ...props }: SectionHeaderProps) { - return ( -
- {children} -
- ) -} - -export interface SectionTitleProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function SectionTitle({ children, className, ...props }: SectionTitleProps) { - return ( -

- {children} -

- ) -} - -export interface SectionDescriptionProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function SectionDescription({ children, className, ...props }: SectionDescriptionProps) { - return ( -

- {children} -

- ) -} - -export interface SectionContentProps extends React.HTMLAttributes { - children: React.ReactNode -} - -export function SectionContent({ children, className, ...props }: SectionContentProps) { - return ( -
- {children} -
- ) -} +export { Section } diff --git a/src/components/ui/stack.tsx b/src/components/ui/stack.tsx index 790ffb0..6a09ff0 100644 --- a/src/components/ui/stack.tsx +++ b/src/components/ui/stack.tsx @@ -2,46 +2,62 @@ import * as React from 'react' import { cn } from '@/lib/utils' export interface StackProps extends React.HTMLAttributes { - children: React.ReactNode direction?: 'horizontal' | 'vertical' - spacing?: 'none' | 'sm' | 'md' | 'lg' | 'xl' - align?: 'start' | 'center' | 'end' | 'stretch' - justify?: 'start' | 'center' | 'end' | 'between' | 'around' + spacing?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 + align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' + justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' + wrap?: boolean } -export function Stack({ - children, - direction = 'vertical', - spacing = 'md', - align = 'stretch', - justify = 'start', - className, - ...props -}: StackProps) { - return ( -
- {children} -
- ) -} +const Stack = React.forwardRef( + ({ + className, + direction = 'vertical', + spacing = 4, + align = 'stretch', + justify = 'start', + wrap = false, + children, + ...props + }, ref) => { + const isHorizontal = direction === 'horizontal' + + const alignmentClass = { + start: 'items-start', + center: 'items-center', + end: 'items-end', + stretch: 'items-stretch', + baseline: 'items-baseline' + }[align] + + const justifyClass = { + start: 'justify-start', + center: 'justify-center', + end: 'justify-end', + between: 'justify-between', + around: 'justify-around', + evenly: 'justify-evenly' + }[justify] + + return ( +
+ {children} +
+ ) + } +) +Stack.displayName = 'Stack' + +export { Stack } diff --git a/src/components/ui/tag.tsx b/src/components/ui/tag.tsx index 31bf63b..5110d0b 100644 --- a/src/components/ui/tag.tsx +++ b/src/components/ui/tag.tsx @@ -1,56 +1,77 @@ import * as React from 'react' import { cn } from '@/lib/utils' -import { Button } from './button' -import { X } from '@phosphor-icons/react' -export interface TagProps extends React.HTMLAttributes { - children: React.ReactNode +export interface TagProps extends React.HTMLAttributes { + variant?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'destructive' | 'info' + size?: 'sm' | 'md' | 'lg' onRemove?: () => void - variant?: 'default' | 'primary' | 'success' | 'warning' | 'destructive' + removable?: boolean } -export function Tag({ - children, - onRemove, - variant = 'default', - className, - ...props -}: TagProps) { - return ( -
- {children} - {onRemove && ( - - )} -
- ) -} +const Tag = React.forwardRef( + ({ + className, + variant = 'default', + size = 'md', + onRemove, + removable = false, + children, + ...props + }, ref) => { + const variantClasses = { + default: 'bg-muted text-muted-foreground hover:bg-muted/80', + primary: 'bg-primary text-primary-foreground hover:bg-primary/90', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + success: 'bg-success/10 text-success hover:bg-success/20', + warning: 'bg-warning/10 text-warning hover:bg-warning/20', + destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20', + info: 'bg-info/10 text-info hover:bg-info/20' + } -export interface TagGroupProps extends React.HTMLAttributes { - children: React.ReactNode -} + const sizeClasses = { + sm: 'text-xs px-2 py-0.5', + md: 'text-sm px-2.5 py-1', + lg: 'text-base px-3 py-1.5' + } -export function TagGroup({ children, className, ...props }: TagGroupProps) { - return ( -
- {children} -
- ) -} + return ( + + {children} + {(onRemove || removable) && ( + + )} + + ) + } +) +Tag.displayName = 'Tag' + +export { Tag } diff --git a/src/hooks/README.md b/src/hooks/README.md index abb2590..f4ae4e3 100644 --- a/src/hooks/README.md +++ b/src/hooks/README.md @@ -1,24 +1,29 @@ # Custom Hook Library -A comprehensive collection of React hooks for the WorkForce Pro platform. +A comprehensive collection of 40+ React hooks for the WorkForce Pro platform. ## Available Hooks -### State Management +### State Management (7 hooks) - **useToggle** - Boolean state toggle with setter - **usePrevious** - Access previous value of state - **useLocalStorage** - Persist state in localStorage - **useDisclosure** - Modal/drawer open/close state -- **useUndo** - Undo/redo state management +- **useUndo** - Undo/redo state management with history - **useFormState** - Form state with validation and dirty tracking +- **useArray** - Array manipulation (push, filter, update, remove, move, swap) +- **useMap** - Map data structure with reactive updates +- **useSet** - Set data structure with reactive updates -### Async Operations +### Async Operations (4 hooks) - **useAsync** - Handle async operations with loading/error states - **useDebounce** - Debounce rapidly changing values - **useThrottle** - Throttle function calls - **useInterval** - Declarative setInterval hook +- **useTimeout** - Declarative setTimeout hook +- **useCountdown** - Countdown timer with start/pause/reset -### UI & Interaction +### UI & Interaction (10 hooks) - **useMediaQuery** - Responsive media query matching - **useIsMobile** - Mobile device detection - **useWindowSize** - Track window dimensions @@ -31,20 +36,21 @@ A comprehensive collection of React hooks for the WorkForce Pro platform. - **useClipboard** - Enhanced clipboard with timeout - **useFocusTrap** - Trap focus within element -### Data Management +### Data Management (5 hooks) - **useFilter** - Filter arrays with debouncing - **useSort** - Sort arrays by key and direction - **usePagination** - Paginate large datasets - **useSelection** - Multi-select management - **useTable** - Complete table with sort/filter/pagination -### Forms & Validation +### Forms & Validation (5 hooks) - **useFormValidation** - Form validation with error handling - **useWizard** - Multi-step form/wizard state - **useMultiStepForm** - Advanced multi-step form with validation - **useConfirmation** - Confirmation dialog state +- **useSteps** - Step navigation with progress tracking -### Browser & Navigation +### Browser & Navigation (3 hooks) - **useQueryParams** - URL query parameter management - **useDownload** - File download utilities (JSON, CSV, etc.) @@ -283,5 +289,122 @@ import { useInterval } from '@/hooks' useInterval(() => { checkForUpdates() -}, 5000) +}, 5000, { enabled: true, immediate: false }) ``` + +### useCountdown +```tsx +import { useCountdown } from '@/hooks' + +const { seconds, minutes, remainingSeconds, start, pause, reset, isFinished } = useCountdown(60) + +
+

{minutes}:{remainingSeconds.toString().padStart(2, '0')}

+ + + +
+``` + +### useTimeout +```tsx +import { useTimeout } from '@/hooks' + +useTimeout(() => { + showNotification() +}, 3000) +``` + +### useArray +```tsx +import { useArray } from '@/hooks' + +const { array, push, remove, update, filter, clear, move, swap } = useArray([1, 2, 3]) + +push(4) +remove(0) +update(1, 99) +filter((item) => item > 2) +move(0, 2) +swap(0, 1) +``` + +### useMap +```tsx +import { useMap } from '@/hooks' + +const { map, set, remove, clear, has, get, values, keys } = useMap() + +set('user-1', { id: '1', name: 'John' }) +const user = get('user-1') +remove('user-1') +``` + +### useSet +```tsx +import { useSet } from '@/hooks' + +const { set, add, remove, toggle, has, clear, values, size } = useSet() + +add('item-1') +toggle('item-2') +has('item-1') +``` + +### useSteps +```tsx +import { useSteps } from '@/hooks' + +const { + currentStep, + nextStep, + previousStep, + goToStep, + canGoNext, + canGoPrevious, + isFirstStep, + isLastStep, + progress +} = useSteps(5, 0) + +
+ + + +
+``` + +## Composing Hooks + +Hooks are designed to work together for complex functionality: + +```tsx +// Full-featured data table +const [search, setSearch] = useState('') +const debouncedSearch = useDebounce(search, 300) + +const filtered = useFilter(items, debouncedSearch, (item, query) => + item.name.toLowerCase().includes(query.toLowerCase()) +) + +const { sortedItems, sortKey, sortDirection, toggleSort } = useSort( + filtered, + 'date', + 'desc' +) + +const { paginatedItems, currentPage, totalPages, nextPage, previousPage } = + usePagination(sortedItems, 10) + +const { selectedIds, toggleSelection, selectAll, clearSelection } = + useSelection(paginatedItems) +``` + +## Best Practices + +1. **Performance**: Use `useDebounce` for search inputs and expensive operations +2. **Data Management**: Combine `useFilter`, `useSort`, and `usePagination` for tables +3. **Forms**: Use `useFormValidation` or `useFormState` for form management +4. **Workflows**: Use `useWizard` or `useSteps` for multi-step processes +5. **State Persistence**: Use `useLocalStorage` for data that should survive page refreshes +6. **Complex State**: Use `useArray`, `useMap`, or `useSet` for advanced data structures diff --git a/src/hooks/index.ts b/src/hooks/index.ts index a5f6feb..0db828c 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -46,7 +46,11 @@ export type { SortDirection } from './use-sort' export type { Step } from './use-wizard' export type { UseTableOptions } from './use-table' export type { UseMultiStepFormOptions } from './use-multi-step-form' -export type { ConfirmationOptions, ConfirmationState } from './use-confirmation' -export type { UseStepsOptions } from './use-steps' -export type { UseMapActions } from './use-map' -export type { UseSetActions } from './use-set' +export type { UseConfirmationOptions, UseConfirmationReturn } from './use-confirmation' +export type { UseStepsReturn } from './use-steps' +export type { UseArrayReturn } from './use-array' +export type { UseUndoReturn } from './use-undo' +export type { UseDisclosureReturn } from './use-disclosure' +export type { UseClipboardOptions } from './use-clipboard' +export type { UseDownloadReturn, DownloadFormat } from './use-download' +export type { UseIntervalOptions } from './use-interval' diff --git a/src/hooks/use-array.ts b/src/hooks/use-array.ts index 67c24b4..9c609ad 100644 --- a/src/hooks/use-array.ts +++ b/src/hooks/use-array.ts @@ -1,32 +1,49 @@ import { useState, useCallback } from 'react' -export function useArray(initialValue: T[] = []) { - const [array, setArray] = useState(initialValue) +export interface UseArrayReturn { + array: T[] + set: (newArray: T[]) => void + push: (element: T) => void + filter: (callback: (item: T, index: number) => boolean) => void + update: (index: number, newElement: T) => void + remove: (index: number) => void + clear: () => void + insert: (index: number, element: T) => void + move: (fromIndex: number, toIndex: number) => void + swap: (indexA: number, indexB: number) => void +} + +export function useArray(initialArray: T[] = []): UseArrayReturn { + const [array, setArray] = useState(initialArray) + + const set = useCallback((newArray: T[]) => { + setArray(newArray) + }, []) const push = useCallback((element: T) => { setArray((prev) => [...prev, element]) }, []) - const remove = useCallback((index: number) => { - setArray((prev) => prev.filter((_, i) => i !== index)) - }, []) - const filter = useCallback((callback: (item: T, index: number) => boolean) => { setArray((prev) => prev.filter(callback)) }, []) const update = useCallback((index: number, newElement: T) => { - setArray((prev) => prev.map((item, i) => (i === index ? newElement : item))) + setArray((prev) => { + const newArray = [...prev] + newArray[index] = newElement + return newArray + }) + }, []) + + const remove = useCallback((index: number) => { + setArray((prev) => prev.filter((_, i) => i !== index)) }, []) const clear = useCallback(() => { setArray([]) }, []) - const set = useCallback((newArray: T[]) => { - setArray(newArray) - }, []) - const insert = useCallback((index: number, element: T) => { setArray((prev) => { const newArray = [...prev] @@ -35,12 +52,19 @@ export function useArray(initialValue: T[] = []) { }) }, []) + const move = useCallback((fromIndex: number, toIndex: number) => { + setArray((prev) => { + const newArray = [...prev] + const [element] = newArray.splice(fromIndex, 1) + newArray.splice(toIndex, 0, element) + return newArray + }) + }, []) + const swap = useCallback((indexA: number, indexB: number) => { setArray((prev) => { const newArray = [...prev] - const temp = newArray[indexA] - newArray[indexA] = newArray[indexB] - newArray[indexB] = temp + ;[newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]] return newArray }) }, []) @@ -49,11 +73,12 @@ export function useArray(initialValue: T[] = []) { array, set, push, - remove, filter, update, + remove, clear, insert, + move, swap } } diff --git a/src/hooks/use-clipboard.ts b/src/hooks/use-clipboard.ts index 1e3e167..99a5ce8 100644 --- a/src/hooks/use-clipboard.ts +++ b/src/hooks/use-clipboard.ts @@ -1,29 +1,41 @@ import { useState, useCallback } from 'react' -export function useClipboard(timeout = 2000) { +export interface UseClipboardOptions { + timeout?: number +} + +export function useClipboard(options: UseClipboardOptions = {}) { + const { timeout = 2000 } = options const [isCopied, setIsCopied] = useState(false) + const [error, setError] = useState(null) const copy = useCallback(async (text: string) => { - if (!navigator?.clipboard) { - console.warn('Clipboard API not available') - return false - } - try { await navigator.clipboard.writeText(text) setIsCopied(true) - + setError(null) + setTimeout(() => { setIsCopied(false) }, timeout) - + return true - } catch (error) { - console.warn('Copy failed', error) + } catch (err) { + setError(err as Error) setIsCopied(false) return false } }, [timeout]) - return { isCopied, copy } + const reset = useCallback(() => { + setIsCopied(false) + setError(null) + }, []) + + return { + isCopied, + error, + copy, + reset + } } diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts index 2868d1f..3ad2047 100644 --- a/src/hooks/use-confirmation.ts +++ b/src/hooks/use-confirmation.ts @@ -1,61 +1,54 @@ import { useState, useCallback } from 'react' -export interface ConfirmationOptions { +export interface UseConfirmationOptions { title?: string message?: string confirmLabel?: string cancelLabel?: string - variant?: 'default' | 'destructive' } -export interface ConfirmationState extends ConfirmationOptions { +export interface UseConfirmationReturn { isOpen: boolean - onConfirm: (() => void) | null - onCancel: (() => void) | null + data: UseConfirmationOptions + confirm: (options?: UseConfirmationOptions) => Promise + handleConfirm: () => void + handleCancel: () => void } -export function useConfirmation() { - const [state, setState] = useState({ - isOpen: false, - onConfirm: null, - onCancel: null, - title: 'Are you sure?', - message: 'This action cannot be undone.', - confirmLabel: 'Confirm', - cancelLabel: 'Cancel', - variant: 'default' - }) +export function useConfirmation(): UseConfirmationReturn { + const [isOpen, setIsOpen] = useState(false) + const [data, setData] = useState({}) + const [resolveRef, setResolveRef] = useState<((value: boolean) => void) | null>(null) - const confirm = useCallback((options?: ConfirmationOptions) => { + const confirm = useCallback((options: UseConfirmationOptions = {}) => { return new Promise((resolve) => { - setState({ - isOpen: true, - title: options?.title || 'Are you sure?', - message: options?.message || 'This action cannot be undone.', - confirmLabel: options?.confirmLabel || 'Confirm', - cancelLabel: options?.cancelLabel || 'Cancel', - variant: options?.variant || 'default', - onConfirm: () => { - setState(prev => ({ ...prev, isOpen: false })) - resolve(true) - }, - onCancel: () => { - setState(prev => ({ ...prev, isOpen: false })) - resolve(false) - } - }) + setData(options) + setIsOpen(true) + setResolveRef(() => resolve) }) }, []) - const close = useCallback(() => { - if (state.onCancel) { - state.onCancel() + const handleConfirm = useCallback(() => { + if (resolveRef) { + resolveRef(true) } - }, [state]) + setIsOpen(false) + setResolveRef(null) + }, [resolveRef]) + + const handleCancel = useCallback(() => { + if (resolveRef) { + resolveRef(false) + } + setIsOpen(false) + setResolveRef(null) + }, [resolveRef]) return { - ...state, + isOpen, + data, confirm, - close + handleConfirm, + handleCancel } } diff --git a/src/hooks/use-disclosure.ts b/src/hooks/use-disclosure.ts index bf55560..fce5705 100644 --- a/src/hooks/use-disclosure.ts +++ b/src/hooks/use-disclosure.ts @@ -1,17 +1,31 @@ import { useState, useCallback } from 'react' -export function useDisclosure(initialState = false) { +export interface UseDisclosureReturn { + isOpen: boolean + open: () => void + close: () => void + toggle: () => void +} + +export function useDisclosure(initialState = false): UseDisclosureReturn { const [isOpen, setIsOpen] = useState(initialState) - const open = useCallback(() => setIsOpen(true), []) - const close = useCallback(() => setIsOpen(false), []) - const toggle = useCallback(() => setIsOpen(prev => !prev), []) + const open = useCallback(() => { + setIsOpen(true) + }, []) + + const close = useCallback(() => { + setIsOpen(false) + }, []) + + const toggle = useCallback(() => { + setIsOpen((prev) => !prev) + }, []) return { isOpen, open, close, - toggle, - onOpenChange: setIsOpen + toggle } } diff --git a/src/hooks/use-download.ts b/src/hooks/use-download.ts index 0e088bc..ea0a216 100644 --- a/src/hooks/use-download.ts +++ b/src/hooks/use-download.ts @@ -1,20 +1,28 @@ -import { useCallback, useState } from 'react' +import { useState, useCallback } from 'react' -export function useDownload() { +export type DownloadFormat = 'csv' | 'json' | 'txt' + +export interface UseDownloadReturn { + download: (data: string, filename: string, format?: DownloadFormat) => void + downloadJSON: (data: any, filename: string) => void + downloadCSV: (data: any[], filename: string) => void + isDownloading: boolean +} + +export function useDownload(): UseDownloadReturn { const [isDownloading, setIsDownloading] = useState(false) - const downloadFile = useCallback(async ( - data: string | Blob, - filename: string, - type?: string - ) => { + const download = useCallback((data: string, filename: string, format: DownloadFormat = 'txt') => { setIsDownloading(true) - - try { - const blob = typeof data === 'string' - ? new Blob([data], { type: type || 'text/plain' }) - : data + try { + const mimeTypes = { + csv: 'text/csv', + json: 'application/json', + txt: 'text/plain' + } + + const blob = new Blob([data], { type: mimeTypes[format] }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url @@ -23,43 +31,39 @@ export function useDownload() { link.click() document.body.removeChild(link) URL.revokeObjectURL(url) - - return true - } catch (error) { - console.error('Download failed:', error) - return false } finally { setIsDownloading(false) } }, []) const downloadJSON = useCallback((data: any, filename: string) => { - const json = JSON.stringify(data, null, 2) - return downloadFile(json, filename, 'application/json') - }, [downloadFile]) + const jsonString = JSON.stringify(data, null, 2) + download(jsonString, `${filename}.json`, 'json') + }, [download]) const downloadCSV = useCallback((data: any[], filename: string) => { - if (data.length === 0) return Promise.resolve(false) - + if (data.length === 0) return + const headers = Object.keys(data[0]) - const csv = [ + const csvRows = [ headers.join(','), - ...data.map(row => + ...data.map(row => headers.map(header => { const value = row[header] - const stringValue = String(value ?? '') - return stringValue.includes(',') ? `"${stringValue}"` : stringValue + const escaped = String(value).replace(/"/g, '""') + return `"${escaped}"` }).join(',') ) - ].join('\n') - - return downloadFile(csv, filename, 'text/csv') - }, [downloadFile]) + ] + + const csvString = csvRows.join('\n') + download(csvString, `${filename}.csv`, 'csv') + }, [download]) return { - isDownloading, - downloadFile, + download, downloadJSON, - downloadCSV + downloadCSV, + isDownloading } } diff --git a/src/hooks/use-interval.ts b/src/hooks/use-interval.ts index 2c5445f..c639324 100644 --- a/src/hooks/use-interval.ts +++ b/src/hooks/use-interval.ts @@ -1,6 +1,16 @@ -import { useEffect, useRef } from 'react' +import { useState, useEffect, useRef } from 'react' -export function useInterval(callback: () => void, delay: number | null) { +export interface UseIntervalOptions { + enabled?: boolean + immediate?: boolean +} + +export function useInterval( + callback: () => void, + delay: number | null, + options: UseIntervalOptions = {} +) { + const { enabled = true, immediate = false } = options const savedCallback = useRef(callback) useEffect(() => { @@ -8,9 +18,60 @@ export function useInterval(callback: () => void, delay: number | null) { }, [callback]) useEffect(() => { - if (delay === null) return + if (!enabled || delay === null) { + return + } + + if (immediate) { + savedCallback.current() + } + + const id = setInterval(() => { + savedCallback.current() + }, delay) - const id = setInterval(() => savedCallback.current(), delay) return () => clearInterval(id) - }, [delay]) + }, [delay, enabled, immediate]) +} + +export function useCountdown(initialSeconds: number) { + const [seconds, setSeconds] = useState(initialSeconds) + const [isRunning, setIsRunning] = useState(false) + + useInterval( + () => { + setSeconds((prev) => { + if (prev <= 0) { + setIsRunning(false) + return 0 + } + return prev - 1 + }) + }, + isRunning ? 1000 : null + ) + + const start = () => { + setIsRunning(true) + } + + const pause = () => { + setIsRunning(false) + } + + const reset = (newSeconds?: number) => { + setSeconds(newSeconds ?? initialSeconds) + setIsRunning(false) + } + + return { + seconds, + isRunning, + start, + pause, + reset, + minutes: Math.floor(seconds / 60), + remainingSeconds: seconds % 60, + isFinished: seconds === 0 + } } diff --git a/src/hooks/use-map.ts b/src/hooks/use-map.ts index 7ed0236..c8e1242 100644 --- a/src/hooks/use-map.ts +++ b/src/hooks/use-map.ts @@ -1,28 +1,19 @@ import { useState, useCallback } from 'react' -export interface UseMapActions { - set: (key: K, value: V) => void - remove: (key: K) => void - clear: () => void - setAll: (entries: [K, V][]) => void -} - -export function useMap( - initialValue?: Map -): [Map, UseMapActions] { - const [map, setMap] = useState>(initialValue || new Map()) +export function useMap(initialMap?: Map) { + const [map, setMap] = useState>(initialMap || new Map()) const set = useCallback((key: K, value: V) => { - setMap((prev) => { - const newMap = new Map(prev) + setMap((prevMap) => { + const newMap = new Map(prevMap) newMap.set(key, value) return newMap }) }, []) const remove = useCallback((key: K) => { - setMap((prev) => { - const newMap = new Map(prev) + setMap((prevMap) => { + const newMap = new Map(prevMap) newMap.delete(key) return newMap }) @@ -32,17 +23,29 @@ export function useMap( setMap(new Map()) }, []) + const has = useCallback((key: K) => { + return map.has(key) + }, [map]) + + const get = useCallback((key: K) => { + return map.get(key) + }, [map]) + const setAll = useCallback((entries: [K, V][]) => { setMap(new Map(entries)) }, []) - return [ + return { map, - { - set, - remove, - clear, - setAll - } - ] + set, + remove, + clear, + has, + get, + setAll, + size: map.size, + values: Array.from(map.values()), + keys: Array.from(map.keys()), + entries: Array.from(map.entries()) + } } diff --git a/src/hooks/use-query-params.ts b/src/hooks/use-query-params.ts index 9a8e472..6b91585 100644 --- a/src/hooks/use-query-params.ts +++ b/src/hooks/use-query-params.ts @@ -1,64 +1,74 @@ import { useState, useCallback, useEffect } from 'react' -export function useQueryParams>() { - const [params, setParams] = useState(() => { - const searchParams = new URLSearchParams(window.location.search) - const result = {} as T - searchParams.forEach((value, key) => { - result[key as keyof T] = value as T[keyof T] +export function useQueryParams>( + initialParams: T +): [T, (key: keyof T, value: string | undefined) => void, (params: Partial) => void] { + const [params, setParamsState] = useState(() => { + if (typeof window === 'undefined') return initialParams + + const urlParams = new URLSearchParams(window.location.search) + const result = { ...initialParams } + + Object.keys(initialParams).forEach((key) => { + const value = urlParams.get(key) + if (value !== null) { + ;(result as any)[key] = value + } }) + return result }) + const updateURL = useCallback((newParams: T) => { + const urlParams = new URLSearchParams() + + Object.entries(newParams).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + urlParams.set(key, value) + } + }) + + const newURL = urlParams.toString() + ? `${window.location.pathname}?${urlParams.toString()}` + : window.location.pathname + + window.history.replaceState({}, '', newURL) + }, []) + + const setParam = useCallback((key: keyof T, value: string | undefined) => { + setParamsState((prev) => { + const newParams = { ...prev, [key]: value } + updateURL(newParams) + return newParams + }) + }, [updateURL]) + + const setParams = useCallback((newParams: Partial) => { + setParamsState((prev) => { + const merged = { ...prev, ...newParams } + updateURL(merged) + return merged + }) + }, [updateURL]) + useEffect(() => { const handlePopState = () => { - const searchParams = new URLSearchParams(window.location.search) - const result = {} as T - searchParams.forEach((value, key) => { - result[key as keyof T] = value as T[keyof T] + const urlParams = new URLSearchParams(window.location.search) + const newParams = { ...initialParams } + + Object.keys(initialParams).forEach((key) => { + const value = urlParams.get(key) + if (value !== null) { + ;(newParams as any)[key] = value + } }) - setParams(result) + + setParamsState(newParams) } window.addEventListener('popstate', handlePopState) return () => window.removeEventListener('popstate', handlePopState) - }, []) + }, [initialParams]) - const updateParams = useCallback((updates: Partial) => { - const searchParams = new URLSearchParams(window.location.search) - - Object.entries(updates).forEach(([key, value]) => { - if (value === null || value === undefined || value === '') { - searchParams.delete(key) - } else { - searchParams.set(key, String(value)) - } - }) - - const newUrl = `${window.location.pathname}?${searchParams.toString()}` - window.history.pushState({}, '', newUrl) - - setParams(prev => { - const newParams = { ...prev } - Object.entries(updates).forEach(([key, value]) => { - if (value === null || value === undefined || value === '') { - delete newParams[key as keyof T] - } else { - newParams[key as keyof T] = value as T[keyof T] - } - }) - return newParams - }) - }, []) - - const clearParams = useCallback(() => { - window.history.pushState({}, '', window.location.pathname) - setParams({} as T) - }, []) - - return { - params, - updateParams, - clearParams - } + return [params, setParam, setParams] } diff --git a/src/hooks/use-set.ts b/src/hooks/use-set.ts index 0dc819f..4f335b4 100644 --- a/src/hooks/use-set.ts +++ b/src/hooks/use-set.ts @@ -1,37 +1,31 @@ import { useState, useCallback } from 'react' -export interface UseSetActions { - add: (item: T) => void - remove: (item: T) => void - toggle: (item: T) => void - clear: () => void - has: (item: T) => boolean -} +export function useSet(initialSet?: Set) { + const [set, setSet] = useState>(initialSet || new Set()) -export function useSet( - initialValue?: Set -): [Set, UseSetActions] { - const [set, setSet] = useState>(initialValue || new Set()) - - const add = useCallback((item: T) => { - setSet((prev) => new Set(prev).add(item)) - }, []) - - const remove = useCallback((item: T) => { - setSet((prev) => { - const newSet = new Set(prev) - newSet.delete(item) + const add = useCallback((value: T) => { + setSet((prevSet) => { + const newSet = new Set(prevSet) + newSet.add(value) return newSet }) }, []) - const toggle = useCallback((item: T) => { - setSet((prev) => { - const newSet = new Set(prev) - if (newSet.has(item)) { - newSet.delete(item) + const remove = useCallback((value: T) => { + setSet((prevSet) => { + const newSet = new Set(prevSet) + newSet.delete(value) + return newSet + }) + }, []) + + const toggle = useCallback((value: T) => { + setSet((prevSet) => { + const newSet = new Set(prevSet) + if (newSet.has(value)) { + newSet.delete(value) } else { - newSet.add(item) + newSet.add(value) } return newSet }) @@ -41,18 +35,23 @@ export function useSet( setSet(new Set()) }, []) - const has = useCallback((item: T) => { - return set.has(item) + const has = useCallback((value: T) => { + return set.has(value) }, [set]) - return [ + const reset = useCallback(() => { + setSet(initialSet || new Set()) + }, [initialSet]) + + return { set, - { - add, - remove, - toggle, - clear, - has - } - ] + add, + remove, + toggle, + clear, + has, + reset, + size: set.size, + values: Array.from(set) + } } diff --git a/src/hooks/use-steps.ts b/src/hooks/use-steps.ts index c016ee2..6594257 100644 --- a/src/hooks/use-steps.ts +++ b/src/hooks/use-steps.ts @@ -1,11 +1,19 @@ import { useState, useCallback } from 'react' -export interface UseStepsOptions { - initialStep?: number - totalSteps: number +export interface UseStepsReturn { + currentStep: number + nextStep: () => void + previousStep: () => void + goToStep: (step: number) => void + reset: () => void + canGoNext: boolean + canGoPrevious: boolean + isFirstStep: boolean + isLastStep: boolean + progress: number } -export function useSteps({ initialStep = 0, totalSteps }: UseStepsOptions) { +export function useSteps(totalSteps: number, initialStep = 0): UseStepsReturn { const [currentStep, setCurrentStep] = useState(initialStep) const nextStep = useCallback(() => { @@ -17,9 +25,7 @@ export function useSteps({ initialStep = 0, totalSteps }: UseStepsOptions) { }, []) const goToStep = useCallback((step: number) => { - if (step >= 0 && step < totalSteps) { - setCurrentStep(step) - } + setCurrentStep(Math.max(0, Math.min(step, totalSteps - 1))) }, [totalSteps]) const reset = useCallback(() => { @@ -28,13 +34,14 @@ export function useSteps({ initialStep = 0, totalSteps }: UseStepsOptions) { return { currentStep, - isFirstStep: currentStep === 0, - isLastStep: currentStep === totalSteps - 1, - progress: ((currentStep + 1) / totalSteps) * 100, nextStep, previousStep, goToStep, reset, - setStep: setCurrentStep + canGoNext: currentStep < totalSteps - 1, + canGoPrevious: currentStep > 0, + isFirstStep: currentStep === 0, + isLastStep: currentStep === totalSteps - 1, + progress: ((currentStep + 1) / totalSteps) * 100 } } diff --git a/src/hooks/use-timeout.ts b/src/hooks/use-timeout.ts index 41d8041..95ab327 100644 --- a/src/hooks/use-timeout.ts +++ b/src/hooks/use-timeout.ts @@ -8,9 +8,14 @@ export function useTimeout(callback: () => void, delay: number | null) { }, [callback]) useEffect(() => { - if (delay === null) return + if (delay === null) { + return + } + + const id = setTimeout(() => { + savedCallback.current() + }, delay) - const id = setTimeout(() => savedCallback.current(), delay) return () => clearTimeout(id) }, [delay]) } diff --git a/src/hooks/use-undo.ts b/src/hooks/use-undo.ts index 0ed46f3..c2cf435 100644 --- a/src/hooks/use-undo.ts +++ b/src/hooks/use-undo.ts @@ -1,28 +1,36 @@ import { useState, useCallback } from 'react' -export function useUndo(initialState: T, maxHistory = 50) { +export interface UseUndoReturn { + state: T + set: (newState: T | ((prev: T) => T)) => void + undo: () => void + redo: () => void + canUndo: boolean + canRedo: boolean + clear: () => void + reset: () => void +} + +export function useUndo(initialState: T, maxHistory = 50): UseUndoReturn { const [history, setHistory] = useState([initialState]) const [currentIndex, setCurrentIndex] = useState(0) - const current = history[currentIndex] - const canUndo = currentIndex > 0 - const canRedo = currentIndex < history.length - 1 + const state = history[currentIndex] - const setState = useCallback((newState: T | ((prev: T) => T)) => { - setHistory(prevHistory => { - const currentState = prevHistory[currentIndex] - const nextState = typeof newState === 'function' - ? (newState as (prev: T) => T)(currentState) + const set = useCallback((newState: T | ((prev: T) => T)) => { + setHistory((prev) => { + const nextState = typeof newState === 'function' + ? (newState as (prev: T) => T)(prev[currentIndex]) : newState - const newHistory = prevHistory.slice(0, currentIndex + 1) + const newHistory = prev.slice(0, currentIndex + 1) newHistory.push(nextState) if (newHistory.length > maxHistory) { newHistory.shift() - setCurrentIndex(currentIndex) + setCurrentIndex(maxHistory - 1) } else { - setCurrentIndex(currentIndex + 1) + setCurrentIndex(newHistory.length - 1) } return newHistory @@ -30,37 +38,35 @@ export function useUndo(initialState: T, maxHistory = 50) { }, [currentIndex, maxHistory]) const undo = useCallback(() => { - if (canUndo) { - setCurrentIndex(prev => prev - 1) + if (currentIndex > 0) { + setCurrentIndex((prev) => prev - 1) } - }, [canUndo]) + }, [currentIndex]) const redo = useCallback(() => { - if (canRedo) { - setCurrentIndex(prev => prev + 1) + if (currentIndex < history.length - 1) { + setCurrentIndex((prev) => prev + 1) } - }, [canRedo]) + }, [currentIndex, history.length]) + + const clear = useCallback(() => { + setHistory([state]) + setCurrentIndex(0) + }, [state]) const reset = useCallback(() => { setHistory([initialState]) setCurrentIndex(0) }, [initialState]) - const clear = useCallback(() => { - setHistory([current]) - setCurrentIndex(0) - }, [current]) - return { - state: current, - setState, + state, + set, undo, redo, - canUndo, - canRedo, - reset, + canUndo: currentIndex > 0, + canRedo: currentIndex < history.length - 1, clear, - history: history.length, - currentIndex + reset } }