feat(ext/vault): vault-status indicator renderer (Plan C Phase 6)

Renders sidebar-footer indicator with ahead/behind/pending state. Pure
DOM; reuses shared/glyphs (four new status glyphs) and shared/relative-time.
Status fetch happens in the wiring layer (Task 6.3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-31 11:32:49 -04:00
parent 3b8368db3a
commit 3121431a7e
3 changed files with 74 additions and 0 deletions

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -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 = `
<div class="vault-status">
<div class="vault-status__state">${parts.join(' · ')}</div>
<div class="vault-status__ts">${ts}</div>
</div>
`;
}