/// <reference types="vite/client" />
import { blankDocument, calculateDocument, documentExamples, documentTitles, validateDocument, DOCUMENT_LIMITS, type BusinessDocument, type DocumentTemplateId } from './model';
import { createDocumentTemplate, generateDocumentPdf } from './pdf';
import { sanitizeLogo, inspectLogoDataUrl } from './logo';
import { fontBytes } from '../shared/pdf';
import { escapeHtml as escape, downloadBlob as download } from '../shared/ui';
import { InputError } from '../shared/validation';
import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import './document.css';

export { documentSchema, validateDocument, documentExamples, calculateDocument } from './model';
export { createDocumentTemplate, generateDocumentPdf } from './pdf';

const DRAFT_KEY = 'essential-tools.document.v1';
const captions: Record<string, string> = { 'business.name': 'Business name', 'business.contact': 'Business contact details', 'client.name': 'Client name', 'client.contact': 'Client contact details', number: 'Document number', date: 'Document date', validUntil: 'Valid until', description: 'Work description', pricing: 'Pricing', items: 'Line items', discount: 'Discount', logo: 'Logo' };
function errorCaption(path: string) { const item = /^items\.(\d+)\.(\w+)$/.exec(path); return item ? `Item ${Number(item[1]) + 1}, ${item[2] === 'unitPrice' ? 'unit price' : item[2]}` : captions[path] ?? path.replace(/\./g, ' '); }

