refactor(ext/vault): extract vault-form-wrapper.ts (Plan C Phase 4)

Moves renderFormWrapped (sticky save bar + header + dirty-state wiring), the
SAVE_HINT/isMac consts, and the __test__ export out of vault.ts into
vault-form-wrapper.ts, taking the VaultController ctx. Repoints the source-text
form-wrapper test to read the new module. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-31 20:02:25 -04:00
parent 7f076b49ac
commit fecf58e54a
3 changed files with 76 additions and 68 deletions

View File

@@ -4,7 +4,7 @@ import * as path from 'path';
describe('fullscreen form dirty subtitle', () => { describe('fullscreen form dirty subtitle', () => {
const vaultSrc = fs.readFileSync( const vaultSrc = fs.readFileSync(
path.resolve(__dirname, '../vault.ts'), path.resolve(__dirname, '../vault-form-wrapper.ts'),
'utf-8', 'utf-8',
); );

View File

@@ -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 = `
<div class="fullscreen-form-header">
<div>
<div class="title">${titleText}</div>
<div class="sub" id="form-dirty-sub">no changes</div>
</div>
<div class="hint">${SAVE_HINT}</div>
</div>
<div class="form-scroll" id="form-scroll"></div>
<div class="sticky-save-bar">
<button class="btn-secondary" id="form-cancel">cancel</button>
<button class="btn-primary" id="form-save">save</button>
</div>
`;
// 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 };

View File

@@ -35,6 +35,7 @@ import {
openDrawer, closeDrawer, renderDrawer, selectItemForDrawer, openDrawer, closeDrawer, renderDrawer, selectItemForDrawer,
ensureDrawerClosedForRoute, ensureDrawerClosedForRoute,
} from './vault-drawer'; } from './vault-drawer';
import { renderFormWrapped } from './vault-form-wrapper';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
@@ -187,71 +188,6 @@ registerHost({
openVaultTab: () => {}, 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 = `
<div class="fullscreen-form-header">
<div>
<div class="title">${titleText}</div>
<div class="sub" id="form-dirty-sub">no changes</div>
</div>
<div class="hint">${SAVE_HINT}</div>
</div>
<div class="form-scroll" id="form-scroll"></div>
<div class="sticky-save-bar">
<button class="btn-secondary" id="form-cancel">cancel</button>
<button class="btn-primary" id="form-save">save</button>
</div>
`;
// 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 // 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. // 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. // Without a type the type-selection screen renders — no sticky bar needed.
if (state.newType) { if (state.newType) {
renderFormWrapped(pane, 'add'); renderFormWrapped(ctx, pane, 'add');
} else { } else {
renderItemForm(pane, 'add'); renderItemForm(pane, 'add');
} }
break; break;
case 'edit': case 'edit':
renderFormWrapped(pane, 'edit'); renderFormWrapped(ctx, pane, 'edit');
break; break;
case 'trash': case 'trash':
renderTrash(pane); renderTrash(pane);