From ed6e21806fcbe330675ccc739af6839da85d21ce Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 30 May 2026 10:03:32 -0400 Subject: [PATCH] =?UTF-8?q?feat(extension):=20trash=20pane=20revamp=20?= =?UTF-8?q?=E2=80=94=20per-item=20purge=20countdown=20+=20glyph=20restore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../popup/components/__tests__/trash.test.ts | 3 +- extension/src/popup/components/trash.ts | 46 ++++++++------- extension/src/popup/styles.css | 58 ++++--------------- extension/src/vault/vault.css | 58 ++++--------------- 4 files changed, 50 insertions(+), 115 deletions(-) diff --git a/extension/src/popup/components/__tests__/trash.test.ts b/extension/src/popup/components/__tests__/trash.test.ts index 87d912c..3638a25 100644 --- a/extension/src/popup/components/__tests__/trash.test.ts +++ b/extension/src/popup/components/__tests__/trash.test.ts @@ -52,7 +52,8 @@ describe('trash view', () => { await renderTrash(app); expect(app.innerHTML).toContain('Test Login'); - expect(app.innerHTML).toContain('restore'); + expect(app.querySelector('[data-restore]')).not.toBeNull(); + expect(app.innerHTML).toContain('purges in'); expect(app.querySelector('#empty-trash-btn')).not.toBeNull(); }); diff --git a/extension/src/popup/components/trash.ts b/extension/src/popup/components/trash.ts index 9c26383..37ce6dd 100644 --- a/extension/src/popup/components/trash.ts +++ b/extension/src/popup/components/trash.ts @@ -6,6 +6,7 @@ import { relativeTime, daysUntilPurge } from '../../shared/relative-time'; import { GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_CARD, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, GLYPH_TYPE_TOTP, + GLYPH_RESTORE, } from '../../shared/glyphs'; const TYPE_ICONS: Record = { @@ -25,7 +26,6 @@ export function teardown(): void { export async function renderTrash(app: HTMLElement): Promise { const state = getState(); - // Fetch trashed items const resp = await sendMessage({ type: 'list_trashed' }); if (!resp.ok) { app.innerHTML = `

Failed to load trash

`; @@ -35,7 +35,6 @@ export async function renderTrash(app: HTMLElement): Promise { const items = (resp.data as { items: Array<[ItemId, ManifestEntry]> }).items; const retention = state.vaultSettings?.trash_retention ?? { kind: 'days', value: 30 }; - // Calculate days until oldest auto-purges let oldestPurgeDays: number | null = null; if (items.length > 0 && retention.kind === 'days') { const oldest = items[items.length - 1][1]; @@ -45,8 +44,8 @@ export async function renderTrash(app: HTMLElement): Promise { const headerInfo = items.length === 0 ? '' : oldestPurgeDays !== null - ? `${items.length} item${items.length === 1 ? '' : 's'} · oldest auto-purges in ${oldestPurgeDays}d` - : `${items.length} item${items.length === 1 ? '' : 's'}`; + ? `${items.length} item${items.length === 1 ? '' : 's'} · oldest purges in ${oldestPurgeDays} days` + : `${items.length} item${items.length === 1 ? '' : 's'} · retained forever`; app.innerHTML = `
@@ -57,25 +56,30 @@ export async function renderTrash(app: HTMLElement): Promise { ${headerInfo ? `

${escapeHtml(headerInfo)}

` : ''} ${items.length === 0 ? `

Trash is empty

` - : items.map(([id, entry]) => ` -
- ${TYPE_ICONS[entry.type] ?? '◻'} -
- ${escapeHtml(entry.title)} - trashed ${relativeTime(entry.trashed_at ?? 0)} -
- -
- `).join('')} + : `
 
+ ${items.map(([id, entry]) => { + const purgeIn = daysUntilPurge(entry.trashed_at ?? 0, retention); + const purgeStr = purgeIn === null ? 'retained forever' : `purges in ${purgeIn} days`; + return ` +
+ ${TYPE_ICONS[entry.type] ?? '◻'} +
+ ${escapeHtml(entry.title)} + trashed ${escapeHtml(relativeTime(entry.trashed_at ?? 0))} · ${escapeHtml(purgeStr)} +
+ +
+ `; + }).join('')}` + } ${items.length > 0 ? ` -
- + ` : ''}
`; - // Wire handlers document.getElementById('back-btn')?.addEventListener('click', () => navigate('list')); document.querySelectorAll('[data-restore]').forEach((btn) => { @@ -90,14 +94,14 @@ export async function renderTrash(app: HTMLElement): Promise { renderTrash(app); } else { setState({ error: result.error }); + btn.disabled = false; + btn.textContent = '⤺'; } }); }); document.getElementById('empty-trash-btn')?.addEventListener('click', async () => { - if (!confirm(`Permanently delete ${items.length} item${items.length === 1 ? '' : 's'}? This cannot be undone.`)) { - return; - } + if (!confirm(`Permanently delete ${items.length} item${items.length === 1 ? '' : 's'}? This cannot be undone.`)) return; const btn = document.getElementById('empty-trash-btn') as HTMLButtonElement; btn.disabled = true; btn.textContent = 'deleting...'; @@ -107,6 +111,8 @@ export async function renderTrash(app: HTMLElement): Promise { renderTrash(app); } else { setState({ error: result.error }); + btn.disabled = false; + btn.textContent = 'empty trash'; } }); } diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 54fed12..5abe74a 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1110,54 +1110,18 @@ textarea { .trash-row { display: flex; align-items: center; - gap: 8px; - padding: 8px; - border-radius: 4px; - background: #161b22; - margin-bottom: 6px; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid var(--border-subtle); } - -.trash-row__icon { - font-size: 16px; - flex-shrink: 0; -} - -.trash-row__info { - flex: 1; - min-width: 0; -} - -.trash-row__title { - display: block; - font-size: 13px; - color: #c9d1d9; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.trash-row__meta { - font-size: 11px; - color: #8b949e; -} - -.trash-row__restore { - font-size: 11px; - padding: 4px 8px; - background: #238636; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.trash-row__restore:hover { - background: #2ea043; -} - -.trash-row__restore:disabled { - opacity: 0.5; - cursor: default; +.trash-row__icon { font-size: 14px; } +.trash-row__info { flex: 1; display: flex; flex-direction: column; } +.trash-row__title { color: var(--text); } +.trash-row__meta { font-size: 11px; color: var(--text-muted); } +.trash-footer { + display: flex; + justify-content: flex-end; + margin-top: 16px; } /* --- Devices view --- */ diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index c4d2ba6..5ee70ab 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1030,54 +1030,18 @@ textarea { .trash-row { display: flex; align-items: center; - gap: 8px; - padding: 8px; - border-radius: 4px; - background: #161b22; - margin-bottom: 6px; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid var(--border-subtle); } - -.trash-row__icon { - font-size: 16px; - flex-shrink: 0; -} - -.trash-row__info { - flex: 1; - min-width: 0; -} - -.trash-row__title { - display: block; - font-size: 13px; - color: #c9d1d9; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.trash-row__meta { - font-size: 11px; - color: #8b949e; -} - -.trash-row__restore { - font-size: 11px; - padding: 4px 8px; - background: #238636; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.trash-row__restore:hover { - background: #2ea043; -} - -.trash-row__restore:disabled { - opacity: 0.5; - cursor: default; +.trash-row__icon { font-size: 14px; } +.trash-row__info { flex: 1; display: flex; flex-direction: column; } +.trash-row__title { color: var(--text); } +.trash-row__meta { font-size: 11px; color: var(--text-muted); } +.trash-footer { + display: flex; + justify-content: flex-end; + margin-top: 16px; } /* --- Devices view --- */