feat(ext/popup): add pop-out to tab for forms
Forms can now be opened in a full browser tab via the ⤴ button, solving Chrome's popup closure on file picker interaction. Deep linking via URL params preserves view, item type, and item ID. Also removes the unused dropdown picker code from item-list.ts. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
/// 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, setState, escapeHtml } from '../popup';
|
import { navigate, getState, setState, escapeHtml, popOutToTab } 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 }> = [
|
const TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string }> = [
|
||||||
@@ -57,6 +57,8 @@ function renderTypeSelection(app: HTMLElement): void {
|
|||||||
<div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;">
|
<div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;">
|
||||||
<button class="btn" id="back-btn">← back</button>
|
<button class="btn" id="back-btn">← back</button>
|
||||||
<h3 style="margin:0;">new item</h3>
|
<h3 style="margin:0;">new item</h3>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-select-list">
|
<div class="type-select-list">
|
||||||
${TYPE_OPTIONS.map((opt) => `
|
${TYPE_OPTIONS.map((opt) => `
|
||||||
@@ -70,6 +72,7 @@ function renderTypeSelection(app: HTMLElement): void {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
|
document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
|
||||||
|
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||||||
|
|
||||||
document.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
|
document.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -223,102 +223,6 @@ function handleListKeydown(e: KeyboardEvent): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// New-item type picker popover
|
|
||||||
// ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
const NEW_TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string; disabled?: boolean; tooltip?: 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: 'totp', icon: '⏱', label: 'totp' },
|
|
||||||
{ type: 'document', icon: '📄', label: 'document' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function showNewTypePicker(anchor: HTMLElement): void {
|
|
||||||
document.querySelectorAll('.new-type-picker').forEach((el) => el.remove());
|
|
||||||
|
|
||||||
const picker = document.createElement('div');
|
|
||||||
picker.className = 'new-type-picker';
|
|
||||||
Object.assign(picker.style, {
|
|
||||||
position: 'absolute',
|
|
||||||
background: '#161b22',
|
|
||||||
border: '1px solid #30363d',
|
|
||||||
borderRadius: '6px',
|
|
||||||
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();
|
|
||||||
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) {
|
|
||||||
const row = document.createElement('div');
|
|
||||||
Object.assign(row.style, {
|
|
||||||
padding: '6px 10px',
|
|
||||||
cursor: opt.disabled ? 'not-allowed' : 'pointer',
|
|
||||||
color: opt.disabled ? '#484f58' : '#c9d1d9',
|
|
||||||
borderRadius: '4px',
|
|
||||||
display: 'flex', alignItems: 'center', gap: '8px',
|
|
||||||
});
|
|
||||||
if (opt.tooltip) row.title = opt.tooltip;
|
|
||||||
const iconSpan = document.createElement('span');
|
|
||||||
Object.assign(iconSpan.style, { fontSize: '14px', width: '16px', display: 'inline-block', textAlign: 'center' });
|
|
||||||
iconSpan.textContent = opt.icon;
|
|
||||||
const labelSpan = document.createElement('span');
|
|
||||||
labelSpan.textContent = opt.label;
|
|
||||||
row.appendChild(iconSpan);
|
|
||||||
row.appendChild(labelSpan);
|
|
||||||
if (!opt.disabled) {
|
|
||||||
row.addEventListener('mouseenter', () => { row.style.background = '#21262d'; });
|
|
||||||
row.addEventListener('mouseleave', () => { row.style.background = 'transparent'; });
|
|
||||||
row.addEventListener('click', (ev) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
picker.remove();
|
|
||||||
document.removeEventListener('click', closeOnOutside);
|
|
||||||
document.removeEventListener('keydown', closeOnEsc);
|
|
||||||
setState({ newType: opt.type });
|
|
||||||
navigate('add');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
picker.appendChild(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.appendChild(picker);
|
|
||||||
|
|
||||||
const closeOnOutside = (ev: MouseEvent) => {
|
|
||||||
if (!picker.contains(ev.target as Node)) {
|
|
||||||
picker.remove();
|
|
||||||
document.removeEventListener('click', closeOnOutside);
|
|
||||||
document.removeEventListener('keydown', closeOnEsc);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const closeOnEsc = (ev: KeyboardEvent) => {
|
|
||||||
if (ev.key === 'Escape') {
|
|
||||||
picker.remove();
|
|
||||||
document.removeEventListener('click', closeOnOutside);
|
|
||||||
document.removeEventListener('keydown', closeOnEsc);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener('click', closeOnOutside);
|
|
||||||
document.addEventListener('keydown', closeOnEsc);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// Settings picker popover (device vs vault)
|
// Settings picker popover (device vs vault)
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// Card: number / holder / expiry MonthYear / cvv / pin / kind.
|
/// Card: number / holder / expiry MonthYear / cvv / pin / kind.
|
||||||
/// Detail view has a styled card-silhouette signature block.
|
/// Detail view has a styled card-silhouette signature block.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
@@ -173,7 +173,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new card' : 'edit card'}</div>
|
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||||
|
<div class="detail-title">${mode === 'add' ? 'new card' : 'edit card'}</div>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||||
|
</div>
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
||||||
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="Amex Gold"></div>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="Amex Gold"></div>
|
||||||
@@ -235,6 +239,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
|
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveCard(mode, existing, sectionsDraft, attachmentsDraft);
|
await saveCard(mode, existing, sectionsDraft, attachmentsDraft);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/// notes/tags + optional supplementary attachments.
|
/// notes/tags + optional supplementary attachments.
|
||||||
/// Primary attachment is referenced by ID from the item's attachments array.
|
/// Primary attachment is referenced by ID from the item's attachments array.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderSectionsEditor, wireSectionsEditor,
|
renderSectionsEditor, wireSectionsEditor,
|
||||||
@@ -84,7 +84,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${isEdit ? 'edit document' : 'new document'}</div>
|
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||||
|
<div class="detail-title">${isEdit ? 'edit document' : 'new document'}</div>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||||
|
</div>
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="label" for="f-title">title <span class="req">*</span></label>
|
<label class="label" for="f-title">title <span class="req">*</span></label>
|
||||||
@@ -180,6 +184,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(isEdit ? 'detail' : 'list');
|
navigate(isEdit ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
|
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||||||
|
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveDocument(mode, existing, primaryId, attachmentsDraft, sectionsDraft);
|
await saveDocument(mode, existing, primaryId, attachmentsDraft, sectionsDraft);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// Identity: full_name, address (multiline), phone, email, date_of_birth.
|
/// Identity: full_name, address (multiline), phone, email, date_of_birth.
|
||||||
/// Detail view shows a "profile card" signature block + plain rows.
|
/// Detail view shows a "profile card" signature block + plain rows.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
@@ -133,7 +133,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new identity' : 'edit identity'}</div>
|
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||||
|
<div class="detail-title">${mode === 'add' ? 'new identity' : 'edit identity'}</div>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||||
|
</div>
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
||||||
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="Aaron Lee · personal"></div>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="Aaron Lee · personal"></div>
|
||||||
@@ -190,6 +194,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
|
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveIdentity(mode, existing, sectionsDraft, attachmentsDraft);
|
await saveIdentity(mode, existing, sectionsDraft, attachmentsDraft);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/// Form's key_material textarea uses CSS text-security to mask characters
|
/// Form's key_material textarea uses CSS text-security to mask characters
|
||||||
/// since <textarea type="password"> isn't a thing.
|
/// since <textarea type="password"> isn't a thing.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderRow, renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderRow, renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
@@ -122,7 +122,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new key' : 'edit key'}</div>
|
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||||
|
<div class="detail-title">${mode === 'add' ? 'new key' : 'edit key'}</div>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||||
|
</div>
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
||||||
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="github ssh"></div>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="github ssh"></div>
|
||||||
@@ -189,6 +193,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
|
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveKey(mode, existing, sectionsDraft, attachmentsDraft);
|
await saveKey(mode, existing, sectionsDraft, attachmentsDraft);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// Login type detail + form. Reference implementation for the shared
|
/// Login type detail + form. Reference implementation for the shared
|
||||||
/// field helpers introduced in Slice 2.
|
/// field helpers introduced in Slice 2.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../popup';
|
||||||
import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig, AttachmentRef } from '../../../shared/types';
|
||||||
import { DEFAULT_PASSWORD_REQUEST } from '../../../shared/types';
|
import { DEFAULT_PASSWORD_REQUEST } from '../../../shared/types';
|
||||||
import { base32Decode, base32Encode } from '../../../shared/base32';
|
import { base32Decode, base32Encode } from '../../../shared/base32';
|
||||||
@@ -243,7 +243,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new login' : 'edit login'}</div>
|
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||||
|
<div class="detail-title">${mode === 'add' ? 'new login' : 'edit login'}</div>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||||
|
</div>
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
||||||
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>
|
||||||
@@ -327,6 +331,8 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||||||
|
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveLogin(mode, existing, sectionsDraft, attachmentsDraft);
|
await saveLogin(mode, existing, sectionsDraft, attachmentsDraft);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// SecureNote: a single multiline body field. Concealed by default in the
|
/// SecureNote: a single multiline body field. Concealed by default in the
|
||||||
/// detail view; the form is just a big <textarea>.
|
/// detail view; the form is just a big <textarea>.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
@@ -111,7 +111,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new secure note' : 'edit secure note'}</div>
|
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||||
|
<div class="detail-title">${mode === 'add' ? 'new secure note' : 'edit secure note'}</div>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||||
|
</div>
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
||||||
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="My recovery codes"></div>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="My recovery codes"></div>
|
||||||
@@ -160,6 +164,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
|
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveSecureNote(mode, existing, sectionsDraft, attachmentsDraft);
|
await saveSecureNote(mode, existing, sectionsDraft, attachmentsDraft);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/// signature block with a thin SVG countdown ring; form has a kind toggle
|
/// signature block with a thin SVG countdown ring; form has a kind toggle
|
||||||
/// (TOTP vs Steam Guard) and a single secret input.
|
/// (TOTP vs Steam Guard) and a single secret input.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types';
|
||||||
import { base32Decode, base32Encode } from '../../../shared/base32';
|
import { base32Decode, base32Encode } from '../../../shared/base32';
|
||||||
import {
|
import {
|
||||||
@@ -212,7 +212,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
const renderInner = (): string => `
|
const renderInner = (): string => `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new totp' : 'edit totp'}</div>
|
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||||
|
<div class="detail-title">${mode === 'add' ? 'new totp' : 'edit totp'}</div>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||||
|
</div>
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
|
||||||
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>
|
||||||
@@ -328,6 +332,7 @@ function wireFormButtons(mode: 'add' | 'edit', existing: Item | null, sectionsDr
|
|||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
|
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveTotp(mode, existing, sectionsDraft, attachmentsDraft);
|
await saveTotp(mode, existing, sectionsDraft, attachmentsDraft);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,29 @@ export function escapeHtml(str: string): string {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Pop out to tab ---
|
||||||
|
|
||||||
|
export function popOutToTab(): void {
|
||||||
|
const state = getState();
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('view', state.view);
|
||||||
|
if (state.newType) params.set('type', state.newType);
|
||||||
|
if (state.selectedId) params.set('id', state.selectedId);
|
||||||
|
chrome.tabs.create({ url: `popup.html?${params.toString()}` });
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUrlParams(): { view?: View; type?: string; id?: string } | null {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const view = params.get('view');
|
||||||
|
if (!view) return null;
|
||||||
|
return {
|
||||||
|
view: view as View,
|
||||||
|
type: params.get('type') ?? undefined,
|
||||||
|
id: params.get('id') ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
|
|
||||||
export type View = 'locked' | 'list' | 'detail' | 'add' | 'edit' | 'settings' | 'settings-vault' | 'trash' | 'devices' | 'field-history';
|
export type View = 'locked' | 'list' | 'detail' | 'add' | 'edit' | 'settings' | 'settings-vault' | 'trash' | 'devices' | 'field-history';
|
||||||
@@ -214,6 +237,28 @@ async function init(): Promise<void> {
|
|||||||
currentState.vaultSettings = vs;
|
currentState.vaultSettings = vs;
|
||||||
currentState.generatorDefaults = vs.generator_defaults;
|
currentState.generatorDefaults = vs.generator_defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check URL params for deep linking (when opened in tab)
|
||||||
|
const urlParams = parseUrlParams();
|
||||||
|
if (urlParams) {
|
||||||
|
currentState.entries = listData.items;
|
||||||
|
if (urlParams.view === 'add' && urlParams.type) {
|
||||||
|
currentState.newType = urlParams.type as import('../shared/types').ItemType;
|
||||||
|
navigate('add');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((urlParams.view === 'edit' || urlParams.view === 'detail') && urlParams.id) {
|
||||||
|
// Fetch the item
|
||||||
|
const itemResp = await sendMessage({ type: 'get_item', id: urlParams.id });
|
||||||
|
if (itemResp.ok) {
|
||||||
|
currentState.selectedId = urlParams.id;
|
||||||
|
currentState.selectedItem = (itemResp.data as { item: Item }).item;
|
||||||
|
navigate(urlParams.view);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
navigate('list', { entries: listData.items });
|
navigate('list', { entries: listData.items });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user