From 39db697ce58a4c8f4f5a17ecc6f030903710001a Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Mon, 27 Apr 2026 01:06:32 -0400 Subject: [PATCH] fix(ext/popup): replace item type dropdown with selection view Clicking "+ new" now navigates to a type selection view instead of showing a dropdown that gets clipped by popup bounds. The selection view displays all item types as buttons in a scrollable list. Co-Authored-By: Claude --- extension/src/popup/components/item-form.ts | 50 ++++++++++++++++++++- extension/src/popup/components/item-list.ts | 16 +++++-- extension/src/popup/styles.css | 33 ++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/extension/src/popup/components/item-form.ts b/extension/src/popup/components/item-form.ts index 2176a9b..c9d8485 100644 --- a/extension/src/popup/components/item-form.ts +++ b/extension/src/popup/components/item-form.ts @@ -1,8 +1,18 @@ /// 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 } from '../popup'; +import { navigate, getState, setState, escapeHtml } from '../popup'; import type { Item, ItemType } from '../../shared/types'; + +const TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string }> = [ + { type: 'login', icon: '🔑', label: 'login' }, + { type: 'secure_note', icon: '📝', label: 'secure note' }, + { type: 'identity', icon: '👤', label: 'identity' }, + { type: 'card', icon: '💳', label: 'card' }, + { type: 'key', icon: '🔐', label: 'key' }, + { type: 'document', icon: '📄', label: 'document' }, + { type: 'totp', icon: '⏱️', label: 'totp' }, +]; import * as login from './types/login'; import * as secureNote from './types/secure-note'; import * as identity from './types/identity'; @@ -12,7 +22,7 @@ import * as totp from './types/totp'; import * as documentType from './types/document'; export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void { - login.teardown(); // detail-view's ticker/listener don't leak into form + login.teardown(); secureNote.teardown(); identity.teardown(); card.teardown(); @@ -21,6 +31,13 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void { documentType.teardown(); const state = getState(); const existing = mode === 'edit' ? state.selectedItem : null; + + // Show type selection for new items when no type is pre-selected + if (mode === 'add' && !state.newType) { + renderTypeSelection(app); + return; + } + const type: ItemType = existing?.type ?? state.newType ?? 'login'; switch (type) { @@ -34,6 +51,35 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void { } } +function renderTypeSelection(app: HTMLElement): void { + app.innerHTML = ` +
+
+ +

new item

+
+
+ ${TYPE_OPTIONS.map((opt) => ` + + `).join('')} +
+
+ `; + + document.getElementById('back-btn')?.addEventListener('click', () => navigate('list')); + + document.querySelectorAll('[data-type]').forEach((btn) => { + btn.addEventListener('click', () => { + const type = btn.dataset.type as ItemType; + setState({ newType: type }); + renderItemForm(app, 'add'); + }); + }); +} + function renderComingSoon(app: HTMLElement, type: ItemType): void { app.innerHTML = `
diff --git a/extension/src/popup/components/item-list.ts b/extension/src/popup/components/item-list.ts index 18b21cf..0d7cfb6 100644 --- a/extension/src/popup/components/item-list.ts +++ b/extension/src/popup/components/item-list.ts @@ -67,9 +67,9 @@ export function renderItemList(app: HTMLElement): void { setState({ searchQuery: searchInput.value, selectedIndex: 0 }); }); - document.getElementById('new-btn')?.addEventListener('click', (e) => { - e.stopPropagation(); - showNewTypePicker(e.currentTarget as HTMLElement); + document.getElementById('new-btn')?.addEventListener('click', () => { + setState({ newType: null }); + navigate('add'); }); document.getElementById('sync-btn')?.addEventListener('click', async () => { @@ -249,12 +249,20 @@ function showNewTypePicker(anchor: HTMLElement): void { boxShadow: '0 4px 12px rgba(0,0,0,0.4)', padding: '4px', minWidth: '160px', + maxHeight: '280px', + overflowY: 'auto', zIndex: '999999', fontSize: '12px', }); const rect = anchor.getBoundingClientRect(); - picker.style.top = `${rect.bottom + 4}px`; + const dropdownHeight = 220; // approx height for 7 items + const spaceBelow = window.innerHeight - rect.bottom; + if (spaceBelow < dropdownHeight && rect.top > dropdownHeight) { + picker.style.bottom = `${window.innerHeight - rect.top + 4}px`; + } else { + picker.style.top = `${rect.bottom + 4}px`; + } picker.style.left = `${rect.left}px`; for (const opt of NEW_TYPE_OPTIONS) { diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 896c583..d498908 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1211,3 +1211,36 @@ textarea { .history-entry__copy:hover { opacity: 0.8; } + +/* --- Type selection --- */ + +.type-select-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.type-select-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + background: #161b22; + border: 1px solid transparent; + border-radius: 6px; + color: #c9d1d9; + font-size: 13px; + cursor: pointer; + text-align: left; +} + +.type-select-row:hover { + background: #21262d; + border-color: #30363d; +} + +.type-select-icon { + font-size: 16px; + width: 20px; + text-align: center; +}