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, nosubscribe, norequestAnimationFrameloop. - 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(), S3ListObjects, 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 onlyWhy 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.