diff --git a/extension/src/vault/__tests__/form-wrapper.test.ts b/extension/src/vault/__tests__/form-wrapper.test.ts
index 791c34d..270300f 100644
--- a/extension/src/vault/__tests__/form-wrapper.test.ts
+++ b/extension/src/vault/__tests__/form-wrapper.test.ts
@@ -4,7 +4,7 @@ import * as path from 'path';
describe('fullscreen form dirty subtitle', () => {
const vaultSrc = fs.readFileSync(
- path.resolve(__dirname, '../vault.ts'),
+ path.resolve(__dirname, '../vault-form-wrapper.ts'),
'utf-8',
);
diff --git a/extension/src/vault/vault-form-wrapper.ts b/extension/src/vault/vault-form-wrapper.ts
new file mode 100644
index 0000000..cf1d9b5
--- /dev/null
+++ b/extension/src/vault/vault-form-wrapper.ts
@@ -0,0 +1,72 @@
+// Fullscreen form wrapper for the vault tab: sticky save bar + scrollable
+// content + header with a live dirty-state subtitle. Receives the
+// VaultController (`ctx`) for the item-type read; imports only from shared/,
+// the popup item-form component, and vault-context.
+
+import { renderItemForm } from '../popup/components/item-form';
+import { type VaultController } from './vault-context';
+
+// ---------------------------------------------------------------------------
+// Platform-aware save hint
+// ---------------------------------------------------------------------------
+
+const isMac = navigator.platform.toLowerCase().includes('mac');
+const SAVE_HINT = isMac ? '⌘+S to save' : 'Ctrl+S to save';
+
+// ---------------------------------------------------------------------------
+// Fullscreen form wrapper — sticky save bar + scrollable content + header
+// ---------------------------------------------------------------------------
+
+export function renderFormWrapped(ctx: VaultController, app: HTMLElement, mode: 'add' | 'edit'): void {
+ const itemType = ctx.state.selectedItem?.type ?? ctx.state.newType ?? 'login';
+ const typeLabelText = itemType.replace('_', ' ');
+ const titleText = mode === 'add' ? `new ${typeLabelText}` : `edit ${typeLabelText}`;
+ const wrapper = document.createElement('div');
+ wrapper.className = 'form-pane';
+ wrapper.innerHTML = `
+
+
+
+
+
+
+ `;
+ // Remove pane padding so form-pane can fill height cleanly
+ app.style.padding = '0';
+ app.style.overflow = 'hidden';
+ app.replaceChildren(wrapper);
+
+ const scrollEl = wrapper.querySelector('#form-scroll') as HTMLElement;
+ renderItemForm(scrollEl, mode);
+
+ const subEl = wrapper.querySelector('#form-dirty-sub') as HTMLElement;
+ let isDirty = false;
+ const markDirty = () => {
+ if (isDirty) return;
+ isDirty = true;
+ subEl.textContent = 'unsaved · esc to cancel';
+ };
+ const markClean = () => {
+ isDirty = false;
+ subEl.textContent = 'no changes';
+ };
+ scrollEl.addEventListener('input', markDirty, true);
+ scrollEl.addEventListener('change', markDirty, true);
+
+ wrapper.querySelector('#form-cancel')?.addEventListener('click', () => {
+ markClean();
+ (scrollEl.querySelector('#cancel-btn') as HTMLButtonElement | null)?.click();
+ });
+ wrapper.querySelector('#form-save')?.addEventListener('click', () => {
+ markClean();
+ (scrollEl.querySelector('#save-btn') as HTMLButtonElement | null)?.click();
+ });
+}
+
+export const __test__ = { renderFormWrapped };
diff --git a/extension/src/vault/vault.ts b/extension/src/vault/vault.ts
index 19c22f4..97285bf 100644
--- a/extension/src/vault/vault.ts
+++ b/extension/src/vault/vault.ts
@@ -35,6 +35,7 @@ import {
openDrawer, closeDrawer, renderDrawer, selectItemForDrawer,
ensureDrawerClosedForRoute,
} from './vault-drawer';
+import { renderFormWrapped } from './vault-form-wrapper';
// ---------------------------------------------------------------------------
// Helpers
@@ -187,71 +188,6 @@ registerHost({
openVaultTab: () => {},
});
-// ---------------------------------------------------------------------------
-// Platform-aware save hint
-// ---------------------------------------------------------------------------
-
-const isMac = navigator.platform.toLowerCase().includes('mac');
-const SAVE_HINT = isMac ? '⌘+S to save' : 'Ctrl+S to save';
-
-// ---------------------------------------------------------------------------
-// Fullscreen form wrapper — sticky save bar + scrollable content + header
-// ---------------------------------------------------------------------------
-
-function renderFormWrapped(app: HTMLElement, mode: 'add' | 'edit'): void {
- const itemType = state.selectedItem?.type ?? state.newType ?? 'login';
- const typeLabelText = itemType.replace('_', ' ');
- const titleText = mode === 'add' ? `new ${typeLabelText}` : `edit ${typeLabelText}`;
- const wrapper = document.createElement('div');
- wrapper.className = 'form-pane';
- wrapper.innerHTML = `
-
-
-
-
-
-
- `;
- // Remove pane padding so form-pane can fill height cleanly
- app.style.padding = '0';
- app.style.overflow = 'hidden';
- app.replaceChildren(wrapper);
-
- const scrollEl = wrapper.querySelector('#form-scroll') as HTMLElement;
- renderItemForm(scrollEl, mode);
-
- const subEl = wrapper.querySelector('#form-dirty-sub') as HTMLElement;
- let isDirty = false;
- const markDirty = () => {
- if (isDirty) return;
- isDirty = true;
- subEl.textContent = 'unsaved · esc to cancel';
- };
- const markClean = () => {
- isDirty = false;
- subEl.textContent = 'no changes';
- };
- scrollEl.addEventListener('input', markDirty, true);
- scrollEl.addEventListener('change', markDirty, true);
-
- wrapper.querySelector('#form-cancel')?.addEventListener('click', () => {
- markClean();
- (scrollEl.querySelector('#cancel-btn') as HTMLButtonElement | null)?.click();
- });
- wrapper.querySelector('#form-save')?.addEventListener('click', () => {
- markClean();
- (scrollEl.querySelector('#save-btn') as HTMLButtonElement | null)?.click();
- });
-}
-
-export const __test__ = { renderFormWrapped };
-
// ---------------------------------------------------------------------------
// Pane rendering — delegates to shared popup components
// ---------------------------------------------------------------------------
@@ -297,13 +233,13 @@ function renderPane(): void {
// Use the form wrapper (sticky bar + header) when a type is already chosen.
// Without a type the type-selection screen renders — no sticky bar needed.
if (state.newType) {
- renderFormWrapped(pane, 'add');
+ renderFormWrapped(ctx, pane, 'add');
} else {
renderItemForm(pane, 'add');
}
break;
case 'edit':
- renderFormWrapped(pane, 'edit');
+ renderFormWrapped(ctx, pane, 'edit');
break;
case 'trash':
renderTrash(pane);