The static marketing, documentation, and playground frontend for the Lab programming language.
- Vite
- React and TypeScript
- Tailwind CSS
- pnpm
pnpm install
pnpm devThe production build is a static client application:
pnpm build
pnpm previewThe host must serve index.html as the fallback for client-side routes such as /docs and /playground.
src/pages/home-page.tsx: marketing pagesrc/pages/docs-page.tsx: documentation layout, sidebar, on-page TOC, pagersrc/pages/playground-page.tsx: editable playground shellsrc/components/: shared site and source-code presentationsrc/data/examples.ts: representative Lab examplessrc/content/docs/: documentation content, see belowsrc/lib/use-page-meta.ts: per-route title, description, canonical and card tagspublic/brand/: brand assets, generated, see below
Everything under public/brand/, plus public/lab-mark.*, is generated by
scripts/brand-assets.py from the geometry in that file. The output is
committed, so regenerating is only needed when the mark or a wordmark changes:
python3 -m venv .venv
.venv/bin/pip install -r scripts/requirements.txt
.venv/bin/python scripts/brand-assets.pyIt also needs rsvg-convert for the PNG exports (brew install librsvg) and
an installed node_modules, since it reads Crimson Pro from the
@fontsource-variable/crimson-pro package.
public/og/lab.png, the card a link to the site unfurls to, is generated the
same way by scripts/og-images.py, which shares its typesetting with
scripts/brandlib.py:
.venv/bin/python scripts/og-images.pyDo not hand-edit these files. The word in each wordmark is emitted as outlines
rather than live text, so there is nothing editable in them anyway; that is
deliberate, because librsvg synthesizes bold for a variable face instead of
selecting its 600 instance and would otherwise set the PNG about a quarter
wider than the SVG. The brand page at /brand documents what each asset is
for.
Docs pages are .mdx files under src/content/docs/, discovered
automatically: adding a file adds a page, no route or nav wiring required.
Each starts with frontmatter:
---
title: The two arrows
eyebrow: Syntax
description: One-sentence dek shown under the page title.
group: Language guide
order: 50
---title,eyebrow,description: rendered in the page header.group: which sidebar section the page belongs to. Must match one of the names inGROUP_ORDERinsrc/lib/docs-content.ts; a new group needs a line added there.order: sort key, both within a group and for the prev/next pager across the whole doc set. Leave gaps (10, 20, 30, …) so a page can be inserted later without renumbering its neighbors.
The page's URL is its file path relative to src/content/docs/, so
guide/the-two-arrows.mdx serves at /docs/guide/the-two-arrows.
Search needs no wiring either. src/lib/remark-doc-search.ts splits each page
at its headings during the MDX build and exports the plaintext as sections,
which src/lib/docs-search.ts ranks; a new page is searchable as soon as it
renders. Results deep-link to a heading, so anchors have to match: both the
index and the rendered heading id come from slugify over the heading's full
text, formatting included.
The body is ordinary Markdown: headings, lists, GFM tables, blockquotes,
styled automatically to match the rest of the site by the component map in
src/components/mdx-components.tsx. Two things need no special syntax:
- A fenced code block's info string is its filename, and renders in the same
bordered, dark "vessel" window used everywhere else on the site:
```lab reporter.lab. Omit it for an unlabeled window. <Callout kind="note">…</Callout>reproduces the site's amber note box;kind="aside"is the neutral variant. Import it from../../components/callout(or../../../components/calloutone level deeper); MDX files are plain modules, so this is a normal import.
Anything else bespoke a page needs can be authored as real JSX directly in
the .mdx file, the same way. No content page should need dangerouslySetInnerHTML
or a one-off page component; if a new visual pattern is needed on more than
one page, add it to mdx-components.tsx or as a shared component instead of
repeating the JSX per file.
The playground runs the real compiler frontend in the browser. src/wasm/lab-ide-wasm/ holds a generated wasm-bindgen bundle of the lab-ide-wasm crate from the sibling Lab checkout, which wraps the same lab-ide workspace the LSP server uses. Diagnostics, document symbols, completions, hover, definition, references, rename, semantic tokens, and formatting are answered by the compiler, not by a JavaScript approximation of it.
src/lib/lab-engine/engine.ts is the only thing that touches the wasm module. LabEngine owns one LabWorkspace, keeps the last text set per path, and converts every span at the boundary: the Rust side is UTF-8 byte-offset native and CodeMirror is UTF-16 index native, so byte-offset.ts translates in both directions, including for spans returned against a file other than the active one. Every method is async even though the underlying calls are synchronous, so moving the workspace to a Web Worker stays an internal change. use-lab-engine.ts is the React binding, and src/components/playground/lab-editor.tsx is its only consumer.
The wasm bundle is committed, because there is no Rust toolchain at deploy time. Regenerate it with scripts/build-wasm.sh whenever lab-ide-wasm or its lab-ide/lab-language dependencies change, then commit the result:
LAB_REPO=/path/to/lab scripts/build-wasm.shThe script defaults LAB_REPO to a sibling ../lab checkout, and fails with a remediation message if the wasm-bindgen CLI is missing, if its version does not match the crate version pinned in the Lab workspace's Cargo.lock, or if the wasm32-unknown-unknown target is not installed. Do not hand-edit anything under src/wasm/.