/ src / components / sync / SyncStatusIndicator.svelte
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>