From a731ebba949de705ce04cebb06d86c7202c95b07 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 5 Aug 2026 17:49:56 +1000 Subject: [PATCH 1/2] Add skills to make dev on hard code spaces easier and hopefully take less tokens --- .claude/skills/collections/SKILL.md | 220 ++++++++++++++++++++++++++ .claude/skills/drag-and-drop/SKILL.md | 144 +++++++++++++++++ .claude/skills/virtualizer/SKILL.md | 181 +++++++++++++++++++++ 3 files changed, 545 insertions(+) create mode 100644 .claude/skills/collections/SKILL.md create mode 100644 .claude/skills/drag-and-drop/SKILL.md create mode 100644 .claude/skills/virtualizer/SKILL.md diff --git a/.claude/skills/collections/SKILL.md b/.claude/skills/collections/SKILL.md new file mode 100644 index 00000000000..304385b14a8 --- /dev/null +++ b/.claude/skills/collections/SKILL.md @@ -0,0 +1,220 @@ +--- +description: Use when answering questions about or modifying the Collections system used by react-aria-components (RAC) and Spectrum 2 (S2) — the two-pass render, the fake DOM / Document, CollectionBuilder, BaseCollection, CollectionNode, useListState/useTreeState, createLeafComponent/createBranchComponent, Section/Item nodes, SSR of collections, or the difference between the new and old (RSP v3) collection builders. +--- + +# Collections (new RAC/S2 system) + +Guidance for the collection architecture behind RAC components (`ListBox`, `Menu`, `Table`, `Tree`, +`GridList`, `TagGroup`, `Tabs`, `Breadcrumbs`) and S2 (which re-exports/wraps RAC). + +Source of truth lives in **`packages/react-aria/src/collections/`** (re-exported publicly as +`@react-aria/collections` / the `react-aria/private/collections/*` aliases). Do not confuse it with +the *old* builder in `packages/react-stately/src/collections/`. + +## The core idea: two-pass render + +Collection children use natural JSX (``), but that +JSX is **not** what ends up in the browser DOM. Rendering happens in two passes: + +1. **Pass 1 — build the Collection.** The collection JSX is rendered by React into a *fake DOM* (a + lightweight in-memory document model, not `document`). This produces an immutable `BaseCollection` + — a `Map` with sibling/parent/child links. Because React does this rendering, + we keep JSX syntax *and* composition/context, and we learn each item's index, level, parent, + sibling keys, and the total item count before rendering anything real. +2. **Pass 2 — render the real DOM.** The `BaseCollection` is fed into state (`useListState` / + `useTreeState`), and a renderer walks the collection and calls each node's stored `render` function + to emit the actual DOM (supporting virtualization / rendering a subset). + +Rationale is documented inline at `packages/react-aria/src/collections/Document.ts:18-30`. + +``` +{item => } + │ + ▼ CollectionBuilder renders content into portal + ┌─────────────────────────── PASS 1 (fake DOM) ───────────────────────────┐ + │ Collection → CollectionRoot → createPortal(children, Document) │ + │ each Item/Section is a createLeafComponent/createBranchComponent → │ + │ renders host elements → React reconciler mutates fake DOM │ + │ Document.getCollection() finalizes an immutable BaseCollection │ + └──────────────────────────────────────────────────────────────────────────┘ + │ collection (BaseCollection) + ▼ + ┌─────────────────────────── PASS 2 (real DOM) ───────────────────────────┐ + │ useListState(collection) → SelectionManager, keyboard delegates │ + │ CollectionRoot walks collection, calls node.render(node) → real
s │ + └──────────────────────────────────────────────────────────────────────────┘ +``` + +## The fake DOM / document model + +React can render into any host environment given a host-config; here the host is a hand-written mock +DOM in `packages/react-aria/src/collections/Document.ts`. **No custom reconciler** is written — instead +`react-dom`'s `createPortal` targets a fake `Document` object that duck-types the DOM API React calls +(`createElement`, `appendChild`, `insertBefore`, `removeChild`, `style`, `setAttribute`, …). + +Key classes (all in `Document.ts`): + +| Class | Role | +|---|---| +| `BaseNode` (`Document.ts:36`) | Base mutable fake-DOM node: `firstChild`/`lastChild`/`nextSibling`/`parentNode` getters+setters that call `ownerDocument.markDirty`. Implements `appendChild`/`insertBefore`/`removeChild` (`Document.ts:126-220`). | +| `ElementNode` (`Document.ts:262`) | A mutable fake element. `nodeType = 8` (COMMENT_NODE — deliberately not ELEMENT_NODE so React DevTools doesn't try to measure it, `Document.ts:263`). Owns one immutable `CollectionNode`. Has `setProps` (`:337`), `updateNode` (`:309`), a fake `style` getter for Suspense `display:none` handling (`:379`), and no-op `setAttribute`/`hasAttribute`. | +| `Document` (`Document.ts:428`) | The portal target. `nodeType = 11` (DOCUMENT_FRAGMENT_NODE). Owns the current immutable `collection`, a `nextCollection` (copy-on-write), a `dirtyNodes` set, and the `useSyncExternalStore` subscription plumbing. | + +How nodes get created & the collection is built: + +- React calls `document.createElement(type)` → `new ElementNode(type, this)` (`Document.ts:452`). +- React sets children via `appendChild`/`insertBefore`; each setter calls `markDirty` and, when + connected, `queueUpdate()` (`Document.ts:148-151`, `:180-182`). +- The `ref` callback on the host element calls `element.setProps(...)`, which lazily constructs (or + copy-on-write clones) the immutable `CollectionNode`, copying `props`, `rendered`, `render`, `value`, + `textValue`, `id` (`Document.ts:337-377`). **`id` is immutable** — changing it throws (`:366-368`). +- `Document.updateCollection()` (`:509`) is the finalize step: removes disconnected/hidden nodes, + recomputes indices, calls `ElementNode.updateNode()` to recompute `index`/`level`/`parentKey`/ + `prevKey`/`nextKey`/`firstChildKey`/`lastChildKey`/`colIndex` (`:309-335`), adds surviving nodes to + `nextCollection`, then `collection.commit(...)` **freezes** it (`:538-548`). +- `getCollection()` (`:495`) runs the finalize and returns the frozen collection to React via + `useSyncExternalStore`. `queueUpdate()` clones the collection so React notices a new snapshot and + schedules the second render (`:551-576`). + +**Mutable fake node vs immutable collection node.** Each `ElementNode` (mutable, stable identity that +React holds onto) owns one `CollectionNode` (immutable, copy-on-write). `getMutableNode()` clones the +`CollectionNode` on first write per update cycle (`Document.ts:295-307`); unchanged nodes are shared, +so updates are cheap. + +`` (`packages/react-aria/src/collections/Hidden.tsx:66`): during SSR there are no portals, so +the hidden collection tree is rendered into a `