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.
This commit is contained in:
45
extension/src/popup/components/__tests__/unlock.test.ts
Normal file
45
extension/src/popup/components/__tests__/unlock.test.ts
Normal file
@@ -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 = '<div id="app"></div>';
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,11 +7,16 @@ export function renderUnlock(app: HTMLElement): void {
|
|||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad" style="text-align:center; padding-top:40px;">
|
<div class="pad" style="text-align:center; padding-top:32px;">
|
||||||
|
<div class="logo-lockup" style="margin-bottom:24px;">
|
||||||
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||||
<div class="brand">Relicario</div>
|
<div class="brand">Relicario</div>
|
||||||
<p class="muted" style="margin:8px 0 24px;">two-factor vault</p>
|
<p class="tagline">two-factor vault</p>
|
||||||
<div class="form-group">
|
</div>
|
||||||
|
|
||||||
|
<div class="glass" style="padding:16px; text-align:left; margin-bottom:16px;">
|
||||||
|
<div class="card-label" style="font-size:10px;text-transform:uppercase;letter-spacing:1.2px;color:var(--text-muted);margin-bottom:8px;">unlock</div>
|
||||||
|
<div class="form-group" style="margin-bottom:10px;">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="passphrase-input"
|
id="passphrase-input"
|
||||||
@@ -22,18 +27,20 @@ export function renderUnlock(app: HTMLElement): void {
|
|||||||
</div>
|
</div>
|
||||||
${state.loading ? '<div style="margin:12px 0;"><span class="spinner"></span></div>' : ''}
|
${state.loading ? '<div style="margin:12px 0;"><span class="spinner"></span></div>' : ''}
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div style="margin-top:24px;">
|
<button class="btn-primary" id="unlock-btn" style="width:100%;justify-content:center;" ${state.loading ? 'disabled' : ''}>unlock vault</button>
|
||||||
<button class="btn" id="vault-btn" style="font-size:11px;">open vault</button>
|
</div>
|
||||||
<button class="btn" id="settings-btn" style="font-size:11px;">settings</button>
|
|
||||||
|
<div style="display:flex; gap:8px; justify-content:center;">
|
||||||
|
<button class="btn-secondary" id="vault-btn">open vault</button>
|
||||||
|
<button class="btn-secondary" id="settings-btn">settings</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const input = document.getElementById('passphrase-input') as HTMLInputElement;
|
const input = document.getElementById('passphrase-input') as HTMLInputElement;
|
||||||
if (input && !state.loading) {
|
const unlockBtn = document.getElementById('unlock-btn') as HTMLButtonElement | null;
|
||||||
input.focus();
|
|
||||||
input.addEventListener('keydown', async (e) => {
|
const submit = async () => {
|
||||||
if (e.key === 'Enter') {
|
|
||||||
const passphrase = input.value;
|
const passphrase = input.value;
|
||||||
if (!passphrase) return;
|
if (!passphrase) return;
|
||||||
setState({ loading: true, error: null });
|
setState({ loading: true, error: null });
|
||||||
@@ -49,12 +56,14 @@ export function renderUnlock(app: HTMLElement): void {
|
|||||||
} else {
|
} else {
|
||||||
setState({ loading: false, error: resp.error });
|
setState({ loading: false, error: resp.error });
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input && !state.loading) {
|
||||||
|
input.focus();
|
||||||
|
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
||||||
}
|
}
|
||||||
});
|
unlockBtn?.addEventListener('click', submit);
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('vault-btn')?.addEventListener('click', () => openVaultTab());
|
document.getElementById('vault-btn')?.addEventListener('click', () => openVaultTab());
|
||||||
|
document.getElementById('settings-btn')?.addEventListener('click', () => navigate('settings'));
|
||||||
const settingsBtn = document.getElementById('settings-btn');
|
|
||||||
settingsBtn?.addEventListener('click', () => navigate('settings'));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<title>Relicario</title>
|
<title>Relicario</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="surface-backdrop">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1550,3 +1550,7 @@ textarea {
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: var(--focus-ring);
|
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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user