Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
36 changes: 36 additions & 0 deletions .claude/skills/docs-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: docs-skill
description: Conventions and verification workflow for writing and restructuring the documentation site (docs/content). Should be used when adding, editing, or reviewing docs pages.
---

# Documentation

The documentation site lives in `docs/content/docs` (fumadocs + Next.js). These are the conventions for writing and verifying it.

## Structure principles

- **Simple first, architecture later.** Lead every feature section with the shortest working setup (install, one snippet, "that's all"). Explain the underlying architecture and extension points afterwards, or in a separate "Custom …" section. Never make the reader wade through the extension contract to find the pre-configured default.
- **One page per feature the user searches for.** Users scan the sidebar for the thing they want — don't bury a feature inside a page about a different feature. Splitting a long page is preferable to a grab-bag: stale content hides in 400-line pages and gets found in 100-line ones.
- **Single-source each mechanism.** Document a mechanism in exactly one place — the page that owns the concept — and link to it from everywhere else it's relevant. Copies of the same explanation on multiple pages drift apart independently.
- **Complete implementations are live examples; page snippets are walkthroughs.** A page-length code block duplicating a full implementation is never compiled or run, so it silently rots — put the complete implementation in an embedded example (which is type-checked and rendered, so it can't) and keep page snippets small, each carrying one concept: a config shape, a component invocation, a signature.
- **Setup snippets are self-contained.** A snippet the reader is meant to copy includes the imports for each symbol it uses — without them it doesn't work when copied. Walkthrough fragments of an implementation the page's example completes may omit imports.
- **Order sections by audience breadth.** Content most readers need comes first; content specific to one feature or use case goes at the end of the page, even when it feels related to an earlier section. A reader following a cross-link to the specific section finds it regardless of position — the reader skimming the page shouldn't wade through it.
- **Live examples cap the setup they demonstrate.** Place an `<Example>` embed after the prose has introduced everything the example's code uses — an embed whose `App.tsx` shows APIs the page never mentions teaches by confusion. Introduced means named, given a purpose, and linked — the example itself (and the linked component pages) can carry the full wiring; expanding every integration inline pushes the example too far down the page. Not at the top (a demo without context motivates but doesn't teach) and not at the bottom (readers rarely reach it); at the end of the "getting it working" narrative, before advanced/optional topics. If an example uses more API than the page should cover, simplify the example rather than the rule.

- **Signature snippets tell the truth for one API.** A snippet formatted as a type signature documents exactly that export — never fold variant differences ("only for the X subpath…") into a doc-comment inside it. Show the signature that is true everywhere, and describe variant-specific options as prose in the variant's own section.
- **Caveats must be actionable.** Only note a limitation if the reader can do something with it (install a package, avoid a pattern, pass an option). Speculative hedges ("rare X may behave differently") and defensive implementation details (what a function guards against internally) erode trust without helping anyone act — cut them.

## Verifying content

- **Verify snippets against the actual package exports, not memory or existing docs.** APIs drift; grep the package source for every symbol a snippet imports (`export function X` / `export const X`) and check option names and shapes. Content copied forward without this check stays wrong after refactors.
- `node docs/validate-links.mjs` validates internal routes **and** `#anchors` (works from any directory). Anchors follow github-slugger: lowercased, spaces to dashes, symbols dropped — "A & B" becomes `#a--b`.
- **Render before declaring done**: `pnpm --filter docs run dev` (port 3000), then check each touched route returns 200 (the dev server compiles MDX on demand, so a 200 also proves the MDX compiles) and grep the HTML for expected headings/anchor ids. For visual checks, `cd tests && pnpm exec playwright screenshot --full-page --wait-for-timeout 8000 <url> <out.png>` and inspect the image — example embeds load lazily, so give them the wait.

## Site mechanics

