Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | 'use client'; import { formatDistanceToNow } from 'date-fns'; import { Icon } from '@/components/ui/icons'; import { cn } from '@/lib/core'; interface AutoSaveIndicatorProps { /** Whether a save operation is in progress */ isSaving: boolean; /** Timestamp of last successful save */ lastSaved: Date | null; /** Whether there are unsaved changes */ hasChanges: boolean; /** Optional error message */ error?: Error | null; /** Optional className for custom styling */ className?: string; /** Show in compact mode (icon only when saved) */ compact?: boolean; } /** * Auto-save status indicator component. * * Shows the current save state: * - Saving spinner when in progress * - Green checkmark with time when saved * - Yellow cloud when unsaved changes exist * - Red error indicator on save failure * * @example * ```tsx * <AutoSaveIndicator * isSaving={isSaving} * lastSaved={lastSaved} * hasChanges={form.formState.isDirty} * /> * ``` */ export function AutoSaveIndicator({ isSaving, lastSaved, hasChanges, error, className, compact = false, }: AutoSaveIndicatorProps) { if (error) { return ( <div className={cn( 'flex items-center gap-2 text-sm text-red-600 dark:text-red-400', className )} role="status" aria-live="polite" > <Icon name="x-circle" className="h-4 w-4" /> <span>{compact ? 'Error' : 'Save failed'}</span> </div> ); } if (isSaving) { return ( <div className={cn( 'flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400', className )} role="status" aria-live="polite" > <Icon name="loader" className="h-4 w-4 animate-spin" /> <span>Saving...</span> </div> ); } if (lastSaved && !hasChanges) { const timeAgo = formatDistanceToNow(lastSaved, { addSuffix: true }); return ( <div className={cn( 'flex items-center gap-2 text-sm text-green-600 dark:text-green-400', className )} role="status" aria-live="polite" > <Icon name="check" className="h-4 w-4" /> {compact ? ( <span title={`Saved ${timeAgo}`}>Saved</span> ) : ( <span>Saved {timeAgo}</span> )} </div> ); } if (hasChanges) { return ( <div className={cn( 'flex items-center gap-2 text-sm text-yellow-600 dark:text-yellow-400', className )} role="status" aria-live="polite" > <Icon name="upload-cloud" className="h-4 w-4" /> <span>Unsaved changes</span> </div> ); } // No state to show return null; } export default AutoSaveIndicator; |