diff --git a/extension/src/shared/glyphs.ts b/extension/src/shared/glyphs.ts index 1a4c6b9..4766e4c 100644 --- a/extension/src/shared/glyphs.ts +++ b/extension/src/shared/glyphs.ts @@ -19,6 +19,10 @@ export const GLYPH_LOCK = '⏻'; // sidebar lock nav export const GLYPH_NEXT = '▸'; // forward / next button (matches ▾/▸ disclosure family) export const GLYPH_COPY = '⎘'; // copy to clipboard export const GLYPH_SYNC = '⇅'; // sync / upload +export const GLYPH_SYNCED = '✓'; // vault status: in sync (no pending/ahead/behind) +export const GLYPH_AHEAD = '↑'; // vault status: local commits ahead of remote +export const GLYPH_BEHIND = '↓'; // vault status: remote commits not yet pulled +export const GLYPH_PENDING = '◌'; // vault status: items changed but not yet synced export const GLYPH_PREVIEW = '⊕'; // preview / expand export const GLYPH_VAULT_TAB = '⧉'; // U+29C9 pop-out to fullscreen vault tab diff --git a/extension/src/vault/__tests__/status-indicator.test.ts b/extension/src/vault/__tests__/status-indicator.test.ts new file mode 100644 index 0000000..d31c581 --- /dev/null +++ b/extension/src/vault/__tests__/status-indicator.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { renderStatusIndicator } from '../vault-status'; + +describe('vault status indicator', () => { + it('renders "in sync" when ahead/behind/pending all zero', () => { + const el = document.createElement('div'); + renderStatusIndicator(el, { ahead: 0, behind: 0, lastSyncAt: 1700000000, pendingItems: 0 }); + expect(el.textContent).toMatch(/in sync/i); + }); + + it('renders "N ahead" when ahead is non-zero', () => { + const el = document.createElement('div'); + renderStatusIndicator(el, { ahead: 3, behind: 0, lastSyncAt: 1700000000, pendingItems: 0 }); + expect(el.textContent).toMatch(/3 ahead/i); + }); + + it('renders "N behind" when behind is non-zero', () => { + const el = document.createElement('div'); + renderStatusIndicator(el, { ahead: 0, behind: 2, lastSyncAt: 1700000000, pendingItems: 0 }); + expect(el.textContent).toMatch(/2 behind/i); + }); + + it('renders "N pending" when pendingItems is non-zero', () => { + const el = document.createElement('div'); + renderStatusIndicator(el, { ahead: 0, behind: 0, lastSyncAt: 1700000000, pendingItems: 5 }); + expect(el.textContent).toMatch(/5 pending/i); + }); + + it('renders "never synced" when lastSyncAt is null', () => { + const el = document.createElement('div'); + renderStatusIndicator(el, { ahead: 0, behind: 0, lastSyncAt: null, pendingItems: 0 }); + expect(el.textContent).toMatch(/never synced/i); + }); +}); diff --git a/extension/src/vault/vault-status.ts b/extension/src/vault/vault-status.ts new file mode 100644 index 0000000..b55132f --- /dev/null +++ b/extension/src/vault/vault-status.ts @@ -0,0 +1,36 @@ +import { + GLYPH_SYNCED, + GLYPH_AHEAD, + GLYPH_BEHIND, + GLYPH_PENDING, +} from '../shared/glyphs'; +import { relativeTime } from '../shared/relative-time'; + +// Local shape for the sidebar-footer indicator. Mirrors the get_vault_status +// response data (ahead/behind/lastSyncAt/pendingItems). lastSyncAt is a unix +// timestamp in SECONDS, or null when the vault has never synced. +export interface VaultStatus { + ahead: number; + behind: number; + lastSyncAt: number | null; + pendingItems: number; +} + +export function renderStatusIndicator(el: HTMLElement, status: VaultStatus): void { + const ts = status.lastSyncAt !== null + ? `last sync ${relativeTime(status.lastSyncAt)}` + : 'never synced'; + + const parts: string[] = []; + if (status.pendingItems > 0) parts.push(`${GLYPH_PENDING} ${status.pendingItems} pending`); + if (status.ahead > 0) parts.push(`${GLYPH_AHEAD} ${status.ahead} ahead`); + if (status.behind > 0) parts.push(`${GLYPH_BEHIND} ${status.behind} behind`); + if (parts.length === 0) parts.push(`${GLYPH_SYNCED} in sync`); + + el.innerHTML = ` +
+
${parts.join(' · ')}
+
${ts}
+
+ `; +}