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 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-27 01:06:32 -04:00
parent eb14946f06
commit 39db697ce5
3 changed files with 93 additions and 6 deletions

View File

@@ -1,8 +1,18 @@
/// Typed-item add/edit form dispatcher. Each type's renderForm lives in /// Typed-item add/edit form dispatcher. Each type's renderForm lives in
/// its own module under ./types/. Document stays "coming soon" until γ. /// 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'; 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 login from './types/login';
import * as secureNote from './types/secure-note'; import * as secureNote from './types/secure-note';
import * as identity from './types/identity'; import * as identity from './types/identity';
@@ -12,7 +22,7 @@ import * as totp from './types/totp';
import * as documentType from './types/document'; import * as documentType from './types/document';
export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void { 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(); secureNote.teardown();
identity.teardown(); identity.teardown();
card.teardown(); card.teardown();
@@ -21,6 +31,13 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void {
documentType.teardown(); documentType.teardown();
const state = getState(); const state = getState();
const existing = mode === 'edit' ? state.selectedItem : null; 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'; const type: ItemType = existing?.type ?? state.newType ?? 'login';
switch (type) { switch (type) {
@@ -34,6 +51,35 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void {
} }
} }
function renderTypeSelection(app: HTMLElement): void {
app.innerHTML = `
<div class="pad">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;">
<button class="btn" id="back-btn">← back</button>
<h3 style="margin:0;">new item</h3>
</div>
<div class="type-select-list">
${TYPE_OPTIONS.map((opt) => `
<button class="type-select-row" data-type="${opt.type}">
<span class="type-select-icon">${opt.icon}</span>
<span>${escapeHtml(opt.label)}</span>
</button>
`).join('')}
</div>
</div>
`;
document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
document.querySelectorAll<HTMLButtonElement>('[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 { function renderComingSoon(app: HTMLElement, type: ItemType): void {
app.innerHTML = ` app.innerHTML = `
<div class="pad"> <div class="pad">

View File

@@ -67,9 +67,9 @@ export function renderItemList(app: HTMLElement): void {
setState({ searchQuery: searchInput.value, selectedIndex: 0 }); setState({ searchQuery: searchInput.value, selectedIndex: 0 });
}); });
document.getElementById('new-btn')?.addEventListener('click', (e) => { document.getElementById('new-btn')?.addEventListener('click', () => {
e.stopPropagation(); setState({ newType: null });
showNewTypePicker(e.currentTarget as HTMLElement); navigate('add');
}); });
document.getElementById('sync-btn')?.addEventListener('click', async () => { 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)', boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
padding: '4px', padding: '4px',
minWidth: '160px', minWidth: '160px',
maxHeight: '280px',
overflowY: 'auto',
zIndex: '999999', zIndex: '999999',
fontSize: '12px', fontSize: '12px',
}); });
const rect = anchor.getBoundingClientRect(); const rect = anchor.getBoundingClientRect();
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.top = `${rect.bottom + 4}px`;
}
picker.style.left = `${rect.left}px`; picker.style.left = `${rect.left}px`;
for (const opt of NEW_TYPE_OPTIONS) { for (const opt of NEW_TYPE_OPTIONS) {

View File

@@ -1211,3 +1211,36 @@ textarea {
.history-entry__copy:hover { .history-entry__copy:hover {
opacity: 0.8; 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;
}