From 152545555e8409830374a649af78a672b6d5b10d Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Mon, 13 Jul 2026 13:42:05 +0530 Subject: [PATCH] docs: live document preview design (approved via ultracode panel) Same-code preview endpoint (no drift), form+A4-paper split layout, server-truth totals strip, permissive-preview/strict-save, + company-letterhead / quote-content / reminder-email sibling previews. Store-product surfaces documented as handoff. Co-Authored-By: Claude Fable 5 --- .../specs/2026-07-13-live-preview-design.md | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-13-live-preview-design.md diff --git a/docs/superpowers/specs/2026-07-13-live-preview-design.md b/docs/superpowers/specs/2026-07-13-live-preview-design.md new file mode 100644 index 0000000..244cc16 --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-live-preview-design.md @@ -0,0 +1,136 @@ +# Live Document Preview — Design (HQ console) + +**Status:** approved (founder, 2026-07-13, via ultracode design panel + brainstorm). +**Origin:** spec [14-SPEC-HQ-CONSOLE.md](../../14-SPEC-HQ-CONSOLE.md) §3 already promised a +"live PDF preview on letterhead" in the composer; this is that, designed properly and +extended to three cheap sibling surfaces. + +## 1. The one idea that makes it safe + +The preview must **never drift from what prints**. That is guaranteed *architecturally, +not by testing*: the preview shows the output of the **same** `documentHtml()` template +and the **same** `computeBill()` tax engine that the PDF uses — there is no second +renderer and no client-side money math. The browser renders that HTML directly in an +iframe (puppeteer only rasterizes the identical HTML for the actual PDF). + +Winning approach = engineering-minimalist core (smallest correct change, one shared +compute path) + grafted UX pieces (server-truth totals strip, permissive preview, +no-blank swap). Chosen by a 2-judge panel (value judge 8.5 ux / cost judge 9 minimal → +minimal core carrying ux's high-value grafts). + +## 2. Server: one new pure function + one route + +**Refactor (no behavior change):** extract the compute block of `createDraft` in +`apps/hq/src/repos-documents.ts` into a pure `prepareDraft(db, input): { lines, totals, +payload }` that does **not** persist. `createDraft` calls `prepareDraft` then inserts — +so save and preview cannot diverge by construction. + +**New route** `POST /api/documents/preview` (`requireAuth`, api.ts style): +- Body: the **exact** `DraftInput` shape `POST /api/documents` takes. +- Builds a synthetic **unsaved** `Doc` (`docNo: null` → template prints "DRAFT"; + `docDate: today`; `fy` via `fyOf`), runs `prepareDraft` + `documentHtml`. +- Returns `{ ok: true, html: string, totals: BillTotals, warnings: string[] }`. +- Persists nothing; writes no audit row (it is a read). Cost = a few SQLite reads + + string templating (single-digit ms); no puppeteer, no browser contention. + +**Permissive vs strict:** the preview endpoint is **permissive** — a line with no +price-book row and no manual rate computes at ₹0 and is reported in `warnings[]` (so +staff can keep typing); **`POST /api/documents` (save) stays strict** and still rejects +the same bad document. Preview forgives; save refuses. + +**Fidelity test:** one golden test asserts the preview route and `renderPdf` consume +**string-identical** HTML for a fixture draft + a `prepareDraft`↔`createDraft` +deep-equal parity test (totals/payload). Guards anyone ever forking the template. + +## 3. Client: the composer becomes a split view + +`apps/hq-web/src/pages/NewDocument.tsx`, layout **"form + A4 paper fills the rest"** +(founder pick): + +- **Left**, fixed ~600px, own scroll: the existing form. Its bottom gets a **sticky + totals strip** — Taxable / (CGST+SGST *or* IGST) / Round-off / Payable — sourced from + the **response's `totals`**, deleting the client-side subtotal reducer at + `NewDocument.tsx:91` (the last client money math; its removal is the single biggest + trust win — the IGST split now appears live for out-of-state clients). +- **Right**, fills remaining width, sticky under the header: grey backdrop + a white + **A4 paper card** (210mm aspect, `transform: scale` to fit width) holding a + **sandboxed `srcdoc` iframe** of the real template HTML. +- **Narrow (<800px, owner's phone):** single-column form; a pinned bottom bar always + shows Payable + GST + a **Preview** button that opens the paper as a full-screen sheet + (fixed-dimension wrapper + `transform: scale` — iOS Safari expands bare iframes). + The totals bar never disappears at any width. + +**Update mechanics:** +- A shared client helper `buildDraftBody()` (extracted from the save path) builds the + request body, so preview and save POST **byte-identical** bodies. +- **Immediate** refire (0ms) on commit-shaped events (client pick, doctype radio, + module/kind/pack select, add/remove line, field blur); **400ms trailing debounce** on + keystrokes (qty/unit/description/content/terms). +- One in-flight request max: `AbortController` cancels superseded calls; a sequence + guard drops stale responses; skip the POST entirely when the serialized body is + unchanged. +- **Never blank the pane:** keep the previous paper visible, show a 2px shimmer + dim + totals to 60% while in flight, then double-buffer swap (write into a hidden stacked + iframe, restore `scrollTop`, 120ms cross-fade) — no white flash, no scroll jump. + Changed totals cells pulse 250ms (cause→effect made visible). + +**Empty/error states (client):** zero valid lines → real letterhead, empty table, ₹0 +(never a mock); no client picked → muted "— pick a client —" block, totals shown with a +"split pending client" tag (18% total is invariant) that clears with a pulse on pick; +`warnings[]` price gaps → inline "No price — enter Unit ₹" + amber rate-cell flag; +server unreachable/400 → keep last good paper, slim "Preview paused — retrying" banner +with as-of timestamp, totals greyed (never present stale numbers as fresh); +`company.state_code` unset → actionable link to Settings → Company. + +**Two honest screen/print deltas:** `@page` 14mm margins → the card draws 14mm padding +with a faint toggleable "Show print area" guide; pagination is puppeteer's → an +approximate dashed page-break line at ~269mm content height plus an on-demand "Exact PDF" +button (real puppeteer render) for the rare must-be-one-page quote. The ~5-line +`@media screen` paper styles live **inside templates.ts** (with a "page.pdf renders +print media" comment), not injected client-side, so the paper itself is template-owned; +only preview *chrome* (guides, pulses, tags) is client overlay. + +## 4. Three cheap sibling surfaces (same round) + +All reuse the one preview route/mechanism — each is S effort: +1. **Company profile editor** — live letterhead (header, GSTIN, bank box, signatory) + rendered from a hardcoded sample doc as the owner edits `company.*`. Errors here + corrupt *every* document, so seeing it matters. +2. **Module quote-content editor** (Modules page) — live render of how the "what's + included" bullets actually print (the 10px grey `ul.line-content`), using a one-line + sample doc. Today they're written blind. +3. **Reminder email preview** (manual queue / send) — the exact outgoing email (subject + + body from the pure `reminderEmail(kind, ctx)`), via `GET /api/reminders/:id/preview`. + Staff currently fire client-facing dunning mail unseen. + +## 5. Deliberately NOT this round + +Credit-note & receipt preview (mechanically derived, low value now); **store-product +surfaces** — documented as handoff to that workstream, not built here: Print Templates +visual editor (doc 09 explicitly specs it), Label Printing (already has a popup preview), +Price-list bulk revise, Schemes editor, WhatsApp/message-catalog text, and the **POS +printer-settings receipt preview** (high value — `packages/printing` already renders +42/32-col receipts as pure functions, so it's cheap when that team wants it). + +## 6. Files + +- `apps/hq/src/repos-documents.ts` — extract `prepareDraft`; `createDraft` calls it. +- `apps/hq/src/api.ts` — `POST /api/documents/preview`, `GET /api/reminders/:id/preview`. +- `apps/hq/src/templates.ts` — add `@media screen` paper styles + sample-doc helpers for + company/quote-content previews. +- `apps/hq-web/src/pages/NewDocument.tsx` — split layout, totals strip, LivePreview. +- `apps/hq-web/src/components/LivePreview.tsx` — new: debounced fetch + double-buffer + iframe (kept modest per YAGNI — a composer helper, generalized only if a 2nd/3rd + surface proves the shape). +- `apps/hq-web/src/pages/CompanyProfile.tsx`, `Modules.tsx`, dashboard reminder queue — + wire the sibling previews. +- Tests: string-identity golden + prepareDraft/createDraft parity + preview-permissive / + save-strict divergence + route auth. + +## 7. Success criteria + +Staff draft a quotation and watch the letterhead build itself in under ~400ms per edit, +with GST (incl. IGST split for out-of-state) always shown by the server; the on-screen +paper is byte-identical to the PDF that later prints; no blank flashes; save remains +strict so bad drafts never persist; the same mechanism previews the company letterhead, +quote-content bullets, and reminder emails.