@taylordb/typst
v0.3.0
Published
Typst for TaylorDB apps: a browser-side Typst compiler (typst.ts / WASM) and a React editor with a live page preview, a collapsible CodeMirror source drawer, and file-backed saving.
Readme
@taylordb/typst
Typst for TaylorDB apps: a browser-side compiler and a React editor with a live page preview and a TinaCMS-style source drawer. The whole edit → compile → preview loop runs client-side, on typst.ts (the Typst compiler and renderer compiled to WebAssembly).
import { TypstEditor } from '@taylordb/typst'
import '@taylordb/typst/styles.css'
<TypstEditor
defaultSource={"= Hello\nWorld"}
inputs={{ name: 'TaylorDB' }} // exposed as sys.inputs in the document
editorSide="right" // drawer side: 'left' | 'right' (default 'right')
drawerWidth={380} // px (default 380)
defaultOpen // drawer starts open (default false)
onSave={saveToServer} // adds the Save button and ⌘S
pdfFilename="report.pdf"
/>Four entry points, split so each side only carries what it needs:
| Import | Contains | Pulls in |
| --- | --- | --- |
| @taylordb/typst | compiler + React editor | typst.ts, React |
| @taylordb/typst/compiler | compiler only | typst.ts |
| @taylordb/typst/node | .typ file store | node:fs |
| @taylordb/typst/styles.css | editor stylesheet | — |
The root and /compiler share one internal chunk, so importing both costs
nothing extra.
Editor
TypstEditor— preview filling the surface plus the source drawer: header with compile status, outline / problems panels, an overflow menu, and a close button; when closed, a TinaCMS-style vertical "Edit" tab (same--taylor-cms-*theme variables, hover slide-out, and periodic nudge as@taylordb/cms) reopens it. The drawer overlays the preview, so it starts closed and never opens by itself. The open/closed choice persists in localStorage (taylordb.typst.drawerOpen, same protocol as Tina'stina.embedSidebarOpen);defaultOpenapplies only before the user has toggled it.?drawer=collapseon the page URL forces the drawer closed for that load (for screenshots), and?drawer=expandseeds it open while the user has not toggled it — the same param@taylordb/cmshonours. The CodeMirror editor (+ Typst grammar, ~130 KB gzip) is code-split and lazy-loaded — prefetched during idle time so opening feels instant, with a loading state in the drawer if it's still arriving. Typst syntax highlighting comes fromcodemirror-lang-typst's wasm-free Lezer parser. Controlled (source+onChange) or uncontrolled (defaultSource).- Saving — pass
onSave(source)and the drawer grows a Save button bound to ⌘S/Ctrl+S. It tracks unsaved changes (Save → Saving… → Saved) and shows Retry if the promise rejects. Pair it with/nodeto persist the document as a.typfile. - Save events — each successful save dispatches a
taylor-cms:saveCustomEvent onwindowwithdetail: { collection, relativePath, savedAt }— the same signal@taylordb/cmssends after a Tina save, which the TaylorDB preview platform commits on. Identify the document withsaveEvent={{ detail: { collection, relativePath } }}, addpostToParentfor a host that frames the app instead of injecting into it, or passsaveEvent={false}to stay silent.announceSave()is exported if you need to fire it yourself. - External changes — pass
externalSourcewhen the app watches the file and re-reads it. With no unsaved edits the editor adopts the new text silently; with unsaved edits it shows a Keep mine / Load changes notice rather than clobbering them. - Unsaved-changes guard — while dirty, ⌘R/Ctrl+R is intercepted and the
editor shows its own Keep editing / Discard & reload / Save & reload
dialog (
confirmReload={false}opts out). A reload from the browser's toolbar or a tab close cannot be intercepted by any page, so those fall back to the browser's native beforeunload prompt, whose wording and styling are fixed by the browser. - Editor panels and commands — the drawer header toggles an Outline
(headings read off the Typst syntax tree, so
=inside raw blocks or code is ignored; selecting one moves the cursor) and a Problems panel (compiler diagnostics with their hints, plus a count badge). The⋯menu carries undo/redo, find & replace, select all, toggle line comment, fold / unfold all, and PDF export. The editor itself has autocompletion for Typst built-ins, symbols, local bindings and labels, heading/block folding, and CodeMirror's search panel on⌘F. TypstPreview— the rendered page cards + diagnostics overlay, with an optional floating toolbar (showToolbar) — one pill, its sections split by hairline dividers: jump-to-cursor (whengetJumpTargetis provided), zoom out / percent / zoom in (clicking the percent resets to fit-width), a Chrome-PDF-style page field — an editable current-page box next to/ 7, tracking the page on screen, with ↑/↓ to step — for multi-page documents, and download + print.onDownloadadds the download button and drives its busy state (downloadingforces it); print is on by default (showPrint={false}to drop it) and prints the already-rendered vector pages without recompiling, titled byprintTitle. SetshowDiagnostics={false}when the host reports errors elsewhere. The compiler's own stylesheet — which contains global rules such assvg { fill: none }— is scoped to the page cards so it can't leak into the surrounding app.useTypstCompiler— debounced compile hook returning{ svg, diagnostics, compiling }; keeps the last good page during errors.
Compiler
compileToSvg({ source, inputs })— compile Typst source to an SVG stringcompileToPdf({ source, inputs })— compile to a PDFUint8ArraycompileToVector({ source, inputs })— typst.ts's compact vector artifactdownloadPdf(pdf, filename)— trigger a browser downloadinitTypst(options)— optional; configure where the wasm modules load from
inputs is exposed to the document as sys.inputs — the hook for injecting
TaylorDB record data into templates.
files mounts a virtual filesystem for the document, which is how images and
data reach a compiler that has no disk and no network:
compileToSvg({
source: '#image("logo.png")\n#json("data.json").title',
files: {
'logo.png': logoBytes, // Uint8Array → binary file
'data.json': '{"title": "Hi"}', // string → source file
},
})The document compiles at /main.typ, so relative references resolve against
the root: #image("logo.png") needs the key logo.png. Entries persist on the
shared compiler between calls, and only changed ones are re-sent — a
keystroke-rate recompile does not re-upload every image.
Compile failures reject with TypstCompileError, carrying the compiler's
diagnostics as { severity, message, hints? } entries. typst.ts surfaces
them as one Rust-Debug-formatted string, so they are parsed back into
individual diagnostics here; the compiler reports opaque span ids rather than
line numbers, so no source range comes with them.
Import these from @taylordb/typst/compiler when you don't want the editor.
That entry has no React dependency at all — useful in a web worker, a
render-only page, or any build where React shouldn't be a peer requirement:
import { compileToPdf, initTypst } from '@taylordb/typst/compiler'The root entry re-exports the same functions, so @taylordb/typst alone is
fine when you're already rendering the editor.
Document files (@taylordb/typst/node)
A Node-only entry point for keeping documents as .typ files on disk — the
server half of an editor whose Save button writes back to the repo. Import it
from server code only; it pulls in node:fs.
import { createTypstFileStore } from '@taylordb/typst/node'
const documents = createTypstFileStore({ dir: 'content/documents' })
await documents.readOrCreate('main.typ', '= Hello\n') // source, creating it if absent
await documents.write('main.typ', source) // write-then-rename, so a crash can't truncate
await documents.list() // ['main.typ', …]
await documents.listAssets() // ['logo.png', …]
await documents.readBytes('logo.png') // Uint8Array | null
// Push external edits (an agent, a script) back to the browser.
const stop = documents.watch((name) => notifyClients(name))Names are restricted to letters, digits, dots, dashes and underscores, so a document name can never escape the store's directory.
Wasm loading
By default the two wasm modules (compiler ~27 MB, renderer ~1 MB) load lazily from the jsDelivr CDN, pinned to the bundled typst.ts version. To self-host them in a Vite app:
import compilerWasm from '@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm?url'
import rendererWasm from '@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm?url'
import { initTypst } from '@taylordb/typst'
initTypst({ compilerWasm, rendererWasm })Call initTypst before the first compile; the first compile call initializes
with the CDN defaults otherwise. Font assets are fetched remotely by typst.ts
on first compile, so the browser needs network access unless you preload fonts.
Playground
See apps/typst/playground for a running example
(pnpm --filter @taylordb/typst-playground dev, port 3002). It edits
content/main.typ on disk: a small Vite middleware in its vite.config.ts
serves GET/POST /api/document through @taylordb/typst/node, so Save
exercises the same load/save path a real app uses.