- Navigation order comes from each directory's `meta.json` (`pages` array, `"..."` = the rest alphabetically). Directories without a `meta.json` sort alphabetically.
- `index.mdx` pages ending in `<CardTable path="..." />` list their section's pages automatically from frontmatter — new pages appear without extra wiring.
- `<Example name="group/project" />` embeds a live example; the name is the example's slug without number prefixes (`examples/06-custom-schema/09-math-block` → `custom-schema/math-block`).
- Frontmatter needs `title` and `description`; the description doubles as the CardTable card text.
- Strange dev-server behavior — `JSON.parse` errors on unrelated pages, new pages missing from the sidebar — usually means stale or corrupted generated state, not a content bug: restart the dev server (fumadocs' source map is built at startup) and, if errors persist, delete `docs/.next` (regenerable cache) before debugging further.
- Renaming or removing a heading breaks external links to its anchor silently — when restructuring, leave a pointer (e.g. a short "Related" section) where a well-known anchor used to be.
30 changes: 15 additions & 15 deletions .claude/skills/testing-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ In most cases, once a feature, bug fix, or other modification has been written,

`/packages/xl-*`: Contain tests for functionality included in a given `xl-*` package.

### Colocated Browser Unit Tests

`packages/*/src/**/*.browser.test.{ts,tsx}`: Unit tests for browser-only implementations (e.g. canvas or DOM-dependent code) live next to the code they test, with a `.browser.test` suffix. They run as part of the browser suite in Docker (the `tests` package's browser config includes them); the packages' own node-mode vitest configs exclude them. Use this when the unit under test genuinely needs a real browser — everything else should be a plain node unit test.

### End-to-End Tests

`tests/src/end-to-end`: Any test which interacts with the editor UI or simulates user interaction goes here. New subdirectories can be added if the functionality being tested is not covered by any of the existing ones. Important note about existing E2E tests - many are written poorly and should only loosely be used as reference. We want to avoid abstraction layers and `waitForTimeout` as much as possible.
`tests/src/end-to-end`: Tests that need a real browser and span multiple packages go here — chiefly tests which interact with the editor UI or simulate user interaction, but also browser integration tests that exercise complete flows without interaction (e.g. exporting a full document, static rendering). New subdirectories can be added if the functionality being tested is not covered by any of the existing ones. Important note about existing E2E tests - many are written poorly and should only loosely be used as reference. We want to avoid abstraction layers and `waitForTimeout` as much as possible.

## When & How to Add Tests

Expand All @@ -29,6 +33,8 @@ However, this may not be true when adding edge case handling or a new feature, w

We want to avoid adding end-to-end tests where it's possible to use unit tests instead.

**Don't use jsdom** (`@vitest-environment jsdom`) in new tests. It's a murky middle ground — `document` exists but rendering doesn't — which makes browser-capability checks pass while the capability itself is broken. Use the default node environment with pluggable seams for logic, and the browser suite (`tests/src/end-to-end`, vitest browser mode in Docker) for anything that needs real rendering.

## Running & Updating Tests

### Unit Tests
Expand All @@ -39,24 +45,18 @@ Updating tests can be done by adding the `-u` argument, i.e. `vp run test -u`. A

### End-to-End Tests

End-to-end tests run inside a docker container. While its possible to run them outside of it, we do not have existing snapshots to compare results with, and the results sometimes differ to when they're run within Docker, so it's not worth doing.

To run end-to-end tests, you must first build the project and run the preview. You can do this by running `vp start` from the root directory.
End-to-end tests run in vitest browser mode (chromium, firefox and webkit) inside a Docker container, so screenshot baselines are identical locally and on CI. Run them from the repository root:

You can then run the tests from the `/tests` directory using the following command:

```
docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.51.1-noble npx playwright test
```bash
bash tests/docker-run.sh -e CI=1 -- --run [filters]
```

A specific test file may be targeted by appending its name, i.e. `... npx playwright test fileName`. Individual tests in a file may be disabled using `skip`, i.e. `test.skip("Test name", ...)` (remember to revert this once all tests pass).
A specific test file may be targeted by appending (part of) its name as a filter. A single browser may be targeted with `--project "e2e (chromium)"`. Individual tests in a file may be disabled using `skip`, i.e. `test.skip("Test name", ...)` (remember to revert this once all tests pass).

Updating tests can be done by adding the `-u` argument, i.e. `... npx playwright test -u`. All of the other things you can do to scope which tests to target still apply.
Screenshot baselines can be regenerated with the `-u` argument, which must come **after** the filters (`--run <filters> -u`): written as `--run -u <filter>`, the filter is parsed as the flag's value and the **whole** suite runs in update mode, silently rewriting unrelated baselines. Note that `-u` only rewrites baselines whose comparison **fails** — a small intended change (e.g. a short text edit) that fits inside the suite's 2% pixel tolerance leaves the baseline stale while the test passes. To force a fresh capture, delete the baseline file first. Baselines are per-browser (`<name>-<browser>-linux.png`); after regenerating, always inspect the images before committing them.

Note that running this command may result in errors or other issues, listed below along with what to do when encountered:

- **Tests failing to navigating to preview**: project should be built and the preview started, after which the command should be run again.
- **Docker not running**: the user should be notified to launch Docker.
- **Incorrect Playwright image version**: update Playwright images and re-run the command.
If Docker isn't running, notify the user to launch it.

When testing a visual change, prefer writing screenshots to verify that the change is working as expected.

**Screenshots of tall content**: browser-suite tests run inside a tester iframe sized to the browser window (1280x720), and element screenshots only contain what the iframe actually paints — anything below its fold captures as blank white, silently. Growing the iframe with `page.viewport()` alone doesn't fix this at full resolution: the harness scales the iframe down to fit the window, shrinking the resulting baseline (`static.test.tsx` accepts that trade-off). For full-resolution captures use `screenshotFull` (`tests/src/utils/screenshotFull.ts`), which grows the iframe past the content and neutralizes the harness's scale transform during the capture — the same mechanism upstream Vitest adopted in vitest-dev/vitest#9745 (milestone 5.0.0; the util can be deleted once vite-plus ships it). Always eyeball newly generated baselines for truncation.
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

BlockNote is a block-based rich text editor for the web. It's designed as a batteries-included product that offers a solid user experience with minimal setup. However, it also offers extensibility via plugins and custom block types.

# Code Conventions

- **Leverage the type system so mistakes surface at compile time, not runtime.** Model states and outcomes explicitly: discriminated unions over boolean flags with optional fields, no `any` or casts that hide a case a caller should handle, exhaustive `switch`es over union members. If the compiler can enforce a contract, prefer that over documentation or runtime checks.
- **Expected failures are values, not exceptions.** When an operation can fail as part of normal use (canonical example: parsing user input, like LaTeX or Mermaid source), that failure is part of the function's contract — so it belongs in the return type. Catch it at the lowest level (the small adapter around the throwing third-party call) and convert it into a Result-style discriminated union (e.g. `{ error?: undefined; ...data } | { error: string }`). The failure then propagates through the type system, and the compiler forces every caller to decide how to handle it. Exceptions don't appear in TypeScript signatures, so a thrown expected error is invisible to callers — and a `try/catch` around a whole pipeline conflates expected failures with genuine bugs.
- **Exceptions are only for unexpected failures** — broken invariants, environment or infrastructure problems, programmer errors. Let them propagate and fail loudly; don't catch-and-continue. Corollary: never render a caught exception's message into user-facing output (documents, UI) — a catch-all can capture anything, and arbitrary messages can leak internals. Only messages carried by typed expected-error results are known-safe to display.
- **Prefer `function name() {}` declarations over `const name = () => {}`** for named functions (anonymous callbacks and returned closures can stay arrows).

# Common Commands

All commands below are listed under `package.json` in the project root. See `vite.config.ts` for relevant configuration settings.
Expand Down
Loading
Loading