From 479e5848f547a8741322d23030dc66f470b9f83f Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 13:29:22 -0400 Subject: [PATCH 01/12] style(ext/popup): add patina palette tokens Replaces bright amber #d2ab43 with patina gold #a88a4a as the new base. Keeps --accent as alias for backwards compatibility. Adds --bg-card and --border-soft for upcoming glass card class. --- extension/src/popup/styles.css | 39 ++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index cf99e92..fcb7520 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1,22 +1,35 @@ /* Relicario extension — terminal dark theme */ :root { - /* Brand */ - --accent: #d2ab43; - --accent-soft: rgba(210, 171, 67, 0.18); - --accent-strong: #aa812a; + /* Patina gold (Phase 2B) */ + --gold-base: #a88a4a; + --gold-mid: #cdb47a; + --gold-shadow: #5a3f12; + --gold-text: #c9a868; + --gold-soft: rgba(184, 149, 86, 0.14); + --gold-ring: rgba(184, 149, 86, 0.18); + --gold-stroke: #b89556; + --gold-hi-end: #dac8a0; + + /* Brand alias (kept for backwards compatibility) */ + --accent: var(--gold-base); + --accent-soft: var(--gold-soft); + --accent-strong: var(--gold-shadow); /* Surfaces */ - --bg-page: #0d1117; - --bg-pane: #161b22; - --bg-elevated: #21262d; - --bg-input: #161b22; - --border-subtle: #30363d; + --bg-page: #0a0e14; + --bg-pane: #11161e; + --bg-elevated: #1c2330; + --bg-card: rgba(22, 27, 34, 0.55); + --bg-input: #0a0e14; + --border-soft: rgba(255, 255, 255, 0.05); + --border-mid: #262d36; + --border-subtle: var(--border-mid); /* Text */ --text: #c9d1d9; --text-muted: #8b949e; - --text-dim: #484f58; + --text-dim: #6b7888; /* Status */ --danger: #ab2b20; @@ -24,7 +37,7 @@ --success: #6cb37a; /* Focus */ - --focus-ring: 0 0 0 2px rgba(210, 171, 67, 0.35); + --focus-ring: 0 0 0 2px var(--gold-ring); } * { @@ -37,7 +50,7 @@ body { width: 360px; max-height: 500px; overflow-y: auto; - background: #0d1117; + background: var(--bg-page); color: #c9d1d9; font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'SF Mono', Menlo, monospace; font-size: 13px; @@ -62,7 +75,7 @@ body { .brand { font-size: 16px; font-weight: 700; - color: #d2ab43; + color: var(--gold-text); letter-spacing: 1px; } From 7370f119ee95a20da6b8a537ea1e8b2068800a48 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 13:31:33 -0400 Subject: [PATCH 02/12] style(ext/vault): add patina palette tokens Mirrors popup/styles.css token block so the two surfaces share a consistent color vocabulary. --- extension/src/vault/vault.css | 39 +++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 3dce143..96f5db1 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1,22 +1,35 @@ /* Relicario vault — terminal dark theme (tab layout) */ :root { - /* Brand */ - --accent: #d2ab43; - --accent-soft: rgba(210, 171, 67, 0.18); - --accent-strong: #aa812a; + /* Patina gold (Phase 2B) */ + --gold-base: #a88a4a; + --gold-mid: #cdb47a; + --gold-shadow: #5a3f12; + --gold-text: #c9a868; + --gold-soft: rgba(184, 149, 86, 0.14); + --gold-ring: rgba(184, 149, 86, 0.18); + --gold-stroke: #b89556; + --gold-hi-end: #dac8a0; + + /* Brand alias (kept for backwards compatibility) */ + --accent: var(--gold-base); + --accent-soft: var(--gold-soft); + --accent-strong: var(--gold-shadow); /* Surfaces */ - --bg-page: #0d1117; - --bg-pane: #161b22; - --bg-elevated: #21262d; - --bg-input: #161b22; - --border-subtle: #30363d; + --bg-page: #0a0e14; + --bg-pane: #11161e; + --bg-elevated: #1c2330; + --bg-card: rgba(22, 27, 34, 0.55); + --bg-input: #0a0e14; + --border-soft: rgba(255, 255, 255, 0.05); + --border-mid: #262d36; + --border-subtle: var(--border-mid); /* Text */ --text: #c9d1d9; --text-muted: #8b949e; - --text-dim: #484f58; + --text-dim: #6b7888; /* Status */ --danger: #ab2b20; @@ -24,7 +37,7 @@ --success: #6cb37a; /* Focus */ - --focus-ring: 0 0 0 2px rgba(210, 171, 67, 0.35); + --focus-ring: 0 0 0 2px var(--gold-ring); } * { @@ -34,7 +47,7 @@ } body { - background: #0d1117; + background: var(--bg-page); color: #c9d1d9; font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'SF Mono', Menlo, monospace; font-size: 13px; @@ -62,7 +75,7 @@ body { .brand { font-size: 16px; font-weight: 700; - color: #d2ab43; + color: var(--gold-text); letter-spacing: 1px; } From da61529de6797ac34717acc655b010c54216e435 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 13:32:39 -0400 Subject: [PATCH 03/12] style(ext): add .surface-backdrop class Subtle radial top-glow + 18px grid texture. Used as the backdrop for the login popup, setup wizard, and fullscreen vault shell. --- extension/src/popup/styles.css | 24 ++++++++++++++++++++++++ extension/src/vault/vault.css | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index fcb7520..350a6e1 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1470,3 +1470,27 @@ textarea { .f-notes--mono { font-family: ui-monospace, "JetBrains Mono", "SF Mono", monospace !important; } + +/* Phase 2B: surface backdrop — subtle radial top-glow + grid texture. + Apply to body or a top-level wrapper. Children must sit above the ::before. */ +.surface-backdrop { + position: relative; + background: + radial-gradient(ellipse 700px 240px at 50% -40px, rgba(184, 149, 86, 0.05), transparent 65%), + linear-gradient(180deg, #11161e 0%, #0a0e14 100%); +} +.surface-backdrop::before { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(255, 255, 255, 0.012) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.012) 1px, transparent 1px); + background-size: 18px 18px; + pointer-events: none; + z-index: 0; +} +.surface-backdrop > * { + position: relative; + z-index: 1; +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 96f5db1..42a3f46 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1500,3 +1500,27 @@ textarea { .f-notes--mono { font-family: ui-monospace, "JetBrains Mono", "SF Mono", monospace !important; } + +/* Phase 2B: surface backdrop — subtle radial top-glow + grid texture. + Apply to body or a top-level wrapper. Children must sit above the ::before. */ +.surface-backdrop { + position: relative; + background: + radial-gradient(ellipse 700px 240px at 50% -40px, rgba(184, 149, 86, 0.05), transparent 65%), + linear-gradient(180deg, #11161e 0%, #0a0e14 100%); +} +.surface-backdrop::before { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(255, 255, 255, 0.012) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.012) 1px, transparent 1px); + background-size: 18px 18px; + pointer-events: none; + z-index: 0; +} +.surface-backdrop > * { + position: relative; + z-index: 1; +} From 91536ee50d3c05829cb5f2b35011a918f80984c0 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 13:32:55 -0400 Subject: [PATCH 04/12] style(ext): add .glass card class Translucent fill, soft border, inner highlight, drop shadow. Used for the unlock card, setup step cards, and form section panels. --- extension/src/popup/styles.css | 14 ++++++++++++++ extension/src/vault/vault.css | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 350a6e1..897ceb9 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1494,3 +1494,17 @@ textarea { position: relative; z-index: 1; } + +/* Phase 2B: glass card. Translucent panel with backdrop blur for the + unlock card, setup step card, and form section panels. Falls back + gracefully on browsers without backdrop-filter (just stays translucent). */ +.glass { + background: var(--bg-card); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid var(--border-soft); + border-radius: 10px; + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.03) inset, + 0 6px 18px rgba(0, 0, 0, 0.35); +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 42a3f46..2d63e40 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1524,3 +1524,17 @@ textarea { position: relative; z-index: 1; } + +/* Phase 2B: glass card. Translucent panel with backdrop blur for the + unlock card, setup step card, and form section panels. Falls back + gracefully on browsers without backdrop-filter (just stays translucent). */ +.glass { + background: var(--bg-card); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid var(--border-soft); + border-radius: 10px; + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.03) inset, + 0 6px 18px rgba(0, 0, 0, 0.35); +} From 60d7c074c3254e0399a4c41ce858e83edf999932 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 13:33:18 -0400 Subject: [PATCH 05/12] style(ext): add .btn-primary and .btn-secondary classes Two-tier button hierarchy. .btn-primary uses patina gold fill; .btn-secondary is a ghost button with muted border. Existing .btn class kept for backwards compatibility. --- extension/src/popup/styles.css | 42 ++++++++++++++++++++++++++++++++++ extension/src/vault/vault.css | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 897ceb9..7ebfa71 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1508,3 +1508,45 @@ textarea { 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 6px 18px rgba(0, 0, 0, 0.35); } + +/* Phase 2B: button hierarchy. Existing .btn class kept for backwards + compatibility; .btn-primary and .btn-secondary express clearer intent + and are used in updated views. */ +.btn-primary { + background: var(--gold-base); + color: var(--bg-page); + border: none; + padding: 9px 14px; + font-size: 12px; + font-weight: 600; + border-radius: 6px; + font-family: inherit; + cursor: pointer; + letter-spacing: 0.3px; + display: inline-flex; + align-items: center; + gap: 8px; + transition: background-color 0.15s; +} +.btn-primary:hover { background: var(--gold-stroke); } +.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } +.btn-primary:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} + +.btn-secondary { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.06); + color: var(--text-muted); + padding: 6px 12px; + font-size: 11px; + border-radius: 5px; + font-family: inherit; + cursor: pointer; +} +.btn-secondary:hover { border-color: rgba(255, 255, 255, 0.12); color: var(--text); } +.btn-secondary:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 2d63e40..08de923 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1538,3 +1538,45 @@ textarea { 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 6px 18px rgba(0, 0, 0, 0.35); } + +/* Phase 2B: button hierarchy. Existing .btn class kept for backwards + compatibility; .btn-primary and .btn-secondary express clearer intent + and are used in updated views. */ +.btn-primary { + background: var(--gold-base); + color: var(--bg-page); + border: none; + padding: 9px 14px; + font-size: 12px; + font-weight: 600; + border-radius: 6px; + font-family: inherit; + cursor: pointer; + letter-spacing: 0.3px; + display: inline-flex; + align-items: center; + gap: 8px; + transition: background-color 0.15s; +} +.btn-primary:hover { background: var(--gold-stroke); } +.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } +.btn-primary:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} + +.btn-secondary { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.06); + color: var(--text-muted); + padding: 6px 12px; + font-size: 11px; + border-radius: 5px; + font-family: inherit; + cursor: pointer; +} +.btn-secondary:hover { border-color: rgba(255, 255, 255, 0.12); color: var(--text); } +.btn-secondary:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} From 308ef2c974808c257e94b7cc1bc54dc0f402caad Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 14:17:55 -0400 Subject: [PATCH 06/12] =?UTF-8?q?feat(ext):=20add=20GLYPH=5FNEXT=20and=20r?= =?UTF-8?q?eplace=20ASCII=20arrows=20with=20=E2=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the ASCII rightwards arrow → with U+25B8 ▸ in settings-vault buttons. Matches the existing ▾/▸ disclosure-glyph family. --- .../src/popup/components/settings-vault.ts | 5 +++-- extension/src/shared/__tests__/glyphs.test.ts | 22 +++++++++++++++++++ extension/src/shared/glyphs.ts | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/settings-vault.ts b/extension/src/popup/components/settings-vault.ts index 84e7935..288c032 100644 --- a/extension/src/popup/components/settings-vault.ts +++ b/extension/src/popup/components/settings-vault.ts @@ -7,6 +7,7 @@ import type { VaultSettings, TrashRetention, HistoryRetention, GeneratorRequest, } from '../../shared/types'; import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from './generator-panel'; +import { GLYPH_NEXT } from '../../shared/glyphs'; let pendingSettings: VaultSettings | null = null; let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null; @@ -161,14 +162,14 @@ export function renderVaultSettings(app: HTMLElement): void {
backup & restore
- +
import
- +
diff --git a/extension/src/shared/__tests__/glyphs.test.ts b/extension/src/shared/__tests__/glyphs.test.ts index e0a6d78..957c28c 100644 --- a/extension/src/shared/__tests__/glyphs.test.ts +++ b/extension/src/shared/__tests__/glyphs.test.ts @@ -1,5 +1,10 @@ import { describe, it, expect } from 'vitest'; import * as glyphs from '../glyphs'; +import { + GLYPH_REVEAL, GLYPH_HIDE, GLYPH_GENERATE, GLYPH_FILL_FROM_TAB, + GLYPH_QR, GLYPH_MONO, GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, + GLYPH_LOCK, GLYPH_NEXT, +} from '../glyphs'; describe('glyphs', () => { it('exports the documented glyph constants', () => { @@ -19,3 +24,20 @@ describe('glyphs', () => { expect(glyphs.REQUIRED_PILL_HTML).toBe('required'); }); }); + +describe('glyph constants', () => { + it('uses single unicode codepoints (no emoji multi-codepoint)', () => { + const all = [ + GLYPH_REVEAL, GLYPH_HIDE, GLYPH_GENERATE, GLYPH_FILL_FROM_TAB, + GLYPH_QR, GLYPH_MONO, GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, + GLYPH_LOCK, GLYPH_NEXT, + ]; + for (const g of all) { + expect([...g].length).toBe(1); + } + }); + + it('GLYPH_NEXT is the small right triangle (U+25B8)', () => { + expect(GLYPH_NEXT).toBe('▸'); + }); +}); diff --git a/extension/src/shared/glyphs.ts b/extension/src/shared/glyphs.ts index 0266a89..a69e3fb 100644 --- a/extension/src/shared/glyphs.ts +++ b/extension/src/shared/glyphs.ts @@ -16,6 +16,7 @@ export const GLYPH_TRASH = '▦'; // sidebar trash nav export const GLYPH_DEVICES = '⌬'; // sidebar devices nav export const GLYPH_SETTINGS = '⚙'; // sidebar settings nav export const GLYPH_LOCK = '⏻'; // sidebar lock nav +export const GLYPH_NEXT = '▸'; // forward / next button (matches ▾/▸ disclosure family) /// Inline HTML snippet for the required-field pill. Use after a label's text: /// `` From 7371eff0bbfed41078ba541a7f81b564d4ab8e38 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 14:21:04 -0400 Subject: [PATCH 07/12] 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; } From 97e351fa61a6df671a4b8a4aa2256a64c88cb39f Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 14:52:14 -0400 Subject: [PATCH 08/12] feat(ext/setup): apply polish vocabulary to setup wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wraps setup content in .surface-backdrop - Each wizard step gets a .glass card - Mode-picker cards become glass cards - 'next' / 'continue' buttons get the ▸ glyph - Migrate from .btn .btn-primary to the new .btn-primary class --- extension/src/setup/setup.ts | 41 +++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/extension/src/setup/setup.ts b/extension/src/setup/setup.ts index 99eab89..bd49f55 100644 --- a/extension/src/setup/setup.ts +++ b/extension/src/setup/setup.ts @@ -16,6 +16,7 @@ import { STRENGTH_LABELS, entropyText, } from './setup-helpers'; +import { GLYPH_NEXT } from '../shared/glyphs'; import type { VaultConfig } from '../shared/types'; import type { SessionHandle } from 'relicario-wasm'; @@ -189,12 +190,14 @@ function render(): void { } app.innerHTML = ` -
- -
Relicario vault setup
- ${progressHtml} - ${state.error ? `
${escapeHtml(state.error)}
` : ''} - ${stepHtml} +
+
+ +
Relicario vault setup
+ ${progressHtml} + ${state.error ? `
${escapeHtml(state.error)}
` : ''} + ${stepHtml} +
`; @@ -214,20 +217,20 @@ function renderStep0(): string { const isNew = state.mode === 'new'; const isAttach = state.mode === 'attach'; return ` -
+

set up Relicario

How are you using Relicario on this device?

- -
- +
`; @@ -267,7 +270,7 @@ function renderStep3Attach(): string { const gateDisabled = state.attaching || !p || !hasImage; return ` -
+

attach this device

Use your existing passphrase and reference image to attach this browser @@ -430,7 +433,7 @@ function renderStep1(): string { `; return ` -

+

choose host

@@ -442,7 +445,7 @@ function renderStep1(): string { ${state.hostType === 'gitea' ? giteaInstructions : githubInstructions}
- +
`; @@ -522,7 +525,7 @@ function renderStep2(): string { !!probe && ((state.mode === 'new' && probe.exists) || (state.mode === 'attach' && !probe.exists)); const nextDisabled = !state.connectionTested || !probe || modeMismatch; return ` -
+

configure connection

@@ -543,7 +546,7 @@ function renderStep2(): string { ${renderProbeBanner()}
- +
`; @@ -643,7 +646,7 @@ function renderStep3New(): string { const counterText = nChars === 0 ? '' : `${nChars} character${nChars === 1 ? '' : 's'}`; return ` -
+

create vault

@@ -907,7 +910,7 @@ function renderStep4(): string { const defaultName = state.deviceName || `${browser} on ${os}`; return ` -
+

name this device

This helps you identify which devices have access to your vault. @@ -918,7 +921,7 @@ function renderStep4(): string {

- +
`; @@ -979,7 +982,7 @@ function renderStep5(): string { const isAttach = state.mode === 'attach'; return ` -
+

${isAttach ? 'device verified' : 'vault created'}

From 058a49f68b52b60a2edf70cb37da5f2879b635df Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 14:55:37 -0400 Subject: [PATCH 09/12] style(ext/vault): apply .surface-backdrop to fullscreen body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subtle radial top-glow + grid texture behind the existing vault shell. No layout changes — existing panes sit above the backdrop's ::before. --- extension/src/vault/vault.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/vault/vault.html b/extension/src/vault/vault.html index 8e12efb..c9b4817 100644 --- a/extension/src/vault/vault.html +++ b/extension/src/vault/vault.html @@ -5,7 +5,7 @@ Relicario — vault - +

From a28b4561915e2f1b659cda7e8e7d16ae48971a41 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 15:01:35 -0400 Subject: [PATCH 10/12] feat(ext/login): add surface flag for two-column fullscreen form renderForm() takes an optional { surface: 'popup' | 'fullscreen' } parameter. When 'fullscreen', the Identity and Credentials field groups render as glass cards inside a .form-grid (two columns, stacks at <=720px). Popup keeps its single-column layout. --- extension/src/popup/components/item-form.ts | 2 +- .../components/types/__tests__/login.test.ts | 34 +++++ extension/src/popup/components/types/login.ts | 143 ++++++++++++------ extension/src/vault/vault.css | 25 +++ 4 files changed, 153 insertions(+), 51 deletions(-) diff --git a/extension/src/popup/components/item-form.ts b/extension/src/popup/components/item-form.ts index c9956ae..f0e72be 100644 --- a/extension/src/popup/components/item-form.ts +++ b/extension/src/popup/components/item-form.ts @@ -41,7 +41,7 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void { const type: ItemType = existing?.type ?? state.newType ?? 'login'; switch (type) { - case 'login': return login.renderForm(app, mode, existing); + case 'login': return login.renderForm(app, mode, existing, { surface: isInTab() ? 'fullscreen' : 'popup' }); case 'secure_note': return secureNote.renderForm(app, mode, existing); case 'identity': return identity.renderForm(app, mode, existing); case 'card': return card.renderForm(app, mode, existing); diff --git a/extension/src/popup/components/types/__tests__/login.test.ts b/extension/src/popup/components/types/__tests__/login.test.ts index 50400bb..7077847 100644 --- a/extension/src/popup/components/types/__tests__/login.test.ts +++ b/extension/src/popup/components/types/__tests__/login.test.ts @@ -63,6 +63,40 @@ describe('login form smart inputs', () => { }); }); +describe('renderForm surface flag', () => { + let app: HTMLElement; + beforeEach(() => { + document.body.innerHTML = '
'; + app = document.getElementById('app')!; + (globalThis as any).chrome = { + storage: { + local: { + get: vi.fn().mockImplementation((_keys: any, cb: any) => cb({})), + set: vi.fn().mockImplementation((_obj: any, cb: any) => cb && cb()), + }, + }, + runtime: { + sendMessage: vi.fn(), + }, + }; + vi.mocked(sendMessage).mockReset(); + vi.mocked(sendMessage).mockResolvedValue({ ok: true, data: { groups: [] } }); + }); + + it('renders single-column when surface is "popup" (default)', () => { + renderForm(app, 'add', null); + expect(app.querySelector('.form-grid')).toBeNull(); + }); + + it('renders two-column .form-grid wrapper when surface is "fullscreen"', () => { + renderForm(app, 'add', null, { surface: 'fullscreen' }); + const grid = app.querySelector('.form-grid'); + expect(grid).toBeTruthy(); + expect(grid!.querySelector('[data-form-section="identity"]')).toBeTruthy(); + expect(grid!.querySelector('[data-form-section="credentials"]')).toBeTruthy(); + }); +}); + describe('Login save shape', () => { beforeEach(() => { document.body.innerHTML = '
'; diff --git a/extension/src/popup/components/types/login.ts b/extension/src/popup/components/types/login.ts index aff7dff..f923bb6 100644 --- a/extension/src/popup/components/types/login.ts +++ b/extension/src/popup/components/types/login.ts @@ -235,7 +235,20 @@ function startTotpTicker(id: ItemId): void { // Form (add / edit) // ---------------------------------------------------------------------- -export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Item | null): void { +export interface RenderFormOptions { + surface?: 'popup' | 'fullscreen'; + /** When true, renderForm skips its own save/cancel buttons (caller provides them in a sticky bar). */ + externalActions?: boolean; +} + +export function renderForm( + app: HTMLElement, + mode: 'add' | 'edit', + existing: Item | null, + opts: RenderFormOptions = {} +): void { + const surface = opts.surface ?? 'popup'; + const externalActions = opts.externalActions ?? false; const state = getState(); const existingCore = (existing?.core.type === 'login') ? (existing.core as LoginCore & { type: 'login' }) @@ -254,58 +267,86 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite : []; let attachmentsDraft: AttachmentRef[] = existing?.attachments ?? []; + const titleFieldHtml = ` +
+
`; + + const urlFieldHtml = ` +
+ +
+ + +
+ +
`; + + const groupFieldHtml = ` +
+
`; + + const usernameFieldHtml = ` +
+
`; + + const passwordFieldHtml = ` +
+ +
+ + + +
+ +
`; + + const totpFieldHtml = ` +
+ +
+ + +
+ + +
`; + + const identityHtml = ` +
+ ${surface === 'fullscreen' ? '
Identity
' : ''} + ${titleFieldHtml} + ${urlFieldHtml} + ${groupFieldHtml} +
`; + + const credentialsHtml = ` +
+ ${surface === 'fullscreen' ? '
Credentials
' : ''} + ${usernameFieldHtml} + ${passwordFieldHtml} + ${totpFieldHtml} +
`; + + const sectionsHtml = surface === 'fullscreen' + ? `
${identityHtml}${credentialsHtml}
` + : `${identityHtml}${credentialsHtml}`; + app.innerHTML = `
- ${renderFormHeader({ titleText: mode === 'add' ? 'new login' : 'edit login' })} + ${surface === 'popup' ? renderFormHeader({ titleText: mode === 'add' ? 'new login' : 'edit login' }) : ''} ${state.error ? `
${escapeHtml(state.error)}
` : ''} -
-
- -
- -
- - -
- -
- -
-
- -
- -
- - - -
- -
- -
- -
- - -
- - -
- -
-
+ ${sectionsHtml}
@@ -317,10 +358,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite ${renderSectionsEditor(sectionsDraft, sectionsExpanded)} ${isInTab() ? renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' }) : ''} + ${externalActions ? '' : `
+ `}
`; @@ -433,7 +476,7 @@ function normalizeUrl(raw: string): { ok: true; value: string } | { ok: false; e } } -async function saveLogin(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise { +export async function saveLogin(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise { const state = getState(); const title = (document.getElementById('f-title') as HTMLInputElement).value.trim(); const rawUrl = (document.getElementById('f-url') as HTMLInputElement).value; diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 08de923..299d3c1 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1580,3 +1580,28 @@ textarea { outline: none; box-shadow: var(--focus-ring); } + +/* Phase 2B: two-column form grid for fullscreen login */ +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + max-width: 960px; + margin: 0 auto; +} +@media (max-width: 720px) { + .form-grid { grid-template-columns: 1fr; } +} +.form-col { + padding: 14px 16px; +} +.col-header { + text-transform: uppercase; + letter-spacing: 1.2px; + font-weight: 500; + color: var(--text-muted); + font-size: 10px; + border-bottom: 1px solid var(--border-mid); + padding-bottom: 6px; + margin-bottom: 12px; +} From b270dfedb4d41145e17f5ac19a1934448f5fe49d Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 15:05:09 -0400 Subject: [PATCH 11/12] feat(ext/vault): sticky save bar in fullscreen forms The form pane gets a flex column layout: scrollable content above, sticky save bar at bottom. Bar uses translucent fill with backdrop-blur and a 24px gradient fade so content scrolls under it. Save / cancel buttons reuse the form's existing handlers via externalActions flag. --- extension/src/popup/components/item-form.ts | 2 +- extension/src/popup/components/types/login.ts | 4 +- extension/src/vault/vault.css | 36 +++++++++ extension/src/vault/vault.ts | 75 ++++++++++++++++++- 4 files changed, 111 insertions(+), 6 deletions(-) diff --git a/extension/src/popup/components/item-form.ts b/extension/src/popup/components/item-form.ts index f0e72be..0460c7c 100644 --- a/extension/src/popup/components/item-form.ts +++ b/extension/src/popup/components/item-form.ts @@ -41,7 +41,7 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void { const type: ItemType = existing?.type ?? state.newType ?? 'login'; switch (type) { - case 'login': return login.renderForm(app, mode, existing, { surface: isInTab() ? 'fullscreen' : 'popup' }); + case 'login': return login.renderForm(app, mode, existing, { surface: isInTab() ? 'fullscreen' : 'popup', externalActions: isInTab() }); case 'secure_note': return secureNote.renderForm(app, mode, existing); case 'identity': return identity.renderForm(app, mode, existing); case 'card': return card.renderForm(app, mode, existing); diff --git a/extension/src/popup/components/types/login.ts b/extension/src/popup/components/types/login.ts index f923bb6..f21bef7 100644 --- a/extension/src/popup/components/types/login.ts +++ b/extension/src/popup/components/types/login.ts @@ -358,12 +358,10 @@ export function renderForm( ${renderSectionsEditor(sectionsDraft, sectionsExpanded)} ${isInTab() ? renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' }) : ''} - ${externalActions ? '' : ` -
+
- `}
`; diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 299d3c1..a507dbd 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1605,3 +1605,39 @@ textarea { padding-bottom: 6px; margin-bottom: 12px; } + +/* Phase 2B: sticky save bar + scrollable form pane */ +.form-pane { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} +.form-scroll { + flex: 1; + overflow-y: auto; + padding: 20px 24px; +} +.sticky-save-bar { + position: sticky; + bottom: 0; + background: rgba(17, 22, 30, 0.7); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + border-top: 1px solid var(--border-mid); + padding: 12px 24px; + display: flex; + justify-content: flex-end; + gap: 8px; + z-index: 10; +} +.sticky-save-bar::before { + content: ''; + position: absolute; + top: -24px; + left: 0; + right: 0; + height: 24px; + background: linear-gradient(to top, rgba(17, 22, 30, 0.7), transparent); + pointer-events: none; +} diff --git a/extension/src/vault/vault.ts b/extension/src/vault/vault.ts index f90449e..4f0bb92 100644 --- a/extension/src/vault/vault.ts +++ b/extension/src/vault/vault.ts @@ -415,6 +415,71 @@ async function selectItem(id: ItemId): Promise { } } +// --------------------------------------------------------------------------- +// Platform-aware save hint +// --------------------------------------------------------------------------- + +const isMac = navigator.platform.toLowerCase().includes('mac'); +const SAVE_HINT = isMac ? '⌘+S to save' : 'Ctrl+S to save'; + +// --------------------------------------------------------------------------- +// Fullscreen form wrapper — sticky save bar + scrollable content + header +// --------------------------------------------------------------------------- + +function renderFormWrapped(app: HTMLElement, mode: 'add' | 'edit'): void { + const itemType = state.selectedItem?.type ?? state.newType ?? 'login'; + const typeLabel = itemType.replace('_', ' '); + const titleText = mode === 'add' ? `new ${typeLabel}` : `edit ${typeLabel}`; + const wrapper = document.createElement('div'); + wrapper.className = 'form-pane'; + wrapper.innerHTML = ` +
+
+
${titleText}
+
no changes
+
+
${SAVE_HINT}
+
+
+
+ + +
+ `; + // Remove pane padding so form-pane can fill height cleanly + app.style.padding = '0'; + app.style.overflow = 'hidden'; + app.replaceChildren(wrapper); + + const scrollEl = wrapper.querySelector('#form-scroll') as HTMLElement; + renderItemForm(scrollEl, mode); + + const subEl = wrapper.querySelector('#form-dirty-sub') as HTMLElement; + let isDirty = false; + const markDirty = () => { + if (isDirty) return; + isDirty = true; + subEl.textContent = 'unsaved · esc to cancel'; + }; + const markClean = () => { + isDirty = false; + subEl.textContent = 'no changes'; + }; + scrollEl.addEventListener('input', markDirty, true); + scrollEl.addEventListener('change', markDirty, true); + + wrapper.querySelector('#form-cancel')?.addEventListener('click', () => { + markClean(); + (scrollEl.querySelector('#cancel-btn') as HTMLButtonElement | null)?.click(); + }); + wrapper.querySelector('#form-save')?.addEventListener('click', () => { + markClean(); + (scrollEl.querySelector('#save-btn') as HTMLButtonElement | null)?.click(); + }); +} + +export const __test__ = { renderFormWrapped }; + // --------------------------------------------------------------------------- // Pane rendering — delegates to shared popup components // --------------------------------------------------------------------------- @@ -453,10 +518,16 @@ function renderPane(): void { // set by the type-selection click handler (which calls setState → // renderPane before the URL hash has been updated to include the type). state.newType = (route.type as ItemType) ?? state.newType ?? null; - renderItemForm(pane, 'add'); + // Use the form wrapper (sticky bar + header) when a type is already chosen. + // Without a type the type-selection screen renders — no sticky bar needed. + if (state.newType) { + renderFormWrapped(pane, 'add'); + } else { + renderItemForm(pane, 'add'); + } break; case 'edit': - renderItemForm(pane, 'edit'); + renderFormWrapped(pane, 'edit'); break; case 'trash': renderTrash(pane); From f1c615c0ed9fef700cae02b014ca2099a7927037 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 15:06:42 -0400 Subject: [PATCH 12/12] feat(ext/vault): fullscreen form header with dirty-state subtitle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Title left ('new login' / 'edit login'), subtitle below cycles between 'no changes' and 'unsaved · esc to cancel' on input events. Right side shows the platform-aware save hint ('⌘+S to save' / 'Ctrl+S to save'). The actual ⌘+S keymap arrives in Phase 3 — this is a visual hint only. --- .../src/vault/__tests__/form-wrapper.test.ts | 45 +++++++++++++++++++ extension/src/vault/vault.css | 23 ++++++++++ 2 files changed, 68 insertions(+) create mode 100644 extension/src/vault/__tests__/form-wrapper.test.ts diff --git a/extension/src/vault/__tests__/form-wrapper.test.ts b/extension/src/vault/__tests__/form-wrapper.test.ts new file mode 100644 index 0000000..791c34d --- /dev/null +++ b/extension/src/vault/__tests__/form-wrapper.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('fullscreen form dirty subtitle', () => { + const vaultSrc = fs.readFileSync( + path.resolve(__dirname, '../vault.ts'), + 'utf-8', + ); + + it('contains renderFormWrapped function', () => { + expect(vaultSrc).toContain('function renderFormWrapped'); + }); + + it('starts pristine: renders "no changes" subtitle', () => { + expect(vaultSrc).toContain("'no changes'"); + }); + + it('switches to dirty on first input event', () => { + expect(vaultSrc).toContain("'unsaved · esc to cancel'"); + }); + + it('listens on input and change events on the scroll element', () => { + expect(vaultSrc).toContain("scrollEl.addEventListener('input', markDirty, true)"); + expect(vaultSrc).toContain("scrollEl.addEventListener('change', markDirty, true)"); + }); + + it('marks clean on save', () => { + expect(vaultSrc).toContain('markClean()'); + }); + + it('contains platform-aware SAVE_HINT', () => { + expect(vaultSrc).toContain('SAVE_HINT'); + expect(vaultSrc).toContain('⌘+S to save'); + expect(vaultSrc).toContain('Ctrl+S to save'); + }); + + it('renders fullscreen-form-header element', () => { + expect(vaultSrc).toContain('fullscreen-form-header'); + }); + + it('renders form-dirty-sub element', () => { + expect(vaultSrc).toContain('form-dirty-sub'); + }); +}); diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index a507dbd..1cfb456 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1606,6 +1606,29 @@ textarea { margin-bottom: 12px; } +/* Phase 2B: fullscreen form header */ +.fullscreen-form-header { + padding: 14px 24px; + border-bottom: 1px solid var(--border-mid); + display: flex; + justify-content: space-between; + align-items: flex-start; +} +.fullscreen-form-header .title { + font-size: 16px; + font-weight: 500; + color: var(--text); +} +.fullscreen-form-header .sub { + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; +} +.fullscreen-form-header .hint { + font-size: 11px; + color: var(--text-dim); +} + /* Phase 2B: sticky save bar + scrollable form pane */ .form-pane { display: flex;