fix(ext/popup): auto-popout for attachment types, keep login/note in popup

- Login and secure_note types stay in popup without attachment UI
- All other types (identity, card, key, totp, document) auto-redirect
  to full tab when selected
- Attachments only shown for login/secure_note when opened in tab

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-27 01:42:35 -04:00
parent c59e6892d8
commit 446949c5ce
5 changed files with 62 additions and 45 deletions

5
.claude/settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"superpowers@claude-plugins-official": true
}
}

View File

@@ -78,7 +78,11 @@ function renderTypeSelection(app: HTMLElement): void {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const type = btn.dataset.type as ItemType; const type = btn.dataset.type as ItemType;
setState({ newType: type }); setState({ newType: type });
renderItemForm(app, 'add'); if (type === 'login' || type === 'secure_note') {
renderItemForm(app, 'add');
} else {
popOutToTab();
}
}); });
}); });
} }

View File

@@ -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, popOutToTab } from '../../popup'; import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } 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';
@@ -267,7 +267,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
<div class="form-group"><label class="label" for="f-notes">notes</label> <div class="form-group"><label class="label" for="f-notes">notes</label>
<textarea id="f-notes" placeholder="recovery codes, security questions...">${escapeHtml(notes)}</textarea></div> <textarea id="f-notes" placeholder="recovery codes, security questions...">${escapeHtml(notes)}</textarea></div>
${renderSectionsEditor(sectionsDraft, sectionsExpanded)} ${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })} ${isInTab() ? renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' }) : ''}
<div class="form-actions"> <div class="form-actions">
<button class="btn" id="cancel-btn">cancel</button> <button class="btn" id="cancel-btn">cancel</button>
<button class="btn btn-primary" id="save-btn">${state.loading ? '<span class="spinner"></span>' : 'save'}</button> <button class="btn btn-primary" id="save-btn">${state.loading ? '<span class="spinner"></span>' : 'save'}</button>
@@ -284,26 +284,28 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
}; };
wireSectionsEditor(app, sectionsDraft, rerender); wireSectionsEditor(app, sectionsDraft, rerender);
const wireDisclosure = (): void => { if (isInTab()) {
wireAttachmentsDisclosure(app, { const wireDisclosure = (): void => {
itemId: existing?.id ?? '', wireAttachmentsDisclosure(app, {
attachments: attachmentsDraft, itemId: existing?.id ?? '',
mode: 'edit', attachments: attachmentsDraft,
onChange: (next) => { mode: 'edit',
attachmentsDraft = next; onChange: (next) => {
const disc = app.querySelector('.attachments-disclosure'); attachmentsDraft = next;
if (disc) { const disc = app.querySelector('.attachments-disclosure');
disc.outerHTML = renderAttachmentsDisclosure({ if (disc) {
itemId: existing?.id ?? '', disc.outerHTML = renderAttachmentsDisclosure({
attachments: attachmentsDraft, itemId: existing?.id ?? '',
mode: 'edit', attachments: attachmentsDraft,
}); mode: 'edit',
wireDisclosure(); });
} wireDisclosure();
}, }
}); },
}; });
wireDisclosure(); };
wireDisclosure();
}
document.getElementById('gen-btn')?.addEventListener('click', (e) => { document.getElementById('gen-btn')?.addEventListener('click', (e) => {
const trigger = e.currentTarget as HTMLElement; const trigger = e.currentTarget as HTMLElement;

View File

@@ -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, popOutToTab } from '../../popup'; import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } 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,
@@ -122,7 +122,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
<div class="form-group"><label class="label" for="f-body">body</label> <div class="form-group"><label class="label" for="f-body">body</label>
<textarea id="f-body" rows="10" placeholder="paste secrets here">${escapeHtml(body)}</textarea></div> <textarea id="f-body" rows="10" placeholder="paste secrets here">${escapeHtml(body)}</textarea></div>
${renderSectionsEditor(sectionsDraft, sectionsExpanded)} ${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })} ${isInTab() ? renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' }) : ''}
<div class="form-actions"> <div class="form-actions">
<button class="btn" id="cancel-btn">cancel</button> <button class="btn" id="cancel-btn">cancel</button>
<button class="btn btn-primary" id="save-btn">save</button> <button class="btn btn-primary" id="save-btn">save</button>
@@ -139,26 +139,28 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
}; };
wireSectionsEditor(app, sectionsDraft, rerender); wireSectionsEditor(app, sectionsDraft, rerender);
const wireDisclosure = (): void => { if (isInTab()) {
wireAttachmentsDisclosure(app, { const wireDisclosure = (): void => {
itemId: existing?.id ?? '', wireAttachmentsDisclosure(app, {
attachments: attachmentsDraft, itemId: existing?.id ?? '',
mode: 'edit', attachments: attachmentsDraft,
onChange: (next) => { mode: 'edit',
attachmentsDraft = next; onChange: (next) => {
const disc = app.querySelector('.attachments-disclosure'); attachmentsDraft = next;
if (disc) { const disc = app.querySelector('.attachments-disclosure');
disc.outerHTML = renderAttachmentsDisclosure({ if (disc) {
itemId: existing?.id ?? '', disc.outerHTML = renderAttachmentsDisclosure({
attachments: attachmentsDraft, itemId: existing?.id ?? '',
mode: 'edit', attachments: attachmentsDraft,
}); mode: 'edit',
wireDisclosure(); });
} wireDisclosure();
}, }
}); },
}; });
wireDisclosure(); };
wireDisclosure();
}
document.getElementById('cancel-btn')?.addEventListener('click', () => { document.getElementById('cancel-btn')?.addEventListener('click', () => {
setState({ error: null }); setState({ error: null });

View File

@@ -30,6 +30,10 @@ export function escapeHtml(str: string): string {
// --- Pop out to tab --- // --- Pop out to tab ---
export function isInTab(): boolean {
return window.location.search.length > 0;
}
export function popOutToTab(): void { export function popOutToTab(): void {
const state = getState(); const state = getState();
const params = new URLSearchParams(); const params = new URLSearchParams();