D
DreamLake

Preview

Design

<FilePreview> is shaped by a few concrete constraints. Each section here is a "why we did X" rather than a "how to use X" — see API for usage.

Why a separate system from dtypes / views / lanes

The TrackerContainer + dtype dispatch system encodes business semantics: a joint_angles track is known to be radians on a robot DoF axis, sampled at 30–250 Hz, scrubbable on a clock. That is the right abstraction for time-synchronized telemetry.

<FilePreview> deliberately does not participate in any of that:

  • No dtype — the input is a URL. Kind is resolved from filename extension; semantics are syntactic, not domain-bound.
  • No clock — render once. No useFrame, no subscribe, no requestAnimationFrame loop.
  • No lane — output is a single frame, not a row in a timeline.

The two systems compose freely (a dashboard typically uses both), but they do not share types.

Why no HEAD request by default

The naive approach to "I have a URL, what kind is it?" is a HEAD request to read Content-Type and Content-Length. We do not do this:

  • Cost — a HEAD request doubles the round-trip count for every file. For a panel of 10 files, that is 10 extra round trips before any pixels.
  • Pre-signed URLs — S3 / GCS / CloudFront pre-signed URLs are typically scoped to a single verb (GET). HEAD frequently fails with 403 even though the file is fetchable.
  • Listing APIs already know — when the file came from a listing API (DreamLake's episode.files(), S3 ListObjects, etc.), the caller already has size and content-type. Forcing a HEAD throws that away.

Instead, kind is resolved from the URL extension. The caller can pass size and contentType directly via props (the listing-API case), or opt into a HEAD probe via the probe prop (see API).

Why hard limits for images, soft limits for text

Images are loaded by the browser's native <img> element. We have no streaming control once the request is in flight, and a 200 MB PNG will pin a tab. So the limit is hard: above the threshold, render a "too large" placeholder with a download link, no fetch.

Text-shaped formats (text, code, markdown, csv, jsonl) tolerate truncation gracefully — a partial CSV missing its last 30 rows is still readable, and a partial markdown missing its last paragraph still renders. So the limit is soft: a Range: bytes=0-N request fetches the prefix, the previewer surfaces a truncation banner, and the user can click through to download.

Why /preview is a submodule export

The previewers pull in heavy deps: highlight.js (~600 KB minified), react-markdown + remark-gfm, @mcap/core, an NPY parser. Bundling all of that into the main @vuer-ai/vuer-m3u entry would inflate every consumer who only wants timeline views.

Instead, <FilePreview> is exported under @vuer-ai/vuer-m3u/preview so importing from the main entry never pulls preview code into the bundle:

import { JointAngleView } from '@vuer-ai/vuer-m3u'           // no preview deps
import { FilePreview } from '@vuer-ai/vuer-m3u/preview'      // preview shell only

Why React.lazy per previewer

Within the preview entry, each kind's previewer is itself behind React.lazy. A consumer who only ever previews CSV never downloads the markdown bundle. Each chunk is named so it shows up clearly in network panels:

preview-image.[hash].js    // ~5 KB (just the <img> wrapper)
preview-text-code.[hash].js // ~620 KB (highlight.js + languages)
preview-markdown.[hash].js  // ~140 KB (react-markdown + remark-gfm)
preview-mcap.[hash].js      // ~280 KB (@mcap/core)

Security choices

  • SVG via <img src> — SVG can contain <script> tags. Rendering as an <img> (rather than inline) means the browser ignores those scripts, regardless of source.
  • react-markdown without rehype-raw — raw HTML in markdown is not interpreted. Untrusted markdown cannot inject <script>.
  • No <iframe> — we do not embed unknown content in iframes. Unsupported kinds render a download link.
  • No eval / new Function — the NPY parser, CSV parser, and JSONL splitter are hand-written; no string-to-code paths.

Why a header bar even for tiny files

The header bar shows filename, size, kind, and a download button. It is always present (there is no "headless" mode in the public API), because:

  • Filename anchors the user's mental model, especially in a panel of switched files.
  • Size makes truncation visible — when a banner says "showing first 5 MB", the header confirms the original size.
  • Download is a fallback for every kind. If the preview is wrong or limited, the file is still one click away.

For embedding inside a custom card, set bare={true} on the surrounding <Preview> (the doc site's wrapper) — the FilePreview's own header still renders.