From 33b3f0b0196b79ba94567240e934883be0ede4f4 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:25:12 -0400 Subject: [PATCH 01/14] feat(ext/shared): glyph constants module for unified icon language Centralizes the unicode glyphs used by sidebar nav and form action buttons so popup and fullscreen surfaces stay in sync. Includes the REQUIRED_PILL snippet used to replace the trailing-asterisk required-field marker. Plan 2026-04-30 fullscreen UX phase 1 task 1. Co-Authored-By: Claude Opus 4.7 --- extension/src/shared/__tests__/glyphs.test.ts | 21 ++++++++++++++++++ extension/src/shared/glyphs.ts | 22 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 extension/src/shared/__tests__/glyphs.test.ts create mode 100644 extension/src/shared/glyphs.ts diff --git a/extension/src/shared/__tests__/glyphs.test.ts b/extension/src/shared/__tests__/glyphs.test.ts new file mode 100644 index 0000000..b0a88d7 --- /dev/null +++ b/extension/src/shared/__tests__/glyphs.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import * as glyphs from '../glyphs'; + +describe('glyphs', () => { + it('exports the documented glyph constants', () => { + expect(glyphs.GLYPH_REVEAL).toBe('⊙'); + expect(glyphs.GLYPH_HIDE).toBe('⊘'); + expect(glyphs.GLYPH_GENERATE).toBe('↻'); + expect(glyphs.GLYPH_FILL_FROM_TAB).toBe('⤓'); + expect(glyphs.GLYPH_QR).toBe('◫'); + expect(glyphs.GLYPH_MONO).toBe('≡'); + expect(glyphs.GLYPH_TRASH).toBe('▦'); + expect(glyphs.GLYPH_DEVICES).toBe('⌬'); + expect(glyphs.GLYPH_SETTINGS).toBe('⚙'); + expect(glyphs.GLYPH_LOCK).toBe('⏻'); + }); + + it('exports REQUIRED_PILL as an HTML snippet', () => { + expect(glyphs.REQUIRED_PILL).toBe('required'); + }); +}); diff --git a/extension/src/shared/glyphs.ts b/extension/src/shared/glyphs.ts new file mode 100644 index 0000000..8ac6c26 --- /dev/null +++ b/extension/src/shared/glyphs.ts @@ -0,0 +1,22 @@ +// +// Unicode glyph constants used across popup and fullscreen surfaces. All +// glyphs are monochrome unicode (no emoji) so they render identically in the +// codebase's monospace font. Pair each button glyph with a `title=` tooltip +// at the call site for accessibility — the constants here are the visual, +// not the affordance. + +export const GLYPH_REVEAL = '⊙'; // password reveal toggle (hidden state) +export const GLYPH_HIDE = '⊘'; // password reveal toggle (revealed state) +export const GLYPH_GENERATE = '↻'; // password / passphrase generate +export const GLYPH_FILL_FROM_TAB = '⤓'; // pull URL from active browser tab +export const GLYPH_QR = '◫'; // paste / upload QR image (TOTP) +export const GLYPH_MONO = '≡'; // toggle notes monospace font + +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 + +/// Inline HTML snippet for the required-field pill. Use after a label's text: +/// `` +export const REQUIRED_PILL = 'required'; From 506ad9711d9a08afdaa444bd5fd6059d365679dd Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:29:49 -0400 Subject: [PATCH 02/14] =?UTF-8?q?refactor(ext/shared):=20rename=20REQUIRED?= =?UTF-8?q?=5FPILL=20=E2=86=92=20REQUIRED=5FPILL=5FHTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review feedback on Task 1: the _HTML suffix makes the 'this is raw HTML, do not escape' contract obvious at every call site. Cheap to do now (zero consumers); would be 8 diffs once Tasks 4-6 wire the constant into the type forms. Plan updated in lockstep so Task 4 references the new name. Co-Authored-By: Claude Opus 4.7 --- ...fullscreen-ux-phase-1-visual-foundation.md | 26 +++++++++---------- extension/src/shared/__tests__/glyphs.test.ts | 4 +-- extension/src/shared/glyphs.ts | 7 +++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md b/docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md index addd42c..08f555b 100644 --- a/docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md +++ b/docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md @@ -4,7 +4,7 @@ **Goal:** Establish the shared visual language (glyph constants, color tokens, focus ring, required pill, header subtitle) and clean up vestigial popup-only UI in the fullscreen vault. No structural or behavioral changes; pure visual foundation that the next three phases will build on. -**Architecture:** A new `extension/src/shared/glyphs.ts` module exports unicode glyph constants and a `REQUIRED_PILL` HTML snippet, consumed by both popup and fullscreen surfaces. CSS custom properties added to `popup/styles.css` and `vault/vault.css` provide the shared color/focus tokens. All eight type forms migrate from `*` to the pill; sidebar nav buttons replace emoji with glyph constants; the popout-to-tab button is gated behind `!isInTab()` so it disappears in fullscreen. Fullscreen forms gain a static "esc to cancel" subtitle (dynamic dirty-state lands in Phase 3). +**Architecture:** A new `extension/src/shared/glyphs.ts` module exports unicode glyph constants and a `REQUIRED_PILL_HTML` HTML snippet, consumed by both popup and fullscreen surfaces. CSS custom properties added to `popup/styles.css` and `vault/vault.css` provide the shared color/focus tokens. All eight type forms migrate from `*` to the pill; sidebar nav buttons replace emoji with glyph constants; the popout-to-tab button is gated behind `!isInTab()` so it disappears in fullscreen. Fullscreen forms gain a static "esc to cancel" subtitle (dynamic dirty-state lands in Phase 3). **Tech stack:** TypeScript, vanilla DOM (no framework), Vitest + happy-dom for unit tests. No new runtime dependencies. @@ -39,8 +39,8 @@ describe('glyphs', () => { expect(glyphs.GLYPH_LOCK).toBe('⏻'); }); - it('exports REQUIRED_PILL as an HTML snippet', () => { - expect(glyphs.REQUIRED_PILL).toBe('required'); + it('exports REQUIRED_PILL_HTML as an HTML snippet', () => { + expect(glyphs.REQUIRED_PILL_HTML).toBe('required'); }); }); ``` @@ -74,8 +74,8 @@ export const GLYPH_SETTINGS = '⚙'; // sidebar settings nav export const GLYPH_LOCK = '⏻'; // sidebar lock nav /// Inline HTML snippet for the required-field pill. Use after a label's text: -/// `` -export const REQUIRED_PILL = 'required'; +/// `` +export const REQUIRED_PILL_HTML = 'required'; ``` - [ ] **Step 4: Run test to verify it passes** @@ -90,7 +90,7 @@ git -C /home/alee/Sources/relicario add extension/src/shared/glyphs.ts extension git -C /home/alee/Sources/relicario commit -m "feat(ext/shared): glyph constants module for unified icon language Centralizes the unicode glyphs used by sidebar nav and form action buttons -so popup and fullscreen surfaces stay in sync. Includes the REQUIRED_PILL +so popup and fullscreen surfaces stay in sync. Includes the REQUIRED_PILL_HTML snippet used to replace the trailing-asterisk required-field marker. Plan 2026-04-30 fullscreen UX phase 1 task 1. @@ -330,7 +330,7 @@ Co-Authored-By: Claude Opus 4.7 " --- -## Task 4: Migrate required-marker sites to REQUIRED_PILL +## Task 4: Migrate required-marker sites to REQUIRED_PILL_HTML **Files (10 sites across 7 files):** - Modify: `extension/src/popup/components/types/card.ts:182` @@ -392,7 +392,7 @@ In `extension/src/popup/components/types/login.ts`: Add an import near the top (after the existing imports): ```typescript -import { REQUIRED_PILL } from '../../../shared/glyphs'; +import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; ``` Find line 252: @@ -402,7 +402,7 @@ Find line 252: Replace with: ```typescript -
+
``` - [ ] **Step 4: Run the test to verify it passes for login** @@ -413,8 +413,8 @@ Expected: PASS. - [ ] **Step 5: Migrate the remaining six files** Apply the same pattern to each of these six files. For each: -1. Add `import { REQUIRED_PILL } from '../../../shared/glyphs';` -2. Replace each `*` with `${REQUIRED_PILL}` +1. Add `import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';` +2. Replace each `*` with `${REQUIRED_PILL_HTML}` | File | Line(s) | |---|---| @@ -444,10 +444,10 @@ Expected: `compiled with 2 warnings`. ```bash git -C /home/alee/Sources/relicario add extension/src/popup/components/types/ extension/src/shared/ -git -C /home/alee/Sources/relicario commit -m "refactor(ext/popup): migrate required-field markers to REQUIRED_PILL +git -C /home/alee/Sources/relicario commit -m "refactor(ext/popup): migrate required-field markers to REQUIRED_PILL_HTML Replaces ten * sites across all seven type -forms with the shared REQUIRED_PILL snippet ('required' badge). Adds a +forms with the shared REQUIRED_PILL_HTML snippet ('required' badge). Adds a regression test pinning the new HTML in the login form. Plan 2026-04-30 fullscreen UX phase 1 task 4. diff --git a/extension/src/shared/__tests__/glyphs.test.ts b/extension/src/shared/__tests__/glyphs.test.ts index b0a88d7..e0a6d78 100644 --- a/extension/src/shared/__tests__/glyphs.test.ts +++ b/extension/src/shared/__tests__/glyphs.test.ts @@ -15,7 +15,7 @@ describe('glyphs', () => { expect(glyphs.GLYPH_LOCK).toBe('⏻'); }); - it('exports REQUIRED_PILL as an HTML snippet', () => { - expect(glyphs.REQUIRED_PILL).toBe('required'); + it('exports REQUIRED_PILL_HTML as an HTML snippet', () => { + expect(glyphs.REQUIRED_PILL_HTML).toBe('required'); }); }); diff --git a/extension/src/shared/glyphs.ts b/extension/src/shared/glyphs.ts index 8ac6c26..0266a89 100644 --- a/extension/src/shared/glyphs.ts +++ b/extension/src/shared/glyphs.ts @@ -18,5 +18,8 @@ export const GLYPH_SETTINGS = '⚙'; // sidebar settings nav export const GLYPH_LOCK = '⏻'; // sidebar lock nav /// Inline HTML snippet for the required-field pill. Use after a label's text: -/// `` -export const REQUIRED_PILL = 'required'; +/// `` +/// +/// Requires the `.req-pill` CSS rule (added in Tasks 2 and 3 to popup/styles.css +/// and vault/vault.css respectively). +export const REQUIRED_PILL_HTML = 'required'; From e5875249bff3b9455e9be931a5cd61e0571e3273 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:36:26 -0400 Subject: [PATCH 03/14] style(ext/popup): add color tokens, focus ring, required-pill class Establishes :root CSS custom properties (accent, surfaces, status, focus ring) and applies the focus ring to inputs/buttons via :focus-visible. Adds .req-pill class used by Task 4 to replace the bare-asterisk required marker. Existing .label .req kept for backward compatibility during the migration window. Plan 2026-04-30 fullscreen UX phase 1 task 2. Co-Authored-By: Claude Opus 4.7 --- extension/src/popup/styles.css | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 362e3ae..9ce5127 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1,5 +1,31 @@ /* relicario extension — terminal dark theme */ +:root { + /* Brand */ + --accent: #d2ab43; + --accent-soft: rgba(210, 171, 67, 0.18); + --accent-strong: #aa812a; + + /* Surfaces */ + --bg-page: #0d1117; + --bg-pane: #161b22; + --bg-elevated: #21262d; + --border-subtle: #30363d; + + /* Text */ + --text: #c9d1d9; + --text-muted: #8b949e; + --text-dim: #484f58; + + /* Status */ + --danger: #ab2b20; + --danger-bg: #791111; + --success: #6cb37a; + + /* Focus */ + --focus-ring: 0 0 0 2px rgba(210, 171, 67, 0.35); +} + * { margin: 0; padding: 0; @@ -56,11 +82,25 @@ body { } .label .req { - color: #aa812a; + color: var(--accent-strong); margin-left: 2px; font-weight: 600; } +.req-pill { + display: inline-block; + font-size: 9px; + padding: 1px 5px; + background: var(--accent-soft); + color: var(--accent); + border-radius: 2px; + margin-left: 6px; + vertical-align: middle; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; +} + .secondary { color: #8b949e; } @@ -94,9 +134,9 @@ body { background: #30363d; } -.btn:focus { - outline: 1px solid #d2ab43; - outline-offset: 1px; +.btn:focus-visible { + outline: none; + box-shadow: var(--focus-ring); } .btn-primary { @@ -133,8 +173,10 @@ input, textarea, select { transition: border-color 0.15s; } -input:focus, textarea:focus, select:focus { - border-color: #d2ab43; +input:focus-visible, textarea:focus-visible, select:focus-visible { + border-color: var(--accent); + box-shadow: var(--focus-ring); + outline: none; } input::placeholder, textarea::placeholder { From f0d8758a8030bed1fdec690730dfc580d0766abc Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:39:46 -0400 Subject: [PATCH 04/14] style(ext/vault): mirror color tokens, focus ring, required-pill class Same :root block and .req-pill rule as popup/styles.css so the two stylesheets share visual tokens. Vault input focus migrated to :focus-visible + box-shadow ring. Plan 2026-04-30 fullscreen UX phase 1 task 3. Co-Authored-By: Claude Opus 4.7 --- extension/src/vault/vault.css | 46 +++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 6184035..ec652c9 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1,5 +1,31 @@ /* relicario vault — terminal dark theme (tab layout) */ +:root { + /* Brand */ + --accent: #d2ab43; + --accent-soft: rgba(210, 171, 67, 0.18); + --accent-strong: #aa812a; + + /* Surfaces */ + --bg-page: #0d1117; + --bg-pane: #161b22; + --bg-elevated: #21262d; + --border-subtle: #30363d; + + /* Text */ + --text: #c9d1d9; + --text-muted: #8b949e; + --text-dim: #484f58; + + /* Status */ + --danger: #ab2b20; + --danger-bg: #791111; + --success: #6cb37a; + + /* Focus */ + --focus-ring: 0 0 0 2px rgba(210, 171, 67, 0.35); +} + * { margin: 0; padding: 0; @@ -61,6 +87,20 @@ body { font-weight: 600; } +.req-pill { + display: inline-block; + font-size: 9px; + padding: 1px 5px; + background: var(--accent-soft); + color: var(--accent); + border-radius: 2px; + margin-left: 6px; + vertical-align: middle; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; +} + .secondary { color: #8b949e; } @@ -133,8 +173,10 @@ input, textarea, select { transition: border-color 0.15s; } -input:focus, textarea:focus, select:focus { - border-color: #d2ab43; +input:focus-visible, textarea:focus-visible, select:focus-visible { + border-color: var(--accent); + box-shadow: var(--focus-ring); + outline: none; } input::placeholder, textarea::placeholder { From 6e720554fa924fc71a60b84513e38bd3130d7f24 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:42:24 -0400 Subject: [PATCH 05/14] style(ext/vault): migrate .btn:focus to :focus-visible + var(--focus-ring) Code-review feedback on Task 3: vault button focus was the last hardcoded #d2ab43 + bare :focus rule not yet migrated. Brings vault button focus into parity with popup (which Task 2 already migrated) and removes the last raw accent literal from the focus-related rules. Co-Authored-By: Claude Opus 4.7 --- extension/src/vault/vault.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index ec652c9..19272c7 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -134,9 +134,9 @@ body { background: #30363d; } -.btn:focus { - outline: 1px solid #d2ab43; - outline-offset: 1px; +.btn:focus-visible { + outline: none; + box-shadow: var(--focus-ring); } .btn-primary { From e2381ed2ecb1b2b50e7a68b1f1eab50e25b644e7 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:46:07 -0400 Subject: [PATCH 06/14] refactor(ext/popup): migrate required-field markers to REQUIRED_PILL_HTML Replaces ten * sites across all seven type forms with the shared REQUIRED_PILL_HTML snippet ('required' badge). Adds a regression test pinning the new HTML in the login form. Plan 2026-04-30 fullscreen UX phase 1 task 4. Co-Authored-By: Claude Opus 4.7 --- .../types/__tests__/required-pill.test.ts | 32 +++++++++++++++++++ extension/src/popup/components/types/card.ts | 3 +- .../src/popup/components/types/document.ts | 5 +-- .../src/popup/components/types/identity.ts | 3 +- extension/src/popup/components/types/key.ts | 5 +-- extension/src/popup/components/types/login.ts | 3 +- .../src/popup/components/types/secure-note.ts | 3 +- extension/src/popup/components/types/totp.ts | 5 +-- 8 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 extension/src/popup/components/types/__tests__/required-pill.test.ts diff --git a/extension/src/popup/components/types/__tests__/required-pill.test.ts b/extension/src/popup/components/types/__tests__/required-pill.test.ts new file mode 100644 index 0000000..5bc79d7 --- /dev/null +++ b/extension/src/popup/components/types/__tests__/required-pill.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('../../../../shared/state', () => ({ + sendMessage: vi.fn(), + getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }), + setState: vi.fn(), + navigate: vi.fn(), + escapeHtml: (s: string) => s, + popOutToTab: vi.fn(), + isInTab: () => false, + openVaultTab: vi.fn(), + registerHost: vi.fn(), +})); + +vi.mock('../../generator-panel', () => ({ + openGeneratorPanel: vi.fn(), + closeGeneratorPanel: vi.fn(), + isGeneratorPanelOpen: () => false, +})); + +import { renderForm } from '../login'; + +describe('required-pill migration', () => { + beforeEach(() => { document.body.innerHTML = '
'; }); + + it('login form title uses the required pill', () => { + renderForm(document.getElementById('app')!, 'add', null); + const titleLabel = document.querySelector('label[for="f-title"]'); + expect(titleLabel?.innerHTML).toContain('required'); + expect(titleLabel?.innerHTML).not.toContain('*'); + }); +}); diff --git a/extension/src/popup/components/types/card.ts b/extension/src/popup/components/types/card.ts index 60fc280..481fa02 100644 --- a/extension/src/popup/components/types/card.ts +++ b/extension/src/popup/components/types/card.ts @@ -2,6 +2,7 @@ /// Detail view has a styled card-silhouette signature block. import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state'; +import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types'; import { renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections, @@ -179,7 +180,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${state.error ? `
${escapeHtml(state.error)}
` : ''} -
+
diff --git a/extension/src/popup/components/types/document.ts b/extension/src/popup/components/types/document.ts index 7a25597..62260df 100644 --- a/extension/src/popup/components/types/document.ts +++ b/extension/src/popup/components/types/document.ts @@ -3,6 +3,7 @@ /// Primary attachment is referenced by ID from the item's attachments array. import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state'; +import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types'; import { renderSectionsEditor, wireSectionsEditor, @@ -91,11 +92,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${state.error ? `
${escapeHtml(state.error)}
` : ''}
- +
- + ${renderPrimary()}
diff --git a/extension/src/popup/components/types/identity.ts b/extension/src/popup/components/types/identity.ts index 0f5330b..1fb09d6 100644 --- a/extension/src/popup/components/types/identity.ts +++ b/extension/src/popup/components/types/identity.ts @@ -2,6 +2,7 @@ /// Detail view shows a "profile card" signature block + plain rows. import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state'; +import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types'; import { renderRow, renderSignatureBlock, wireFieldHandlers, renderSections, @@ -139,7 +140,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${state.error ? `
${escapeHtml(state.error)}
` : ''} -
+
diff --git a/extension/src/popup/components/types/key.ts b/extension/src/popup/components/types/key.ts index 50215c9..ec5ce03 100644 --- a/extension/src/popup/components/types/key.ts +++ b/extension/src/popup/components/types/key.ts @@ -3,6 +3,7 @@ /// since diff --git a/extension/src/popup/components/types/login.ts b/extension/src/popup/components/types/login.ts index b0feb52..0d59319 100644 --- a/extension/src/popup/components/types/login.ts +++ b/extension/src/popup/components/types/login.ts @@ -2,6 +2,7 @@ /// field helpers introduced in Slice 2. import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state'; +import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig, AttachmentRef } from '../../../shared/types'; import { DEFAULT_PASSWORD_REQUEST } from '../../../shared/types'; import { base32Decode, base32Encode } from '../../../shared/base32'; @@ -249,7 +250,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${state.error ? `
${escapeHtml(state.error)}
` : ''} -
+
diff --git a/extension/src/popup/components/types/secure-note.ts b/extension/src/popup/components/types/secure-note.ts index 1ecc788..571c7d3 100644 --- a/extension/src/popup/components/types/secure-note.ts +++ b/extension/src/popup/components/types/secure-note.ts @@ -2,6 +2,7 @@ /// detail view; the form is just a big
diff --git a/extension/src/popup/components/types/totp.ts b/extension/src/popup/components/types/totp.ts index ef5edcb..7e7a2d5 100644 --- a/extension/src/popup/components/types/totp.ts +++ b/extension/src/popup/components/types/totp.ts @@ -3,6 +3,7 @@ /// (TOTP vs Steam Guard) and a single secret input. import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state'; +import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types'; import { base32Decode, base32Encode } from '../../../shared/base32'; import { @@ -218,7 +219,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite ${state.error ? `
${escapeHtml(state.error)}
` : ''} -
+
@@ -227,7 +228,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite

${formKind === 'steam' ? 'Steam Mobile Authenticator (5-char alphanumeric)' : 'Standard time-based codes (6 digits)'}

-
+
From a634b6c7454aaf329eec0df969eb0c5eb63cd943 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:52:26 -0400 Subject: [PATCH 07/14] refactor(ext): broaden required-pill test + drop dead .label .req CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review feedback on Task 4: - Test expanded from login-only to it.each across all 7 type forms (14 assertions total). A future revert to * in any form now fails CI. - .label .req rule removed from popup/styles.css and vault/vault.css — zero consumers after the REQUIRED_PILL_HTML migration. Co-Authored-By: Claude Opus 4.7 --- .../types/__tests__/required-pill.test.ts | 31 +++++++++++++++---- extension/src/popup/styles.css | 6 ---- extension/src/vault/vault.css | 6 ---- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/extension/src/popup/components/types/__tests__/required-pill.test.ts b/extension/src/popup/components/types/__tests__/required-pill.test.ts index 5bc79d7..3ad1027 100644 --- a/extension/src/popup/components/types/__tests__/required-pill.test.ts +++ b/extension/src/popup/components/types/__tests__/required-pill.test.ts @@ -18,15 +18,34 @@ vi.mock('../../generator-panel', () => ({ isGeneratorPanelOpen: () => false, })); -import { renderForm } from '../login'; +import * as login from '../login'; +import * as secureNote from '../secure-note'; +import * as identity from '../identity'; +import * as card from '../card'; +import * as key from '../key'; +import * as totp from '../totp'; +import * as documentType from '../document'; + +const forms: Array<[string, (app: HTMLElement, mode: 'add' | 'edit', existing: null) => void]> = [ + ['login', login.renderForm], + ['secure-note', secureNote.renderForm], + ['identity', identity.renderForm], + ['card', card.renderForm], + ['key', key.renderForm], + ['totp', totp.renderForm], + ['document', documentType.renderForm], +]; describe('required-pill migration', () => { beforeEach(() => { document.body.innerHTML = '
'; }); - it('login form title uses the required pill', () => { - renderForm(document.getElementById('app')!, 'add', null); - const titleLabel = document.querySelector('label[for="f-title"]'); - expect(titleLabel?.innerHTML).toContain('required'); - expect(titleLabel?.innerHTML).not.toContain('*'); + it.each(forms)('%s form has no legacy markup', (_name, render) => { + render(document.getElementById('app')!, 'add', null); + expect(document.body.innerHTML).not.toContain('class="req"'); + }); + + it.each(forms)('%s form contains the .req-pill markup on at least one label', (_name, render) => { + render(document.getElementById('app')!, 'add', null); + expect(document.body.innerHTML).toContain('class="req-pill">required'); }); }); diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 9ce5127..a4d07e6 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -81,12 +81,6 @@ body { margin-bottom: 4px; } -.label .req { - color: var(--accent-strong); - margin-left: 2px; - font-weight: 600; -} - .req-pill { display: inline-block; font-size: 9px; diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 19272c7..e00dc0a 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -81,12 +81,6 @@ body { margin-bottom: 4px; } -.label .req { - color: #aa812a; - margin-left: 2px; - font-weight: 600; -} - .req-pill { display: inline-block; font-size: 9px; From e2260e9df4445e6f372dfafc4fdf3d4c1a33cd74 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:53:50 -0400 Subject: [PATCH 08/14] style(ext/vault): replace sidebar emoji nav with monochrome glyphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ▦ trash · ⌬ devices · ⚙ settings · ⏻ lock — all imported from the new shared/glyphs module so popup and fullscreen stay in sync. Regression test scans the source for the old escape-coded emoji to prevent backsliding. Plan 2026-04-30 fullscreen UX phase 1 task 5. Co-Authored-By: Claude Opus 4.7 --- .../vault/__tests__/sidebar-glyphs.test.ts | 29 +++++++++++++++++++ extension/src/vault/vault.ts | 9 +++--- 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 extension/src/vault/__tests__/sidebar-glyphs.test.ts diff --git a/extension/src/vault/__tests__/sidebar-glyphs.test.ts b/extension/src/vault/__tests__/sidebar-glyphs.test.ts new file mode 100644 index 0000000..1800bcd --- /dev/null +++ b/extension/src/vault/__tests__/sidebar-glyphs.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('vault sidebar glyphs', () => { + const vaultSrc = fs.readFileSync( + path.resolve(__dirname, '../vault.ts'), + 'utf-8', + ); + + it('uses GLYPH_TRASH instead of the trash emoji', () => { + expect(vaultSrc).not.toMatch(/\u{1F5D1}/u); + expect(vaultSrc).toContain('GLYPH_TRASH'); + }); + + it('uses GLYPH_DEVICES instead of the devices emoji', () => { + expect(vaultSrc).not.toMatch(/\u{1F4F1}/u); + expect(vaultSrc).toContain('GLYPH_DEVICES'); + }); + + it('uses GLYPH_LOCK instead of the lock emoji', () => { + expect(vaultSrc).not.toMatch(/\u{1F512}/u); + expect(vaultSrc).toContain('GLYPH_LOCK'); + }); + + it('uses GLYPH_SETTINGS for the settings nav', () => { + expect(vaultSrc).toContain('GLYPH_SETTINGS'); + }); +}); diff --git a/extension/src/vault/vault.ts b/extension/src/vault/vault.ts index 32f4d0d..77e8af8 100644 --- a/extension/src/vault/vault.ts +++ b/extension/src/vault/vault.ts @@ -9,6 +9,7 @@ import type { ItemId, ItemType, ManifestEntry, Item, VaultSettings, GeneratorRequest, } from '../shared/types'; import { registerHost } from '../shared/state'; +import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK } from '../shared/glyphs'; import { renderItemDetail } from '../popup/components/item-detail'; import { renderItemForm } from '../popup/components/item-form'; import { renderTrash, teardown as teardownTrash } from '../popup/components/trash'; @@ -248,10 +249,10 @@ function renderShell(app: HTMLElement): void {
- - - - + + + +
From 05b1fae9f4c63de7b7abf51162f9b01e4254e9c3 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:57:00 -0400 Subject: [PATCH 09/14] style(ext/popup): replace settings nav emoji with shared glyphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ▦ trash and ⌬ devices in the popup settings panel now match the fullscreen sidebar's glyph language. Lowercased labels match the brand. Plan 2026-04-30 fullscreen UX phase 1 task 6. Co-Authored-By: Claude Opus 4.7 --- extension/src/popup/components/settings.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/settings.ts b/extension/src/popup/components/settings.ts index d4cff97..0942d83 100644 --- a/extension/src/popup/components/settings.ts +++ b/extension/src/popup/components/settings.ts @@ -2,6 +2,7 @@ import { sendMessage, navigate, escapeHtml } from '../../shared/state'; import type { DeviceSettings } from '../../shared/types'; +import { GLYPH_TRASH, GLYPH_DEVICES } from '../../shared/glyphs'; export async function renderSettings(app: HTMLElement): Promise { app.innerHTML = '
'; @@ -55,8 +56,8 @@ export async function renderSettings(app: HTMLElement): Promise {
- - + +
From 71ad91592dabc0ff14175a7e2f9e69af666922c9 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 21:01:47 -0400 Subject: [PATCH 10/14] feat(ext/popup): hide popout-to-tab button in fullscreen forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ⤴ popout button is meaningless when the form is already in vault.html — gate it on !isInTab(). Affects all seven type forms plus the type-selection screen. Regression tests cover both popup (button present) and fullscreen (button absent) contexts via it.each across all 7 forms. Plan 2026-04-30 fullscreen UX phase 1 task 7. Co-Authored-By: Claude Opus 4.7 --- extension/src/popup/components/item-form.ts | 4 +- .../popout-button-fullscreen.test.ts | 46 +++++++++++++++++++ .../types/__tests__/popout-button.test.ts | 46 +++++++++++++++++++ extension/src/popup/components/types/card.ts | 4 +- .../src/popup/components/types/document.ts | 4 +- .../src/popup/components/types/identity.ts | 4 +- extension/src/popup/components/types/key.ts | 4 +- extension/src/popup/components/types/login.ts | 2 +- .../src/popup/components/types/secure-note.ts | 2 +- extension/src/popup/components/types/totp.ts | 4 +- 10 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 extension/src/popup/components/types/__tests__/popout-button-fullscreen.test.ts create mode 100644 extension/src/popup/components/types/__tests__/popout-button.test.ts diff --git a/extension/src/popup/components/item-form.ts b/extension/src/popup/components/item-form.ts index 6f513c2..5773e6e 100644 --- a/extension/src/popup/components/item-form.ts +++ b/extension/src/popup/components/item-form.ts @@ -1,7 +1,7 @@ /// Typed-item add/edit form dispatcher. Each type's renderForm lives in /// its own module under ./types/. Document stays "coming soon" until γ. -import { navigate, getState, setState, escapeHtml, popOutToTab } from '../../shared/state'; +import { navigate, getState, setState, escapeHtml, popOutToTab, isInTab } from '../../shared/state'; import type { Item, ItemType } from '../../shared/types'; const TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string }> = [ @@ -58,7 +58,7 @@ function renderTypeSelection(app: HTMLElement): void {

new item

- + ${isInTab() ? '' : ''}
${TYPE_OPTIONS.map((opt) => ` diff --git a/extension/src/popup/components/types/__tests__/popout-button-fullscreen.test.ts b/extension/src/popup/components/types/__tests__/popout-button-fullscreen.test.ts new file mode 100644 index 0000000..b9ccfb3 --- /dev/null +++ b/extension/src/popup/components/types/__tests__/popout-button-fullscreen.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('../../../../shared/state', () => ({ + sendMessage: vi.fn(), + getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }), + setState: vi.fn(), + navigate: vi.fn(), + escapeHtml: (s: string) => s, + popOutToTab: vi.fn(), + isInTab: () => true, // FULLSCREEN context + openVaultTab: vi.fn(), + registerHost: vi.fn(), +})); + +vi.mock('../../generator-panel', () => ({ + openGeneratorPanel: vi.fn(), + closeGeneratorPanel: vi.fn(), + isGeneratorPanelOpen: () => false, +})); + +import * as login from '../login'; +import * as secureNote from '../secure-note'; +import * as identity from '../identity'; +import * as card from '../card'; +import * as key from '../key'; +import * as totp from '../totp'; +import * as documentType from '../document'; + +const forms: Array<[string, (app: HTMLElement, mode: 'add' | 'edit', existing: null) => void]> = [ + ['login', login.renderForm], + ['secure-note', secureNote.renderForm], + ['identity', identity.renderForm], + ['card', card.renderForm], + ['key', key.renderForm], + ['totp', totp.renderForm], + ['document', documentType.renderForm], +]; + +describe('popout-to-tab button (fullscreen context)', () => { + beforeEach(() => { document.body.innerHTML = '
'; }); + + it.each(forms)('%s form does NOT render the popout button', (_name, render) => { + render(document.getElementById('app')!, 'add', null); + expect(document.getElementById('popout-btn')).toBeNull(); + }); +}); diff --git a/extension/src/popup/components/types/__tests__/popout-button.test.ts b/extension/src/popup/components/types/__tests__/popout-button.test.ts new file mode 100644 index 0000000..3232f02 --- /dev/null +++ b/extension/src/popup/components/types/__tests__/popout-button.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('../../../../shared/state', () => ({ + sendMessage: vi.fn(), + getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }), + setState: vi.fn(), + navigate: vi.fn(), + escapeHtml: (s: string) => s, + popOutToTab: vi.fn(), + isInTab: () => false, // POPUP context + openVaultTab: vi.fn(), + registerHost: vi.fn(), +})); + +vi.mock('../../generator-panel', () => ({ + openGeneratorPanel: vi.fn(), + closeGeneratorPanel: vi.fn(), + isGeneratorPanelOpen: () => false, +})); + +import * as login from '../login'; +import * as secureNote from '../secure-note'; +import * as identity from '../identity'; +import * as card from '../card'; +import * as key from '../key'; +import * as totp from '../totp'; +import * as documentType from '../document'; + +const forms: Array<[string, (app: HTMLElement, mode: 'add' | 'edit', existing: null) => void]> = [ + ['login', login.renderForm], + ['secure-note', secureNote.renderForm], + ['identity', identity.renderForm], + ['card', card.renderForm], + ['key', key.renderForm], + ['totp', totp.renderForm], + ['document', documentType.renderForm], +]; + +describe('popout-to-tab button (popup context)', () => { + beforeEach(() => { document.body.innerHTML = '
'; }); + + it.each(forms)('%s form renders the popout button', (_name, render) => { + render(document.getElementById('app')!, 'add', null); + expect(document.getElementById('popout-btn')).not.toBeNull(); + }); +}); diff --git a/extension/src/popup/components/types/card.ts b/extension/src/popup/components/types/card.ts index 481fa02..6c7725b 100644 --- a/extension/src/popup/components/types/card.ts +++ b/extension/src/popup/components/types/card.ts @@ -1,7 +1,7 @@ /// Card: number / holder / expiry MonthYear / cvv / pin / kind. /// Detail view has a styled card-silhouette signature block. -import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state'; +import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state'; import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types'; import { @@ -177,7 +177,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${mode === 'add' ? 'new card' : 'edit card'}
- + ${isInTab() ? '' : ''}
${state.error ? `
${escapeHtml(state.error)}
` : ''}
diff --git a/extension/src/popup/components/types/document.ts b/extension/src/popup/components/types/document.ts index 62260df..cccdcf8 100644 --- a/extension/src/popup/components/types/document.ts +++ b/extension/src/popup/components/types/document.ts @@ -2,7 +2,7 @@ /// notes/tags + optional supplementary attachments. /// Primary attachment is referenced by ID from the item's attachments array. -import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state'; +import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state'; import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types'; import { @@ -88,7 +88,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${isEdit ? 'edit document' : 'new document'}
- + ${isInTab() ? '' : ''}
${state.error ? `
${escapeHtml(state.error)}
` : ''}
diff --git a/extension/src/popup/components/types/identity.ts b/extension/src/popup/components/types/identity.ts index 1fb09d6..90fffc3 100644 --- a/extension/src/popup/components/types/identity.ts +++ b/extension/src/popup/components/types/identity.ts @@ -1,7 +1,7 @@ /// Identity: full_name, address (multiline), phone, email, date_of_birth. /// Detail view shows a "profile card" signature block + plain rows. -import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state'; +import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state'; import { REQUIRED_PILL_HTML } from '../../../shared/glyphs'; import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types'; import { @@ -137,7 +137,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${mode === 'add' ? 'new identity' : 'edit identity'}
- + ${isInTab() ? '' : ''}
${state.error ? `
${escapeHtml(state.error)}
` : ''}
diff --git a/extension/src/popup/components/types/key.ts b/extension/src/popup/components/types/key.ts index ec5ce03..e227870 100644 --- a/extension/src/popup/components/types/key.ts +++ b/extension/src/popup/components/types/key.ts @@ -2,7 +2,7 @@ /// Form's key_material textarea uses CSS text-security to mask characters /// since