diff --git a/.cursor/commands/add-locale-string.md b/.cursor/commands/add-locale-string.md new file mode 100644 index 0000000..c0fa4fd --- /dev/null +++ b/.cursor/commands/add-locale-string.md @@ -0,0 +1,34 @@ +# Add locale string (i18n) + +Use for **user-visible** text in the **library** (`packages/excalidraw`), not hardcoded English. + +## 1. Pick a key + +- Prefer existing namespaces: **`labels.*`**, **`toolBar.*`**, **`hints.*`**, etc. — follow keys near similar UI in `packages/excalidraw/locales/` (e.g. `en.json`). + +## 2. Add English source + +- Edit the appropriate locale JSON under **`packages/excalidraw/locales/`** (start from **`en.json`** or the pattern used for your language). +- Keep placeholders consistent with existing messages (`{variable}` style if the codebase uses interpolation — match siblings). + +## 3. Use in UI + +- Import **`useI18n`** from `../i18n` (or the established relative path from the component). +- Call **`const { t } = useI18n()`** and use **`t("your.key")`** for labels, titles, buttons, tooltips. + +## 4. Actions + +- Action **`label`** (and **`keywords`**) should use **translation keys**, not raw English strings, when exposed in menus/command palette. + +## 5. Crowdin + +- Project translations are managed via **Crowdin** (per project docs); adding keys in English is the usual first step; translators pick up new keys there. + +## Do not + +- Hardcode user-facing strings in **`packages/excalidraw/components/`** for production UI. +- For **`excalidraw-app/`**-only UI, follow existing app patterns (some strings may be app-specific — mirror nearby files). + +## See also + +`dev-docs/docs/a-docs/08-adding-features.md`, `.cursor/rules/excalidraw-ui.mdc`. diff --git a/.cursor/commands/app-only-feature.md b/.cursor/commands/app-only-feature.md new file mode 100644 index 0000000..7495a68 --- /dev/null +++ b/.cursor/commands/app-only-feature.md @@ -0,0 +1,32 @@ +# App-only feature (excalidraw.com) + +Use when the change targets **`excalidraw-app/`** only and must **not** bloat **`@excalidraw/excalidraw`**. + +## Boundaries + +- **App shell:** routing, welcome, share dialogs, Plus hooks, Firebase, collab wiring — **`excalidraw-app/`**. +- **Reusable editor:** canvas, actions, element model — **`packages/excalidraw/`** (published npm package). + +Prefer **props and callbacks** into `` rather than forking core editor logic into the app. + +## State + +- **App-level Jotai:** **`excalidraw-app/app-jotai.ts`** — not raw `jotai`. +- **Editor state:** still **`AppState` + elements** inside the package; app listens via **`onChange`** / APIs as today. + +## Config & services + +- **Env:** `VITE_APP_*` in `.env.development` / `.env.production`. +- **Firebase / WebSocket URLs:** see app `data/` and `collab/`; reuse **`packages/excalidraw/data`** for encode/decode and reconcile — do not duplicate JSON formats. + +## UI placement + +- Components in **`excalidraw-app/components/`** with colocated **`.scss`** when needed (match **`AppSidebar.scss`** patterns). + +## Finish + +`yarn test:typecheck`, tests under **`excalidraw-app/tests/`** if applicable, `yarn fix`. + +## See also + +`dev-docs/docs/a-docs/03-codebase-navigation.md` (app-level data), `.cursor/rules/excalidraw-app-integration.mdc`, `excalidraw-ui-app.mdc`. diff --git a/.cursor/commands/debug-collab.md b/.cursor/commands/debug-collab.md new file mode 100644 index 0000000..2a168ac --- /dev/null +++ b/.cursor/commands/debug-collab.md @@ -0,0 +1,30 @@ +# Debug collaboration + +Use when **live collaboration**, **shared rooms**, or **remote cursors** misbehave (drift, lost updates, duplicates, stale scenes). + +## Mental model + +- **Socket.io** (`excalidraw-app/collab/Portal.tsx`) carries realtime updates. +- **Firebase** (`excalidraw-app/data/firebase.ts`) persists encrypted scene + files for rooms. +- **Merge** uses **`reconcileElements`** in **`packages/excalidraw/data/reconcile.ts`** (version-based; ties / tombstones matter). +- **Encryption key** is in the URL **hash** — not sent to HTTP servers; do not log full share URLs in production debug. + +## Investigation order + +1. **Repro** — two browsers, same room, minimal steps (draw, delete, reorder, image paste). +2. **Local vs remote** — does the bug appear offline? If only online, suspect **reconcile**, **socket**, or **Firebase** timing. +3. **`Collab.tsx`** — `startCollaboration`, `syncElements`, `handleRemoteSceneUpdate`, save queues. +4. **`reconcile.ts`** — same element id, **version** / **isDeleted** / ordering after merge. +5. **Element mutations** — direct mutation or missing **version** bumps break reconciliation and undo. +6. **Images** — `FileManager`, Firebase file prefix, lazy fetch paths for peers’ assets. + +## Files to read first + +- `excalidraw-app/collab/Collab.tsx` +- `excalidraw-app/collab/Portal.tsx` +- `packages/excalidraw/data/reconcile.ts` +- `packages/excalidraw/data/encryption.ts` + +## See also + +`dev-docs/docs/a-docs/04-key-business-flows.md` (collaboration section), `.cursor/rules/excalidraw-data-collab.mdc`, `excalidraw-app-integration.mdc`. diff --git a/.cursor/commands/element-schema-change.md b/.cursor/commands/element-schema-change.md new file mode 100644 index 0000000..626c464 --- /dev/null +++ b/.cursor/commands/element-schema-change.md @@ -0,0 +1,50 @@ +# Element schema change + +Use when adding or changing fields on **drawing elements** (shapes, text, arrows, frames, images, etc.). + +## 1. Types + +- **`packages/element/src/types.ts`** — extend the correct element interface or shared base type. + +## 2. Creation defaults + +- **`packages/element/src/newElement.ts`** (and any specialized factories) — set defaults for **new** elements. + +## 3. Load / paste / migrate + +- **`packages/excalidraw/data/restore.ts`** — defaults and migrations for **old** JSON, clipboard, and shared links so older files still open. + +## 4. Mutation + +- Use **`mutateElement`** (or established helpers). **Never** mutate element objects in place without proper **version** / **versionNonce** behavior. + +## 5. Ordering / scene + +- If z-order or structure changes: **`packages/element/src/Scene.ts`** and **fractional `index`** rules — prefer Scene APIs over hand-rolled indices. + +## 6. Rendering & export + +- Canvas: **`packages/excalidraw/renderer/staticScene.ts`** +- SVG: **`packages/excalidraw/renderer/staticSvgScene.ts`** +- PNG / export pipeline: **`packages/excalidraw/scene/export.ts`** as needed + +## 7. Serialization + +- **`packages/excalidraw/data/json.ts`** (and related types) if the field must round-trip in `.excalidraw` JSON. + +## 8. Collaboration + +- **`packages/excalidraw/data/reconcile.ts`** — merging uses **version**; deleted elements stay as tombstones for a reason. +- Test **two clients** editing the same scene when the change affects concurrent updates. + +## 9. Tests + +- Add or extend tests under **`packages/element/tests/`** and **`packages/excalidraw/tests/`** as appropriate. + +## Finish + +`yarn test:typecheck`, `yarn test:update`, `yarn fix`. + +## See also + +`restore-migration.md`, `.cursor/rules/excalidraw-element-model.mdc`, `excalidraw-data-collab.mdc`. diff --git a/.cursor/commands/new-action.md b/.cursor/commands/new-action.md new file mode 100644 index 0000000..87a2e70 --- /dev/null +++ b/.cursor/commands/new-action.md @@ -0,0 +1,30 @@ +# New editor action + +Add a **user-triggered** state change via the action system (`ActionManager`). + +## Steps + +1. **Create** `packages/excalidraw/actions/actionYourFeature.ts` or `actionYourFeature.tsx`. +2. **Import** `register` from `./register`, `CaptureUpdateAction` from `@excalidraw/element`, and types with `import type` from `./types` and `../types` as needed. +3. **Implement** `export const actionYourFeature = register({ name, label, perform, captureUpdate, … })`: + - `perform(elements, appState, formData, app)` → `{ elements?, appState?, files?, captureUpdate }` or `false` to no-op. + - Choose **`captureUpdate`** to match the closest existing action (`IMMEDIATELY`, `EVENTUALLY`, `NEVER`, etc.). +4. **Export** from `packages/excalidraw/actions/index.ts` (required for registration). +5. **Shortcut** (optional): `keyTest` on the action and/or `packages/excalidraw/actions/shortcuts.ts`. +6. **UI** (optional): toolbar / menu / command palette — follow patterns in `Actions.tsx`, `MainMenu.tsx`, or command palette registrations. +7. **Label / i18n:** use a translation key for `label` (e.g. `"labels.myFeature"`) and add the string under `packages/excalidraw/locales/` (English + structure for Crowdin). +8. **Tests:** `packages/excalidraw/actions/actionYourFeature.test.tsx` with `render` / `unmountComponent` from `../tests/test-utils`, `API`, `Keyboard` where relevant. + +## Conventions + +- Do not import from `packages/excalidraw/index.tsx` inside the package; use concrete module paths. +- Prefer keeping pure state transforms in **`perform`**; use `App.tsx` only when pointer/tool lifecycle requires it. +- Mirror **`actionToggleStats.tsx`**, **`actionDeleteSelected`**, or a similar action for structure. + +## Finish + +Run `yarn test:typecheck`, `yarn test:update` if snapshots change, `yarn fix`. + +## See also + +`update-state-flow.md`, `dev-docs/docs/a-docs/08-adding-features.md`, `.cursor/rules/excalidraw-actions-state.mdc`. diff --git a/.cursor/commands/new-vitest-excalidraw.md b/.cursor/commands/new-vitest-excalidraw.md new file mode 100644 index 0000000..9550448 --- /dev/null +++ b/.cursor/commands/new-vitest-excalidraw.md @@ -0,0 +1,38 @@ +# New Vitest test (Excalidraw editor) + +Add or extend tests using the project’s **Vitest + jsdom + Testing Library** setup. + +## Setup (global) + +- **`setupTests.ts`** (repo root) — canvas, IndexedDB, fonts, RAF mocks apply automatically. +- **`vitest.config.mts`** — path aliases **`@excalidraw/*`** must match imports in tests. + +## Component / integration tests + +1. Import **`render`**, **`unmountComponent`** from **`packages/excalidraw/tests/test-utils`** (adjust relative path from test file). +2. **`beforeEach`:** `await render()` (or the minimal wrapper used in sibling tests). +3. **`afterEach`:** `unmountComponent()` when using that helper. +4. Use **`API`** (`../tests/helpers/api`), **`Keyboard`**, **`Mouse`** from **`../tests/helpers/ui`** when simulating editor actions — prefer existing helpers over brittle DOM queries. + +## Pure logic tests + +- **`packages/element/tests/`**, **`packages/math/tests/`**, **`packages/common/`** colocated `*.test.ts` — no React unless needed. + +## Running + +```bash +yarn test:app -- path/to/file.test.tsx +yarn test:app -- -t "partial test name" +``` + +## Snapshots + +- If output is correct, run **`yarn test:update`** and **review diffs** before committing. + +## Finish + +`yarn test:typecheck`, `yarn fix`. + +## See also + +`dev-docs/docs/a-docs/06-testing-guide.md`, `.cursor/rules/excalidraw-testing.mdc`. diff --git a/.cursor/commands/pre-pr-check.md b/.cursor/commands/pre-pr-check.md new file mode 100644 index 0000000..a59d0d2 --- /dev/null +++ b/.cursor/commands/pre-pr-check.md @@ -0,0 +1,33 @@ +# Pre-PR check + +Run this before opening a PR or pushing a feature branch. + +## Commands (repo root) + +```bash +yarn test:update +yarn test:typecheck +yarn fix +``` + +1. **`yarn test:update`** — runs Vitest and updates snapshots where intended. **Review every snapshot diff**; do not commit accidental UI regressions. +2. **`yarn test:typecheck`** — full TypeScript check for the monorepo configuration. +3. **`yarn fix`** — Prettier + ESLint auto-fix (`fix:other` + `fix:code`). + +## CI parity (optional but thorough) + +```bash +yarn test:all +``` + +Equivalent to typecheck + eslint + prettier check + tests (non-watch). + +## Manual sanity + +- [ ] New user-facing strings go through **`locales/`** (library UI). +- [ ] Element changes: **`mutateElement`** + version / collab implications considered. +- [ ] New deps: bundle / **size-limit** CI (`.github/workflows/size-limit.yml`) if the change affects published packages. + +## If something fails + +Fix in order: **types** → **lint** → **tests** → **snapshots** (only if output is correct). diff --git a/.cursor/commands/rendering-change.md b/.cursor/commands/rendering-change.md new file mode 100644 index 0000000..4167fb6 --- /dev/null +++ b/.cursor/commands/rendering-change.md @@ -0,0 +1,39 @@ +# Rendering / canvas change + +Use when changing **how elements are drawn** or how **canvas layers** behave. + +## Architecture + +- **Static canvas** — committed elements, **Rough.js**, should **not** redraw every pointer move. +- **Interactive canvas** — selection, handles, snaps, remote cursors. +- **New element canvas** — in-progress shape before commit. + +Orchestration lives under **`packages/excalidraw/components/canvases/`**; core drawing in **`packages/excalidraw/renderer/`**. + +## Files + +| Concern | Typical files | +|--------|----------------| +| Static bitmap | `renderer/staticScene.ts` | +| Selection overlay | `renderer/interactiveScene.ts` | +| SVG export | `renderer/staticSvgScene.ts` | +| Export to PNG / clipboard | `scene/export.ts` | + +## Performance + +- Preserve **caching** of Rough drawables (tied to element version — do not recreate all drawables on every frame). +- Avoid pushing static-layer updates from **`pointerMove`** unless necessary. +- Stress-test with **many elements** (hundreds+) after changes. + +## Consistency + +- If canvas appearance changes, check **SVG export** matches for the same scene where applicable. + +## Tests / checks + +- Relevant renderer tests or visual snapshots; `yarn test:app` for affected areas. +- `yarn test:typecheck` + +## See also + +`dev-docs/docs/a-docs/01-architecture-overview.md` (rendering), `.cursor/rules/excalidraw-rendering.mdc`, `excalidraw-canvas-components.mdc`. diff --git a/.cursor/commands/restore-migration.md b/.cursor/commands/restore-migration.md new file mode 100644 index 0000000..5b6cb1a --- /dev/null +++ b/.cursor/commands/restore-migration.md @@ -0,0 +1,30 @@ +# Restore & migration (`restore.ts`) + +Use when **saved files**, **clipboard**, or **shared links** must keep working after changing element or app state shape. + +## Primary file + +- **`packages/excalidraw/data/restore.ts`** — `restoreElements`, `restoreAppState`, `restoreLibraryItems`, version bumps where required. + +## Goals + +1. **Old JSON** still parses — add defaults for new fields; rename fields with explicit migration steps if needed. +2. **Invalid / partial data** — normalize defensively; match style of existing restore branches. +3. **Collaboration** — after restore, **element versions** must remain consistent with reconciliation expectations (see **`bumpElementVersions`** and related helpers in this file / callers). + +## Related + +- **`packages/element/src/types.ts`** — source of truth for element shape. +- **`packages/excalidraw/data/json.ts`** — serialization / types for file format. +- **Tests** — add fixtures or extend tests under **`packages/excalidraw/tests/`** or **`packages/element/tests/`** for old-format samples when possible. + +## Checklist + +- [ ] New field has a default in **`restore.ts`** +- [ ] No regression loading a **minimal** and a **real** saved `.excalidraw` file +- [ ] Clipboard paste from an **old** build still works if users mix versions +- [ ] `yarn test:typecheck` and targeted tests pass + +## See also + +`element-schema-change.md`, `dev-docs/docs/a-docs/05-important-components.md` (restore / migration), `.cursor/rules/excalidraw-data-collab.mdc`. diff --git a/.cursor/commands/size-budget.md b/.cursor/commands/size-budget.md new file mode 100644 index 0000000..7ce3206 --- /dev/null +++ b/.cursor/commands/size-budget.md @@ -0,0 +1,30 @@ +# Size budget & bundle impact + +Use before adding **heavy dependencies** or **large imports** to **`packages/excalidraw`** or the app entry. + +## Why + +- The published editor is sensitive to bundle size; CI may enforce limits. +- **`@size-limit/preset-big-lib`** is a dev dependency of **`packages/excalidraw`**; workflow automation lives under **`.github/workflows/size-limit.yml`**. + +## What to do + +1. **Prefer** existing utilities in **`@excalidraw/common`**, tree-shakeable imports, and **dynamic import** for rare code paths when the codebase already does so for similar features. +2. **Avoid** importing whole libraries when only a small API is needed. +3. After significant dependency adds, run a **production build** locally and compare size if unsure: + + ```bash + yarn build:packages + yarn build + ``` + +4. Open a PR early if the change is large — CI **size-limit** will report regressions. + +## Library vs app + +- **`packages/excalidraw`** — stricter; affects all embedders. +- **`excalidraw-app`** — still matters for first load; be deliberate with new chunks. + +## See also + +`packages/excalidraw/package.json` (size-limit scripts if present), `.github/workflows/size-limit.yml`. diff --git a/.cursor/commands/update-state-flow.md b/.cursor/commands/update-state-flow.md new file mode 100644 index 0000000..c4ce0b5 --- /dev/null +++ b/.cursor/commands/update-state-flow.md @@ -0,0 +1,110 @@ +# Update editor state (actions & related touchpoints) + +Use this flow when adding or changing **how the Excalidraw editor state is updated** (elements, `AppState`, files, undo/redo, shortcuts, or reactive UI). Follow branches that apply; skip the rest. + +## 0. Classify the change + +| Need | Primary path | +|------|----------------| +| User-triggered change (menu, shortcut, toolbar, command palette) | **Action** + `ActionManager` | +| New/changed field on a **drawing element** | **`@excalidraw/element`** types + factories + **`restore`** + often renderer/export | +| New/changed **editor chrome** state (tool, zoom, selection flags, …) | **`AppState`** / `types.ts` + **`appState.ts`** defaults + readers (hooks / UI) | +| Shared **reactive** UI state (panels, library, collab handles) | **Jotai** via `editor-jotai.ts` or `excalidraw-app/app-jotai.ts` | +| Behavior during **pointer / drag / tool** lifecycle | **`App.tsx`** handlers (keep small; prefer actions for pure state transforms) | +| **Binary attachments** (images, …) | `ActionResult.files`, `replaceFiles`; align with existing file APIs | + +State is **not** a single Redux store: it combines **`AppState` + elements**, **Jotai** atoms, **`Scene`**, and **History** deltas. + +--- + +## 1. New or changed **action** (most common) + +`ActionResult` shape (see `packages/excalidraw/actions/types.ts`): + +- `elements` — new element array or omit/null per existing patterns +- `appState` — partial `AppState` updates +- `files` / `replaceFiles` — when binary files change +- `captureUpdate` — **required**; controls **undo/redo** (`CaptureUpdateAction` from `@excalidraw/element`). Match a similar existing action. +- Return **`false`** to no-op. + +**Steps:** + +1. Add `packages/excalidraw/actions/actionYourFeature.ts` (or `.tsx`) using `register()` like sibling `action*.ts` files. +2. Implement `perform(elements, appState, formData, app)` → `ActionResult` or `false`. +3. Optional: `keyTest`, `predicate`, `PanelComponent`, `trackEvent`, icons — mirror nearby actions. +4. **Export** the action from `packages/excalidraw/actions/index.ts` (required for registration). +5. If it needs a **shortcut**, update `packages/excalidraw/actions/shortcuts.ts` and/or wire UI in the relevant component (`Actions.tsx`, menus, command palette). +6. **Tests:** `packages/excalidraw/actions/actionYourFeature.test.tsx` using `tests/test-utils`, `API`, `Keyboard`, etc. +7. Run `yarn test:typecheck`, `yarn test:update` if snapshots change, `yarn fix`. + +**Imports:** `import type` for types; inside `packages/excalidraw` do not import from the package barrel `index.tsx`. Jotai only from `editor-jotai.ts` / `app-jotai.ts`. + +--- + +## 2. New **element** field or behavior + +1. **`packages/element/src/types.ts`** — extend the right element / base type. +2. **`packages/element/src/newElement.ts`** (and any focused factories) — defaults for new elements. +3. **`packages/excalidraw/data/restore.ts`** — migrations / defaults for loaded or pasted data. +4. If it affects appearance or export: **`packages/excalidraw/renderer/staticScene.ts`**, **`staticSvgScene.ts`**, and **`scene/export.ts`** as needed. +5. **Immutability:** use **`mutateElement`** (and version discipline); never mutate element objects in place. +6. **Collaboration:** reconcile uses **version**; test concurrent edits if z-order or structure changes (`packages/excalidraw/data/reconcile.ts`). + +--- + +## 3. New **`AppState` / UI editor state** field + +1. **`packages/excalidraw/types.ts`** — `AppState` / `UIAppState` (or relevant subsection). +2. **`packages/excalidraw/appState.ts`** — default value and any reset paths. +3. Update **readers** (hooks like `useAppStateValue`, components, `App.tsx`) and any **serialization** if the field must persist (JSON / localStorage / share link) — follow how similar fields are saved in `data/json.ts` and app `LocalData` if applicable. + +--- + +## 4. **Jotai** (reactive UI, not the canvas element array) + +- Library/editor scope: **`packages/excalidraw/editor-jotai.ts`**. +- Hosted app scope: **`excalidraw-app/app-jotai.ts`**. +- Do not import `jotai` directly in feature code. + +Use atoms for UI that many components subscribe to; keep **elements** and core **`AppState`** updates flowing through **actions** when the change must be **undoable** and consistent with the rest of the editor. + +--- + +## 5. **Pointer / tool** wiring + +If the change starts from canvas interaction: + +- **`packages/excalidraw/components/App.tsx`** — `handleCanvasPointerDown` / `Move` / `Up` (and related). Prefer delegating the final state commit to an **action** or shared helper so `perform` stays testable. +- New **tool type**: `ActiveTool` in `types.ts`, toolbar UI, shortcuts, and `App.tsx` tool branches (see `dev-docs/docs/a-docs/08-adding-features.md`). + +--- + +## 6. **History (undo/redo)** + +- Incorrect **`captureUpdate`** breaks undo/redo or records noise. +- History uses **deltas**; avoid assumptions that break after **collab reconcile**. + +--- + +## 7. **Persistence, export, collab** + +- **JSON / clipboard / share:** `packages/excalidraw/data/json.ts`, `blob.ts`, app `data/` for backend. +- **Collaboration:** `excalidraw-app/collab/` + **`reconcileElements`**; encryption keys stay in URL **hash**. + +--- + +## 8. Checklist before PR + +- [ ] Types and defaults updated (`types.ts`, `appState.ts`, `newElement.ts`, `restore.ts` as needed) +- [ ] Action exported in `actions/index.ts` with correct `captureUpdate` +- [ ] Shortcuts / UI entry points wired +- [ ] Renderer / export updated if visual or SVG/PNG output changes +- [ ] Tests added or updated; `yarn test:update` if snapshots intentional +- [ ] `yarn test:typecheck` and `yarn fix` +- [ ] User-visible strings use **`locales/`** (library UI) + +## Reference docs & rules + +- `dev-docs/docs/a-docs/08-adding-features.md` +- `dev-docs/docs/a-docs/04-key-business-flows.md` +- `.cursor/rules/excalidraw-actions-state.mdc`, `excalidraw-element-model.mdc`, `excalidraw-data-collab.mdc`, `excalidraw-typescript-imports.mdc` diff --git a/.cursor/rules/excalidraw-actions-state.mdc b/.cursor/rules/excalidraw-actions-state.mdc new file mode 100644 index 0000000..a31ab20 --- /dev/null +++ b/.cursor/rules/excalidraw-actions-state.mdc @@ -0,0 +1,27 @@ +--- +description: ActionManager, ActionResult, captureUpdate, and registration +globs: + - packages/excalidraw/actions/**/*.ts + - packages/excalidraw/actions/**/*.tsx +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-actions-state APPLIED ⚡ + +# Actions (`packages/excalidraw/actions`) + +- Prefer **actions** for user-triggered, undoable editor changes. Wire keyboard shortcuts via **`keyTest`** and register with **`register()`** like existing `action*.ts(x)` files. +- **`perform`** should return an **`ActionResult`**: `{ elements, appState, captureUpdate, … }`, or **`false`** to no-op. +- Set **`captureUpdate`** correctly for **undo/redo** (e.g. `CaptureUpdateAction.IMMEDIATELY` when the change should be recorded — match nearby actions). +- After adding an action file, **export** it from **`actions/index.ts`** so it is registered with **`ActionManager`**. +- **`App.tsx`** holds pointer/tool orchestration; if an action only needs state transformation, keep logic in the action and keep `App.tsx` changes minimal. + +## How to verify + +- Confirm the new action is **exported** in **`packages/excalidraw/actions/index.ts`** and appears in UI/shortcuts as intended. +- **Manual:** trigger the action from menu, keyboard, and (if wired) command palette; confirm **undo/redo** matches expectations for the chosen **`captureUpdate`**. +- Run **`yarn test:typecheck`** and add or extend **`packages/excalidraw/actions/*.test.tsx`**; run **`yarn test:app --`** that file path. diff --git a/.cursor/rules/excalidraw-app-integration.mdc b/.cursor/rules/excalidraw-app-integration.mdc new file mode 100644 index 0000000..0ca89fc --- /dev/null +++ b/.cursor/rules/excalidraw-app-integration.mdc @@ -0,0 +1,26 @@ +--- +description: excalidraw-app collab, persistence, and boundaries with the library +globs: + - excalidraw-app/**/*.ts + - excalidraw-app/**/*.tsx +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-app-integration APPLIED 🌐 + +# App shell (`excalidraw-app`) + +- **`collab/`:** `Collab.tsx` orchestrates sessions; **`Portal.tsx`** wraps Socket.io. On remote updates, run **`reconcileElements`** (from `@excalidraw/excalidraw` data layer) before applying scene updates. +- **`data/`:** Local persistence (`LocalData`, IndexedDB), Firebase, file manager, tab sync — app-specific. Reuse **`packages/excalidraw/data`** for encode/decode, restore, and reconcile. +- **Env:** Vite exposes `VITE_APP_*` variables; don’t hardcode production URLs in new code without checking existing `.env.*` patterns. +- **Jotai:** app-level atoms live in **`excalidraw-app/app-jotai.ts`**, not in raw `jotai`. + +## How to verify + +- Run **`yarn start`** and exercise the feature (localStorage / share / collab as relevant). +- Run **`yarn test:typecheck`** and **`yarn test:app -- excalidraw-app`** (or specific tests under **`excalidraw-app/tests/`**). +- **Collab / Firebase:** test with dev **`VITE_APP_*`** defaults; confirm no new hardcoded prod URLs without env alignment. diff --git a/.cursor/rules/excalidraw-canvas-components.mdc b/.cursor/rules/excalidraw-canvas-components.mdc new file mode 100644 index 0000000..f4451dc --- /dev/null +++ b/.cursor/rules/excalidraw-canvas-components.mdc @@ -0,0 +1,23 @@ +--- +description: Static, interactive, and new-element canvas components +globs: packages/excalidraw/components/canvases/**/*.tsx +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-canvas-components APPLIED 🖼️ + +# Canvas components (`components/canvases`) + +- Keep the **multi-canvas** split: static layer for committed geometry, interactive layer for selection/handles/cursors, new-element layer for in-progress drawing. +- Avoid wiring **pointer move** handlers that unnecessarily bump static-canvas props or force full static re-renders; prefer updating the interactive or preview layer when possible. +- Coordinate with **`packages/excalidraw/renderer/`** — canvas components should orchestrate, not duplicate rendering logic that belongs in `renderStaticScene` / `renderInteractiveScene`. + +## How to verify + +- **Manual:** exercise tools that use each canvas layer (draw, select, resize, multi-select); confirm static content does not “flash” or rebuild unnecessarily on every move. +- Compare **export PNG/SVG** with on-screen appearance after your change. +- Run **`yarn test:typecheck`** and integration tests that hit the canvas (**`yarn test:app --`** relevant `*.test.tsx` under **`packages/excalidraw/tests/`**). diff --git a/.cursor/rules/excalidraw-data-collab.mdc b/.cursor/rules/excalidraw-data-collab.mdc new file mode 100644 index 0000000..467269f --- /dev/null +++ b/.cursor/rules/excalidraw-data-collab.mdc @@ -0,0 +1,27 @@ +--- +description: reconcile, restore, encryption, and shared serialization +globs: + - packages/excalidraw/data/**/*.ts + - packages/excalidraw/data/**/*.tsx +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-data-collab APPLIED 🔐 + +# Data layer (`packages/excalidraw/data`) + +- **`reconcileElements`:** merging uses **element version**; be careful with **ties** and **deleted** elements (`isDeleted`). Do not remove deleted elements casually — reconciliation and tombstones depend on them. +- **`restore.ts`:** add **defaults and migrations** when the element or appState schema changes so older JSON/clipboard data still works. +- **Encryption / share links:** sensitive material belongs in the URL **hash** (fragment), which is not sent to the server. Never log full share URLs or move keys into query strings. +- Keep **serialization** concerns here; **`excalidraw-app`** should orchestrate Firebase/network, not reimplement core JSON/binary formats. + +## How to verify + +- **Round-trip:** save → reload → export JSON; paste from clipboard; open a **share link** / old file fixture after **`restore`** changes. +- **Collab:** two clients, same room — concurrent edits, deletes, and reordering; watch for duplicates or vanishing elements after **`reconcileElements`** changes. +- Run **`yarn test:typecheck`** and tests touching **`packages/excalidraw/data/`** (e.g. **`yarn test:app -- packages/excalidraw/data`** or specific `*.test.ts`). +- **Encryption:** ensure keys stay in the URL **hash** only; grep for accidental logging of full share URLs in new code. diff --git a/.cursor/rules/excalidraw-element-model.mdc b/.cursor/rules/excalidraw-element-model.mdc new file mode 100644 index 0000000..5d57b98 --- /dev/null +++ b/.cursor/rules/excalidraw-element-model.mdc @@ -0,0 +1,35 @@ +--- +description: Immutable elements, versions, Scene ordering, and schema changes +globs: + - packages/element/**/*.ts + - packages/element/**/*.tsx +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-element-model APPLIED 🧱 + +# Element model (`@excalidraw/element`) + +- Treat elements as **immutable**. Do not assign to element fields directly; use **`mutateElement`** (and follow project rules for **`version`** / **`versionNonce`** so render, history, and collab stay consistent). +- **Z-order** uses fractional **`index`** strings. Prefer **`Scene`** APIs for reordering; avoid inventing index strings by hand unless you follow the same invariants as `Scene.ts`. +- **New element properties:** update types in **`types.ts`**, defaults in **`newElement.ts`** (or equivalent factories), and **migration / defaults** in **`packages/excalidraw/data/restore.ts`** so old files still load. +- **Collaboration:** reconciliation uses **version**; bugs in versioning here cause dropped updates or bad merges. + +```typescript +// ❌ BAD +element.x = 100; + +// ✅ GOOD +mutateElement(element, { x: 100 }); +``` + +## How to verify + +- Run **`yarn test:typecheck`**. +- Run targeted tests: **`yarn test:app -- packages/element`** (or a specific file under **`packages/element/tests/`**). +- **Manual:** draw, move, undo/redo, and (if you changed structure) **collaborate with two browsers** on the same room to confirm versions and ordering. +- **Regression:** load an **old saved `.excalidraw` file** after schema changes to confirm **`restore.ts`** paths still work. diff --git a/.cursor/rules/excalidraw-monorepo.mdc b/.cursor/rules/excalidraw-monorepo.mdc new file mode 100644 index 0000000..b83f675 --- /dev/null +++ b/.cursor/rules/excalidraw-monorepo.mdc @@ -0,0 +1,28 @@ +--- +description: Yarn workspaces, package layers, and where to read architecture docs +alwaysApply: true +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-monorepo APPLIED 🏗️ + +# Excalidraw monorepo + +- Use **Yarn Classic (v1)** workspaces. Do not switch to npm or Yarn Berry unless the team explicitly decides to. +- **Dependency direction** (only depend downward): + - `packages/common` and `packages/math` — no internal `@excalidraw/*` deps. + - `packages/element` → `common`, `math`. + - `packages/excalidraw` → `element`, `common`, `math`, `utils`. + - `excalidraw-app` → `@excalidraw/excalidraw` plus app-only services (Firebase, Socket.io, etc.). +- New utilities belong in the **lowest** package that can own them: math → `math`, element ops → `element`, shared non-geometry → `common`, export/bounds for consumers → `utils`. +- For architecture, flows, and pitfalls, prefer **`dev-docs/docs/a-docs/`** (onboarding guide) before guessing. + +## How to verify + +- Run **`yarn test:typecheck`** — catches bad cross-package imports and missing types after refactors. +- Run **`yarn test:code`** (ESLint) — enforces import rules and dependency boundaries in many cases. +- After changing **`package.json`** / workspaces: **`yarn install`** from repo root; **`yarn build:packages`** then **`yarn build`** for a full compile smoke test. +- If you added a dependency to **`packages/common`** or **`packages/math`**, confirm it does not import other **`@excalidraw/*`** packages (review `package.json` + imports). diff --git a/.cursor/rules/excalidraw-rendering.mdc b/.cursor/rules/excalidraw-rendering.mdc new file mode 100644 index 0000000..5324d67 --- /dev/null +++ b/.cursor/rules/excalidraw-rendering.mdc @@ -0,0 +1,26 @@ +--- +description: Multi-canvas layers, static vs interactive redraws, Rough.js caching +globs: + - packages/excalidraw/renderer/**/*.ts + - packages/excalidraw/renderer/**/*.tsx +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-rendering APPLIED 🎨 + +# Rendering and canvases + +- Architecture splits **static** (committed elements), **interactive** (handles, selection, remote cursors), and **new element** preview. Do not force **static** scene redraws on every pointer move unless unavoidable. +- **`renderStaticScene`:** Rough.js drawables are **cached** per element/version; avoid recreating drawable objects every frame when only the interactive overlay should change. +- SVG export paths (`staticSvgScene`, etc.) should stay consistent with canvas rendering for the same elements. +- Before large renderer changes, consider **performance** with many elements (hundreds+); keep hot loops allocation-light. + +## How to verify + +- **Manual:** draw, select, drag with **many elements** on canvas; confirm no obvious jank on pointer move. +- **Export:** PNG and SVG for the same scene; visuals should match expectations vs on-screen canvas. +- Run **`yarn test:typecheck`** and renderer-related tests if present (**`yarn test:app -- packages/excalidraw/renderer`** or **`export.test`** under **`packages/excalidraw/tests/`**). diff --git a/.cursor/rules/excalidraw-testing.mdc b/.cursor/rules/excalidraw-testing.mdc new file mode 100644 index 0000000..2a6f817 --- /dev/null +++ b/.cursor/rules/excalidraw-testing.mdc @@ -0,0 +1,29 @@ +--- +description: Vitest, test-utils, snapshots, path aliases, and setupTests +globs: + - "**/*.test.ts" + - "**/*.test.tsx" + - setupTests.ts +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-testing APPLIED 🧪 + +# Testing + +- Framework: **Vitest** + **jsdom** + **@testing-library/react**. Global mocks live in root **`setupTests.ts`** (canvas, IndexedDB, fonts, etc.). +- For editor tests, use **`packages/excalidraw/tests/test-utils`** (`render`, `unmountComponent`) and helpers (**`API`**, **`Keyboard`**, etc.) instead of ad-hoc mounting. +- Path aliases **`@excalidraw/*`** must match **`vitest.config.mts`** and **`tsconfig.json`** — use the same import style as production code. +- When snapshots change intentionally, run **`yarn test:update`** and review diffs before committing. +- Prefer **`*.test.ts`** / **`*.test.tsx`** naming; colocate small unit tests or use package **`tests/`** directories per existing layout. +- In **`.tsx`** tests: use **`unmountComponent()`** in `afterEach` when using the shared `render` helper. + +## How to verify + +- Run the test file you changed: **`yarn test:app -- path/to/file.test.tsx`**. +- Run **`yarn test:app --watch=false`** before commit; use **`yarn test:update`** only when snapshot changes are intentional, then **review diffs**. +- Run **`yarn test:typecheck`** — ensures test imports and **`@excalidraw/*`** aliases resolve like CI. diff --git a/.cursor/rules/excalidraw-typescript-imports.mdc b/.cursor/rules/excalidraw-typescript-imports.mdc new file mode 100644 index 0000000..7f1a6f2 --- /dev/null +++ b/.cursor/rules/excalidraw-typescript-imports.mdc @@ -0,0 +1,37 @@ +--- +description: TypeScript imports, Jotai entry points, and cross-package paths +globs: + - "**/*.ts" + - "**/*.tsx" +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-typescript-imports APPLIED 📎 + +# TypeScript and imports + +- Use `import type { … }` for **type-only** imports (ESLint enforces this). +- **Inside `packages/excalidraw`**, do not import from the package barrel **`index.tsx`**. Import the **specific module** path instead. +- **Jotai:** import atoms/helpers from `packages/excalidraw/editor-jotai.ts` or `excalidraw-app/app-jotai.ts`. Do not import from the `jotai` package directly in app/editor code. +- Cross-package: use **`@excalidraw/common`**, `@excalidraw/element`, `@excalidraw/math`, `@excalidraw/utils` — not long relative paths into another package’s `src`. +- Avoid new **browser-only** APIs in code paths used by **SSR** (e.g. Next.js example) unless there is a safe guard or fallback. + +```typescript +// ❌ BAD (inside packages/excalidraw) +import { foo } from "../index"; +import { atom } from "jotai"; + +// ✅ GOOD +import { foo } from "../components/some-module"; +import { atom } from "../editor-jotai"; +``` + +## How to verify + +- Run **`yarn test:code`** — ESLint flags **`import/type`** and many forbidden import paths. +- Run **`yarn test:typecheck`** — resolves **`@excalidraw/*`** aliases the same as CI; catches wrong relative hops between packages. +- For **Next.js / SSR**: build or run **`examples/with-nextjs`** (or your SSR path) after changes that might touch `window` / `document` without guards. diff --git a/.cursor/rules/excalidraw-ui-app.mdc b/.cursor/rules/excalidraw-ui-app.mdc new file mode 100644 index 0000000..e4cd6a6 --- /dev/null +++ b/.cursor/rules/excalidraw-ui-app.mdc @@ -0,0 +1,23 @@ +--- +description: excalidraw.com-only UI and styling +globs: excalidraw-app/components/**/*.tsx +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-ui-app APPLIED 📱 + +# App UI (`excalidraw-app/components`) + +- These components are **not** part of **`@excalidraw/excalidraw`**; they wrap or extend the editor for the hosted app (share, welcome, account, etc.). +- Prefer reusing **`packages/excalidraw`** APIs and props rather than duplicating editor internals. +- Colocate **`.scss`** with components; follow patterns from sibling files in this folder. + +## How to verify + +- Run **`yarn start`** and walk through the app flows that use the component (share, welcome, sidebar, etc.). +- Run **`yarn test:typecheck`** and **`yarn test:app -- excalidraw-app`** when tests cover the area. +- Confirm **`excalidraw-app`** styling does not break the embedded editor layout (regression pass on main canvas). diff --git a/.cursor/rules/excalidraw-ui.mdc b/.cursor/rules/excalidraw-ui.mdc new file mode 100644 index 0000000..4861e0a --- /dev/null +++ b/.cursor/rules/excalidraw-ui.mdc @@ -0,0 +1,24 @@ +--- +description: React UI, SCSS, i18n, and library vs app component placement +globs: packages/excalidraw/components/**/*.tsx +alwaysApply: false +--- + +## Important (debug) + +For transcript debugging: **when this rule is in your context for the user’s message**, print this exact line once at the **start** of your reply as **plain text** (no backticks, no code fence), then continue: + +RULE excalidraw-ui APPLIED 🎯 + +# UI components (`packages/excalidraw/components`) + +- Colocate **`.scss`** (or SCSS modules) with the component; follow existing spacing and BEM-like naming in sibling files. +- **User-visible strings** go through **`packages/excalidraw/locales/`** (i18n); do not hardcode English for labels, menus, or toasts. +- Use **Jotai** via **`editor-jotai.ts`** and **`useAppStateValue`** (and related hooks) for reactive editor state — not raw `jotai`. +- **`App.tsx`** is high-risk: pointer, keyboard, tools, and canvas behavior intersect here; keep changes focused and test multiple tools and inputs when touching it. + +## How to verify + +- **Manual:** open menus, dialogs, and toolbars you touched; test **keyboard** and **pointer** paths; check **mobile** or narrow viewport if the component is responsive. +- **i18n:** confirm new strings exist in **`packages/excalidraw/locales/`** and render correctly via **`t(...)`** (switch language if you test locales). +- Run **`yarn test:typecheck`**, **`yarn test:code`**, and relevant **`yarn test:app --`** UI tests; update snapshots only when expected (**`yarn test:update`**). diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..20be8d0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,145 @@ +# AGENTS.md + +Persistent context for AI coding agents and developers working in this repository. Aligns with **`dev-docs/docs/a-docs/`**, **`.cursor/rules/`**, and project tooling. + +--- + +## Project + +**Excalidraw monorepo** — open-source collaborative virtual whiteboard: hand-drawn style canvas, real-time collaboration, end-to-end encrypted sharing, export (PNG, SVG, JSON). + +| Layer | Role | +|-------|------| +| **`packages/excalidraw`** | Published npm package **`@excalidraw/excalidraw`** — embeddable React editor | +| **`excalidraw-app`** | Full web app (excalidraw.com) — Vite app, Firebase, Socket.io collab, local persistence | +| **`packages/element`** | Element model, scene, transforms, bindings, z-order (`@excalidraw/element`) | +| **`packages/common`** | Shared constants and utilities (`@excalidraw/common`) | +| **`packages/math`** | Geometry only, no UI deps (`@excalidraw/math`) | +| **`packages/utils`** | Export and bounds helpers for consumers (`@excalidraw/utils`) | +| **`examples/*`** | Integration samples (e.g. Next.js, Vite) | +| **`dev-docs/`** | Docusaurus site; deep onboarding lives in **`dev-docs/docs/a-docs/`** | + +**Tech (high level):** React 19, TypeScript (strict), Yarn Classic workspaces, Vite (app), esbuild (packages), Vitest + jsdom + Testing Library, HTML Canvas 2D + Rough.js, SCSS, Jotai (scoped providers), Socket.io + Firebase in the app. + +--- + +## Teams & audiences (who cares about what) + +| Audience | Focus | +|----------|--------| +| **Library consumers** | Stable **`@excalidraw/excalidraw`** API, bundle size, SSR (e.g. Next.js example), minimal breaking changes | +| **Product / web app** | **`excalidraw-app`**: collab, auth-adjacent flows, env-specific config, hosting | +| **Core editor contributors** | **`packages/excalidraw`** + **`packages/element`**: actions, rendering, data format, undo/redo | +| **Infrastructure / release** | `scripts/`, `.github/workflows`, Docker/nginx as used by the project | + +When unsure whether code belongs in the **library** or the **app**, default to: reusable editor behavior → **`packages/excalidraw`** (or lower packages); excalidraw.com-only → **`excalidraw-app`**. + +--- + +## Architecture (mental model) + +- **State:** Hybrid — large **`AppState`** object + immutable **`elements`** array; **Jotai** atoms for fine-grained UI (`editor-jotai.ts`, `app-jotai.ts`); **`Scene`** for element ordering (fractional indices); **History** uses deltas for undo/redo. +- **User operations:** Prefer the **action** system — `ActionManager` dispatches **`perform` → `ActionResult`** (`elements`, `appState`, `files`, **`captureUpdate`** for history). Not a single Redux-style store. +- **Rendering:** **Multi-canvas** — static (committed shapes, Rough.js, cache-friendly), interactive (selection, handles, cursors), new-element preview. Avoid redrawing the static layer on every pointer move. +- **Collaboration (app):** Socket.io for realtime; Firebase for durable storage; **`reconcileElements`** merges by element **version**; encryption key in URL **hash** (fragment), not query string. +- **Dependencies:** Flow **downward** — `common` / `math` have no internal `@excalidraw/*` deps; `element` → `common`, `math`; `excalidraw` → all; `excalidraw-app` → `@excalidraw/excalidraw` + app services. + +**Heavy-touch files:** `packages/excalidraw/components/App.tsx` (class-based editor core — pointer, keyboard, tools). Treat changes there as high blast radius. + +--- + +## Commands + +Use **Yarn Classic v1** from the **repository root** (`packageManager: yarn@1.22.22`). Node **>= 18**. + +| Command | Purpose | +|---------|---------| +| `yarn` | Install all workspace dependencies | +| `yarn start` | Dev server for **`excalidraw-app`** (default port from `.env.development`, often `3001`) | +| `yarn build` | Production build of the web app | +| `yarn build:packages` | Build npm packages only (`common`, `math`, `element`, `excalidraw`) | +| `yarn test` / `yarn test:app` | Vitest (watch mode by default for `test:app`) | +| `yarn test:update` | Tests + update snapshots — run before commit when snapshots are intentional | +| `yarn test:typecheck` | `tsc` for the monorepo | +| `yarn test:code` | ESLint | +| `yarn test:other` | Prettier check | +| `yarn test:all` | CI-like: typecheck + lint + prettier + tests (no watch) | +| `yarn fix` | Prettier write + ESLint fix | +| `yarn clean-install` | Remove workspace `node_modules` and reinstall | + +**Examples:** `yarn start:example` — build packages + run browser example. **`VITE_APP_*`** env vars live in `.env.development` / `.env.production`. + +--- + +## Repository map (where to look) + +| Topic | Location | +|-------|-----------| +| Actions | `packages/excalidraw/actions/` — `manager.tsx`, `types.ts`, `action*.ts(x)` | +| Main editor | `packages/excalidraw/components/App.tsx` | +| Canvases | `packages/excalidraw/components/canvases/` | +| Renderers | `packages/excalidraw/renderer/` | +| Serialization / reconcile | `packages/excalidraw/data/` — `json.ts`, `restore.ts`, `reconcile.ts`, `encryption.ts` | +| Collab | `excalidraw-app/collab/` | +| App persistence | `excalidraw-app/data/` | +| i18n (library UI) | `packages/excalidraw/locales/` + `useI18n` | +| Tests setup | Root `setupTests.ts`, `vitest.config.mts` | +| Onboarding docs | `dev-docs/docs/a-docs/README.md` (index) | +| Agent rules | `.cursor/rules/*.mdc` | +| Slash-command prompts | `.cursor/commands/*.md` | + +--- + +## Code style & conventions + +- **TypeScript:** **`strict: true`** (root `tsconfig.json`). Use **`import type { … }`** for type-only imports (ESLint enforced). +- **React:** Prefer **functional components** and hooks for **new UI**; the core **`App`** remains a **class** — follow existing patterns when touching it. +- **Exports:** Prefer **named exports** for new modules; match neighboring files when editing legacy code. +- **Imports:** Use path aliases **`@excalidraw/common`**, `@excalidraw/element`, `@excalidraw/math`, `@excalidraw/utils`. **Inside `packages/excalidraw`**, do **not** import from the package barrel **`index.tsx`** — import the **specific module**. +- **Jotai:** Use **`packages/excalidraw/editor-jotai.ts`** or **`excalidraw-app/app-jotai.ts`** — do not import **`jotai`** directly in feature code. +- **Styling:** SCSS, often colocated (e.g. `Component.scss`). Library styles frequently nest under **`.excalidraw`**. +- **Naming:** Components **PascalCase**; utilities/modules **camelCase**; actions **`actionName.ts`**; tests **`*.test.ts(x)`**. +- **i18n:** No hardcoded English for **library** user-visible strings — use **`locales/`** and **`t(...)`**. + +--- + +## State & data rules + +- **Elements are immutable** — use **`mutateElement`** (and correct **version** / **versionNonce** behavior). Direct field assignment breaks render optimization, history, and collaboration. +- **Z-order:** Fractional **`index`** on elements; prefer **`Scene`** APIs for reordering. +- **Schema changes:** Update **`types.ts`**, **`newElement.ts`**, **`restore.ts`**, and rendering/export as needed. +- **Collaboration:** Be careful with **`reconcileElements`** and **tombstones** (`isDeleted`); test concurrent editing when changing element structure or versioning. + +--- + +## Testing + +- **Vitest** + **jsdom** + **@testing-library/react**; **`packages/excalidraw/tests/test-utils`** (`render`, `unmountComponent`) and helpers **`API`**, **`Keyboard`**, etc. +- Run **`yarn test:update`** when snapshots change intentionally; **review snapshot diffs** before committing. +- Path aliases in tests must match **`vitest.config.mts`**. + +--- + +## Constraints & non-goals + +- **Package manager:** Yarn Classic workspaces — do not switch to npm or Yarn Berry without an explicit team decision. +- **Dependencies:** Avoid new **heavy** dependencies in **`packages/excalidraw`** without considering **bundle size** and **`.github/workflows/size-limit.yml`**. Discuss significant additions. +- **No direct mutation** of element objects or ad-hoc editor state that bypasses **actions** when the change should be **undoable** and consistent. +- **SSR / embeds:** Avoid browser-only APIs on code paths used by **Next.js** (or other SSR) without guards or dynamic import patterns consistent with the codebase. +- **Security:** Do not log full **share URLs** or move **encryption keys** from the URL **hash** to query strings. + +--- + +## Contributing & external docs + +- **`CONTRIBUTING.md`** points to [Excalidraw contributing docs](https://docs.excalidraw.com/docs/introduction/contributing). +- First-time deep dive: start with **`dev-docs/docs/a-docs/09-tldr-new-devs.md`**, then **`01-architecture-overview.md`**, **`03-codebase-navigation.md`**, **`08-adding-features.md`**. + +--- + +## Cursor integration (this repo) + +- **Rules:** `.cursor/rules/` — monorepo layers, imports, element model, actions, data/collab, app, rendering, testing, UI (each includes **How to verify** where relevant). +- **Commands:** `.cursor/commands/` — e.g. **`update-state-flow`**, **`new-action`**, **`pre-pr-check`**, **`element-schema-change`**, **`add-locale-string`**, **`debug-collab`**, **`rendering-change`**, etc. Invoke via **`/`** in Cursor chat. + +When answering in this project, prefer citing **real paths** and **existing patterns** over generic React advice. diff --git a/dev-docs/docs/a-docs/01-architecture-overview.md b/dev-docs/docs/a-docs/01-architecture-overview.md new file mode 100644 index 0000000..4bffafa --- /dev/null +++ b/dev-docs/docs/a-docs/01-architecture-overview.md @@ -0,0 +1,210 @@ +# Architecture Overview + +## High-Level Summary + +Excalidraw is an open-source virtual whiteboard built with **React** and **TypeScript**. The codebase is organized as a **Yarn workspaces monorepo** with a clear separation between the **reusable library** (published to npm as `@excalidraw/excalidraw`) and the **web application** (excalidraw.com). + +### Tech Stack + +| Layer | Technology | +|-------|-----------| +| UI Framework | React 19 + TypeScript 5.9 | +| State Management | Jotai (atomic state) + imperative `AppState` | +| Build System | Vite (app), esbuild (packages) | +| Testing | Vitest + jsdom + @testing-library/react | +| Canvas Rendering | HTML Canvas 2D + [Rough.js](https://roughjs.com/) (hand-drawn style) | +| Real-time Collaboration | Socket.io (WebSocket) | +| Backend Storage | Firebase Firestore + Firebase Storage | +| Styling | SCSS modules | +| Package Manager | Yarn (workspaces) | +| CI/CD | GitHub Actions | +| Containerization | Docker + nginx | + +--- + +## Monorepo Architecture + +```mermaid +graph TD + subgraph "Web Application" + APP["excalidraw-app/"] + end + + subgraph "Core Packages" + EXC["@excalidraw/excalidraw
(Main React Component)"] + ELE["@excalidraw/element
(Element Logic)"] + COM["@excalidraw/common
(Shared Utilities)"] + MAT["@excalidraw/math
(Geometry)"] + UTL["@excalidraw/utils
(Export & Helpers)"] + end + + subgraph "External Services" + FB["Firebase
(Firestore + Storage)"] + WS["WebSocket Server
(Socket.io)"] + end + + APP --> EXC + APP --> FB + APP --> WS + EXC --> ELE + EXC --> COM + EXC --> MAT + EXC --> UTL + ELE --> COM + ELE --> MAT +``` + +--- + +## Package Responsibilities + +### `packages/excalidraw/` — Main Editor Component + +The heart of the project. Published to npm as `@excalidraw/excalidraw`. + +- **`components/App.tsx`** — Main editor: canvas rendering, pointer/keyboard events, tool management, state orchestration +- **`actions/`** — Atomic operations (copy, paste, delete, style changes, zoom, etc.) +- **`renderer/`** — Canvas rendering pipeline (static scenes, interactive overlays, SVG export) +- **`scene/`** — Scene export (PNG, SVG, clipboard), render configurations +- **`data/`** — Serialization, deserialization, encryption, compression, reconciliation +- **`components/`** — All UI: menus, sidebars, color picker, font picker, library, command palette +- **`hooks/`** — Custom React hooks +- **`fonts/`** — Font loading and subsetting (Virgil, Excalifont, Nunito, etc.) +- **`locales/`** — i18n translations (managed via Crowdin) + +### `packages/element/` — Element System + +All logic for manipulating drawing elements (shapes, text, arrows, images, frames). + +- **`types.ts`** — Element type definitions (`ExcalidrawRectangleElement`, `ExcalidrawArrowElement`, etc.) +- **`Scene.ts`** — Scene state management with fractional indexing +- Modules: binding, bounds, collision detection, crop, delta, duplicate, groups, transform, z-index + +### `packages/common/` — Shared Utilities + +Cross-package utilities used by all other packages. + +- Constants (`APP_NAME`, `CURSOR_TYPE`, `POINTER_BUTTON`, `THEME`, `MIME_TYPES`) +- Binary heap, color utilities, key constants, event emitter, queue +- Font metadata, URL helpers, random ID generation + +### `packages/math/` — Geometry Primitives + +Pure mathematical operations with no UI dependencies. + +- Points, vectors, angles, segments, lines +- Curves, ellipses, polygons, rectangles, triangles + +### `packages/utils/` — Export & Bounds Utilities + +Higher-level utilities for consumers of the library. + +- `exportToCanvas`, `exportToSvg`, `exportToBlob` +- `withinBounds`, shape utilities, bounding box calculations + +### `excalidraw-app/` — Web Application + +The full-featured application at excalidraw.com, built on top of `@excalidraw/excalidraw`. + +- **`App.tsx`** — App shell: scene initialization, URL routing, localStorage, collaboration wiring +- **`collab/`** — Real-time collaboration (Socket.io + Firebase) +- **`data/`** — Firebase integration, local persistence, file management, tab sync +- **`components/`** — App-specific UI (menus, share dialog, welcome screen, AI features) + +--- + +## Dependency Graph + +``` +@excalidraw/excalidraw +├── @excalidraw/element +│ ├── @excalidraw/common +│ └── @excalidraw/math +├── @excalidraw/common +├── @excalidraw/math +└── @excalidraw/utils + +excalidraw-app +└── @excalidraw/excalidraw (+ Firebase, Socket.io, Sentry) +``` + +Key rule: packages only depend **downward**. `common` and `math` have no internal dependencies. `element` depends on `common` and `math`. `excalidraw` depends on everything. + +--- + +## State Management Architecture + +```mermaid +graph LR + subgraph "Jotai Atoms" + EA["Editor Atoms
(editor-jotai.ts)"] + AA["App Atoms
(app-jotai.ts)"] + end + + subgraph "AppState" + AS["appState
(theme, tool, zoom, selection...)"] + end + + subgraph "Elements" + EL["elements[]
(shapes, text, arrows...)"] + end + + subgraph "History" + H["History
(undo/redo stacks)"] + end + + EA --> AS + AA --> AS + AS --> EL + EL --> H +``` + +State is managed through two complementary systems: + +1. **Jotai atoms** — Fine-grained reactive state for UI components. The editor uses `EditorJotaiProvider` (scoped per editor instance) and the app adds `AppJotaiProvider`. +2. **`AppState`** — A large flat object containing all editor state (current tool, theme, zoom, selection, collaborators, etc.). Defined in `packages/excalidraw/appState.ts`. +3. **Elements array** — The source of truth for all drawing content. Each element is immutable; changes create new objects with incremented `version`. + +--- + +## Rendering Architecture + +```mermaid +graph TD + EL["Elements Array"] --> SC["StaticCanvas
(committed shapes)"] + EL --> IC["InteractiveCanvas
(selection, handles, cursors)"] + EL --> NC["NewElementCanvas
(shape being drawn)"] + SC --> RS["renderStaticScene()
(Rough.js)"] + IC --> RI["renderInteractiveScene()"] + NC --> RN["renderNewElement()"] +``` + +The editor uses a **multi-canvas architecture**: + +- **StaticCanvas** — Renders all committed elements using Rough.js for the hand-drawn aesthetic +- **InteractiveCanvas** — Overlays selection boxes, resize handles, remote cursors +- **NewElementCanvas** — Renders the element currently being drawn (before commit) + +This separation allows the static layer to be cached and only re-rendered when elements change, while the interactive layer updates on every pointer move. + +--- + +## Data Flow: User Action → State Update + +```mermaid +sequenceDiagram + participant User + participant App as App.tsx + participant AM as ActionManager + participant Action + participant State as AppState + Elements + + User->>App: Keyboard/pointer event + App->>AM: handleKeyDown() / handlePointerDown() + AM->>Action: action.perform(elements, appState) + Action-->>AM: ActionResult {elements, appState, captureUpdate} + AM->>State: updater(ActionResult) + State-->>App: Re-render +``` + +Every user interaction flows through the **Action system**. Actions are pure functions that receive current state and return new state, making them predictable and testable. diff --git a/dev-docs/docs/a-docs/02-local-setup.md b/dev-docs/docs/a-docs/02-local-setup.md new file mode 100644 index 0000000..c2f17a6 --- /dev/null +++ b/dev-docs/docs/a-docs/02-local-setup.md @@ -0,0 +1,183 @@ +# Local Setup Guide + +## Prerequisites + +| Tool | Version | How to Verify | +|------|---------|---------------| +| **Node.js** | >= 18.0.0 | `node --version` | +| **Yarn** | 1.x (Classic) | `yarn --version` | +| **Git** | Any recent | `git --version` | + +> The project uses Yarn Classic (v1) workspaces. Do **not** use npm or Yarn Berry. + +## Step 1: Clone the Repository + +```bash +git clone https://github.com/excalidraw/excalidraw.git +cd excalidraw +``` + +## Step 2: Install Dependencies + +```bash +yarn +``` + +This installs dependencies for all workspaces: `excalidraw-app/`, `packages/*`, and `examples/*`. + +If you run into issues, try a clean install: + +```bash +yarn clean-install +``` + +This removes all `node_modules` directories and reinstalls everything. + +## Step 3: Start the Development Server + +```bash +yarn start +``` + +This starts the Vite dev server for `excalidraw-app/` on **http://localhost:3001** (configured via `VITE_APP_PORT` in `.env.development`). + +The app hot-reloads on changes to both `excalidraw-app/` and `packages/` source files thanks to the path aliases in the Vite config. + +## Step 4: Verify It Works + +1. Open **http://localhost:3001** in your browser +2. You should see the Excalidraw whiteboard editor +3. Try drawing a rectangle — if the hand-drawn style renders, fonts and canvas are working + +--- + +## Environment Variables + +Environment variables are defined in two files at the project root: + +| File | Purpose | +|------|---------| +| `.env.development` | Used during `yarn start` | +| `.env.production` | Used during `yarn build` | + +All variables are prefixed with `VITE_APP_` (Vite convention). + +### Key Variables + +| Variable | Purpose | Default (dev) | +|----------|---------|---------------| +| `VITE_APP_BACKEND_V2_GET_URL` | Backend for loading shared drawings | `https://json.excalidraw.com/api/v2/` | +| `VITE_APP_BACKEND_V2_POST_URL` | Backend for saving shared drawings | `https://json.excalidraw.com/api/v2/post/` | +| `VITE_APP_WS_SERVER_URL` | WebSocket server for collaboration | `https://oss-collab.excalidraw.com` | +| `VITE_APP_FIREBASE_CONFIG` | Firebase config (JSON string) | Pre-configured for dev | +| `VITE_APP_PORT` | Dev server port | `3001` | +| `VITE_APP_DISABLE_TRACKING` | Disable analytics | (not set in dev) | +| `VITE_APP_ENABLE_TRACKING` | Enable analytics | (not set in dev) | + +> For local-only development, the defaults work out of the box. You do **not** need to configure Firebase or WebSocket for basic editor work. + +--- + +## Building the Project + +### Build the web app + +```bash +yarn build +``` + +Output goes to `excalidraw-app/build/`. You can serve it: + +```bash +yarn start:production +``` + +This builds and serves on **http://localhost:5001**. + +### Build only the packages (for library development) + +```bash +yarn build:packages +``` + +This builds all packages in order: `common` → `math` → `element` → `excalidraw`. + +### Build individual packages + +```bash +yarn build:common +yarn build:math +yarn build:element +yarn build:excalidraw +``` + +--- + +## Running with Docker + +```bash +docker-compose up --build -d +``` + +This builds the app in a Node 18 container and serves it via nginx on **http://localhost:3000**. + +--- + +## Running Examples + +### Browser script example + +```bash +yarn start:example +``` + +Runs the Vite-based example at `examples/with-script-in-browser/` on port 5002. + +### Next.js example + +```bash +cd examples/with-nextjs +yarn dev +``` + +Runs on port 3005. + +--- + +## Common Issues and Fixes + +### `canvas` module errors in tests + +The project uses `vitest-canvas-mock` to mock the Canvas API. If you see canvas-related errors, ensure `setupTests.ts` is being picked up (configured in `vitest.config.mts`). + +### Font loading failures + +Fonts are loaded from local files during development. The `setupTests.ts` file mocks font fetches. If fonts don't render in the browser, check that `public/` assets are being served correctly. + +### Port already in use + +If port 3001 is occupied, either kill the existing process or change `VITE_APP_PORT` in `.env.development`. + +### `yarn start` fails with module resolution errors + +Run `yarn` again to ensure all workspace symlinks are correct. If that doesn't help: + +```bash +yarn clean-install +``` + +### TypeScript errors after pulling new changes + +```bash +yarn test:typecheck +``` + +This runs `tsc` across the entire project and will show you what needs fixing. + +### Snapshot test failures after pulling + +```bash +yarn test:update +``` + +This updates all Vitest snapshots. diff --git a/dev-docs/docs/a-docs/03-codebase-navigation.md b/dev-docs/docs/a-docs/03-codebase-navigation.md new file mode 100644 index 0000000..f47d443 --- /dev/null +++ b/dev-docs/docs/a-docs/03-codebase-navigation.md @@ -0,0 +1,176 @@ +# Codebase Navigation Guide + +## Top-Level Structure + +``` +excalidraw/ +├── excalidraw-app/ # Web application (excalidraw.com) +├── packages/ +│ ├── excalidraw/ # Main React editor component (@excalidraw/excalidraw) +│ ├── element/ # Element types and manipulation (@excalidraw/element) +│ ├── common/ # Shared utilities (@excalidraw/common) +│ ├── math/ # Geometry primitives (@excalidraw/math) +│ └── utils/ # Export and bounds helpers (@excalidraw/utils) +├── examples/ +│ ├── with-script-in-browser/ # Vite integration example +│ └── with-nextjs/ # Next.js integration example +├── dev-docs/ # Docusaurus documentation site +├── firebase-project/ # Firebase configuration +├── public/ # Static assets (favicon, service worker) +├── scripts/ # Build and release scripts +├── .github/ # CI/CD workflows +├── .env.development # Dev environment variables +├── .env.production # Prod environment variables +├── vitest.config.mts # Test configuration +├── tsconfig.json # Root TypeScript config with path aliases +└── package.json # Root workspace config +``` + +--- + +## Where to Find Things + +### Business Logic + +| What | Where | +|------|-------| +| Element types and definitions | `packages/element/src/types.ts` | +| Element creation | `packages/element/src/newElement.ts` | +| Element mutation | `packages/element/src/mutateElement.ts` | +| Element transforms (resize, rotate) | `packages/element/src/transform.ts` | +| Arrow binding logic | `packages/element/src/binding.ts` | +| Text wrapping and sizing | `packages/element/src/textElement.ts` | +| Grouping | `packages/element/src/groups.ts` | +| Frame containment | `packages/element/src/frame.ts` | +| Z-index ordering | `packages/element/src/zindex.ts` | +| Collision detection | `packages/element/src/collision.ts` | +| Scene state + fractional indexing | `packages/element/src/Scene.ts` | + +### User Actions + +| What | Where | +|------|-------| +| Action type definitions | `packages/excalidraw/actions/types.ts` | +| Action manager (registration, dispatch) | `packages/excalidraw/actions/manager.tsx` | +| All action implementations | `packages/excalidraw/actions/action*.ts(x)` | +| Keyboard shortcuts | `packages/excalidraw/actions/shortcuts.ts` | + +Key actions: `actionDeleteSelected`, `actionDuplicateSelection`, `actionCopy`, `actionPaste`, `actionGroup`, `actionChangeStrokeColor`, `actionChangeFontSize`, `actionZoomIn`, `actionToggleTheme`, `actionSaveToActiveFile`, `actionLoadScene`. + +### Data Layer (Serialization, Persistence) + +| What | Where | +|------|-------| +| JSON serialization/deserialization | `packages/excalidraw/data/json.ts` | +| Binary file loading (PNG/SVG/JSON) | `packages/excalidraw/data/blob.ts` | +| Data compression/encoding | `packages/excalidraw/data/encode.ts` | +| Encryption for collaboration | `packages/excalidraw/data/encryption.ts` | +| Element reconciliation (collab merge) | `packages/excalidraw/data/reconcile.ts` | +| State restoration and migration | `packages/excalidraw/data/restore.ts` | +| Library persistence | `packages/excalidraw/data/library.ts` | +| PNG metadata embedding | `packages/excalidraw/data/image.ts` | +| File System Access API | `packages/excalidraw/data/filesystem.ts` | +| Exported data types | `packages/excalidraw/data/types.ts` | + +### App-Level Data (excalidraw.com specific) + +| What | Where | +|------|-------| +| Firebase integration | `excalidraw-app/data/firebase.ts` | +| Local storage persistence | `excalidraw-app/data/LocalData.ts` | +| File upload/download management | `excalidraw-app/data/FileManager.ts` | +| Cross-tab synchronization | `excalidraw-app/data/tabSync.ts` | +| Text-to-diagram storage | `excalidraw-app/data/TTDStorage.ts` | + +### UI Components + +| What | Where | +|------|-------| +| Main editor (canvas, events, state) | `packages/excalidraw/components/App.tsx` | +| Layer controls | `packages/excalidraw/components/LayerUI.tsx` | +| Static canvas rendering | `packages/excalidraw/components/canvases/StaticCanvas.tsx` | +| Interactive canvas (selection, handles) | `packages/excalidraw/components/canvases/InteractiveCanvas.tsx` | +| Main menu | `packages/excalidraw/components/main-menu/MainMenu.tsx` | +| Sidebar | `packages/excalidraw/components/Sidebar/Sidebar.tsx` | +| Color picker | `packages/excalidraw/components/ColorPicker/` | +| Font picker | `packages/excalidraw/components/FontPicker/` | +| Library panel | `packages/excalidraw/components/LibraryMenu.tsx` | +| Command palette | `packages/excalidraw/components/CommandPalette/CommandPalette.tsx` | +| Welcome screen | `packages/excalidraw/components/welcome-screen/` | +| Text-to-diagram dialog | `packages/excalidraw/components/TTDDialog/TTDDialog.tsx` | +| Element stats inspector | `packages/excalidraw/components/Stats/` | + +### Rendering + +| What | Where | +|------|-------| +| Static scene rendering | `packages/excalidraw/renderer/staticScene.ts` | +| Interactive scene rendering | `packages/excalidraw/renderer/interactiveScene.ts` | +| SVG export rendering | `packages/excalidraw/renderer/staticSvgScene.ts` | +| Scene export (PNG/SVG/clipboard) | `packages/excalidraw/scene/export.ts` | +| Render config types | `packages/excalidraw/scene/types.ts` | + +### Collaboration + +| What | Where | +|------|-------| +| Collaboration manager | `excalidraw-app/collab/Collab.tsx` | +| WebSocket portal | `excalidraw-app/collab/Portal.tsx` | +| Collab error handling | `excalidraw-app/collab/CollabError.tsx` | +| Live collaboration trigger UI | `packages/excalidraw/components/live-collaboration/` | + +### State Management + +| What | Where | +|------|-------| +| Default app state | `packages/excalidraw/appState.ts` | +| Editor Jotai provider | `packages/excalidraw/editor-jotai.ts` | +| App Jotai provider | `excalidraw-app/app-jotai.ts` | +| App state hooks | `packages/excalidraw/hooks/useAppStateValue.ts` | +| Core types (AppState, Props, etc.) | `packages/excalidraw/types.ts` | + +--- + +## Naming Conventions + +### Files + +- **React components**: PascalCase (e.g., `LayerUI.tsx`, `ColorPicker.tsx`) +- **Utilities and modules**: camelCase (e.g., `mutateElement.ts`, `reconcile.ts`) +- **Action files**: `action` prefix (e.g., `actionDeleteSelected.ts`, `actionZoomToFit.ts`) +- **Test files**: `.test.ts` or `.test.tsx` suffix, either colocated or in a `tests/` directory +- **Style files**: `.scss` next to their component (e.g., `App.scss`, `LayerUI.scss`) +- **Type-only files**: `types.ts` for shared type definitions + +### Code + +- **Element types**: string literals (`"rectangle"`, `"arrow"`, `"text"`, `"freedraw"`) +- **Actions**: objects conforming to the `Action` interface with a unique `name` string +- **Jotai atoms**: suffixed with `Atom` (e.g., `collabAPIAtom`, `libraryItemsAtom`) +- **Constants**: UPPER_SNAKE_CASE in `@excalidraw/common` constants +- **TypeScript**: Strict mode, `consistent-type-imports` enforced via ESLint + +### Imports + +The project uses **path aliases** for cross-package imports: + +```typescript +import { something } from "@excalidraw/common"; +import { ExcalidrawElement } from "@excalidraw/element"; +import { pointFrom } from "@excalidraw/math"; +``` + +These are resolved via `tsconfig.json` paths and duplicated in `vitest.config.mts` for tests. + +> **Important**: ESLint enforces that you do NOT import from barrel `index.ts` files within `@excalidraw/excalidraw` when you're inside that package. Import from the specific module instead. + +--- + +## Key Entry Points + +| Entry Point | File | Description | +|-------------|------|-------------| +| npm package | `packages/excalidraw/index.tsx` | Exports `Excalidraw` component and all public APIs | +| Web app | `excalidraw-app/index.tsx` → `App.tsx` | Bootstraps the full application | +| Browser example | `examples/with-script-in-browser/index.tsx` | Minimal integration example | +| Next.js example | `examples/with-nextjs/src/app/page.tsx` | SSR integration example | diff --git a/dev-docs/docs/a-docs/04-key-business-flows.md b/dev-docs/docs/a-docs/04-key-business-flows.md new file mode 100644 index 0000000..22fffbd --- /dev/null +++ b/dev-docs/docs/a-docs/04-key-business-flows.md @@ -0,0 +1,330 @@ +# Key Business Flows + +This document covers the most important data flows in Excalidraw. Understanding these will help you reason about how user interactions translate to state changes, persistence, and collaboration. + +--- + +## 1. Drawing an Element + +When a user clicks and drags on the canvas to draw a shape: + +```mermaid +sequenceDiagram + participant User + participant App as App.tsx + participant NE as newElement.ts + participant NC as NewElementCanvas + participant Scene as Scene + participant SC as StaticCanvas + + User->>App: pointerDown (canvas) + App->>App: Determine active tool (e.g., "rectangle") + App->>NE: newElement({type, x, y, ...styleProps}) + NE-->>App: ExcalidrawElement (initial) + App->>NC: Render in-progress element + + loop While dragging + User->>App: pointerMove + App->>App: Update element dimensions (width, height) + App->>NC: Re-render preview + end + + User->>App: pointerUp + App->>Scene: Commit element to elements array + Scene->>SC: Re-render static canvas + App->>App: captureUpdate → push to History +``` + +**Key files:** +- `packages/excalidraw/components/App.tsx` — pointer event handlers +- `packages/element/src/newElement.ts` — element factory functions +- `packages/excalidraw/components/canvases/NewElementCanvas.tsx` — preview rendering +- `packages/element/src/Scene.ts` — scene state management + +--- + +## 2. App Bootstrap & Scene Loading + +When the app first loads: + +```mermaid +sequenceDiagram + participant Browser + participant Index as index.tsx + participant App as ExcalidrawWrapper + participant Init as initializeScene() + participant LS as localStorage + participant Backend as Backend API + participant Collab as Collab + + Browser->>Index: Load page + Index->>App: Render ExcalidrawApp + App->>Init: useEffect → initializeScene() + + alt URL has #room=... + Init->>Collab: startCollaboration(roomLinkData) + else URL has #json=... + Init->>Backend: importFromBackend(id, key) + Backend-->>Init: Decrypted scene data + else URL has #url=... + Init->>Backend: fetch(url) → loadFromBlob() + else No URL params + Init->>LS: importFromLocalStorage() + LS-->>Init: Saved scene + appState + end + + Init-->>App: initialData + App->>App: Render Excalidraw with initialData +``` + +**Key files:** +- `excalidraw-app/App.tsx` — `initializeScene()` function (lines ~215–370) +- `excalidraw-app/data/index.ts` — `importFromBackend()` +- `excalidraw-app/data/localStorage.ts` — `importFromLocalStorage()` +- `packages/excalidraw/data/blob.ts` — `loadFromBlob()` + +--- + +## 3. Real-Time Collaboration + +When a user starts or joins a collaborative session: + +```mermaid +sequenceDiagram + participant User as User A + participant App as App.tsx + participant Collab as Collab.tsx + participant Portal as Portal (Socket.io) + participant WS as WebSocket Server + participant Firebase as Firebase + participant UserB as User B + + User->>App: Click "Live Collaboration" + App->>Collab: startCollaboration() + Collab->>Collab: generateRoomId() + encryptionKey + Collab->>Portal: open(socket, roomId) + Portal->>WS: Connect + join room + Collab->>Firebase: loadFromFirebase(roomId) + Firebase-->>Collab: Existing scene (if any) + + User->>App: Draw / modify elements + App->>Collab: onChange → syncElements() + Collab->>Collab: reconcileElements(local, remote) + Collab->>Portal: broadcastScene(elements) + Portal->>WS: Emit encrypted scene data + WS->>UserB: Forward to other clients + + UserB->>WS: Send their changes + WS->>Portal: Receive remote update + Portal->>Collab: handleRemoteSceneUpdate() + Collab->>Collab: reconcileElements() + Collab->>App: updateScene(reconciledElements) + + par Background save + Collab->>Firebase: queueSaveToFirebase() + end +``` + +**Key concepts:** +- **Room ID + encryption key** are encoded in the URL hash (never sent to server) +- **Reconciliation** merges local and remote elements by comparing `version` numbers — the higher version wins +- **Cursor sync** is a separate channel: `broadcastMouseLocation()` sends pointer position + username +- **Firebase** acts as durable storage; Socket.io is for real-time relay only + +**Key files:** +- `excalidraw-app/collab/Collab.tsx` — orchestrates the collaboration lifecycle +- `excalidraw-app/collab/Portal.tsx` — Socket.io wrapper +- `packages/excalidraw/data/reconcile.ts` — `reconcileElements()` merge algorithm +- `packages/excalidraw/data/encryption.ts` — end-to-end encryption + +--- + +## 4. Save & Load + +### Local Persistence (Auto-save) + +Every change triggers a debounced save to `localStorage` + `IndexedDB`: + +```mermaid +sequenceDiagram + participant App as App.tsx + participant LD as LocalData + participant LS as localStorage + participant IDB as IndexedDB + + App->>LD: onChange → LocalData.save(elements, appState) + LD->>LS: Save appState (JSON) + LD->>IDB: Save elements + files (binary data) +``` + +### Export to File + +```mermaid +sequenceDiagram + participant User + participant Action as actionSaveToActiveFile + participant JSON as json.ts + participant FS as filesystem.ts + + User->>Action: Ctrl+S + Action->>JSON: serializeAsJSON(elements, appState, files) + JSON-->>Action: JSON string + Action->>FS: fileSave(blob, filename) + FS->>FS: File System Access API or download fallback +``` + +### Share via Link + +```mermaid +sequenceDiagram + participant User + participant Export as exportToBackend() + participant Backend as Backend V2 API + participant Firebase as Firebase Storage + + User->>Export: Click "Share" → export link + Export->>Export: encryptData(elements + appState) + Export->>Backend: POST encrypted data + Backend-->>Export: {id} + Export->>Firebase: saveFilesToFirebase(files) + Export-->>User: URL with #json=id,encryptionKey +``` + +**Key files:** +- `excalidraw-app/data/LocalData.ts` — auto-save logic +- `packages/excalidraw/data/json.ts` — `serializeAsJSON()`, `saveAsJSON()`, `loadFromJSON()` +- `packages/excalidraw/data/filesystem.ts` — File System Access API wrapper +- `excalidraw-app/data/index.ts` — `exportToBackend()`, `importFromBackend()` + +--- + +## 5. Export to Image (PNG/SVG) + +```mermaid +sequenceDiagram + participant User + participant Action as exportCanvas() + participant Scene as scene/export.ts + participant Renderer as renderStaticScene() + participant Clipboard as Clipboard API + + User->>Action: Export as PNG / Copy as PNG + Action->>Scene: exportToCanvas(elements, appState) + Scene->>Renderer: renderStaticScene() on offscreen canvas + Renderer-->>Scene: Canvas with rendered elements + + alt Export to file + Scene->>Scene: canvas.toBlob("image/png") + Scene-->>User: Download PNG file + else Copy to clipboard + Scene->>Clipboard: copyBlobToClipboardAsPng(blob) + else Export as SVG + Scene->>Scene: renderSceneToSvg() + Scene-->>User: SVG string / file + end +``` + +**Key files:** +- `packages/excalidraw/scene/export.ts` — `exportToCanvas()`, `exportToSvg()` +- `packages/excalidraw/renderer/staticScene.ts` — `renderStaticScene()` +- `packages/excalidraw/renderer/staticSvgScene.ts` — `renderSceneToSvg()` +- `packages/excalidraw/data/image.ts` — PNG metadata embedding (scene data inside PNG) + +--- + +## 6. Undo/Redo + +```mermaid +sequenceDiagram + participant User + participant App as App.tsx + participant History as History + participant Delta as HistoryDelta + + User->>App: Make a change (draw, move, delete) + App->>History: record(delta) + History->>History: Push inverse delta to undoStack + + User->>App: Ctrl+Z (Undo) + App->>History: undo() + History->>Delta: undoStack.pop() + Delta->>Delta: applyTo(currentState) + Delta-->>History: New state + inverse delta + History->>History: Push inverse to redoStack + History-->>App: Updated elements + appState +``` + +**Key concepts:** +- History uses **deltas**, not full snapshots — this is memory-efficient +- Each `HistoryDelta` extends `StoreDelta` from `@excalidraw/element` +- `captureUpdate` in `ActionResult` controls whether the action is recorded in history + +**Key files:** +- `packages/excalidraw/history.ts` — `History` class with `undoStack`/`redoStack` +- `packages/excalidraw/actions/actionHistory.tsx` — undo/redo action definitions + +--- + +## 7. Clipboard (Copy/Paste) + +```mermaid +sequenceDiagram + participant User + participant Action as actionCopy/actionPaste + participant Clipboard as clipboard.ts + participant System as System Clipboard + + User->>Action: Ctrl+C + Action->>Clipboard: copyToClipboard(elements) + Clipboard->>Clipboard: serializeAsClipboardJSON(elements) + Clipboard->>System: navigator.clipboard.writeText(json) + + User->>Action: Ctrl+V + Action->>Clipboard: parseClipboard(event) + Clipboard->>System: navigator.clipboard.read() + System-->>Clipboard: ClipboardItems + + alt Excalidraw JSON detected + Clipboard-->>Action: Parsed elements + Action->>Action: Insert elements at cursor + else Plain text + Clipboard-->>Action: Create text element + else Image + Clipboard-->>Action: Create image element + end +``` + +**Key files:** +- `packages/excalidraw/clipboard.ts` — `copyToClipboard()`, `parseClipboard()` +- `packages/excalidraw/actions/actionClipboard.tsx` — copy/paste/cut actions + +--- + +## 8. Library (Reusable Components) + +```mermaid +sequenceDiagram + participant User + participant LibMenu as LibraryMenu + participant Library as library.ts + participant IDB as IndexedDB + participant Scene as Scene + + User->>LibMenu: Open library panel + LibMenu->>Library: Load libraryItemsAtom + Library->>IDB: LibraryPersistenceAdapter.load() + IDB-->>Library: LibraryItem[] + Library-->>LibMenu: Render items + + User->>LibMenu: Click item to insert + LibMenu->>Scene: onInsertLibraryItems(elements) + Scene->>Scene: Add elements to canvas + + User->>LibMenu: Select elements → "Add to Library" + LibMenu->>Library: Save new LibraryItem + Library->>IDB: LibraryPersistenceAdapter.save() +``` + +**Key files:** +- `packages/excalidraw/components/LibraryMenu.tsx` — UI +- `packages/excalidraw/data/library.ts` — persistence adapter and Jotai atom diff --git a/dev-docs/docs/a-docs/05-important-components.md b/dev-docs/docs/a-docs/05-important-components.md new file mode 100644 index 0000000..8bd62df --- /dev/null +++ b/dev-docs/docs/a-docs/05-important-components.md @@ -0,0 +1,277 @@ +# Important Components + +This document covers the most critical classes, services, and modules in the codebase. Understanding these is essential for making safe, effective changes. + +--- + +## Core Components + +### `App.tsx` — The Editor Core + +**Path:** `packages/excalidraw/components/App.tsx` + +This is the largest and most critical file in the project. It is the main React component for the Excalidraw editor. + +**Responsibilities:** +- Canvas rendering orchestration (static + interactive + new element canvases) +- All pointer event handling (click, drag, resize, rotate, pan, zoom) +- All keyboard event handling (shortcuts, text input) +- Tool state machine (selection, drawing, eraser, laser pointer, etc.) +- Drag-and-drop handling (files, library items) +- Gesture handling (pinch-to-zoom on mobile) +- Collaboration state (remote cursors, selections) + +**Side effects:** +- Registers global event listeners (`window.addEventListener`) +- Interacts with clipboard API +- Triggers history recording via `captureUpdate` + +**When you'll touch it:** Adding new tools, changing pointer behavior, modifying canvas interactions. + +> **Warning:** Changes to `App.tsx` can have wide-reaching effects. Test thoroughly with different tools and input devices. + +--- + +### `ActionManager` — Action Dispatch System + +**Path:** `packages/excalidraw/actions/manager.tsx` + +Manages the registry of all user actions and dispatches them. + +**Responsibilities:** +- Registers action objects at startup +- Routes keyboard events to matching actions via `keyTest` +- Executes `action.perform()` and feeds `ActionResult` to the state updater +- Provides `renderAction()` for toolbar/panel rendering + +**How it works:** +``` +User presses Ctrl+D + → ActionManager.handleKeyDown(event) + → Finds actionDuplicateSelection (keyTest matches) + → action.perform(elements, appState, formData, app) + → Returns ActionResult {elements, appState, captureUpdate} + → updater(result) → state update → re-render +``` + +**When you'll touch it:** Adding new keyboard shortcuts or action infrastructure. + +--- + +### `Scene` — Element State Container + +**Path:** `packages/element/src/Scene.ts` + +Holds the array of all elements and manages their ordering. + +**Responsibilities:** +- Stores elements with `getNonDeletedElements()` for rendering +- Manages **fractional indexing** for element ordering (`syncMovedIndices`, `syncInvalidIndices`) +- Provides element lookup by ID +- Throttled validation of element indices + +**Key concept — Fractional Indexing:** +Elements have an `index` field (a fractional string like `"a0"`, `"a1"`, `"a0V"`) that determines their z-order. This allows inserting elements between others without re-indexing the entire array — critical for collaboration where multiple users may reorder simultaneously. + +--- + +### `History` — Undo/Redo Engine + +**Path:** `packages/excalidraw/history.ts` + +Manages undo and redo stacks using delta-based state tracking. + +**Responsibilities:** +- Records state deltas when actions are performed +- Applies inverse deltas for undo, forward deltas for redo +- Manages stack size and memory + +**Key detail:** Uses `HistoryDelta` (extends `StoreDelta` from `@excalidraw/element`) — these are diffs, not full snapshots. Each delta knows how to apply itself to current state and produce an inverse. + +**Risk:** If a delta is applied to state it wasn't computed against (e.g., after a collaboration merge), results may be unexpected. The reconciliation logic handles this, but be aware when modifying either system. + +--- + +## Data Layer Components + +### `reconcileElements()` — Collaboration Merge + +**Path:** `packages/excalidraw/data/reconcile.ts` + +The critical function that merges local and remote element arrays during collaboration. + +**Algorithm:** +1. Iterate through both local and remote element arrays +2. For each element ID, compare `version` numbers +3. Higher version wins; ties go to remote (server authority) +4. Deleted elements (`isDeleted: true`) are preserved for conflict resolution +5. Returns merged array maintaining correct ordering + +**Risk:** Bugs here cause data loss or duplication in collaborative sessions. Any changes must be tested with concurrent editing scenarios. + +--- + +### `restore.ts` — Data Migration + +**Path:** `packages/excalidraw/data/restore.ts` + +Restores and migrates saved data to the current format. + +**Responsibilities:** +- `restoreElements()` — normalizes element data, applies defaults, migrates old formats +- `restoreAppState()` — normalizes app state, applies defaults +- `restoreLibraryItems()` — normalizes library data +- `bumpElementVersions()` — increments versions after restoration + +**When it matters:** When the element schema changes (new properties, renamed fields), migration logic goes here. This ensures old saved files continue to work. + +--- + +### Encryption Module + +**Path:** `packages/excalidraw/data/encryption.ts` + +Handles end-to-end encryption for shared scenes and collaboration. + +**Functions:** +- `generateEncryptionKey()` — creates an AES-GCM key +- `encryptData(key, data)` — encrypts scene data +- `decryptData(key, iv, ciphertext)` — decrypts scene data + +**Key design:** The encryption key is stored in the URL **hash** (fragment), which browsers do not send to servers. This means the server storing the encrypted data cannot read it. + +--- + +## Collaboration Components + +### `Collab.tsx` — Collaboration Orchestrator + +**Path:** `excalidraw-app/collab/Collab.tsx` + +Manages the full lifecycle of a collaborative session. + +**Responsibilities:** +- Start/stop collaboration sessions +- Sync elements between local state and remote participants +- Manage file (image) uploads to Firebase Storage +- Handle connection/disconnection events +- Queue saves to Firebase Firestore + +**Key methods:** +- `startCollaboration()` — generates room, connects socket, loads existing data +- `syncElements()` — called on every local change, broadcasts to peers +- `handleRemoteSceneUpdate()` — receives remote changes, reconciles, updates local state +- `fetchImageFilesFromFirebase()` — lazy-loads images that other participants added + +--- + +### `Portal.tsx` — WebSocket Transport + +**Path:** `excalidraw-app/collab/Portal.tsx` + +Low-level Socket.io wrapper for real-time communication. + +**Responsibilities:** +- Establish/close WebSocket connections +- Broadcast scene data (`broadcastScene`) +- Broadcast mouse positions (`broadcastMouseLocation`) +- Broadcast visible scene bounds (`broadcastVisibleSceneBounds`) + +**Protocol subtypes:** `INIT`, `UPDATE`, `MOUSE_LOCATION`, `USER_VISIBLE_SCENE_BOUNDS`, `IDLE_STATUS` + +--- + +## Firebase Integration + +**Path:** `excalidraw-app/data/firebase.ts` + +Firebase is used for durable storage in collaborative sessions and for shared links. + +**Functions:** +- `loadFromFirebase(roomId)` — reads encrypted scene from Firestore `scenes` collection +- `saveToFirebase(roomId, elements)` — writes encrypted scene to Firestore +- `loadFilesFromFirebase(prefix, ids)` — reads image files from Firebase Storage +- `saveFilesToFirebase(prefix, files)` — writes image files to Firebase Storage + +**Configuration:** Loaded from `VITE_APP_FIREBASE_CONFIG` env var (JSON string parsed at runtime). + +--- + +## Rendering Components + +### `renderStaticScene()` + +**Path:** `packages/excalidraw/renderer/staticScene.ts` + +Renders all committed elements onto the static canvas. + +**How it works:** +1. Clears canvas +2. Applies zoom and scroll transforms +3. Iterates visible elements +4. For each element, generates Rough.js drawable (cached) and renders +5. Handles special cases: images, text, frames, embeds + +### `renderInteractiveScene()` + +**Path:** `packages/excalidraw/renderer/interactiveScene.ts` + +Renders the overlay layer with selection UX. + +**What it renders:** +- Selection rectangles and handles +- Rotation handles +- Binding indicators (arrow snap points) +- Remote user cursors and selections +- Snap guidelines +- Lasso selection path + +--- + +## Background Services + +### `LocalData` — Auto-Persistence + +**Path:** `excalidraw-app/data/LocalData.ts` + +Runs debounced saves to `localStorage` (for `appState`) and `IndexedDB` (for elements and binary files). + +**Trigger:** Called from `App.tsx`'s `onChange` callback on every state change. + +### `FileManager` — Binary File Management + +**Path:** `excalidraw-app/data/FileManager.ts` + +Manages image file uploads, downloads, and caching for collaboration. + +### `tabSync` — Cross-Tab Sync + +**Path:** `excalidraw-app/data/tabSync.ts` + +Synchronizes state across multiple browser tabs using `BroadcastChannel` or `localStorage` events. + +--- + +## Library System + +### `library.ts` + +**Path:** `packages/excalidraw/data/library.ts` + +Manages the reusable element library (templates/components users can save and reuse). + +**Architecture:** +- `LibraryPersistenceAdapter` — interface for storage backends (IndexedDB by default) +- `libraryItemsAtom` — Jotai atom holding the library state +- Items are arrays of elements with metadata (`id`, `name`, `status`, `created`) + +### Font System + +**Path:** `packages/excalidraw/fonts/` + +Manages loading and subsetting of fonts (Virgil, Excalifont, Nunito, etc.). + +- `ExcalidrawFontFace.ts` — font face loading abstraction +- `index.ts` — font registration and loading orchestration +- `fonts.css` — `@font-face` declarations +- Font subsetting in `subset/` for optimized export diff --git a/dev-docs/docs/a-docs/06-testing-guide.md b/dev-docs/docs/a-docs/06-testing-guide.md new file mode 100644 index 0000000..e5c2a7f --- /dev/null +++ b/dev-docs/docs/a-docs/06-testing-guide.md @@ -0,0 +1,262 @@ +# Testing Guide + +## Overview + +Excalidraw uses **Vitest** with **jsdom** as the test environment and **@testing-library/react** for component testing. Tests run in a simulated browser environment with mocked Canvas API, IndexedDB, clipboard, and fonts. + +--- + +## Running Tests + +| Command | Purpose | +|---------|---------| +| `yarn test:app` | Run tests in watch mode | +| `yarn test:update` | Run all tests + update snapshots (use before committing) | +| `yarn test:all` | Full suite: typecheck + lint + prettier + tests | +| `yarn test:typecheck` | TypeScript type checking only | +| `yarn test:code` | ESLint only | +| `yarn test:other` | Prettier check only | +| `yarn test:coverage` | Run tests with coverage report | +| `yarn test:ui` | Vitest UI with coverage visualization | + +### Running a specific test file + +```bash +yarn test:app -- packages/excalidraw/tests/excalidraw.test.tsx +``` + +### Running tests matching a pattern + +```bash +yarn test:app -- -t "should render toolbar" +``` + +--- + +## Test Structure + +### Test file locations + +Tests are found in two patterns: + +1. **Dedicated `tests/` directories** — most common + - `packages/excalidraw/tests/` + - `packages/element/tests/` + - `packages/math/tests/` + - `excalidraw-app/tests/` + +2. **Colocated with source** — for small utility modules + - `packages/common/src/utils.test.ts` + +### Test file naming + +- `*.test.ts` — pure logic tests +- `*.test.tsx` — component/integration tests that render React + +--- + +## Test Setup + +### Global setup file: `setupTests.ts` + +Located at the project root, this file runs before all tests and sets up: + +| Mock | Purpose | +|------|---------| +| `vitest-canvas-mock` | Mocks HTML Canvas 2D API | +| `fake-indexeddb/auto` | Mocks IndexedDB | +| `throttleRAF` mock | Replaces `requestAnimationFrame` throttle with immediate execution | +| `setPointerCapture` | Mocked to no-op | +| `window.matchMedia` | Returns mock MediaQueryList | +| `FontFace` | Mocked constructor | +| `document.fonts` | Mocked FontFaceSet | +| Font fetch | Intercepts font file requests and returns empty responses | +| `#root` div | Creates root element for React rendering | + +### Vitest configuration: `vitest.config.mts` + +Key settings: +- **Path aliases**: Maps `@excalidraw/*` to source directories (not build output) +- **Environment**: jsdom +- **Globals**: `describe`, `it`, `expect`, `vi` available without imports +- **Coverage thresholds**: lines 60%, branches 70%, functions 63%, statements 60% + +--- + +## Test Helpers + +The project has a rich set of test utilities. Learn these before writing tests. + +### `test-utils.ts` + +**Path:** `packages/excalidraw/tests/test-utils.ts` + +| Export | Purpose | +|--------|---------| +| `render()` | Renders the editor in a test container, waits for initialization | +| `unmountComponent()` | Cleanly unmounts the rendered component | +| `GlobalTestState` | Shared state accessible across test helpers | +| `toggleMenu(name)` | Opens/closes menus by test ID | + +### `helpers/api.ts` + +**Path:** `packages/excalidraw/tests/helpers/api.ts` + +The `API` object provides programmatic control of the editor in tests: + +| Method | Purpose | +|--------|---------| +| `API.updateScene({elements, appState})` | Directly set scene state | +| `API.setAppState(patch)` | Update app state | +| `API.setElements(elements)` | Replace all elements | +| `API.createElement({type, ...})` | Create a test element | +| `API.getAppState()` | Read current app state | +| `API.getElements()` | Read current elements | + +### `helpers/ui.ts` + +**Path:** `packages/excalidraw/tests/helpers/ui.ts` + +Simulates user interactions: + +| Helper | Purpose | +|--------|---------| +| `UI.clickTool(toolName)` | Click a tool button in the toolbar | +| `Keyboard.keyDown(key)` | Simulate keyboard events | +| `Keyboard.withModifierKeys({ctrl: true}, () => {...})` | Simulate modifier combinations | +| `Pointer` / mouse helpers | Simulate pointer events at specific coordinates | + +### `helpers/mocks.ts` + +**Path:** `packages/excalidraw/tests/helpers/mocks.ts` + +| Mock | Purpose | +|------|---------| +| `mockThrottleRAF()` | Makes RAF-throttled functions execute synchronously | +| `mockMermaidToExcalidraw()` | Mocks the mermaid-to-excalidraw integration | +| `mockHTMLImageElement()` | Mocks Image loading | + +--- + +## Mocking Strategy + +### General approach + +- **Canvas API**: Fully mocked via `vitest-canvas-mock` (no real rendering in tests) +- **IndexedDB**: Mocked via `fake-indexeddb` for persistence tests +- **Network**: Use `vi.mock()` for Firebase, fetch, and Socket.io +- **requestAnimationFrame**: Mocked to execute synchronously via `throttleRAF` mock +- **Fonts**: Mocked at the fetch level to avoid loading real font files +- **Crypto**: Override `window.crypto` in tests that need encryption + +### Mocking imports + +Use Vitest's `vi.mock()` for module-level mocks: + +```typescript +vi.mock("../data/firebase", () => ({ + loadFromFirebase: vi.fn().mockResolvedValue(null), + saveToFirebase: vi.fn().mockResolvedValue(undefined), +})); +``` + +### Snapshot testing + +Some tests use snapshot assertions for rendered output. When you change UI, snapshots may break: + +```bash +yarn test:update +``` + +Review the snapshot diffs in `__snapshots__/` directories before committing. + +--- + +## Writing a New Test + +Here's a template for a typical component/integration test: + +```typescript +import { vi } from "vitest"; +import { render, unmountComponent } from "../tests/test-utils"; +import { API } from "../tests/helpers/api"; +import { UI, Keyboard } from "../tests/helpers/ui"; +import { Excalidraw } from "../index"; + +describe("my feature", () => { + beforeEach(async () => { + localStorage.clear(); + await render(); + }); + + afterEach(() => { + unmountComponent(); + }); + + it("should do something when user clicks", async () => { + // Arrange: set up initial state + const rect = API.createElement({ type: "rectangle", x: 100, y: 100 }); + API.setElements([rect]); + + // Act: simulate user interaction + UI.clickTool("select"); + // ... simulate clicks, drags, etc. + + // Assert: check the result + const elements = API.getElements(); + expect(elements).toHaveLength(1); + expect(elements[0].type).toBe("rectangle"); + }); + + it("should handle keyboard shortcuts", async () => { + const rect = API.createElement({ type: "rectangle" }); + API.setElements([rect]); + + // Select all + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyDown("a"); + }); + + const appState = API.getAppState(); + expect(appState.selectedElementIds[rect.id]).toBe(true); + }); +}); +``` + +### For pure unit tests (no React): + +```typescript +import { pointFrom, pointRotateRads } from "@excalidraw/math"; + +describe("point rotation", () => { + it("should rotate point around origin", () => { + const result = pointRotateRads( + pointFrom(10, 0), + pointFrom(0, 0), + Math.PI / 2, + ); + expect(result[0]).toBeCloseTo(0); + expect(result[1]).toBeCloseTo(10); + }); +}); +``` + +--- + +## CI Integration + +Tests run automatically in GitHub Actions: + +| Workflow | Trigger | What it checks | +|----------|---------|----------------| +| `test.yml` | Push to `master` | `yarn test:app` | +| `lint.yml` | Pull requests | `yarn test:other` + `yarn test:code` + `yarn test:typecheck` | +| `test-coverage-pr.yml` | Pull requests | Coverage report via `davelosert/vitest-coverage-report-action` | + +### Before submitting a PR, always run: + +```bash +yarn test:update # Tests + snapshot updates +yarn test:typecheck # Type checking +yarn fix # Auto-fix lint and formatting +``` diff --git a/dev-docs/docs/a-docs/07-common-pitfalls.md b/dev-docs/docs/a-docs/07-common-pitfalls.md new file mode 100644 index 0000000..2bdd434 --- /dev/null +++ b/dev-docs/docs/a-docs/07-common-pitfalls.md @@ -0,0 +1,177 @@ +# Common Pitfalls + +Real risks and gotchas that can cause bugs, data loss, or production issues. + +--- + +## 1. Element Immutability + +**Risk:** Mutating elements directly instead of creating new versions. + +Elements in Excalidraw are treated as **immutable**. Every modification must create a new object with an incremented `version` and new `versionNonce`. + +```typescript +// WRONG — direct mutation +element.x = 100; + +// CORRECT — use mutateElement or create a new element +mutateElement(element, { x: 100 }); +``` + +**Why it matters:** +- The rendering pipeline uses reference equality to skip unchanged elements +- The collaboration reconciliation algorithm uses `version` to determine which copy wins +- History (undo/redo) relies on deltas computed from version changes + +If you mutate an element without incrementing its version, collaboration will silently drop the change, undo won't work correctly, and the canvas may not re-render. + +--- + +## 2. Collaboration Reconciliation Edge Cases + +**Risk:** Data loss when local and remote changes conflict. + +The `reconcileElements()` function merges element arrays by comparing versions. The higher version wins; **ties go to remote** (server authority). This means: + +- If two users modify the same element in the same frame, one change will be lost +- Deleted elements (`isDeleted: true`) must be kept in the array for a period to prevent "resurrection" by stale remote updates +- Adding new properties to elements requires careful handling in `restoreElements()` to ensure old saved data gets sensible defaults + +**What to watch for:** +- Never remove deleted elements from arrays during reconciliation +- Always test concurrent editing scenarios when changing element structure +- Be aware that `version` increments happen per-mutation, not per-action + +--- + +## 3. Fractional Indexing Corruption + +**Risk:** Ordering bugs or crashes from invalid fractional indices. + +Elements use **fractional indexing** (the `index` field) for z-ordering. This avoids the need to re-number all elements when one is moved, which is critical for collaboration. + +**Pitfalls:** +- Inserting an element without computing a valid fractional index between its neighbors +- Bulk operations that don't maintain monotonically increasing indices +- `Scene.ts` has validation (`syncMovedIndices`, `syncInvalidIndices`) but it's throttled — bugs may not surface immediately + +**Safe approach:** Use the `Scene` methods for reordering; don't manually assign `index` values. + +--- + +## 4. `App.tsx` Side Effects + +**Risk:** Breaking seemingly unrelated features by changing `App.tsx`. + +`App.tsx` is the largest file in the project and handles many cross-cutting concerns. Event handler changes can have unexpected effects: + +- Changing `pointerDown` handling may break drawing, selection, AND resize +- Modifying keyboard handlers may break shortcuts, text editing, AND command palette +- Gesture handling code affects both desktop and mobile + +**Approach:** +- Test on both desktop and mobile (or at least with touch event simulation) +- Test with multiple tools active (selection, rectangle, arrow, text, freedraw) +- Test with collaboration active (remote cursors, selections) + +--- + +## 5. Canvas Rendering Performance + +**Risk:** Rendering lag or jank from unnecessary re-renders. + +The multi-canvas architecture is specifically designed to avoid re-rendering the static canvas on every pointer move. Breaking this optimization can make the editor feel sluggish. + +**Don'ts:** +- Don't force static canvas re-render on pointer move events +- Don't add expensive computations inside render loops +- Don't create new Rough.js drawables on every render (they're cached per element version) + +**Check:** If your change affects rendering, test with 500+ elements on the canvas. + +--- + +## 6. Import Path Restrictions + +**Risk:** Build failures or circular dependencies from wrong import paths. + +ESLint enforces strict import rules: + +- **Inside `@excalidraw/excalidraw`**: Do NOT import from the barrel `index.tsx` — import from the specific module +- **Jotai imports**: Must come from `@excalidraw/excalidraw/editor-jotai` or `excalidraw-app/app-jotai`, NOT directly from `jotai` +- **Cross-package imports**: Only go through the `@excalidraw/*` aliases, never relative paths to other packages + +```typescript +// WRONG (inside packages/excalidraw/) +import { something } from "./index"; +import { atom } from "jotai"; + +// CORRECT +import { something } from "./specific-module"; +import { atom } from "../editor-jotai"; +``` + +--- + +## 7. Encryption Key in URL Hash + +**Risk:** Accidentally exposing encryption keys. + +Shared scene encryption keys are stored in the URL **hash** (fragment). Browsers don't send the hash to servers, which is the security model. But: + +- Logging the full URL (e.g., in error tracking) may leak the key +- Redirects to other domains may include the hash +- Browser extensions have access to the full URL + +**Rule:** Never log or transmit the full URL including hash in analytics or error reporting. + +--- + +## 8. State Update Ordering + +**Risk:** Stale state bugs from async state updates. + +The action system returns `ActionResult` objects that update state synchronously. But some operations (font loading, image processing, file I/O) are async. Mixing sync and async state updates can cause: + +- Reading stale `appState` after an async operation +- Lost updates when two async operations complete out of order +- React re-render timing issues + +**Approach:** Use `captureUpdate` in `ActionResult` to ensure history records the correct state boundary. For async operations, consider whether the state might have changed by the time the promise resolves. + +--- + +## 9. Font Loading and Text Measurement + +**Risk:** Incorrect text bounding boxes when fonts aren't loaded. + +Text elements compute their dimensions based on font metrics. If a font hasn't loaded when `measureText()` runs, the measurements will be wrong, causing: + +- Overlapping text +- Incorrect auto-sizing of text containers +- Wrong bounding boxes for selection/export + +**The project handles this** via the `InitializeApp` component which loads fonts at startup and the font mock in tests. But if you add a new font or change text measurement logic, verify that measurements are correct after fonts load. + +--- + +## 10. `isDeleted` Flag vs Array Removal + +**Risk:** Breaking undo, collaboration, or references by removing elements from arrays. + +Elements are **soft-deleted** by setting `isDeleted: true`. They are NOT removed from the elements array. This is by design: + +- Undo needs deleted elements to restore them +- Collaboration needs deleted elements to prevent "ghost" resurrections +- Bound elements (arrows to shapes) reference IDs that may point to deleted elements + +**Rule:** Use `isDeleted: true` for deletion. Use `getNonDeletedElements()` when you need the visible set. Never filter deleted elements out of the source-of-truth array. + +--- + +## 11. Testing Pitfalls + +- **Forgetting `await` with `render()`**: The test `render()` function is async. Forgetting to `await` it causes timing-related flaky tests. +- **Not cleaning up**: Always call `unmountComponent()` in `afterEach`. Leaking mounted components causes state pollution between tests. +- **Snapshot drift**: If you see unexpected snapshot changes, don't blindly update. Review the diffs — they may indicate a real regression. +- **Canvas mocking limitations**: `vitest-canvas-mock` doesn't implement all Canvas 2D methods. Some rendering logic cannot be tested in unit tests. diff --git a/dev-docs/docs/a-docs/08-adding-features.md b/dev-docs/docs/a-docs/08-adding-features.md new file mode 100644 index 0000000..1b9f923 --- /dev/null +++ b/dev-docs/docs/a-docs/08-adding-features.md @@ -0,0 +1,273 @@ +# How to Add New Features + +A practical guide to extending Excalidraw while following existing patterns. + +--- + +## Adding a New Action + +Actions are the standard way to add user-triggerable operations (keyboard shortcuts, toolbar buttons, menu items). + +### Step 1: Create the action file + +Create `packages/excalidraw/actions/actionMyFeature.ts`: + +```typescript +import { register } from "./register"; +import type { ActionResult } from "./types"; + +export const actionMyFeature = register({ + name: "myFeature", + label: "My Feature", + icon: MyFeatureIcon, // optional, for toolbar + trackEvent: { category: "element" }, + + // Keyboard shortcut (optional) + keyTest: (event) => + event.key === "m" && event[KEYS.CTRL_OR_CMD], + + // Whether the action is available in current state + predicate: (elements, appState) => { + return appState.selectedElementIds.length > 0; + }, + + // The actual logic + perform: (elements, appState, formData, app) => { + // Your logic here + const newElements = /* transform elements */; + + return { + elements: newElements, + appState: { ...appState /* state changes */ }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + }, + + // Panel component for the toolbar (optional) + PanelComponent: ({ elements, appState, updateData }) => ( + + ), +}); +``` + +### Step 2: Register the action + +Add the export to `packages/excalidraw/actions/index.ts`: + +```typescript +export { actionMyFeature } from "./actionMyFeature"; +``` + +### Step 3: Write tests + +Create `packages/excalidraw/actions/actionMyFeature.test.tsx`: + +```typescript +import { render, unmountComponent } from "../tests/test-utils"; +import { API } from "../tests/helpers/api"; +import { Keyboard } from "../tests/helpers/ui"; +import { Excalidraw } from "../index"; + +describe("actionMyFeature", () => { + beforeEach(async () => { + await render(); + }); + afterEach(() => unmountComponent()); + + it("should do the thing", () => { + // Arrange + API.setElements([API.createElement({ type: "rectangle" })]); + + // Act + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyDown("m"); + }); + + // Assert + expect(API.getElements()).toSatisfyMyCondition(); + }); +}); +``` + +### Key points for actions: + +- `captureUpdate` controls undo/redo: use `CaptureUpdateAction.IMMEDIATELY` for undoable actions +- Return `false` from `perform` to no-op (don't update state) +- `formData` comes from `PanelComponent`'s `updateData()` call +- `app` provides access to `AppClassProperties` for imperative operations + +--- + +## Adding a New Element Property + +When you need to add a new visual or behavioral property to elements. + +### Step 1: Update the type + +In `packages/element/src/types.ts`, add the property to the relevant element type or base type: + +```typescript +type _ExcalidrawElementBase = { + // ... existing properties + myNewProp?: string; +}; +``` + +### Step 2: Set the default + +In `packages/element/src/newElement.ts`, include the default value: + +```typescript +export const newElement = (opts) => ({ + // ... existing defaults + myNewProp: opts.myNewProp ?? "default", +}); +``` + +### Step 3: Add migration logic + +In `packages/excalidraw/data/restore.ts`, handle old elements that don't have the property: + +```typescript +// Inside restoreElement() +element.myNewProp = element.myNewProp ?? "default"; +``` + +### Step 4: Wire up the UI + +If the property is user-editable, create a panel component in `packages/excalidraw/components/` and register an action that changes it. + +### Step 5: Update rendering (if visual) + +If the property affects how elements look, update the rendering in: +- `packages/excalidraw/renderer/staticScene.ts` — for canvas rendering +- `packages/excalidraw/renderer/staticSvgScene.ts` — for SVG export + +--- + +## Adding a New Tool + +Tools determine what happens when the user interacts with the canvas. + +### Step 1: Register the tool type + +Add the tool type to `packages/excalidraw/types.ts` in the `ActiveTool` definition. + +### Step 2: Add the toolbar button + +Update `packages/excalidraw/components/Actions.tsx` or the relevant toolbar component. + +### Step 3: Handle canvas interactions + +The main event handlers are in `packages/excalidraw/components/App.tsx`: +- `handleCanvasPointerDown()` — start of interaction +- `handleCanvasPointerMove()` — during interaction +- `handleCanvasPointerUp()` — end of interaction + +Add your tool's behavior in the appropriate switch/if blocks. + +### Step 4: Add keyboard shortcut + +Add the shortcut key in `packages/excalidraw/actions/shortcuts.ts`. + +--- + +## Adding a New UI Component + +### For library-level components (part of the npm package): + +Place it in `packages/excalidraw/components/`: + +``` +packages/excalidraw/components/ +├── MyComponent.tsx +└── MyComponent.scss (if needed) +``` + +Follow existing patterns: +- Use Jotai atoms from `editor-jotai.ts` for state +- Use `useAppStateValue()` to read app state +- Use SCSS modules for styling (or plain SCSS with BEM-like naming) +- Use `useDevice()` from `@excalidraw/common` for responsive behavior + +### For app-level components (excalidraw.com only): + +Place it in `excalidraw-app/components/`: + +``` +excalidraw-app/components/ +├── MyAppComponent.tsx +└── MyAppComponent.scss +``` + +--- + +## Adding a New Package Utility + +If adding a utility function to one of the core packages: + +1. **Determine the right package:** + - Pure math/geometry → `packages/math/` + - Element manipulation → `packages/element/` + - Shared utility (no element/UI deps) → `packages/common/` + - Export/bounds helpers → `packages/utils/` + +2. **Add to the appropriate source file** (or create a new module) + +3. **Export from the package index** (`src/index.ts`) + +4. **Respect the dependency graph:** `common` and `math` cannot import from other `@excalidraw` packages + +--- + +## Patterns to Follow + +### State updates via actions + +All user-initiated state changes should go through the action system. Don't directly modify state from event handlers when an action would be more appropriate. + +### Element immutability + +Always use `mutateElement()` or create new element objects. Never modify element properties directly. + +### Consistent type imports + +Use `import type { ... }` for type-only imports (enforced by ESLint): + +```typescript +import type { ExcalidrawElement } from "@excalidraw/element/types"; +``` + +### Jotai atom scoping + +Editor-level atoms go through `editor-jotai.ts`. App-level atoms through `app-jotai.ts`. Never import `jotai` directly. + +--- + +## What to Avoid + +- **Don't bypass the action system** for state changes that should be undoable +- **Don't add dependencies** to `packages/common/` or `packages/math/` on other `@excalidraw` packages +- **Don't import from barrel files** (`index.ts`) when inside the same package +- **Don't add large dependencies** without discussing — the bundle size is monitored (`size-limit.yml`) +- **Don't add browser-specific APIs** without fallbacks — the library runs in SSR contexts (Next.js) +- **Don't skip snapshot updates** — run `yarn test:update` and review diffs +- **Don't hardcode strings** — use the i18n system for user-facing text (see `packages/excalidraw/locales/`) + +--- + +## Checklist for New Features + +- [ ] Types defined (in appropriate `types.ts`) +- [ ] Default values set (in `newElement.ts` or `appState.ts`) +- [ ] Migration logic added (in `restore.ts`) if changing element schema +- [ ] Action created with `keyTest` if it has a shortcut +- [ ] UI wired up (toolbar, panel, menu) +- [ ] Rendering updated if it's visual +- [ ] Tests written (unit + integration) +- [ ] Snapshots updated (`yarn test:update`) +- [ ] Type checking passes (`yarn test:typecheck`) +- [ ] Linting passes (`yarn fix` then `yarn test:code`) +- [ ] Works on both desktop and mobile +- [ ] Works with collaboration (if it changes elements) +- [ ] Export/import handles the new data (PNG, SVG, JSON) diff --git a/dev-docs/docs/a-docs/09-tldr-new-devs.md b/dev-docs/docs/a-docs/09-tldr-new-devs.md new file mode 100644 index 0000000..2d4ebe6 --- /dev/null +++ b/dev-docs/docs/a-docs/09-tldr-new-devs.md @@ -0,0 +1,130 @@ +# TL;DR for New Developers + +Your quick-start reference. Bookmark this. + +--- + +## Day 1 Checklist + +- [ ] Clone the repo: `git clone https://github.com/excalidraw/excalidraw.git` +- [ ] Ensure Node.js >= 18 and Yarn Classic are installed +- [ ] Run `yarn` to install all dependencies +- [ ] Run `yarn start` and open http://localhost:3001 +- [ ] Draw a few shapes, export a PNG, try undo/redo — confirm everything works +- [ ] Run `yarn test:app --watch=false` to verify tests pass +- [ ] Run `yarn test:typecheck` to verify TypeScript compiles +- [ ] Read this TL;DR page fully + +--- + +## Key Commands + +| Command | What it does | +|---------|-------------| +| `yarn start` | Start dev server (port 3001) | +| `yarn test:update` | Run tests + update snapshots (**always run before committing**) | +| `yarn test:typecheck` | TypeScript type check | +| `yarn fix` | Auto-fix formatting and linting | +| `yarn test:all` | Full CI-equivalent check | +| `yarn build` | Build the web app | +| `yarn build:packages` | Build only the npm packages | + +--- + +## Files to Read First + +In order of priority: + +1. **`packages/excalidraw/index.tsx`** — main entry point, see what's exported +2. **`packages/element/src/types.ts`** — element type definitions (the data model) +3. **`packages/excalidraw/types.ts`** — `AppState` and component props +4. **`packages/excalidraw/appState.ts`** — default app state (all the knobs) +5. **`packages/excalidraw/actions/types.ts`** — how the action system works +6. **`packages/excalidraw/actions/manager.tsx`** — action dispatch logic +7. **`excalidraw-app/App.tsx`** — app bootstrap and scene loading +8. **`packages/excalidraw/data/reconcile.ts`** — collaboration merge logic +9. **`setupTests.ts`** — test environment setup + +--- + +## The Mental Model + +``` +User draws a shape + → App.tsx handles pointer events + → Creates a new element via newElement() + → ActionManager processes it + → Returns ActionResult {elements, appState, captureUpdate} + → State updates → Canvas re-renders + → History records the delta + → LocalData auto-saves + → If collaborating: sync to peers via Socket.io + Firebase +``` + +--- + +## Project Structure in 30 Seconds + +``` +excalidraw/ +├── excalidraw-app/ ← The website (excalidraw.com) +│ ├── collab/ ← Real-time collaboration +│ └── data/ ← Firebase, localStorage, file management +├── packages/ +│ ├── excalidraw/ ← The npm library (editor component) +│ │ ├── actions/ ← User actions (copy, paste, delete, etc.) +│ │ ├── components/ ← React UI components +│ │ ├── renderer/ ← Canvas rendering +│ │ ├── data/ ← Serialization, encryption, import/export +│ │ └── scene/ ← Export helpers, render configs +│ ├── element/ ← Element types, mutation, transforms +│ ├── common/ ← Shared constants and utilities +│ ├── math/ ← Geometry (points, vectors, curves) +│ └── utils/ ← Export and bounds helpers +└── examples/ ← Integration examples (Next.js, Vite) +``` + +--- + +## The 5 Most Important Concepts + +1. **Elements are immutable** — use `mutateElement()`, never modify properties directly +2. **Actions are the state change API** — all user operations go through `ActionManager` +3. **Jotai for reactive state** — atoms from `editor-jotai.ts`, not raw `jotai` +4. **Multi-canvas rendering** — static (cached), interactive (selection), new element (preview) +5. **Collaboration via reconciliation** — `reconcileElements()` merges by version numbers + +--- + +## Before Your First PR + +```bash +yarn test:update # Tests + snapshot updates +yarn test:typecheck # TypeScript +yarn fix # Formatting + linting +``` + +All three must pass. CI will check them. + +--- + +## When You're Stuck + +| Problem | Where to look | +|---------|---------------| +| "How does X tool work?" | `packages/excalidraw/components/App.tsx` — pointer handlers | +| "How is Y element created?" | `packages/element/src/newElement.ts` | +| "How does Z action work?" | `packages/excalidraw/actions/actionZ.ts` | +| "Why is my element not rendering?" | `packages/excalidraw/renderer/staticScene.ts` | +| "How does collab work?" | `excalidraw-app/collab/Collab.tsx` | +| "Where is state defined?" | `packages/excalidraw/appState.ts` + `types.ts` | +| "How do tests work?" | `setupTests.ts` + `packages/excalidraw/tests/helpers/` | +| "How is data saved?" | `packages/excalidraw/data/json.ts` + `excalidraw-app/data/LocalData.ts` | + +--- + +## Quick Links + +- [Excalidraw Docs](https://docs.excalidraw.com/) — official documentation +- [Contributing Guide](https://docs.excalidraw.com/docs/introduction/contributing) — PR guidelines +- [Development Guide](https://docs.excalidraw.com/docs/introduction/development) — official setup guide diff --git a/dev-docs/docs/a-docs/README.md b/dev-docs/docs/a-docs/README.md new file mode 100644 index 0000000..1b816f4 --- /dev/null +++ b/dev-docs/docs/a-docs/README.md @@ -0,0 +1,31 @@ +# Excalidraw — Developer Onboarding Guide + +Welcome to the Excalidraw project! This guide will help you get productive quickly. + +## Table of Contents + +| # | Document | What You'll Learn | +|---|----------|-------------------| +| 1 | [Architecture Overview](./01-architecture-overview.md) | Monorepo structure, package responsibilities, how components interact | +| 2 | [Local Setup](./02-local-setup.md) | Prerequisites, installation, running the app, environment variables | +| 3 | [Codebase Navigation](./03-codebase-navigation.md) | Folder structure, where to find things, naming conventions | +| 4 | [Key Business Flows](./04-key-business-flows.md) | Drawing, collaboration, save/load, export, undo/redo | +| 5 | [Important Components](./05-important-components.md) | Core classes, managers, event handlers, integration services | +| 6 | [Testing Guide](./06-testing-guide.md) | Test structure, running tests, mocking strategy, writing new tests | +| 7 | [Common Pitfalls](./07-common-pitfalls.md) | Race conditions, data consistency, things that break production | +| 8 | [Adding New Features](./08-adding-features.md) | Patterns to follow, where to add logic, what to avoid | +| 9 | [TL;DR for New Developers](./09-tldr-new-devs.md) | Day-1 checklist, key commands, files to read first | + +There is also a printable combined version: + +- [**new-employee-guide.html**](./new-employee-guide.html) — styled HTML combining key content, optimized for printing/PDF export + +## How to Use This Guide + +**Day 1:** Start with the [TL;DR](./09-tldr-new-devs.md), then follow the [Local Setup](./02-local-setup.md). + +**Day 2–3:** Read the [Architecture Overview](./01-architecture-overview.md) and [Codebase Navigation](./03-codebase-navigation.md). + +**First week:** Study the [Key Business Flows](./04-key-business-flows.md) and [Important Components](./05-important-components.md). + +**Before your first PR:** Read the [Testing Guide](./06-testing-guide.md), [Common Pitfalls](./07-common-pitfalls.md), and [Adding New Features](./08-adding-features.md). diff --git a/dev-docs/docs/rule-validation/excalidraw-element-model-ab-validation.md b/dev-docs/docs/rule-validation/excalidraw-element-model-ab-validation.md new file mode 100644 index 0000000..f07cfd3 --- /dev/null +++ b/dev-docs/docs/rule-validation/excalidraw-element-model-ab-validation.md @@ -0,0 +1,82 @@ +# A/B rule validation: `excalidraw-element-model.mdc` + +This document records a **Rule Validation: A/B method** check for the Cursor rule +`.cursor/rules/excalidraw-element-model.mdc`. + +**Method (summary):** + +1. **A — Rule ON:** Same prompt with the rule active (`.mdc` present, globs match). +2. **B — Rule OFF:** Rename rule to `.mdc.off` (or otherwise disable), **same prompt**. +3. **Compare** outputs → conclude **rule works** vs **rewrite rule**. + +--- + +## Rule under test + +| Field | Value | +|-------|--------| +| File | `.cursor/rules/excalidraw-element-model.mdc` | +| Globs | `packages/element/**/*.ts`, `packages/element/**/*.tsx` | +| `alwaysApply` | `false` | + +**Intent of the rule:** Immutable elements, use `mutateElement`, Scene for z-order, schema → `types` / `newElement` / `restore`, collaboration versioning. + +--- + +## Documented test scenario + +| Item | Value | +|------|--------| +| **Context** | User attached `@packages/element/src/mutateElement.ts` and had element-package context (glob match). | +| **Prompt** | “How should I move a rectangle in Excalidraw’s element package?” | + +--- + +## Result A (rule ON) + +- **Debug line:** Reply began with + `RULE excalidraw-element-model APPLIED 🧱` + (per **Important (debug)** section in the rule). +- **Framing:** Opened with not relying on raw `element.x = …` alone; tied behavior to **collaboration, history, and version / versionNonce / updated** early. +- **Structure:** Numbered sections — (1) core `mutateElement`, (2) `Scene.mutateElement` for React updates, (3) `newElementWith`. +- **Technical content:** `mutateElement(element, elementsMap, { x, y })`, cited warning about re-renders, cited `Scene.mutateElement` + `triggerUpdate`, cited `newElementWith`, code references to `packages/element/src/mutateElement.ts` and `Scene.ts`. + +--- + +## Result B (rule OFF) + +- **Debug line:** **Absent**; reply noted that **`excalidraw-element-model`** appeared disabled (e.g. `excalidraw-element-model.off`) and that the rule’s debug line was therefore skipped. +- **Framing:** Went straight into **how** to move (x/y, `mutateElement`), then re-render caveat, then immutable alternative. +- **Structure:** Headings “Move a rectangle”, “Re-renders in the editor”, “Immutable alternative”, closing “Avoid …”. +- **Technical content:** **Same** APIs, same files, same citations — `mutateElement`, `elementsMap`, `Scene.mutateElement`, `newElementWith`, avoid `rectangle.x = …` without versioning. + +--- + +## Comparison + +| Dimension | A (rule ON) | B (rule OFF) | +|-----------|-------------|--------------| +| **RULE … APPLIED line** | Yes | No | +| **Upfront immutability / collab story** | Stronger, earlier | Present but later / lighter | +| **Correct move API (`mutateElement`, `Scene`)** | Yes | Yes | +| **Substantive technical difference** | **Small** for this prompt | — | + +--- + +## Conclusion + +- **The rule clearly “fires” from a validation perspective** when the **debug line** appears in A and not in B — that is a reliable **A/B signal**. +- For **this specific prompt**, the **substantive** answers were **largely the same**, because **`mutateElement.ts`** (attached / in context) already documents the correct pattern, version bumps, and the “use `scene.mutateElement` for component updates” warning. The model does not need the `.mdc` text to give a correct low-level answer in that situation. +- **Recommendation for future A/B tests of this rule:** use a prompt **not fully answered by a single open file**, for example: + - *“I’m adding a new optional field on frame elements; list every file to touch and in what order.”* + Here the rule’s **`restore.ts`**, **`types.ts`**, **`newElement.ts`**, rendering/export, and collab/version checklist should create a **larger** gap between rule ON vs OFF. + +**Verdict:** **Rule works** for **enforcement visibility** (debug banner + framing). For **content-only** checks, pair with prompts where the rule adds information **beyond** the currently focused source file. + +--- + +## Related + +- Other project rules: `.cursor/rules/*.mdc` +- Onboarding / element conventions: `dev-docs/docs/a-docs/` (e.g. `08-adding-features.md`, `07-common-pitfalls.md`) +- Agent overview: `AGENT.md` (repo root)