diff --git a/extension/src/popup/components/item-list.ts b/extension/src/popup/components/item-list.ts index 9b7c885..75820fd 100644 --- a/extension/src/popup/components/item-list.ts +++ b/extension/src/popup/components/item-list.ts @@ -32,7 +32,7 @@ export function renderItemList(app: HTMLElement): void { const rowsHtml = filtered.length > 0 ? filtered.map(([id, e], i) => `
- ${escapeHtml(e.title)} + ${escapeHtml(e.title)}${e.attachment_summaries.length > 0 ? ' 📎' : ''} ${escapeHtml(metaLine(e))}
`).join('') diff --git a/extension/src/popup/components/types/card.ts b/extension/src/popup/components/types/card.ts index bb4fc00..68438ed 100644 --- a/extension/src/popup/components/types/card.ts +++ b/extension/src/popup/components/types/card.ts @@ -2,11 +2,16 @@ /// Detail view has a styled card-silhouette signature block. import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup'; -import type { Item, ItemId, ManifestEntry, CardKind, Section } from '../../../shared/types'; +import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types'; import { renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections, renderSectionsEditor, wireSectionsEditor, } from '../fields'; +import { + renderAttachmentsDisclosure, + wireAttachmentsDisclosure, + teardownAttachmentsDisclosure, +} from '../attachments-disclosure'; const CARD_KINDS: CardKind[] = ['credit', 'debit', 'gift', 'loyalty', 'other']; @@ -15,6 +20,7 @@ let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null; let sectionsExpanded = false; export function teardown(): void { + teardownAttachmentsDisclosure(); if (activeKeyHandler) { document.removeEventListener('keydown', activeKeyHandler); activeKeyHandler = null; @@ -83,6 +89,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise ${c.cvv ? renderConcealedRow({ id: 'card-cvv', label: 'cvv', value: c.cvv, monospace: true }) : ''} ${c.pin ? renderConcealedRow({ id: 'card-pin', label: 'pin', value: c.pin, monospace: true }) : ''} ${renderSections(item, 'card')} + ${renderAttachmentsDisclosure({ itemId: item.id, attachments: item.attachments, mode: 'view' })}
@@ -94,6 +101,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise // The card-number reveal lives inside the signature block, so wireFieldHandlers // picks it up alongside the cvv/pin rows. wireFieldHandlers(app); + wireAttachmentsDisclosure(app, { itemId: item.id, attachments: item.attachments, mode: 'view' }); document.getElementById('back-btn')?.addEventListener('click', () => { teardown(); navigate('list'); }); document.getElementById('edit-btn')?.addEventListener('click', () => { teardown(); navigate('edit'); }); @@ -146,6 +154,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite const sectionsDraft: Section[] = existing ? JSON.parse(JSON.stringify(existing.sections)) as Section[] : []; + let attachmentsDraft: AttachmentRef[] = existing?.attachments ?? []; const monthOptions = Array.from({ length: 12 }, (_, i) => { const m = String(i + 1).padStart(2, '0'); @@ -184,6 +193,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${renderSectionsEditor(sectionsDraft, sectionsExpanded)} + ${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })}
@@ -200,12 +210,33 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite }; wireSectionsEditor(app, sectionsDraft, rerender); + const wireDisclosure = (): void => { + wireAttachmentsDisclosure(app, { + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + onChange: (next) => { + attachmentsDraft = next; + const disc = app.querySelector('.attachments-disclosure'); + if (disc) { + disc.outerHTML = renderAttachmentsDisclosure({ + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + }); + wireDisclosure(); + } + }, + }); + }; + wireDisclosure(); + document.getElementById('cancel-btn')?.addEventListener('click', () => { setState({ error: null }); navigate(mode === 'edit' ? 'detail' : 'list'); }); document.getElementById('save-btn')?.addEventListener('click', async () => { - await saveCard(mode, existing, sectionsDraft); + await saveCard(mode, existing, sectionsDraft, attachmentsDraft); }); const escHandler = (e: KeyboardEvent) => { @@ -220,7 +251,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite (document.getElementById('f-title') as HTMLInputElement | null)?.focus(); } -async function saveCard(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise { +async function saveCard(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise { const state = getState(); const title = (document.getElementById('f-title') as HTMLInputElement).value.trim(); if (!title) { setState({ error: 'Title is required' }); return; } @@ -260,7 +291,7 @@ async function saveCard(mode: 'add' | 'edit', existing: Item | null, sectionsDra modified: now, trashed_at: undefined, core, sections: sectionsDraft, - attachments: existing?.attachments ?? [], + attachments: attachmentsDraft, field_history: existing?.field_history ?? {}, }; diff --git a/extension/src/popup/components/types/identity.ts b/extension/src/popup/components/types/identity.ts index 3df175b..b3f0138 100644 --- a/extension/src/popup/components/types/identity.ts +++ b/extension/src/popup/components/types/identity.ts @@ -2,17 +2,23 @@ /// Detail view shows a "profile card" signature block + plain rows. import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup'; -import type { Item, ItemId, ManifestEntry, Section } from '../../../shared/types'; +import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types'; import { renderRow, renderSignatureBlock, wireFieldHandlers, renderSections, renderSectionsEditor, wireSectionsEditor, } from '../fields'; +import { + renderAttachmentsDisclosure, + wireAttachmentsDisclosure, + teardownAttachmentsDisclosure, +} from '../attachments-disclosure'; let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null; let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null; let sectionsExpanded = false; export function teardown(): void { + teardownAttachmentsDisclosure(); if (activeKeyHandler) { document.removeEventListener('keydown', activeKeyHandler); activeKeyHandler = null; @@ -61,6 +67,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise ${c.address ? renderRow({ label: 'address', value: c.address, multiline: true }) : ''} ${c.date_of_birth ? renderRow({ label: 'born', value: formatDate(c.date_of_birth) }) : ''} ${renderSections(item, 'identity')} + ${renderAttachmentsDisclosure({ itemId: item.id, attachments: item.attachments, mode: 'view' })}
@@ -70,6 +77,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise `; wireFieldHandlers(app); + wireAttachmentsDisclosure(app, { itemId: item.id, attachments: item.attachments, mode: 'view' }); document.getElementById('back-btn')?.addEventListener('click', () => { teardown(); navigate('list'); }); document.getElementById('edit-btn')?.addEventListener('click', () => { teardown(); navigate('edit'); }); @@ -121,6 +129,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite const sectionsDraft: Section[] = existing ? JSON.parse(JSON.stringify(existing.sections)) as Section[] : []; + let attachmentsDraft: AttachmentRef[] = existing?.attachments ?? []; app.innerHTML = `
@@ -139,6 +148,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${renderSectionsEditor(sectionsDraft, sectionsExpanded)} + ${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })}
@@ -155,12 +165,33 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite }; wireSectionsEditor(app, sectionsDraft, rerender); + const wireDisclosure = (): void => { + wireAttachmentsDisclosure(app, { + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + onChange: (next) => { + attachmentsDraft = next; + const disc = app.querySelector('.attachments-disclosure'); + if (disc) { + disc.outerHTML = renderAttachmentsDisclosure({ + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + }); + wireDisclosure(); + } + }, + }); + }; + wireDisclosure(); + document.getElementById('cancel-btn')?.addEventListener('click', () => { setState({ error: null }); navigate(mode === 'edit' ? 'detail' : 'list'); }); document.getElementById('save-btn')?.addEventListener('click', async () => { - await saveIdentity(mode, existing, sectionsDraft); + await saveIdentity(mode, existing, sectionsDraft, attachmentsDraft); }); const escHandler = (e: KeyboardEvent) => { @@ -175,7 +206,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite (document.getElementById('f-title') as HTMLInputElement | null)?.focus(); } -async function saveIdentity(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise { +async function saveIdentity(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise { const state = getState(); const title = (document.getElementById('f-title') as HTMLInputElement).value.trim(); if (!title) { setState({ error: 'Title is required' }); return; } @@ -202,7 +233,7 @@ async function saveIdentity(mode: 'add' | 'edit', existing: Item | null, section modified: now, trashed_at: undefined, core, sections: sectionsDraft, - attachments: existing?.attachments ?? [], + attachments: attachmentsDraft, field_history: existing?.field_history ?? {}, }; diff --git a/extension/src/popup/components/types/key.ts b/extension/src/popup/components/types/key.ts index 7a9b0d6..a6d7f59 100644 --- a/extension/src/popup/components/types/key.ts +++ b/extension/src/popup/components/types/key.ts @@ -3,17 +3,23 @@ /// since
${renderSectionsEditor(sectionsDraft, sectionsExpanded)} + ${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })}
@@ -145,6 +155,27 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite }; wireSectionsEditor(app, sectionsDraft, rerender); + const wireDisclosure = (): void => { + wireAttachmentsDisclosure(app, { + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + onChange: (next) => { + attachmentsDraft = next; + const disc = app.querySelector('.attachments-disclosure'); + if (disc) { + disc.outerHTML = renderAttachmentsDisclosure({ + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + }); + wireDisclosure(); + } + }, + }); + }; + wireDisclosure(); + // Show/hide toggle for the key_material textarea. let revealed = false; document.getElementById('key-show-btn')?.addEventListener('click', () => { @@ -159,7 +190,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite navigate(mode === 'edit' ? 'detail' : 'list'); }); document.getElementById('save-btn')?.addEventListener('click', async () => { - await saveKey(mode, existing, sectionsDraft); + await saveKey(mode, existing, sectionsDraft, attachmentsDraft); }); const escHandler = (e: KeyboardEvent) => { @@ -174,7 +205,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite (document.getElementById('f-title') as HTMLInputElement | null)?.focus(); } -async function saveKey(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise { +async function saveKey(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise { const state = getState(); const title = (document.getElementById('f-title') as HTMLInputElement).value.trim(); if (!title) { setState({ error: 'Title is required' }); return; } @@ -203,7 +234,7 @@ async function saveKey(mode: 'add' | 'edit', existing: Item | null, sectionsDraf modified: now, trashed_at: undefined, core, sections: sectionsDraft, - attachments: existing?.attachments ?? [], + attachments: attachmentsDraft, field_history: existing?.field_history ?? {}, }; diff --git a/extension/src/popup/components/types/login.ts b/extension/src/popup/components/types/login.ts index ad617b5..5193fa0 100644 --- a/extension/src/popup/components/types/login.ts +++ b/extension/src/popup/components/types/login.ts @@ -2,7 +2,7 @@ /// field helpers introduced in Slice 2. import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup'; -import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig } from '../../../shared/types'; +import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig, AttachmentRef } from '../../../shared/types'; import { DEFAULT_PASSWORD_REQUEST } from '../../../shared/types'; import { base32Decode, base32Encode } from '../../../shared/base32'; import { @@ -15,10 +15,16 @@ import { wireSectionsEditor, } from '../fields'; import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from '../generator-panel'; +import { + renderAttachmentsDisclosure, + wireAttachmentsDisclosure, + teardownAttachmentsDisclosure, +} from '../attachments-disclosure'; /// Called by the dispatcher before each render. Stops any in-flight /// tickers / intervals / listeners the previous view may have attached. export function teardown(): void { + teardownAttachmentsDisclosure(); stopTotpTicker(); if (activeKeyHandler) { document.removeEventListener('keydown', activeKeyHandler); @@ -68,6 +74,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise ` : ''} ${item.notes ? renderRow({ label: 'notes', value: item.notes, multiline: true }) : ''} ${renderSections(item, 'login')} + ${renderAttachmentsDisclosure({ itemId: item.id, attachments: item.attachments, mode: 'view' })}
@@ -78,6 +85,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise `; wireFieldHandlers(app); + wireAttachmentsDisclosure(app, { itemId: item.id, attachments: item.attachments, mode: 'view' }); document.getElementById('back-btn')?.addEventListener('click', () => { teardown(); @@ -226,6 +234,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite const sectionsDraft: Section[] = existing ? JSON.parse(JSON.stringify(existing.sections)) as Section[] : []; + let attachmentsDraft: AttachmentRef[] = existing?.attachments ?? []; app.innerHTML = `
@@ -249,6 +258,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${renderSectionsEditor(sectionsDraft, sectionsExpanded)} + ${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })}
@@ -265,6 +275,27 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite }; wireSectionsEditor(app, sectionsDraft, rerender); + const wireDisclosure = (): void => { + wireAttachmentsDisclosure(app, { + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + onChange: (next) => { + attachmentsDraft = next; + const disc = app.querySelector('.attachments-disclosure'); + if (disc) { + disc.outerHTML = renderAttachmentsDisclosure({ + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + }); + wireDisclosure(); + } + }, + }); + }; + wireDisclosure(); + document.getElementById('gen-btn')?.addEventListener('click', (e) => { const trigger = e.currentTarget as HTMLElement; if (isGeneratorPanelOpen()) { @@ -292,7 +323,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite }); document.getElementById('save-btn')?.addEventListener('click', async () => { - await saveLogin(mode, existing, sectionsDraft); + await saveLogin(mode, existing, sectionsDraft, attachmentsDraft); }); const escHandler = (e: KeyboardEvent) => { @@ -320,7 +351,7 @@ function normalizeUrl(raw: string): { ok: true; value: string } | { ok: false; e } } -async function saveLogin(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise { +async function saveLogin(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise { const state = getState(); const title = (document.getElementById('f-title') as HTMLInputElement).value.trim(); const rawUrl = (document.getElementById('f-url') as HTMLInputElement).value; @@ -371,7 +402,7 @@ async function saveLogin(mode: 'add' | 'edit', existing: Item | null, sectionsDr trashed_at: undefined, core, sections: sectionsDraft, - attachments: existing?.attachments ?? [], + attachments: attachmentsDraft, field_history: existing?.field_history ?? {}, }; diff --git a/extension/src/popup/components/types/secure-note.ts b/extension/src/popup/components/types/secure-note.ts index 190e623..94cc663 100644 --- a/extension/src/popup/components/types/secure-note.ts +++ b/extension/src/popup/components/types/secure-note.ts @@ -2,17 +2,23 @@ /// detail view; the form is just a big
${renderSectionsEditor(sectionsDraft, sectionsExpanded)} + ${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })}
@@ -125,12 +135,33 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite }; wireSectionsEditor(app, sectionsDraft, rerender); + const wireDisclosure = (): void => { + wireAttachmentsDisclosure(app, { + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + onChange: (next) => { + attachmentsDraft = next; + const disc = app.querySelector('.attachments-disclosure'); + if (disc) { + disc.outerHTML = renderAttachmentsDisclosure({ + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + }); + wireDisclosure(); + } + }, + }); + }; + wireDisclosure(); + document.getElementById('cancel-btn')?.addEventListener('click', () => { setState({ error: null }); navigate(mode === 'edit' ? 'detail' : 'list'); }); document.getElementById('save-btn')?.addEventListener('click', async () => { - await saveSecureNote(mode, existing, sectionsDraft); + await saveSecureNote(mode, existing, sectionsDraft, attachmentsDraft); }); const escHandler = (e: KeyboardEvent) => { @@ -145,7 +176,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite (document.getElementById('f-title') as HTMLInputElement | null)?.focus(); } -async function saveSecureNote(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise { +async function saveSecureNote(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise { const state = getState(); const title = (document.getElementById('f-title') as HTMLInputElement).value.trim(); const body = (document.getElementById('f-body') as HTMLTextAreaElement).value; @@ -164,7 +195,7 @@ async function saveSecureNote(mode: 'add' | 'edit', existing: Item | null, secti trashed_at: undefined, core: { type: 'secure_note', body }, sections: sectionsDraft, - attachments: existing?.attachments ?? [], + attachments: attachmentsDraft, field_history: existing?.field_history ?? {}, }; diff --git a/extension/src/popup/components/types/totp.ts b/extension/src/popup/components/types/totp.ts index c7ed87a..384174b 100644 --- a/extension/src/popup/components/types/totp.ts +++ b/extension/src/popup/components/types/totp.ts @@ -3,12 +3,17 @@ /// (TOTP vs Steam Guard) and a single secret input. import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup'; -import type { Item, ItemId, ManifestEntry, Section, TotpKind } from '../../../shared/types'; +import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types'; import { base32Decode, base32Encode } from '../../../shared/base32'; import { renderRow, renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections, renderSectionsEditor, wireSectionsEditor, } from '../fields'; +import { + renderAttachmentsDisclosure, + wireAttachmentsDisclosure, + teardownAttachmentsDisclosure, +} from '../attachments-disclosure'; // ---------------------------------------------------------------------- // Module-scope lifecycle state @@ -26,6 +31,7 @@ function stopTotpTicker(): void { /// Called by the dispatcher before each render. Stops the countdown ticker /// AND removes the detail-view's keyboard handler so they don't leak. export function teardown(): void { + teardownAttachmentsDisclosure(); stopTotpTicker(); if (activeKeyHandler) { document.removeEventListener('keydown', activeKeyHandler); @@ -87,6 +93,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise ${renderRow({ label: 'kind', value: isSteam ? 'Steam Guard' : 'TOTP' })} ${renderConcealedRow({ id: 'totp-secret', label: 'secret', value: secretB32, monospace: true })} ${renderSections(item, 'totp')} + ${renderAttachmentsDisclosure({ itemId: item.id, attachments: item.attachments, mode: 'view' })}
@@ -96,6 +103,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise `; wireFieldHandlers(app); + wireAttachmentsDisclosure(app, { itemId: item.id, attachments: item.attachments, mode: 'view' }); // Start the ticker — re-fetches code + countdown every second from the SW. startTotpTicker(item.id, c.config.period_seconds || 30); @@ -200,6 +208,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite const sectionsDraft: Section[] = existing ? JSON.parse(JSON.stringify(existing.sections)) as Section[] : []; + let attachmentsDraft: AttachmentRef[] = existing?.attachments ?? []; const renderInner = (): string => `
@@ -221,6 +230,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${renderSectionsEditor(sectionsDraft, sectionsExpanded)} + ${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })}
@@ -248,8 +258,9 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite (document.getElementById('f-issuer') as HTMLInputElement).value = issuerVal; (document.getElementById('f-label') as HTMLInputElement).value = labelVal; wireKindToggle(); - wireFormButtons(mode, existing, sectionsDraft); + wireFormButtons(mode, existing, sectionsDraft, attachmentsDraft); wireSectionsEditor(app, sectionsDraft, sectionsRerender); + wireDisclosure(); }; // Rerender only the sections editor in place (used by structural section @@ -264,6 +275,26 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite wireSectionsEditor(app, sectionsDraft, sectionsRerender); }; + const wireDisclosure = (): void => { + wireAttachmentsDisclosure(app, { + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + onChange: (next) => { + attachmentsDraft = next; + const disc = app.querySelector('.attachments-disclosure'); + if (disc) { + disc.outerHTML = renderAttachmentsDisclosure({ + itemId: existing?.id ?? '', + attachments: attachmentsDraft, + mode: 'edit', + }); + wireDisclosure(); + } + }, + }); + }; + const wireKindToggle = (): void => { document.getElementById('kind-totp')?.addEventListener('click', () => { formKind = 'totp'; @@ -276,8 +307,9 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite }; wireKindToggle(); - wireFormButtons(mode, existing, sectionsDraft); + wireFormButtons(mode, existing, sectionsDraft, attachmentsDraft); wireSectionsEditor(app, sectionsDraft, sectionsRerender); + wireDisclosure(); const escHandler = (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -291,17 +323,17 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite (document.getElementById('f-title') as HTMLInputElement | null)?.focus(); } -function wireFormButtons(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): void { +function wireFormButtons(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): void { document.getElementById('cancel-btn')?.addEventListener('click', () => { setState({ error: null }); navigate(mode === 'edit' ? 'detail' : 'list'); }); document.getElementById('save-btn')?.addEventListener('click', async () => { - await saveTotp(mode, existing, sectionsDraft); + await saveTotp(mode, existing, sectionsDraft, attachmentsDraft); }); } -async function saveTotp(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise { +async function saveTotp(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise { const state = getState(); const title = (document.getElementById('f-title') as HTMLInputElement).value.trim(); if (!title) { setState({ error: 'Title is required' }); return; } @@ -345,7 +377,7 @@ async function saveTotp(mode: 'add' | 'edit', existing: Item | null, sectionsDra modified: now, trashed_at: undefined, core, sections: sectionsDraft, - attachments: existing?.attachments ?? [], + attachments: attachmentsDraft, field_history: existing?.field_history ?? {}, }; diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index aeb4107..54f69ad 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -235,6 +235,12 @@ textarea { margin-top: 2px; } +.entry-row__attach-indicator { + font-size: 9px; + opacity: 0.6; + margin-right: 4px; +} + /* Detail view */ .detail-header { display: flex;