105 lines
4.7 KiB
TypeScript
105 lines
4.7 KiB
TypeScript
/// 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, isInTab } from '../../shared/state';
|
||
import type { Item, ItemType } from '../../shared/types';
|
||
import {
|
||
GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
|
||
GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
|
||
} from '../../shared/glyphs';
|
||
|
||
const TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string; description: string }> = [
|
||
{ type: 'login', icon: GLYPH_TYPE_LOGIN, label: 'Login', description: 'Username + password' },
|
||
{ type: 'secure_note', icon: GLYPH_TYPE_SECURE_NOTE, label: 'Secure Note', description: 'Encrypted text note' },
|
||
{ type: 'identity', icon: GLYPH_TYPE_IDENTITY, label: 'Identity', description: 'Personal details' },
|
||
{ type: 'card', icon: GLYPH_TYPE_CARD, label: 'Card', description: 'Credit / debit card' },
|
||
{ type: 'key', icon: GLYPH_TYPE_KEY, label: 'SSH / API Key', description: 'Keys and tokens' },
|
||
{ type: 'document', icon: GLYPH_TYPE_DOCUMENT, label: 'Document', description: 'File attachment' },
|
||
{ type: 'totp', icon: GLYPH_TYPE_TOTP, label: 'TOTP', description: '2FA authenticator' },
|
||
];
|
||
import * as login from './types/login';
|
||
import * as secureNote from './types/secure-note';
|
||
import * as identity from './types/identity';
|
||
import * as card from './types/card';
|
||
import * as key from './types/key';
|
||
import * as totp from './types/totp';
|
||
import * as documentType from './types/document';
|
||
|
||
export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void {
|
||
login.teardown();
|
||
secureNote.teardown();
|
||
identity.teardown();
|
||
card.teardown();
|
||
key.teardown();
|
||
totp.teardown();
|
||
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) {
|
||
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);
|
||
case 'key': return key.renderForm(app, mode, existing);
|
||
case 'totp': return totp.renderForm(app, mode, existing);
|
||
case 'document': return documentType.renderForm(app, mode, existing);
|
||
}
|
||
}
|
||
|
||
function renderTypeSelection(app: HTMLElement): void {
|
||
app.innerHTML = `
|
||
<div class="pad">
|
||
<div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
|
||
<button class="btn" id="back-btn">◂ back</button>
|
||
<span style="font-size:14px; font-weight:600;">New item</span>
|
||
<span style="flex:1;"></span>
|
||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⧉</button>'}
|
||
</div>
|
||
<div class="type-card-grid">
|
||
${TYPE_OPTIONS.map((opt) => `
|
||
<button class="type-card" data-type="${opt.type}">
|
||
<span class="type-card__icon" aria-hidden="true">${opt.icon}</span>
|
||
<span class="type-card__label">${escapeHtml(opt.label)}</span>
|
||
<span class="type-card__desc">${escapeHtml(opt.description)}</span>
|
||
</button>
|
||
`).join('')}
|
||
</div>
|
||
<div class="keyhints"><span><kbd>Esc</kbd> back</span></div>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
|
||
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') navigate('list');
|
||
}, { once: true });
|
||
|
||
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 {
|
||
app.innerHTML = `
|
||
<div class="pad">
|
||
<div class="detail-title" style="margin-bottom:16px;">${type.replace('_', ' ')}</div>
|
||
<p class="muted">Editing <strong>${type}</strong> items is not available yet.</p>
|
||
<div class="form-actions"><button class="btn" id="back-btn">back</button></div>
|
||
</div>
|
||
`;
|
||
document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
|
||
}
|