SyncStatusIndicator.svelte
1 <script lang="ts"> 2 import type { SyncState, SyncStatus } from '../../lib/sync/types'; 3 4 interface Props { 5 state: SyncState; 6 compact?: boolean; 7 onRefresh?: () => void; 8 } 9 10 let { state, compact = false, onRefresh }: Props = $props(); 11 12 const statusConfig: Record< 13 SyncStatus, 14 { label: string; icon: string; colorClass: string } 15 > = { 16 disconnected: { 17 label: 'Disconnected', 18 icon: '⚪', 19 colorClass: 'text-gray-400', 20 }, 21 connecting: { 22 label: 'Connecting...', 23 icon: '🟡', 24 colorClass: 'text-yellow-500', 25 }, 26 connected: { 27 label: 'Connected', 28 icon: '🟢', 29 colorClass: 'text-green-500', 30 }, 31 syncing: { 32 label: 'Syncing...', 33 icon: '🔄', 34 colorClass: 'text-blue-500', 35 }, 36 error: { 37 label: 'Error', 38 icon: '🔴', 39 colorClass: 'text-red-500', 40 }, 41 }; 42 43 const config = $derived(statusConfig[state.status]); 44 const lastSyncFormatted = $derived( 45 state.lastSyncTime 46 ? new Date(state.lastSyncTime).toLocaleTimeString() 47 : 'Never' 48 ); 49 </script> 50 51 {#if compact} 52 <button 53 type="button" 54 class="sync-status-compact {config.colorClass}" 55 title="Sync Status: {config.label}{state.error 56 ? ` - ${state.error}` 57 : ''}\nLast sync: {lastSyncFormatted}" 58 onclick={onRefresh} 59 > 60 <span class="status-icon" class:animate-spin={state.status === 'syncing'} 61 >{config.icon}</span 62 > 63 </button> 64 {:else} 65 <div class="sync-status-full"> 66 <div class="status-header"> 67 <span class="status-icon {config.colorClass}">{config.icon}</span> 68 <span class="status-label">{config.label}</span> 69 {#if onRefresh && state.status === 'connected'} 70 <button type="button" class="refresh-btn" onclick={onRefresh} title="Sync now"> 71 🔄 72 </button> 73 {/if} 74 </div> 75 76 {#if state.error} 77 <div class="status-error"> 78 {state.error} 79 </div> 80 {/if} 81 82 <div class="status-details"> 83 <div class="detail-row"> 84 <span class="detail-label">Last sync:</span> 85 <span class="detail-value">{lastSyncFormatted}</span> 86 </div> 87 {#if state.configDir} 88 <div class="detail-row"> 89 <span class="detail-label">Directory:</span> 90 <span class="detail-value truncate" title={state.configDir} 91 >{state.configDir}</span 92 > 93 </div> 94 {/if} 95 {#if state.hostVersion} 96 <div class="detail-row"> 97 <span class="detail-label">Host version:</span> 98 <span class="detail-value">{state.hostVersion}</span> 99 </div> 100 {/if} 101 </div> 102 </div> 103 {/if} 104 105 <style> 106 .sync-status-compact { 107 display: inline-flex; 108 align-items: center; 109 justify-content: center; 110 width: 24px; 111 height: 24px; 112 padding: 0; 113 background: transparent; 114 border: none; 115 border-radius: 4px; 116 cursor: pointer; 117 transition: background-color 0.15s; 118 } 119 120 .sync-status-compact:hover { 121 background: rgba(255, 255, 255, 0.1); 122 } 123 124 .sync-status-full { 125 padding: 12px; 126 background: rgba(0, 0, 0, 0.2); 127 border-radius: 8px; 128 font-size: 14px; 129 } 130 131 .status-header { 132 display: flex; 133 align-items: center; 134 gap: 8px; 135 margin-bottom: 8px; 136 } 137 138 .status-icon { 139 font-size: 16px; 140 } 141 142 .status-label { 143 font-weight: 500; 144 color: #e8e8e8; 145 } 146 147 .refresh-btn { 148 margin-left: auto; 149 padding: 4px 8px; 150 background: rgba(255, 255, 255, 0.1); 151 border: none; 152 border-radius: 4px; 153 cursor: pointer; 154 transition: background-color 0.15s; 155 } 156 157 .refresh-btn:hover { 158 background: rgba(255, 255, 255, 0.2); 159 } 160 161 .status-error { 162 margin-bottom: 8px; 163 padding: 8px; 164 background: rgba(239, 68, 68, 0.2); 165 border-radius: 4px; 166 color: #fca5a5; 167 font-size: 12px; 168 } 169 170 .status-details { 171 display: flex; 172 flex-direction: column; 173 gap: 4px; 174 } 175 176 .detail-row { 177 display: flex; 178 gap: 8px; 179 font-size: 12px; 180 } 181 182 .detail-label { 183 color: #9ca3af; 184 min-width: 80px; 185 } 186 187 .detail-value { 188 color: #d1d5db; 189 } 190 191 .truncate { 192 max-width: 200px; 193 overflow: hidden; 194 text-overflow: ellipsis; 195 white-space: nowrap; 196 } 197 198 .animate-spin { 199 animation: spin 1s linear infinite; 200 } 201 202 @keyframes spin { 203 from { 204 transform: rotate(0deg); 205 } 206 to { 207 transform: rotate(360deg); 208 } 209 } 210 211 .text-gray-400 { 212 color: #9ca3af; 213 } 214 .text-yellow-500 { 215 color: #eab308; 216 } 217 .text-green-500 { 218 color: #22c55e; 219 } 220 .text-blue-500 { 221 color: #3b82f6; 222 } 223 .text-red-500 { 224 color: #ef4444; 225 } 226 </style>