export function mount(root: HTMLElement): () => void {
  let data = structuredClone(documentExamples.quotation);
  let sample = true; let remember = false; let dirty = false; let revision = 0; let renderedRevision = -1; let stopped = false; let busy = false;
  let timer: ReturnType<typeof setTimeout>; let pdfBytes: Uint8Array | undefined;
  let pdfDocument: import('pdfjs-dist').PDFDocumentProxy | undefined; let renderTask: import('pdfjs-dist').RenderTask | undefined;
  let pageNumber = 1; let fontPromise: Promise<Uint8Array> | undefined;
  const events = new AbortController();
  const query = <T extends HTMLElement = HTMLElement>(selector: string) => root.querySelector<T>(selector)!;
  root.classList.add('et-document');
  root.innerHTML = `<div class="et-doc-toolbar"><div><span class="et-doc-step">1</span><label for="et-doc-template">Choose a document</label><select id="et-doc-template" data-action="template"><option value="quotation">Quotation</option><option value="work-order">Work order</option><option value="service-report">Service report</option></select></div><div class="et-doc-actions"><button type="button" data-action="blank">Start blank</button><button type="button" data-action="sample">Load sample</button><button type="button" data-action="import">Import saved JSON</button><input class="et-visually-hidden" type="file" accept=".json,application/json" data-import tabindex="-1" aria-label="Import an Essential document JSON file"></div></div><p class="et-doc-sample" data-sample>Fictional sample data — replace it with your own details.</p><div class="et-doc-layout"><div class="et-doc-editor"><div class="et-doc-editor-title"><span class="et-doc-step">2</span><h2>Edit your details</h2></div><form novalidate data-form></form><div class="et-doc-errors" data-errors role="region" aria-label="Document validation" tabindex="-1" hidden></div><p data-notice role="status" aria-live="polite"></p><div class="et-doc-storage"><label><input type="checkbox" data-action="remember"> Remember this draft on this device</label><p>Optional. Browser storage is not an encrypted vault. Leave this off on shared devices.</p><div class="et-doc-actions"><button type="button" data-action="restore">Restore saved draft</button><button type="button" data-action="clear">Clear draft</button></div></div></div><section class="et-doc-preview" aria-label="PDF preview"><div class="et-doc-preview-heading"><div><span class="et-doc-step">3</span><h2>Preview & download</h2></div><span class="et-doc-paper" data-paper>A4</span></div><p class="et-doc-preview-status" data-preview-status role="status" aria-live="polite">Preparing your PDF preview…</p><div class="et-doc-sheet" data-sheet><canvas data-canvas aria-label="Current PDF page"></canvas></div><div class="et-doc-page-controls"><button type="button" data-action="previous" aria-label="Previous PDF page">←</button><span data-page>Page 1</span><button type="button" data-action="next" aria-label="Next PDF page">→</button></div><div class="et-doc-totals" data-totals></div><button type="button" class="et-doc-primary" data-action="pdf" disabled>Download PDF</button><div class="et-doc-downloads"><button type="button" data-action="json">Save document JSON</button><details><summary>Developer template files</summary><p>Reusable pdfme layout and inputs for your own code. These are separate from the full application source kit.</p><button type="button" data-action="template-json">Download pdfme template</button><button type="button" data-action="inputs-json">Download current inputs</button><p>Use the included Noto Sans PDF font and the text, table and image plugins. Template imports are not supported here.</p></details></div><details class="et-doc-text-preview"><summary>Read the current page as text</summary><p data-page-text></p></details><p class="et-doc-exact">This preview displays the same PDF file that you download, including its page breaks.</p></section></div>`;
  const notice = (message: string) => { query('[data-notice]').textContent = message; };
  const field = (label: string, path: string, value: unknown, options: { type?: string; area?: boolean; max?: number; help?: string; required?: boolean; full?: boolean } = {}) => `<label class="et-doc-field${options.full ? ' et-doc-full' : ''}"><span>${label}${options.required ? ' <span aria-hidden="true">*</span>' : ''}</span>${options.area ? `<textarea rows="${options.max && options.max > 1200 ? 3 : 2}" data-field="${path}" maxlength="${options.max ?? 8000}" ${options.required ? 'required' : ''}>${escape(value)}</textarea>` : `<input type="${options.type === 'decimal' ? 'text' : options.type ?? 'text'}" data-field="${path}" value="${escape(value)}" ${options.type === 'decimal' ? 'inputmode="decimal"' : ''} maxlength="${options.max ?? 240}" ${options.required ? 'required' : ''}>`}${options.help ? `<small>${options.help}</small>` : ''}</label>`;
  function renderForm() {
    query<HTMLSelectElement>('[data-action="template"]').value = data.templateId;
    query('[data-sample]').textContent = sample ? 'Fictional sample data — replace it with your own details.' : 'Your document stays in this browser unless you choose to save or download it.';
    const quote = data.templateId === 'quotation'; const report = data.templateId === 'service-report';
    query('[data-form]').innerHTML = `<fieldset><legend>Your business & client</legend><div class="et-doc-fields">${field('Business name', 'business.name', data.business.name, { required: true })}${field('Client name', 'client.name', data.client.name, { required: true })}${field('Business contact details', 'business.contact', data.business.contact, { area: true, max: 1200 })}${field('Client contact details', 'client.contact', data.client.contact, { area: true, max: 1200 })}<div class="et-doc-field et-doc-full"><label for="et-doc-logo">Your logo (optional)</label><input id="et-doc-logo" type="file" accept="image/png,image/jpeg" data-logo><small>PNG or JPEG, up to 2 MB. No logo is needed to create a document.</small>${data.logo ? '<span>Logo added. <button type="button" data-action="remove-logo">Remove logo</button></span>' : ''}</div></div></fieldset><fieldset><legend>Document details</legend><div class="et-doc-fields">${field(`${documentTitles[data.templateId]} number`, 'number', data.number, { required: true, max: 80 })}${field(quote ? 'Issue date' : report ? 'Service date' : 'Scheduled date', 'date', data.date, { type: 'date', required: true })}${quote ? field('Valid until (optional)', 'validUntil', data.validUntil, { type: 'date' }) : field('Assigned to / technician', 'person', data.person)}${!quote ? field('Work location', 'location', data.location, { max: 800, full: true }) : ''}${field(report ? 'Work completed' : 'Work description', 'description', data.description, { area: true, full: true, max: 8000 })}<label class="et-doc-field"><span>Page size</span><select data-field="paper"><option value="A4" ${data.paper === 'A4' ? 'selected' : ''}>A4</option><option value="Letter" ${data.paper === 'Letter' ? 'selected' : ''}>US Letter</option></select></label><label class="et-doc-field"><span>Document colour</span><select data-field="accent">${[['#173B35', 'Forest'], ['#26364B', 'Slate'], ['#6B46FF', 'Purple'], ['#171717', 'Black']].map(([v, label]) => `<option value="${v}" ${data.accent === v ? 'selected' : ''}>${label}</option>`).join('')}</select></label></div></fieldset><fieldset><legend>Items & pricing</legend>${!quote ? `<label class="et-doc-check"><input type="checkbox" data-action="pricing" ${data.pricing ? 'checked' : ''}> Include prices in this document</label><p class="et-doc-help">When this is off, prices, taxes, discounts and totals are removed from the document and its saved data.</p>` : ''}${data.pricing ? `<label class="et-doc-field"><span>Currency</span><select data-field="currency"><option value="CAD" ${data.currency === 'CAD' ? 'selected' : ''}>CAD — Canadian dollars</option><option value="USD" ${data.currency === 'USD' ? 'selected' : ''}>USD — US dollars</option></select><small>Changing currency changes the label. It does not convert amounts.</small></label>` : ''}<div class="et-doc-items" data-items>${data.items.map((item, i) => `<div class="et-doc-item"><div class="et-doc-item-heading"><strong>Item ${i + 1}</strong><div><button type="button" data-action="up" data-index="${i}" aria-label="Move item ${i + 1} up" ${i === 0 ? 'disabled' : ''}>↑</button><button type="button" data-action="down" data-index="${i}" aria-label="Move item ${i + 1} down" ${i === data.items.length - 1 ? 'disabled' : ''}>↓</button><button type="button" data-action="remove" data-index="${i}" aria-label="Remove item ${i + 1}">Remove</button></div></div>${field('Description', `items.${i}.description`, item.description, { area: true, max: 1200, required: true })}<div class="et-doc-fields">${field('Quantity', `items.${i}.quantity`, item.quantity, { type: 'decimal', required: true })}${data.pricing && 'unitPrice' in item ? field('Unit price', `items.${i}.unitPrice`, item.unitPrice, { type: 'decimal', required: true }) : ''}</div></div>`).join('')}</div><button type="button" data-action="add" ${data.items.length >= DOCUMENT_LIMITS.items ? 'disabled' : ''}>+ Add item</button>${data.pricing ? `<div class="et-doc-financial">${field('Discount amount', 'discount', data.discount, { type: 'decimal', help: 'A fixed amount, before tax. Enter 0 for no discount.' })}<p class="et-doc-help">Enter your own tax names and rates. Each tax applies separately to the amount after the discount.</p>${data.taxes.map((tax, i) => `<div class="et-doc-fields">${field(`Tax ${i + 1} name`, `taxes.${i}.name`, tax.name, { max: 40 })}${field(`Tax ${i + 1} rate (%)`, `taxes.${i}.rate`, tax.rate, { type: 'decimal' })}</div>`).join('')}${data.taxes.length < 2 ? '<button type="button" data-action="add-tax">+ Add tax line</button>' : ''}</div>` : ''}</fieldset><fieldset><legend>${report ? 'Findings & next steps' : 'Notes & instructions'}</legend><div class="et-doc-fields">${report ? `${field('Findings', 'findings', data.findings, { area: true, full: true, max: 8000 })}${field('Recommendations', 'recommendations', data.recommendations, { area: true, full: true, max: 8000 })}${field('Next steps', 'nextSteps', data.nextSteps, { area: true, full: true, max: 8000 })}` : ''}${field(data.templateId === 'work-order' ? 'Instructions' : 'Notes', 'notes', data.notes, { area: true, full: true, max: 8000 })}${quote ? field('Terms', 'terms', data.terms, { area: true, full: true, max: 8000 }) : ''}</div></fieldset>`;
  }
  function validate() {
    const result = validateDocument(data); const panel = query('[data-errors]');
    panel.hidden = result.ok;
    panel.innerHTML = result.ok ? '' : `<strong>Please check these details</strong><ul>${result.errors.slice(0, 12).map(error => `<li>${escape(errorCaption(error.path))}: ${escape(error.message)}</li>`).join('')}</ul>`;
    root.querySelectorAll<HTMLElement>('[data-field]').forEach(element => { const invalid = !result.ok && result.errors.some(error => error.path === element.dataset.field); element.setAttribute('aria-invalid', String(invalid)); });
    for (const action of ['pdf', 'json', 'template-json', 'inputs-json']) query<HTMLButtonElement>(`[data-action="${action}"]`).disabled = !result.ok || (action === 'pdf' && (busy || renderedRevision !== revision));
    if (!result.ok) { query('[data-preview-status]').textContent = 'Correct the details shown on the left to update this preview.'; query('[data-totals]').textContent = ''; return result; }
    const totals = calculateDocument(result.value);
    query('[data-totals]').innerHTML = totals.pricing ? `<span>Total (${escape(totals.currency)})</span><strong>${escape(new Intl.NumberFormat('en-CA', { style: 'currency', currency: totals.currency }).format(Number(totals.total)))}</strong>` : '<span>Prices are not included in this document.</span>';
    return result;
  }
  function saveDraft() { if (!remember) return; const valid = validateDocument(data); if (!valid.ok) return; try { localStorage.setItem(DRAFT_KEY, JSON.stringify(valid.value)); } catch { remember = false; query<HTMLInputElement>('[data-action="remember"]').checked = false; notice('This browser could not save the draft. Download the JSON file to keep a copy.'); } }
  function changed() { revision++; dirty = true; sample = false; saveDraft(); validate(); clearTimeout(timer); timer = setTimeout(() => void updatePreview(), 650); }
  async function displayPage() {
    if (!pdfDocument || stopped) return;
    renderTask?.cancel(); const active = pdfDocument;
    pageNumber = Math.min(Math.max(1, pageNumber), active.numPages);
    const page = await active.getPage(pageNumber); if (active !== pdfDocument || stopped) return;
    const canvas = query<HTMLCanvasElement>('[data-canvas]'); const base = page.getViewport({ scale: 1 });
    const scale = Math.min(1.4, Math.max(0.2, (query('[data-sheet]').clientWidth - 24) / base.width)); const ratio = Math.min(window.devicePixelRatio || 1, 2);
    const viewport = page.getViewport({ scale: scale * ratio }); canvas.width = viewport.width; canvas.height = viewport.height; canvas.style.width = `${viewport.width / ratio}px`; canvas.style.height = `${viewport.height / ratio}px`;
    const context = canvas.getContext('2d'); if (!context) throw new Error('Your browser could not display the PDF preview.');
    renderTask = page.render({ canvasContext: context, canvas, viewport });
    try { await renderTask.promise; } catch (error) { if (error instanceof Error && error.name === 'RenderingCancelledException') return; throw error; }
    if (active !== pdfDocument || stopped) return;
    query('[data-page]').textContent = `Page ${pageNumber} of ${active.numPages}`;
    query<HTMLButtonElement>('[data-action="previous"]').disabled = pageNumber === 1; query<HTMLButtonElement>('[data-action="next"]').disabled = pageNumber === active.numPages;
    const text = await page.getTextContent(); query('[data-page-text]').textContent = text.items.map(item => 'str' in item ? `${item.str}${item.hasEOL ? '\n' : ' '}` : '').join('');
  }
  async function updatePreview() {
    if (busy || stopped) return;
    const valid = validate(); if (!valid.ok) return;
    const thisRevision = revision; busy = true; query<HTMLButtonElement>('[data-action="pdf"]').disabled = true;
    query('[data-preview-status]').textContent = 'Updating your PDF preview…';
    try {
      const snapshot = structuredClone(valid.value); const font = await (fontPromise ??= fontBytes());
      const bytes = await generateDocumentPdf(snapshot, { fontBytes: font });
      if (stopped || revision !== thisRevision) return;
      const pdfjs = await import('pdfjs-dist'); pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;
      const nextPdf = await pdfjs.getDocument({ data: bytes.slice(), useSystemFonts: false, useWasm: false }).promise;
      if (stopped || revision !== thisRevision) { await nextPdf.loadingTask.destroy(); return; }
      renderTask?.cancel(); await pdfDocument?.loadingTask.destroy(); pdfDocument = nextPdf; pdfBytes = bytes; renderedRevision = thisRevision;
      await displayPage(); query('[data-paper]').textContent = snapshot.paper === 'Letter' ? 'US Letter' : 'A4';
      query('[data-preview-status]').textContent = `Preview ready — ${nextPdf.numPages} ${nextPdf.numPages === 1 ? 'page' : 'pages'}.`;
    } catch (error) { if (!stopped && error instanceof InputError) { query('[data-preview-status]').textContent = error.message; notice(error.message); } else if (!stopped) { query('[data-preview-status]').textContent = 'The PDF preview could not load. Your entries are still here. Try again, or save the JSON file and reload.'; notice('PDF generation failed. Check your connection for the first engine load, then select Retry preview.'); if (!root.querySelector('[data-action="retry"]')) query('[data-preview-status]').insertAdjacentHTML('afterend', '<button type="button" data-action="retry">Retry preview</button>'); } }
    finally { busy = false; if (!stopped) { validate(); if (revision !== thisRevision) timer = setTimeout(() => void updatePreview(), 50); } }
  }
  async function replace(next: BusinessDocument, message: string) {
    if (!window.confirm('Replace the current document? Download its JSON first if you want to keep it.')) return false;
    data = structuredClone(next); pageNumber = 1; renderForm(); changed(); notice(message); return true;
  }
  root.addEventListener('input', event => {
    const target = event.target as HTMLInputElement; const path = target.dataset.field; if (!path) return;
    const parts = path.split('.'); let value: unknown = data;
    for (const key of parts.slice(0, -1)) value = (value as Record<string, unknown>)[key];
    (value as Record<string, unknown>)[parts.at(-1)!] = target.value;
    changed();
  }, { signal: events.signal });
  root.addEventListener('change', async event => {
    const target = event.target as HTMLInputElement;
    try {
      if (target.dataset.action === 'template') { const replaced = await replace(documentExamples[target.value as DocumentTemplateId], 'Sample loaded. Replace the fictional details with your own.'); if (replaced) { sample = true; renderForm(); } else target.value = data.templateId; }
      if (target.dataset.action === 'pricing') {
        if (!target.checked && !window.confirm('Remove all pricing from this document, including saved and exported data?')) { target.checked = true; return; }
        if (target.checked && !data.pricing) data = { ...data, pricing: true, currency: 'CAD', discount: '0.00', taxes: [{ name: 'Tax 1', rate: '0' }, { name: 'Tax 2', rate: '0' }], items: data.items.map(item => ({ ...item, unitPrice: '0.00' })) };
        else if (!target.checked && data.pricing) { const { currency: _currency, discount: _discount, taxes: _taxes, ...rest } = data; data = { ...rest, pricing: false, items: data.items.map(({ unitPrice: _price, ...item }) => item) }; }
        renderForm(); changed();
      }
      if (target.dataset.action === 'remember') { remember = target.checked; if (remember) { saveDraft(); notice('Valid changes will be saved on this device while this option is on.'); } else { try { localStorage.removeItem(DRAFT_KEY); notice('The saved draft has been removed from this device.'); } catch { notice('Browser storage is unavailable.'); } } }
      if (target.matches('[data-logo]') && target.files?.[0]) { data.logo = await sanitizeLogo(target.files[0]); renderForm(); changed(); notice('Logo added and prepared locally. Image metadata has been removed.'); }
      if (target.matches('[data-import]') && target.files?.[0]) {
        const file = target.files[0]; if (file.size > DOCUMENT_LIMITS.jsonBytes) throw new Error('Choose an Essential document JSON file smaller than 3 MB.');
        let incoming: unknown; try { incoming = JSON.parse(await file.text()); } catch { throw new Error('This file is not valid JSON. Your current document has been kept.'); }
        const checked = validateDocument(incoming); if (!checked.ok) throw new Error(`This document cannot be imported: ${checked.errors[0]?.message}`);
        if (checked.value.logo) { const logo = inspectLogoDataUrl(checked.value.logo); checked.value.logo = await sanitizeLogo(new Blob([logo.bytes], { type: logo.mime })); }
        await replace(checked.value, 'Document imported. Your previous draft was replaced only after confirmation.');
        target.value = '';
      }
    } catch (error) { notice(error instanceof Error ? error.message : 'This action could not be completed. Your current document has been kept.'); target.value = ''; }
  }, { signal: events.signal });
  root.addEventListener('click', async event => {
    const button = (event.target as HTMLElement).closest<HTMLButtonElement>('button[data-action]'); if (!button) return;
    const action = button.dataset.action; const index = Number(button.dataset.index);
    try {
      if (action === 'add') { if (data.items.length >= DOCUMENT_LIMITS.items) return; const item = { id: `item-${crypto.randomUUID()}`, description: '', quantity: '1' }; if (data.pricing) data.items.push({ ...item, unitPrice: '0.00' }); else data.items.push(item); renderForm(); changed(); query<HTMLInputElement>(`[data-field="items.${data.items.length - 1}.description"]`).focus(); }
      if (action === 'remove') { data.items.splice(index, 1); renderForm(); changed(); query<HTMLButtonElement>('[data-action="add"]').focus(); }
      if (action === 'up' || action === 'down') { const other = action === 'up' ? index - 1 : index + 1; if (other < 0 || other >= data.items.length) return; [data.items[index], data.items[other]] = [data.items[other], data.items[index]]; renderForm(); changed(); query<HTMLButtonElement>(`[data-action="${action}"][data-index="${other}"]`).focus(); }
      if (action === 'add-tax' && data.pricing && data.taxes.length < 2) { data.taxes.push({ name: `Tax ${data.taxes.length + 1}`, rate: '0' }); renderForm(); changed(); }
      if (action === 'remove-logo') { delete data.logo; renderForm(); changed(); }
      if (action === 'blank') await replace(blankDocument(data.templateId), 'Blank document ready. Complete the required details to create a preview.');
      if (action === 'sample') { if (await replace(documentExamples[data.templateId], 'Fictional sample loaded.')) { sample = true; renderForm(); } }
      if (action === 'import') query<HTMLInputElement>('[data-import]').click();
      if (action === 'clear') { if (await replace(blankDocument(data.templateId), 'Current document cleared.')) { try { localStorage.removeItem(DRAFT_KEY); } catch { notice('Current document cleared. Browser storage could not be accessed.'); } remember = false; query<HTMLInputElement>('[data-action="remember"]').checked = false; } }
      if (action === 'restore') { let stored: string | null; try { stored = localStorage.getItem(DRAFT_KEY); } catch { throw new Error('This browser does not allow saved drafts. Use JSON import instead.'); } if (!stored) throw new Error('No draft is saved on this device.'); if (stored.length > DOCUMENT_LIMITS.jsonBytes) throw new Error('The saved draft is too large. Your current document has been kept.'); let parsed: unknown; try { parsed = JSON.parse(stored); } catch { throw new Error('The saved draft is damaged. Your current document has been kept.'); } const checked = validateDocument(parsed); if (!checked.ok) throw new Error('The saved draft is damaged or uses an unsupported version. Your current document has been kept.'); await replace(checked.value, 'Saved draft restored. Remember this draft is still off until you select it.'); }
      if (action === 'previous' || action === 'next') { pageNumber += action === 'previous' ? -1 : 1; await displayPage(); }
      if (action === 'retry') { button.remove(); fontPromise = undefined; await updatePreview(); }
      if (['pdf', 'json', 'template-json', 'inputs-json'].includes(action ?? '')) {
        const checked = validate(); if (!checked.ok) { query('[data-errors]').focus(); return; }
        if (action === 'pdf') { if (!pdfBytes || renderedRevision !== revision) return; download(pdfBytes, `${data.templateId}.pdf`, 'application/pdf'); notice('PDF download prepared.'); }
        if (action === 'json') { download(JSON.stringify(checked.value, null, 2), 'essential-document-v1.json', 'application/json'); dirty = false; notice('Document JSON download prepared.'); }
        if (action === 'template-json' || action === 'inputs-json') { const bundle = createDocumentTemplate(checked.value); download(JSON.stringify(action === 'template-json' ? bundle.template : bundle.inputs, null, 2), action === 'template-json' ? 'pdfme-document-template.json' : 'pdfme-document-inputs.json', 'application/json'); notice('Reusable developer file download prepared.'); }
      }
    } catch (error) { notice(error instanceof Error ? error.message : 'This action could not be completed.'); }
  }, { signal: events.signal });
  query<HTMLFormElement>('[data-form]').addEventListener('submit', event => event.preventDefault(), { signal: events.signal });
  window.addEventListener('beforeunload', event => {
    // A failed validation leaves the last saved draft unchanged, even with opt-in on.
    if (dirty && (!remember || !validateDocument(data).ok)) { event.preventDefault(); event.returnValue = ''; }
  }, { signal: events.signal });
  let resizeTimer: ReturnType<typeof setTimeout>;
  const observer = new ResizeObserver(() => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { void displayPage().catch(() => {}); }, 200); }); observer.observe(query('[data-sheet]'));
  renderForm(); validate(); void updatePreview();
  return () => { stopped = true; events.abort(); observer.disconnect(); clearTimeout(timer); clearTimeout(resizeTimer); renderTask?.cancel(); void pdfDocument?.loadingTask.destroy(); root.innerHTML = ''; };
}
