From 7371eff0bbfed41078ba541a7f81b564d4ab8e38 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 14:21:04 -0400 Subject: [PATCH] feat(ext/popup): polish unlock view with logo lockup + glass card Restructures the unlock screen so the form sits in a glass card with a primary 'unlock vault' button. Logo, brand, and tagline are grouped as a lockup. Open-vault and settings are demoted to secondary buttons. Body gets the .surface-backdrop wrapper. --- .../popup/components/__tests__/unlock.test.ts | 45 ++++++++++ extension/src/popup/components/unlock.ts | 87 ++++++++++--------- extension/src/popup/index.html | 2 +- extension/src/popup/styles.css | 4 + 4 files changed, 98 insertions(+), 40 deletions(-) create mode 100644 extension/src/popup/components/__tests__/unlock.test.ts diff --git a/extension/src/popup/components/__tests__/unlock.test.ts b/extension/src/popup/components/__tests__/unlock.test.ts new file mode 100644 index 0000000..4b3c590 --- /dev/null +++ b/extension/src/popup/components/__tests__/unlock.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { renderUnlock } from '../unlock'; + +vi.mock('../../../shared/state', () => ({ + getState: () => ({ loading: false, error: null }), + setState: vi.fn(), + sendMessage: vi.fn(), + navigate: vi.fn(), + escapeHtml: (s: string) => s, + openVaultTab: vi.fn(), +})); + +describe('renderUnlock', () => { + let app: HTMLElement; + beforeEach(() => { + document.body.innerHTML = '
'; + app = document.getElementById('app')!; + }); + + it('renders the logo lockup (logo + brand + tagline)', () => { + renderUnlock(app); + expect(app.querySelector('.brand-logo')).toBeTruthy(); + expect(app.querySelector('.brand')?.textContent).toBe('Relicario'); + expect(app.querySelector('.tagline')?.textContent).toContain('two-factor'); + }); + + it('renders the unlock form inside a .glass card', () => { + renderUnlock(app); + const glass = app.querySelector('.glass'); + expect(glass).toBeTruthy(); + expect(glass!.querySelector('#passphrase-input')).toBeTruthy(); + expect(glass!.querySelector('.btn-primary')).toBeTruthy(); + }); + + it('renders open-vault and settings as secondary buttons outside the card', () => { + renderUnlock(app); + const vaultBtn = app.querySelector('#vault-btn'); + const settingsBtn = app.querySelector('#settings-btn'); + expect(vaultBtn?.classList.contains('btn-secondary')).toBe(true); + expect(settingsBtn?.classList.contains('btn-secondary')).toBe(true); + // They should NOT be inside the .glass card + const glass = app.querySelector('.glass'); + expect(glass!.contains(vaultBtn!)).toBe(false); + }); +}); diff --git a/extension/src/popup/components/unlock.ts b/extension/src/popup/components/unlock.ts index dfcf21c..2073a11 100644 --- a/extension/src/popup/components/unlock.ts +++ b/extension/src/popup/components/unlock.ts @@ -7,54 +7,63 @@ export function renderUnlock(app: HTMLElement): void { const state = getState(); app.innerHTML = ` -
- -
Relicario
-

two-factor vault

-
- +
+
+ +
Relicario
+

two-factor vault

- ${state.loading ? '
' : ''} - ${state.error ? `
${escapeHtml(state.error)}
` : ''} -
- - + +
+
unlock
+
+ +
+ ${state.loading ? '
' : ''} + ${state.error ? `
${escapeHtml(state.error)}
` : ''} + +
+ +
+ +
`; const input = document.getElementById('passphrase-input') as HTMLInputElement; + const unlockBtn = document.getElementById('unlock-btn') as HTMLButtonElement | null; + + const submit = async () => { + const passphrase = input.value; + if (!passphrase) return; + setState({ loading: true, error: null }); + const resp = await sendMessage({ type: 'unlock', passphrase }); + if (resp.ok) { + const listResp = await sendMessage({ type: 'list_items' }); + if (listResp.ok) { + const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> }; + navigate('list', { entries: data.items }); + } else { + setState({ loading: false, error: listResp.error }); + } + } else { + setState({ loading: false, error: resp.error }); + } + }; + if (input && !state.loading) { input.focus(); - input.addEventListener('keydown', async (e) => { - if (e.key === 'Enter') { - const passphrase = input.value; - if (!passphrase) return; - setState({ loading: true, error: null }); - const resp = await sendMessage({ type: 'unlock', passphrase }); - if (resp.ok) { - const listResp = await sendMessage({ type: 'list_items' }); - if (listResp.ok) { - const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> }; - navigate('list', { entries: data.items }); - } else { - setState({ loading: false, error: listResp.error }); - } - } else { - setState({ loading: false, error: resp.error }); - } - } - }); + input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); }); } + unlockBtn?.addEventListener('click', submit); document.getElementById('vault-btn')?.addEventListener('click', () => openVaultTab()); - - const settingsBtn = document.getElementById('settings-btn'); - settingsBtn?.addEventListener('click', () => navigate('settings')); + document.getElementById('settings-btn')?.addEventListener('click', () => navigate('settings')); } diff --git a/extension/src/popup/index.html b/extension/src/popup/index.html index 8e3f018..0175394 100644 --- a/extension/src/popup/index.html +++ b/extension/src/popup/index.html @@ -6,7 +6,7 @@ Relicario - +
diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 7ebfa71..d41fcd9 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1550,3 +1550,7 @@ textarea { outline: none; box-shadow: var(--focus-ring); } + +.logo-lockup .brand-logo { width: 42px; height: 42px; margin: 0 auto 10px; } +.logo-lockup .brand { font-size: 17px; font-weight: 600; color: var(--gold-text); letter-spacing: 0.5px; } +.tagline { color: var(--text-dim); font-size: 11px; margin-top: 4px; letter-spacing: 0.3px; }