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 = ` +
+
+
${titleText}
+
no changes
+
+
${SAVE_HINT}
+
+
+
+ + +
+ `; + // 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 = ` -
-
-
${titleText}
-
no changes
-
-
${SAVE_HINT}
-
-
-
- - -
- `; - // 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);