diff --git a/src/App.jsx b/src/App.jsx
index 997cb51..8ccf14b 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -15,6 +15,7 @@ import BrewCard from "./components/BrewCard";
import BottomNav from "./components/BottomNav";
import IosPromptModal from "./components/IosPromptModal";
import UpdatePrompt from "./components/UpdatePrompt";
+import SyncIndicator from "./components/SyncIndicator";
// Import constants
@@ -196,13 +197,7 @@ export default function CoffeeLogbook() {
{pageSubtitles[view] || "Coffee Logbook"}
{pageTitles[view] || "Brew Journal"}
- {showSyncedStatus && (
-
- ●
- {isOnline ? (syncing ? "Syncing..." : "Synced") : "Offline"}
-
- )}
+
{/* Content */}
diff --git a/src/components/SyncIndicator.jsx b/src/components/SyncIndicator.jsx
new file mode 100644
index 0000000..e89f104
--- /dev/null
+++ b/src/components/SyncIndicator.jsx
@@ -0,0 +1,69 @@
+/**
+ * SyncIndicator — a minimal coffee-cup icon that communicates sync results.
+ *
+ * Success: outlined cup → steam rises → checkmark appears → fades out (~2.4s)
+ * Offline: outlined cup + subtle "!" warning, stays visible.
+ */
+export default function SyncIndicator({ isOnline, syncing, showSyncedStatus }) {
+ const showSuccess = isOnline && showSyncedStatus && !syncing;
+ const showOffline = !isOnline;
+
+ if (!showSuccess && !showOffline) return null;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/index.css b/src/index.css
index f92fed4..950584b 100644
--- a/src/index.css
+++ b/src/index.css
@@ -138,4 +138,40 @@
width: 4px;
border-radius: 0 4px 4px 0;
}
+
+ /* ── Sync cup indicator ── */
+ .sync-cup-success {
+ animation: sync-cup-fade 2.4s ease-in-out forwards;
+ }
+
+ @keyframes sync-cup-fade {
+ 0% { opacity: 0; transform: scale(0.88); }
+ 8% { opacity: 1; transform: scale(1); }
+ 68% { opacity: 1; }
+ 100% { opacity: 0; }
+ }
+
+ .sync-steam {
+ opacity: 0;
+ }
+
+ .sync-steam-1 { animation: sync-steam-rise 1.4s ease-out 0.1s forwards; }
+ .sync-steam-2 { animation: sync-steam-rise 1.4s ease-out 0.3s forwards; }
+ .sync-steam-3 { animation: sync-steam-rise 1.4s ease-out 0.5s forwards; }
+
+ @keyframes sync-steam-rise {
+ 0% { opacity: 0; transform: translateY(0); }
+ 25% { opacity: 0.5; }
+ 100% { opacity: 0; transform: translateY(-4px); }
+ }
+
+ .sync-check {
+ stroke-dasharray: 14;
+ stroke-dashoffset: 14;
+ animation: sync-check-draw 0.45s ease-out 0.65s forwards;
+ }
+
+ @keyframes sync-check-draw {
+ to { stroke-dashoffset: 0; }
+ }
}
diff --git a/src/pages/ProfilePage.jsx b/src/pages/ProfilePage.jsx
index 744cd8f..f0cc829 100644
--- a/src/pages/ProfilePage.jsx
+++ b/src/pages/ProfilePage.jsx
@@ -23,12 +23,10 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
{user?.username}
{user?.email}
- {showSyncedStatus && (
-
- ●
- {isOnline ? (syncing ? "Syncing…" : "All data synced") : "Offline — saved locally"}
-
- )}
+
+ ●
+ {isOnline ? (syncing ? "Syncing…" : "All data synced") : "Offline — saved locally"}
+
{/* Account section */}
diff --git a/vite.config.js b/vite.config.js
index 7a3577d..cedc464 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -25,12 +25,7 @@ function getGitVersion() {
return `v0.0.0-${raw}`;
}
- // If it's a tag + commits + hash (e.g., v0.1.1-5-g4a9f6b6)
- // Format it cleanly to v0.1.1-4a9f6b6
- const match = raw.match(/^(.*)-\d+-g([0-9a-f]+)$/);
- if (match) {
- return `${match[1]}-${match[2]}`;
- }
+ // Otherwise, return raw (e.g., v0.1.1-5-g4a9f6b6 or v1.2.0)
// Exact tag (e.g., v1.2.0)
return raw;