-
${mode === 'add' ? 'new secure note' : 'edit secure note'}
+
+
${mode === 'add' ? 'new secure note' : 'edit secure note'}
+
+
+
${state.error ? `
${escapeHtml(state.error)}
` : ''}
@@ -160,6 +164,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
setState({ error: null });
navigate(mode === 'edit' ? 'detail' : 'list');
});
+ document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
document.getElementById('save-btn')?.addEventListener('click', async () => {
await saveSecureNote(mode, existing, sectionsDraft, attachmentsDraft);
});
diff --git a/extension/src/popup/components/types/totp.ts b/extension/src/popup/components/types/totp.ts
index 384174b..67b5c54 100644
--- a/extension/src/popup/components/types/totp.ts
+++ b/extension/src/popup/components/types/totp.ts
@@ -2,7 +2,7 @@
/// signature block with a thin SVG countdown ring; form has a kind toggle
/// (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 { base32Decode, base32Encode } from '../../../shared/base32';
import {
@@ -212,7 +212,11 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
const renderInner = (): string => `
-
${mode === 'add' ? 'new totp' : 'edit totp'}
+
+
${mode === 'add' ? 'new totp' : 'edit totp'}
+
+
+
${state.error ? `
${escapeHtml(state.error)}
` : ''}
@@ -328,6 +332,7 @@ function wireFormButtons(mode: 'add' | 'edit', existing: Item | null, sectionsDr
setState({ error: null });
navigate(mode === 'edit' ? 'detail' : 'list');
});
+ document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
document.getElementById('save-btn')?.addEventListener('click', async () => {
await saveTotp(mode, existing, sectionsDraft, attachmentsDraft);
});
diff --git a/extension/src/popup/popup.ts b/extension/src/popup/popup.ts
index 43048c7..99428eb 100644
--- a/extension/src/popup/popup.ts
+++ b/extension/src/popup/popup.ts
@@ -28,6 +28,29 @@ export function escapeHtml(str: string): string {
.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 ---
export type View = 'locked' | 'list' | 'detail' | 'add' | 'edit' | 'settings' | 'settings-vault' | 'trash' | 'devices' | 'field-history';
@@ -214,6 +237,28 @@ async function init(): Promise
{
currentState.vaultSettings = vs;
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 });
return;
}