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:
@@ -19,6 +19,10 @@ export const GLYPH_LOCK = '⏻'; // sidebar lock nav
|
|||||||
export const GLYPH_NEXT = '▸'; // forward / next button (matches ▾/▸ disclosure family)
|
export const GLYPH_NEXT = '▸'; // forward / next button (matches ▾/▸ disclosure family)
|
||||||
export const GLYPH_COPY = '⎘'; // copy to clipboard
|
export const GLYPH_COPY = '⎘'; // copy to clipboard
|
||||||
export const GLYPH_SYNC = '⇅'; // sync / upload
|
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_PREVIEW = '⊕'; // preview / expand
|
||||||
|
|
||||||
export const GLYPH_VAULT_TAB = '⧉'; // U+29C9 pop-out to fullscreen vault tab
|
export const GLYPH_VAULT_TAB = '⧉'; // U+29C9 pop-out to fullscreen vault tab
|
||||||
|
|||||||
34
extension/src/vault/__tests__/status-indicator.test.ts
Normal file
34
extension/src/vault/__tests__/status-indicator.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
36
extension/src/vault/vault-status.ts
Normal file
36
extension/src/vault/vault-status.ts
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user