From 0c042ac89fc0822fcbe09c48702857faa5494ae1 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sun, 7 Dec 2025 23:34:03 +0900
Subject: feat(client): add sync status indicator component
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add SyncStatusIndicator component to display current sync state in the
UI header. The component shows online/offline status, syncing progress,
pending changes count, and sync errors.

- Create SyncProvider context to wrap SyncManager for React components
- Add SyncStatusIndicator component with visual status indicators
- Integrate indicator into HomePage header
- Add comprehensive tests for SyncStatusIndicator and SyncProvider
- Update existing tests to include SyncProvider wrapper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/client/components/SyncStatusIndicator.tsx | 82 +++++++++++++++++++++++++++
 1 file changed, 82 insertions(+)
 create mode 100644 src/client/components/SyncStatusIndicator.tsx

(limited to 'src/client/components/SyncStatusIndicator.tsx')

diff --git a/src/client/components/SyncStatusIndicator.tsx b/src/client/components/SyncStatusIndicator.tsx
new file mode 100644
index 0000000..23e3ec6
--- /dev/null
+++ b/src/client/components/SyncStatusIndicator.tsx
@@ -0,0 +1,82 @@
+import { useSync } from "../stores";
+import { SyncStatus } from "../sync";
+
+export function SyncStatusIndicator() {
+	const { isOnline, isSyncing, pendingCount, lastError, status } = useSync();
+
+	const getStatusText = (): string => {
+		if (!isOnline) {
+			return "Offline";
+		}
+		if (isSyncing) {
+			return "Syncing...";
+		}
+		if (status === SyncStatus.Error && lastError) {
+			return "Sync error";
+		}
+		if (pendingCount > 0) {
+			return `${pendingCount} pending`;
+		}
+		return "Synced";
+	};
+
+	const getStatusColor = (): string => {
+		if (!isOnline) {
+			return "#6c757d"; // gray
+		}
+		if (isSyncing) {
+			return "#007bff"; // blue
+		}
+		if (status === SyncStatus.Error) {
+			return "#dc3545"; // red
+		}
+		if (pendingCount > 0) {
+			return "#ffc107"; // yellow
+		}
+		return "#28a745"; // green
+	};
+
+	const getStatusIcon = (): string => {
+		if (!isOnline) {
+			return "\u25CB"; // hollow circle
+		}
+		if (isSyncing) {
+			return "\u21BB"; // rotating arrows
+		}
+		if (status === SyncStatus.Error) {
+			return "\u2717"; // cross mark
+		}
+		if (pendingCount > 0) {
+			return "\u25D4"; // partial circle
+		}
+		return "\u2713"; // check mark
+	};
+
+	return (
+		<div
+			data-testid="sync-status-indicator"
+			style={{
+				display: "inline-flex",
+				alignItems: "center",
+				gap: "0.25rem",
+				padding: "0.25rem 0.5rem",
+				borderRadius: "4px",
+				backgroundColor: "#f8f9fa",
+				border: "1px solid #dee2e6",
+				fontSize: "0.875rem",
+			}}
+			title={lastError || undefined}
+		>
+			<span
+				style={{
+					color: getStatusColor(),
+					fontWeight: "bold",
+				}}
+				aria-hidden="true"
+			>
+				{getStatusIcon()}
+			</span>
+			<span style={{ color: getStatusColor() }}>{getStatusText()}</span>
+		</div>
+	);
+}
-- 
cgit v1.2.3-70-g09d2

