← All writing

Technical note · 2026-09-13

One attachment entry: splitting preview paths by format

Focus on dispatch: classify capability by extension first, lazy-load a parser per format, and set explicit limits for sheets, PDF pages and download fallback instead of one modal that owns every file type.

  • Attachment preview
  • PDF.js
  • SheetJS
  • docx-preview
  • Code splitting

Related case: Smart ledger and order management. This note follows a local attachment-preview module’s dispatch and render boundaries. Examples keep the type decision and limits, and omit business URLs and host-bridge details. Compatibility is covered separately in previews that fail in an older WebView.

One “Preview” button is several capability paths

In a ledger or order view, “Preview” feels like one action. Implementation may be text, a spreadsheet, Word or PDF. If every parser lives in one component:

  • opening the list can pull PDF/Excel dependencies and slow the first paint;
  • unsupported types show a silent blank, so users do not know whether to download or retry;
  • large sheets or long PDFs can freeze the main thread and look like a crash.

A clearer boundary: the entry only decides “what kind of file is this, and can we preview it”; each format owns “how to render, and how far.”

Classify first, then choose a renderer

Locally, preview type is derived from the URL/extension, then dispatched:

type PreviewType = 'image' | 'pdf' | 'text' | 'excel' | 'docx' | 'download'

function attachmentPreviewType(url: string): PreviewType {
  if (/\.(?:jpe?g|png|gif|webp|svg)(?:$|[?#])/i.test(url)) return 'image'
  const ext = extensionOf(url)
  if (ext === '.pdf') return 'pdf'
  if (ext === '.txt') return 'text'
  if (ext === '.xlsx' || ext === '.xls') return 'excel'
  if (ext === '.docx') return 'docx'
  return 'download'
}

Three points matter:

  1. unknown types degrade to download instead of guessing a MIME and forcing a renderer;
  2. classification is separate from rendering, so list items can show a type chip without parsing the file;
  3. network errors and parse errors are different messages—HTTP failure is not “workbook has no sheets”.

Lazy-load parsers and publish display limits

Import libraries only when needed: SheetJS for Excel, docx-preview for Word, PDF.js for PDF. Attachments that are never opened never pay for those bundles.

Limits belong in the interaction, not only in comments:

  • Excel: cap rows/columns (for example first 1000 rows, 100 columns) and say “showing a subset”;
  • PDF: cap pages (for example first 30) so long documents do not flood canvases;
  • text: empty files get an explicit “0 bytes” note instead of a blank box.
Open attachment
  ├─ type = image/pdf/text/excel/docx
  │     └─ lazy-load renderer → bounded preview + download
  └─ type = download
        └─ download/host save path, no parser

Download is a first-class path, not a failure

Preview capability will always be smaller than “whatever users upload”. Treating “not previewable, please download” as a success branch is more honest than pretending every file can render.

Inside a desktop host, download may also need a native save API that keeps the original filename. Keep browser a[download] and host-bridge save under one download entry, instead of making the preview component guess the runtime.

Verify with minimal samples per format

Cover at least: a small PDF, a multi-page PDF, a multi-sheet xlsx, an empty txt, an unsupported extension, and a 4xx on download. Check that only used dependencies load, limit notices appear, failures are actionable, and host downloads keep filenames.

Split preview into classify → lazy render → explicit limits → download fallback, and the entry stays stable while formats keep changing.