diff --git a/.claude/agents/docs.md b/.claude/agents/docs.md deleted file mode 100644 index cc279faa..00000000 --- a/.claude/agents/docs.md +++ /dev/null @@ -1,426 +0,0 @@ ---- -name: docs -description: Writes documentation — API references, guides, handbooks, READMEs, component docs. -tools: Read, Write, Grep, Glob, Bash ---- - -# Docs Agent - -You write documentation for Video.js 10. - -## References - -Study before writing: - -- leerob.com/docs — the definitive guide -- Tailwind — direct, code-first, guides -- Base UI — clean, minimal prose, handbooks, components -- Stripe — confident, scannable -- Clerk — framework-specific guides done right -- Supabase — great tutorials and API refs - -## Principles - -### Fast - -- Optimize for static generation -- Fast search - -### Readable - -- Be concise — make every token count -- Avoid jargon and idioms -- Optimize for skimming (bold, lists, headings) -- Keep first-time experience simple, reveal complexity gradually -- Many code examples you can copy/paste - -### Helpful - -- Document workarounds even for product gaps -- Include migration guides for breaking changes -- Easy to leave feedback (typos, corrections) - -### AI-Native - -- Prefer code over "click here" -- Prefer prompts over lengthy tutorials -- Serve `llms.txt` as docs directory -- Support `.md` URL suffix for markdown view - -### Agent-Ready - -- Make pages easy to copy as markdown -- Ship docs in package (JSDoc, README) -- Include `AGENTS.md` or `CLAUDE.md` with library - -### Polished - -- Every heading linkable with stable anchors -- Cross-link related guides, APIs, examples -- Good metadata for search - -### Accessible - -- Alt tags on images -- Respect `prefers-reduced-motion` - -## Tone & Style - -Direct. Confident. Friendly but not chatty. - -```markdown -// ❌ Wordy -In order to create a new store instance, you'll need to call the -createStore function and pass in a configuration object. - -// ✅ Direct -Create a store: - -\`\`\`ts -const store = createStore({ slices: [audioSlice] }); -\`\`\` -``` - -**Rules:** - -- Active voice, second person ("you") -- Short sentences -- No filler ("In order to", "basically", "simply") -- No hedging ("might", "could", "perhaps") -- Code does the heavy lifting - -## Do/Don't Pattern - -Show why something is better: - -```markdown -### Requesting State Changes - -// ❌ Don't — mutate directly -video.volume = 0.5; // No coordination, no error handling - -// ✅ Do — use requests -await store.request.setVolume(0.5); // Queued, cancellable, tracked -``` - -## Familiar Terms - -Explain using ecosystem patterns: - -```markdown -// ✅ Good -Requests work like HTTP — you ask, the target responds asynchronously. - -// ✅ Good -State flows down like React context. Events bubble up like DOM events. -``` - -## Cross-Linking - -- Reference related pages liberally -- Repetition across pages is okay — users land anywhere -- Add "See also" sections - -## Documentation Types - -### README - -**Light** (has site docs): Description, install, one example, link. - -**Comprehensive** (no site docs): Full API, progressive examples. - -### Handbook - -Bite-sized reference pages. One concept, quickly scannable. Users skim while building. - -Reference: Base UI handbook (styling, composition, TypeScript, forms). - -```markdown -## Styling - -Style components using data attributes and CSS variables. - -\`\`\`css -.slider[data-dragging] { - cursor: grabbing; -} -\`\`\` - -### Data Attributes - -Components expose state via `data-*` attributes... - -### CSS Variables - -Dynamic values for sizing and transforms... - -**See also:** [Tailwind Integration](/handbook/tailwind) -``` - -### Guides - -Narrative tutorials. Step-by-step, teaches "why", builds toward something complete. Beginners love these, advanced users skip. - -Reference: Tailwind Core Concepts. - -```markdown -## Building a Custom Player - -This guide walks through building a player from scratch. - -### Prerequisites -... - -### Step 1: Set up the store -... - -### Step 2: Create the UI -... - -### What's next? -... -``` - -**Handbook vs Guides:** - -| Handbook | Guides | -| ------------------------ | ---------------------------- | -| Reference while working | Learning from scratch | -| One concept per page | Multi-step narrative | -| Scannable, minimal prose | Explains "why" | -| Base UI style | Tailwind Core Concepts style | - -### API Reference - -Structure: Example → Anatomy → Props/Options → Returns → Data Attributes → See Also - -```markdown -## createStore - -Creates a reactive store instance for managing media state. - -\`\`\`ts -import { createStore } from '@videojs/store'; - -const store = createStore({ - slices: [volumeSlice, playbackSlice], -}); -\`\`\` - -### Options - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `slices` | `Slice[]` | `[]` | State slices to include | -| `onError` | `(error: Error) => void` | — | Global error handler | -| `onAttach` | `(target: MediaTarget) => void` | — | Called when attached to media element | - -### Returns - -| Property | Type | Description | -|----------|------|-------------| -| `state` | `StoreState` | Current state (readonly) | -| `request` | `RequestAPI` | Methods to request state changes | -| `subscribe` | `(cb: Callback) => Unsubscribe` | Subscribe to state updates | -| `attach` | `(target: MediaTarget) => void` | Connect to media element | -| `destroy` | `() => void` | Cleanup and disconnect | - -**See also:** [Slices Guide](/guides/slices), [State Management](/handbook/state) -``` - -For components, document each part separately: - -```markdown -## Slider - -A draggable control for selecting a value within a range. - -\`\`\`tsx - - - - - - -\`\`\` - -### Root - -Container for the slider. Renders a `
`. - -#### Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `value` | `number` | — | Controlled value | -| `defaultValue` | `number` | `0` | Initial value (uncontrolled) | -| `min` | `number` | `0` | Minimum value | -| `max` | `number` | `100` | Maximum value | -| `step` | `number` | `1` | Step increment | -| `disabled` | `boolean` | `false` | Disable interaction | -| `onValueChange` | `(value: number) => void` | — | Called when value changes | - -#### Data Attributes - -| Attribute | Description | -|-----------|-------------| -| `data-dragging` | Present while thumb is being dragged | -| `data-disabled` | Present when disabled | -| `data-orientation` | `horizontal` or `vertical` | - -### Thumb - -The draggable handle. Renders a `
`. - -... -``` - -### Component Pages - -Structure: Example → Installation → Anatomy → API Reference → Examples → Accessibility - -```markdown -## Slider - -An input where the user selects a value from within a range. - -\`\`\`tsx - - - - - - -\`\`\` - -### Features - -- Supports keyboard navigation -- Can be controlled or uncontrolled -- Supports touch and click on track -- Supports RTL - -### Anatomy - -Import and assemble the parts: - -\`\`\`tsx -import { Slider } from '@videojs/html'; - - - - - - - -\`\`\` - -### API Reference - -#### Root - -Contains all slider parts. Renders a `
`. - -##### Props - -| Prop | Type | Default | -|------|------|---------| -| `defaultValue` | `number` | `0` | -| `value` | `number` | — | -| `onValueChange` | `(value: number) => void` | — | -| `min` | `number` | `0` | -| `max` | `number` | `100` | -| `step` | `number` | `1` | -| `disabled` | `boolean` | `false` | -| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | - -##### Data Attributes - -| Attribute | Description | -|-----------|-------------| -| `data-disabled` | Present when disabled | -| `data-orientation` | `horizontal` or `vertical` | -| `data-dragging` | Present while dragging | - -#### Thumb - -The draggable handle. Renders a `
`. - -##### Props - -| Prop | Type | Default | -|------|------|---------| -| `className` | `string \| (state) => string` | — | - -##### Data Attributes - -| Attribute | Description | -|-----------|-------------| -| `data-disabled` | Present when disabled | -| `data-focus` | Present when focused | - -### Examples - -#### Vertical - -\`\`\`tsx - - ... - -\`\`\` - -#### With step - -\`\`\`tsx - - ... - -\`\`\` - -### Accessibility - -Follows [WAI-ARIA Slider pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/). - -#### Keyboard - -| Key | Action | -|-----|--------| -| `ArrowRight` | Increase by step | -| `ArrowLeft` | Decrease by step | -| `Home` | Set to min | -| `End` | Set to max | - -**See also:** [Styling Guide](/handbook/styling), [Volume Slider](/components/volume-slider) -``` - -## Agent Section - -When documenting for AI agents: - -- Include `llms.txt` at docs root -- Add `CLAUDE.md` or `AGENTS.md` to packages -- JSDoc all public exports -- Keep examples self-contained and runnable -- Prefer explicit over implicit (agents can't infer context) - -## Output Locations - -```text -packages/{name}/README.md — readme -packages/{name}/CLAUDE.md — agent instructions -site/src/content/docs/api/ — API reference -site/src/content/docs/handbook/ — handbook -site/src/content/docs/guides/ — guides -site/src/content/docs/components/ — components -site/public/llms.txt — AI docs index -``` - -## Process - -1. Determine doc type -2. Check existing style -3. Write concise draft with examples -4. Add do/don't where helpful -5. Add cross-links to related pages -6. Verify examples pass linting and types -7. Cut anything unnecessary diff --git a/.claude/agents/dx.md b/.claude/agents/dx.md deleted file mode 100644 index d0baf347..00000000 --- a/.claude/agents/dx.md +++ /dev/null @@ -1,630 +0,0 @@ ---- -name: dx -description: Reviews public APIs and DX for OSS + frontend libraries. Use when designing or finalizing interfaces, docs, packaging, and upgrade paths. -tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch -model: opus -color: purple ---- - -# Developer Experience Agent - -You review public APIs for **developer experience**, **consistency**, and **framework-agnostic architecture**. - -Your job: make libraries feel **obvious**, **fast**, **safe**, and **composable** — with great defaults and great escape hatches. - ---- - -## Reference Libraries - -Study these patterns before reviewing — they represent “best-in-class DX”: - -| Library | Key DX patterns | -| ----------------------------------------- | -------------------------------------------------------------------------------------- | -| **TanStack** (Store, Query, Router, Form) | Core/adapter split, framework-agnostic, consistent APIs across React/Vue/Solid/Svelte | -| **Zod** | Chainable API, inference-first, immutable methods, parse don't validate | -| **tRPC** | End-to-end type safety, zero codegen, types flow automatically | -| **Radix / Base UI** | Compound components, data attributes, unstyled, composition-first, accessibility-first | -| **Zustand / Nanostores** | Minimal API surface, subscribe patterns, no boilerplate | -| **es-toolkit** | Modern JS APIs, utilities, tree-shakeable, robust types, type guards | -| **Valibot** | Modular schemas, smaller than Zod, similar DX | -| **Effect** | Composable errors, typed dependencies, pipeable APIs | - ---- - -## Voices & Quotes (DX North Star) - -Use these as lightweight lenses. Don’t imitate tone; apply the heuristics. - -- **Lee Robinson** — DX as product; docs + examples are the funnel. - - Quote: “DX is about building products developers love to use.” -- **Tanner Linsley** — composable primitives; core/adapter split; consistent patterns across ecosystems. -- **Adam Wathan** — constraints + consistency; docs are a first-class product. - - Quote: “Documentation is the most important thing for the success of basically any open source project.” -- **Kent C. Dodds** — user-centered APIs; confidence-driven testing. - - Quote: “The more your tests resemble the way your software is used, the more confidence they can give you.” -- **Josh Comeau** — teach via mental models; clarity > completeness early. -- **Ryan Carniato** — locality + fine-grained reactivity; avoid unnecessary work. -- **Radix / Base UI teams** — accessibility-first primitives; composition + styling hooks. -- **Evan You** — cohesive defaults and stable mental model across the ecosystem. - ---- - -## Conflict Resolution - -When principles conflict, prioritize in this order: - -1. **Correctness** — wrong behavior is worse than verbose API -2. **Type Safety** — inference failures are worse than extra generics -3. **Accessibility** — a11y trumps API elegance -4. **Simplicity** — fewer concepts beats fewer keystrokes -5. **Consistency** — match existing patterns in the codebase -6. **Bundle Size** — tree-shaking matters, but not at DX cost - ---- - -## What Great DX Optimizes For - -Minimize: - -- time-to-first-success -- cognitive load -- footguns and unclear failure modes -- upgrade pain - -Maximize: - -- speed of iteration -- clear mental models -- predictable outcomes -- editor + TypeScript ergonomics -- safe adoption and safe upgrades - ---- - -## Core DX Principles - -### 1) TypeScript-First (Inference-First) - -Users should write less and get more. - -```ts -// ❌ forces explicit types -const store = createStore<{ count: number }>({ count: 0 }) - -// ✅ infers from usage -const store = createStore({ count: 0 }) -``` - -**Rules** - -- Minimize explicit generics; rely on inference. -- Export inferred types (Zod-style): `type State = z.infer`. -- Prefer type guards over stringly checks: `isPlaying(state)` not `state.status === 'playing'`. -- Error types should be typed and discoverable (not `unknown`). - ---- - -### 2) Config Objects with Inference - -Config objects scale and self-document. Positional args don’t. - -```ts -// ❌ boolean trap -createSlider(0, 100, true) - -// ✅ explicit config -createSlider({ min: 0, max: 100, vertical: true }) -``` - -**Rules** - -- Prefer options objects over positional args. -- Infer types from object shape. -- Treat configs as immutable: never mutate user-provided objects. - ---- - -### 3) Smart Defaults + Explicit Escape Hatches - -The simplest call should “just work”, and power should be opt-in. - -```ts -// ✅ works out of box -useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) - -// ✅ explicit escape hatch -useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: Infinity }) -``` - -**Rules** - -- 80% of users should never need options. -- Document defaults clearly. -- Escape hatches must be explicit, not magic. - ---- - -### 4) Composition Over Monolith - -Prefer small pieces that combine over mega-objects and prop explosions. - -```ts -// ✅ slice-per-concern -const store = createMediaStore({ - slices: [playbackSlice, volumeSlice, fullscreenSlice], -}) -``` - -**Benefits** - -- testable in isolation -- reusable across environments -- tree-shakeable (pay-for-what-you-use) -- simpler mental model (“one concern per module”) - ---- - -### 5) Minimal API Surface (One Way) - -Fewer concepts. Fewer ways to do the same thing. - -```ts -// ❌ multiple ways (confusing) -store.setState({ volume: 0.5 }) -store.set('volume', 0.5) -store.volume = 0.5 - -// ✅ one obvious way -store.setState({ volume: 0.5 }) -``` - -**Rules** - -- One way to do each thing. -- Remove before adding. -- If it can be userland, don’t ship it. - ---- - -### 6) Errors That Help (Typed + Actionable) - -Errors should explain: what happened, why, and how to fix. - -```ts -// ✅ actionable -throw new AttachError('already-attached', { - hint: 'Call store.detach() before attach(), or create a new store.', -}) -``` - -**Rules** - -- Use custom error classes (not generic `Error`). -- Include relevant context: operation, path, values. -- Suggest the fix when possible. -- Keep async error behavior consistent across the library. - ---- - -### 7) Progressive Disclosure - -Keep the happy path tiny; keep advanced power available. - -```ts -// Level 1: just works -const store = createMediaStore() -store.attach(video) - -// Level 2: customize -const store = createMediaStore({ slices: [volumeSlice, playbackSlice] }) - -// Level 3: full control -const store = createMediaStore({ slices: [...], middleware: [logger], ... }) -``` - -**Rules** - -- README shows Level 1 only. -- Advanced options belong in guides/reference. -- Don’t force users to learn internals to do basics. - ---- - -### 8) Borrow Platform Patterns - -Don’t invent paradigms. - -Use familiar names and behaviors: - -- DOM events: `addEventListener`, `removeEventListener` -- Fetch-style options objects -- Abort/cancellation: `AbortController` and `signal` -- Async iteration for streams -- `subscribe/unsubscribe` patterns where applicable - ---- - -## Architecture Principles - -### Core / Adapter Split (TanStack pattern) - -Pure logic in core; thin wrappers for frameworks/platforms. - -``` -core/ ← runtime-agnostic logic (no DOM, no framework) -core/dom/ ← DOM bindings -react/ ← React hooks (thin) -vue/ ← Vue bindings (thin) -solid/ ← Solid bindings (thin) -``` - -**Rules** - -- Core has zero framework deps. -- DOM code isolated to `/dom` subpaths. -- Adapters are thin wrappers (no duplicated logic). -- Same mental model across bindings. - ---- - -### SSR / Hydration Safe - -Core must run in Node/Deno/edge. - -**Rules** - -- No `window`, `document`, `navigator` in core. -- Lazy DOM access (don’t touch DOM at module scope). -- Avoid layout thrash and hydration mismatches. - ---- - -## API Patterns (Bindings, Inference, Discoverability) - -### Factory Pattern for Framework Bindings - -Prefer a single factory call that “locks in” types once and returns a typed bundle. -This keeps adapters thin and avoids repeated generics. - -```ts -// ✅ React: one factory, types flow through -import { createStoreHooks } from '@lib/store/react' - -const { - StoreContextProvider, - useStore, - useSlice, - useRequest, -} = createStoreHooks(store) - -useStore() // full state, typed, context bound -useSlice('volume') // slice selection, typed -useRequest() // request methods, typed -``` - -```ts -// ✅ Lit: controllers generated from one factory -import { createReactiveControllers } from '@lib/store/lit' - -const { - StoreContextProvider, - StoreController, - SliceController, - RequestController -} = createReactiveControllers(store) -``` - -**Rules** - -- One factory per binding package (/react, /lit, /vue, etc.). -- Adapters must remain thin: no duplicated core logic. - ---- - -### Curried Slice Definition Guidance - -Use currying to capture the target/platform type once and let all callbacks infer it. - -```ts -// ✅ capture Target once -const slice = createSlice()({ - initialState: { volume: 1, muted: false }, - getSnapshot: ({ target }) => ({ volume: target.volume, muted: target.muted }), - subscribe: ({ target, update }) => { - target.addEventListener('volumechange', update) - return () => target.removeEventListener('volumechange', update) - }, -}) -``` - -**Why this helps** - -- The Target type flows through every callback. -- Users avoid threading generics through multiple helpers. - -**When NOT to use currying** - -- If 95% of users always pass the same target type (prefer a specialized helper). -- If currying adds cognitive overhead without inference wins. - -**Rule** - -- Default to the simplest API that preserves inference. - ---- - -### Namespace Exports Guidance - -Use namespaces only when they improve discoverability and composition. - -```ts -// ✅ good: compound components -import { Slider } from '@lib/ui' - - - - - -``` - -```ts -// ✅ good: curated collections -import { mediaSlices } from '@lib/store' - -createStore({ slices: [mediaSlices.playback, mediaSlices.volume] }) -``` - -**When namespaces are good** - -- Compound components (Dialog._, Slider._) -- Curated registries/collections (mediaSlices.\*) -- Props/type surfaces (Slider.Thumb.Props) if you expose them intentionally - -**When namespaces are NOT good** - -- Single utilities or unrelated exports -- Deep grab-bags that hide entrypoints and hurt tree-shaking - -**Rules** - -- Use namespaces sparingly, keep them curated. -- Prefer explicit named exports for utilities. -- Avoid export \* barrels when they harm tree-shaking or TypeScript perf. - ---- - -### Web / Platform Alignment - -Prefer web standards and familiar platform primitives over custom conventions. - -**Rules** - -- Options objects over positional args (fetch-style). -- Promise/async-first APIs; avoid callbacks unless unavoidable. -- Support cancellation via AbortController + signal. -- Prefer EventTarget for observable/eventful objects. -- Use async iterators for streams where it fits (for await ... of). -- Avoid global DOM access in core; isolate platform code to /dom. - -```ts -// ✅ cancellation (standard) -const controller = new AbortController() -await store.request.load({ signal: controller.signal }) -controller.abort() -``` - -```ts -// ✅ EventTarget pattern -class Store extends EventTarget { - emitState(state: State) { - this.dispatchEvent(new CustomEvent('statechange', { detail: state })) - } -} -``` - ---- - -## UI Component Library Principles (Radix/Base UI style) - -### Headless by Default: Ship Behavior, Not Styles - -- Core ships no CSS. -- Consumers style via hooks: classnames, data attributes, CSS variables. - -### Compound Components > Prop Explosion - -```tsx - - - - - - -``` - -### Styling Hooks: Data Attributes + CSS Variables - -**Data attributes** for discrete states (boolean presence): - -```tsx - + + + + + Title + + + + +``` + +**Why great:** + +- Compound structure is intuitive +- `data-state` enables CSS styling +- CSS variables for animation values +- Built-in focus management + +--- + +### React Aria / React Stately + +**Pattern:** Separated state + behavior, mergeProps + +```tsx +function Checkbox(props) { + const state = useToggleState(props); // State logic + const ref = useRef(null); + const { inputProps } = useCheckbox(props, state, ref); // DOM behavior + const { focusProps, isFocusVisible } = useFocusRing(); + + return ; +} +``` + +**Why great:** + +- Stately hooks work with React Native +- Aria hooks add web-specific behavior +- `mergeProps` chains handlers correctly +- Maximum accessibility out of box + +--- + +## Validation / Schema + +### Zod + +**Pattern:** Chainable API, inference-first + +```ts +const userSchema = z.object({ + name: z.string().min(1), + email: z.string().email(), + age: z.number().int().positive().optional(), +}); + +type User = z.infer; + +const result = userSchema.safeParse(input); +if (!result.success) { + console.log(result.error.issues); +} +``` + +**Why great:** + +- `z.infer` for types +- `.safeParse()` returns Result, not throws +- Chainable refinements read naturally +- Excellent error messages + +--- + +## Data Fetching + +### TanStack Query + +**Pattern:** Smart defaults, stale-while-revalidate + +```ts +const { data, isLoading, error } = useQuery({ + queryKey: ['todos', userId], + queryFn: () => fetchTodos(userId), + staleTime: 5 * 60 * 1000, +}); +``` + +**Why great:** + +- `queryKey` for cache identity +- Automatic background refetch +- Optimistic updates via `useMutation` +- Devtools included and excellent + +--- + +## Quick Reference + +| Library | Key Innovation | +| -------------- | ------------------------------------------------------ | +| Zustand | No Provider, curried inference, middleware composition | +| Jotai | Atomic primitives, derived state auto-tracking | +| TanStack Store | Same API across all frameworks | +| XState v5 | `setup()` API, visualizable, impossible states | +| Base UI | `render` prop with state access | +| Radix | Compound components, data attributes | +| React Aria | Separated state/behavior, accessibility by default | +| Zod | `z.infer`, chainable, Result pattern | +| TanStack Query | `queryKey`, SWR pattern, devtools | + +--- + +## See Also + +- [Voices](voices.md) — practitioner perspectives and URLs +- [Principles](principles.md) — design principles these libraries embody diff --git a/.claude/skills/api/references/principles.md b/.claude/skills/api/references/principles.md new file mode 100644 index 00000000..a89f9166 --- /dev/null +++ b/.claude/skills/api/references/principles.md @@ -0,0 +1,270 @@ +# Core Principles + +Foundational principles for designing TypeScript library APIs with great developer experience. + +## Emergent Extensibility + +> "Middleware in Zustand is not a built-in feature—there's no special logic for it in the library. Instead, it's a capability that **naturally emerges** from how `createStore` is designed." — Daishi Kato + +Design core APIs that naturally enable extension rather than bolting on plugin infrastructure. The best extension points don't look like extension points—they look like well-designed APIs. + +**What enables emergent extensibility:** + +- Higher-order functions enable interception +- Closures preserve configuration context +- Mutable API objects allow downstream interception +- Curried signatures enable TypeScript inference + +**Red flag:** Explicit plugin registration (`registerPlugin()`, `use()`) with hooks, events, and lifecycle callbacks when simpler function composition would suffice. + +--- + +## Composition Over Configuration + +Why `create(devtools(persist(immer(fn))))` beats `create({ middlewares: [logger, persist] })`: + +| Aspect | Composition | Configuration | +| -------------- | --------------------- | ------------------------- | +| Runtime cost | Zero overhead | Array iteration, dispatch | +| Type inference | Flows naturally | Requires annotations | +| Ordering | Explicit in structure | Hidden, needs docs | + +**Why composition wins:** + +- No abstraction overhead—each middleware is just a function call during creation +- TypeScript infers through nested calls; config objects need explicit types +- The nesting structure _is_ the ordering—no ambiguity + +**When configuration wins:** When ordering doesn't matter, when non-developers configure (JSON/YAML), or when options are numerous. + +--- + +## The Onion Model + +Two orderings coexist in middleware systems: + +- **Creation-time (outer → inner):** `devtools` executes first, then `persist`, then `immer` +- **Runtime (inner → outer):** `immer` transforms first, `persist` saves second, `devtools` logs last + +**The ordering principle:** Place transformative middleware innermost, observational middleware outermost. + +| Layer | Purpose | Examples | +| --------- | --------------------- | ------------------------ | +| Outermost | Observation/debugging | devtools, logger | +| Middle | Business logic | rate limiting, analytics | +| Innermost | State transformation | immer, normalization | + +--- + +## TypeScript-First (Inference-First) + +Users write less, get more. Types flow from usage. + +```ts +// Great: types just work +const store = createStore({ count: 0 }); +// State type is inferred as { count: number } + +// Poor: requires explicit annotation +const store = createStore<{ count: number }>({ count: 0 }); +``` + +**What to look for:** + +- Can you use the API without explicit generics? +- Do return types narrow based on input? +- Are helper types exported for extracting types when needed? + +> "It's really a TypeScript-first-designed API. I don't do anything in tRPC that can't be typed well." — KATT + +--- + +## Config Objects Over Positional Args + +Config objects scale and self-document. Positional args don't. + +```ts +// Great: clear what each value means +createSlider({ min: 0, max: 100, vertical: true }); + +// Poor: what does `true` mean? +createSlider(0, 100, true); +``` + +TanStack Query v5 reduced TypeScript types by **80%** (125 → 25 lines) by eliminating function overloads in favor of config objects. + +**The decision framework:** + +| Situation | Pattern | Why | +| ---------------------------------- | ------------- | ----------------- | +| 1-2 required params, clear meaning | Direct args | Minimal, obvious | +| 3+ params or many optionals | Config object | Named, extensible | +| Complex multi-step construction | Builder | Type accumulation | + +--- + +## Flat Returns for Independent Values + +Return structure should reflect usage patterns, not implementation details. + +| Situation | Pattern | Why | +| ------------------------------------------- | ------------------ | ------------------ | +| Properties independent, used separately | Flat object | Direct destructure | +| Properties form cohesive unit | Namespaced | Semantic grouping | +| Performance requires selective subscription | Namespaced + Proxy | Lazy evaluation | + +**Rule of two:** Tuples for exactly 2 values (`[count, setCount]`), objects for 3+. + +--- + +## Smart Defaults + Escape Hatches + +Simplest call "just works"; power is opt-in. + +```ts +// Works out of box +useQuery({ queryKey: ['todos'], queryFn: fetchTodos }); + +// Explicit escape hatch when needed +useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: Infinity }); +``` + +**The three-layer pattern:** + +1. **Convention layer:** Works without configuration +2. **Configuration layer:** Explicit but simple overrides +3. **Escape hatch layer:** Full programmatic control + +**Critical:** Each layer composes with ones above. Using an escape hatch shouldn't require reimplementing defaults. + +--- + +## Progressive Disclosure + +> "The complexity of the call site should grow with the complexity of the use case." — Apple SwiftUI team + +| Level | Complexity | What Users See | +| ----------------------- | ---------- | -------------- | +| Zero config | None | It just works | +| Options | Low | Tweak behavior | +| Composition | Medium | Combine pieces | +| Headless/hooks | High | Full control | +| Framework-agnostic core | Expert | Build adapters | + +**The 80/20 test:** 80% of users should succeed at level 1-2. The remaining 20% have a path to levels 3-5. + +--- + +## Explicit Contracts Over Implicit Requirements + +Base UI's evolution shows why explicit beats implicit: + +| Generation | Pattern | Problem | +| --------------- | ----------------- | ------------------------------------ | +| slots/slotProps | Implicit mapping | TypeScript couldn't maintain types | +| asChild (Radix) | Clone element | Silent breakage if ref not forwarded | +| Render props | Explicit function | Contract is clear, typed | + +```tsx +// Explicit: you see exactly what's required and available + {state.checked ? '✓' : '✗'}} /> +``` + +--- + +## Minimal API Surface + +Fewer concepts. Fewer ways to do the same thing. + +```ts +// Great: one obvious way +store.setState({ volume: 0.5 }); + +// Poor: multiple ways (which to use?) +store.setState({ volume: 0.5 }); +store.set('volume', 0.5); +store.volume = 0.5; +``` + +**What to look for:** + +- Is there one clear way to do each task? +- Are similar operations consistent? +- Does the API avoid aliases that do the same thing? + +--- + +## Errors That Help + +Errors explain: what happened, why, and how to fix. + +```ts +// Great: actionable error +throw new AttachError('already-attached', { + hint: 'Call store.detach() before attach(), or create a new store.', +}); + +// Poor: cryptic error +throw new Error('Invalid state'); +``` + +--- + +## Borrow Platform Patterns + +Don't invent paradigms. Use familiar names and behaviors: + +- DOM events: `addEventListener`, `removeEventListener` +- Fetch-style options objects +- Abort/cancellation: `AbortController` and `signal` +- Async iteration for streams +- `subscribe/unsubscribe` patterns + +--- + +## Controlled + Uncontrolled Support + +Support both patterns with consistent naming: + +```tsx +// Uncontrolled - library manages state +... + +// Controlled - consumer manages state +... +``` + +**Convention:** `defaultValue`/`value`, `defaultOpen`/`open`, with `onXxxChange` callbacks. + +--- + +## Make Wrong Things Possible But Obvious + +**Pit of success principle:** Short names for correct usage, long names for dangerous operations. + +| Correct Path | Dangerous Path | +| ------------ | ------------------------- | +| `render` | `dangerouslySetInnerHTML` | +| `set` | `shamefullySendNext` | +| `usePlayer` | `preventBaseUIHandler()` | + +--- + +## Solve Complexity Once + +Every consumer shouldn't solve the same problems. If your library requires boilerplate for common cases, the abstraction is at the wrong level. + +**Manifestations:** + +- **Parse at boundaries:** Zod/tRPC validate at API edges, then trust types internally +- **Coordinate async in core:** TanStack Query handles race conditions, caching, revalidation +- **Sensible defaults:** SWR's stale-while-revalidate works without configuration + +--- + +## See Also + +- [TypeScript Patterns](typescript.md) — type inference techniques +- [State Patterns](state.md) — state management design +- [Extensibility](extensibility.md) — middleware and plugin patterns +- [Anti-Patterns](anti-patterns.md) — what to avoid diff --git a/.claude/skills/api/references/state.md b/.claude/skills/api/references/state.md new file mode 100644 index 00000000..ce720329 --- /dev/null +++ b/.claude/skills/api/references/state.md @@ -0,0 +1,360 @@ +# State Management Patterns + +Patterns for designing and using state management in TypeScript libraries. + +## Mental Models + +Daishi Kato created Zustand, Jotai, and Valtio with intentionally different architectures because **different problems need different mental models**. + +| Model | Mental Model | Best For | +| ---------------------- | ------------------------------------- | ------------------------------ | +| Top-down (Zustand) | Single store, slice into pieces | Module state, non-React access | +| Bottom-up (Jotai) | Composable atoms, build up | useState replacement | +| State machine (XState) | Explicit states and transitions | Complex workflows | +| Proxy (Valtio) | Mutable-looking, immutable underneath | Mutable-preferring devs | + +**The principle:** Don't force a mental model. Choose based on how developers naturally think about the domain. + +--- + +## Core Subscription Interface + +The minimal interface any framework can consume: + +```ts +interface Store { + get(): T; + subscribe(listener: (value: T) => void): () => void; +} +``` + +This enables framework adapters to be ~10 lines using `useSyncExternalStore` (React), `shallowRef` (Vue), or `createSignal` (Solid). + +--- + +## Middleware as Higher-Order Functions + +Middleware wraps the state creator, not the store: + +```ts +// Zustand pattern +create( + devtools( + persist( + immer((set) => ({ count: 0 })), + { name: 'store' } + ) + ) +); +``` + +**Benefits:** + +- Composable in any order +- Type-safe (each middleware can modify types) +- Tree-shakeable (unused middleware not bundled) + +--- + +## Slice Pattern + +Split state by concern, combine at creation time: + +```ts +// Each module factory receives (set, get) and returns state slice +const createVolumeSlice = (set, get) => ({ + volume: 1, + setVolume: (v) => set({ volume: v }), +}); + +const createPlaybackSlice = (set, get) => ({ + playing: false, + play: () => set({ playing: true }), +}); + +// Combine by spreading at creation time +const useStore = create((...a) => ({ + ...createVolumeSlice(...a), + ...createPlaybackSlice(...a), +})); +``` + +**Why creation-time composition:** + +| Benefit | Why | +| --------------------------- | -------------------------------- | +| Cross-module access natural | `get`/`set` see entire store | +| Atomic updates span modules | One `set` updates multiple areas | +| Middleware applies to whole | No per-module confusion | +| Tree-shaking possible | Unused modules excluded | + +--- + +## Atomic Composition (Jotai Pattern) + +Build complex state from simple atoms: + +```ts +const countAtom = atom(0); +const doubledAtom = atom((get) => get(countAtom) * 2); +const asyncAtom = atom(async (get) => fetch(`/api/${get(countAtom)}`)); +``` + +**Benefits:** + +- Fine-grained subscriptions +- Derived state is automatic +- Async handled uniformly + +--- + +## Selector Pattern + +Minimize re-renders with selectors: + +```ts +// Subscribes to entire state (causes re-renders) +const state = useStore(); + +// Subscribes to selected slice (fine-grained) +const volume = useStore((state) => state.volume); +``` + +**Implementation considerations:** + +- Shallow equality by default +- Custom equality function option +- Memoized selector support + +--- + +## Split Stores When Truly Isolated + +| Situation | Recommendation | Why | +| ----------------------------- | ------------------------- | -------------------------- | +| Totally isolated concerns | Multiple stores | Cleaner boundaries | +| Might ever want cross-updates | Single store with modules | Atomic updates possible | +| Multiple contexts needed | Single store | Simpler provider hierarchy | + +**Heuristic:** "When you feel that something is getting difficult to maintain, that's the moment to start splitting." + +--- + +## Cross-Store Access + +When stores must communicate, make the relationship explicit: + +```ts +interface AppContext { + userStore: Store; + settingsStore: Store; +} + +function createFeatureStore(context: AppContext) { + return create((set, get) => ({ + syncWithUser: () => { + const user = context.userStore.getState(); + set({ userId: user.id }); + }, + })); +} +``` + +**Why explicit references:** + +- Coordination goes through typed interface +- TypeScript catches mismatches +- No hidden global state +- Easy to test with mock stores + +--- + +## Derived State Patterns + +### Selector-Based + +Compute derived state at subscription time: + +```ts +store.subscribe( + (state) => state.items.reduce((sum, item) => sum + item.price, 0), + (totalPrice) => updateUI(totalPrice) +); +``` + +### Computed Atoms + +Define derived state as a dependency graph: + +```ts +const itemsAtom = atom([]); +const totalPriceAtom = atom((get) => get(itemsAtom).reduce((sum, item) => sum + item.price, 0)); +``` + +| Pattern | Use When | +| ------------------ | ----------------------------------- | +| Selectors | Derived data varies by consumer | +| Computed atoms | Derived data shared across app | +| Memoized selectors | Expensive computation needs caching | + +--- + +## Optimistic Updates + +For responsive UI during async operations: + +```ts +async function updateVolume(newVolume: number) { + const previous = store.getState().volume; + + // Optimistic update + store.setState({ volume: newVolume }); + + try { + await api.setVolume(newVolume); + } catch { + // Rollback on failure + store.setState({ volume: previous }); + } +} +``` + +--- + +## State Initialization + +### Lazy Initialization + +Defer expensive work until needed: + +```ts +createStore((set) => ({ + data: null, + initialize: async () => { + const data = await fetchExpensiveData(); + set({ data }); + }, +})); +``` + +### Hydration Support + +Allow external state injection (SSR, persistence): + +```ts +createStore((set) => ({ + data: null, + hydrate: (serverState) => set(serverState), +})); + +// Client: store.getState().hydrate(window.__INITIAL_STATE__) +``` + +### Reset Pattern + +Return to known initial state: + +```ts +const initialState = { count: 0, items: [] }; + +createStore((set) => ({ + ...initialState, + reset: () => set(initialState), +})); +``` + +--- + +## Request State Pattern + +Standardized shape for async operations: + +```ts +type RequestState = + | { status: 'idle' } + | { status: 'pending' } + | { status: 'success'; data: T; timestamp: number } + | { status: 'error'; error: E; timestamp: number }; +``` + +Helper functions: + +```ts +function isLoading(state: RequestState): boolean; +function isSuccess(state: RequestState): state is SuccessState; +function isError(state: RequestState): state is ErrorState; +``` + +--- + +## Proxy-Based Reactivity (Valtio) + +Mutate naturally, get automatic tracking: + +```ts +const state = proxy({ count: 0, nested: { value: 1 } }); + +// Mutations work directly +state.count++; +state.nested.value = 2; + +// Subscribe to changes +subscribe(state, () => console.log('changed')); + +// React hook tracks accessed properties +const snap = useSnapshot(state); +``` + +**Trade-offs:** + +- Simpler mental model +- Proxies can obscure types +- Deep reactivity automatic but sometimes unwanted + +--- + +## Presets as Transparent Collections + +Make presets visible and extensible: + +```ts +// Good: Preset is just an array, visible and extensible +const websitePreset = [analyticsModule, cachingModule, loggingModule]; + +createStore({ modules: websitePreset }); +createStore({ modules: [...websitePreset, customModule] }); + +// Bad: Preset hides internals +createStore({ preset: 'website' }); // What's in it? +``` + +--- + +## Dev Tools Integration + +Patterns for debugging support: + +```ts +// Named stores for devtools +const useStore = create( + devtools( + (set) => ({ ... }), + { name: 'MediaStore' } + ) +) + +// Action names +set({ volume: 0.5 }, false, 'setVolume') + +// Time-travel support via snapshots +const snapshot = store.getState() +store.setState(previousSnapshot) +``` + +--- + +## See Also + +- [Principles](principles.md) — composition and extensibility +- [Extensibility](extensibility.md) — middleware patterns +- [Libraries](libraries.md) — reference implementations diff --git a/.claude/skills/api/references/typescript.md b/.claude/skills/api/references/typescript.md new file mode 100644 index 00000000..305704cc --- /dev/null +++ b/.claude/skills/api/references/typescript.md @@ -0,0 +1,281 @@ +# TypeScript Patterns + +Type inference techniques for library authors and evaluation patterns for consumers. + +## The Partial Inference Problem + +TypeScript infers all generics or none. Libraries need techniques to enable partial inference. + +### Currying Pattern + +Split creation into two function calls to create separate inference sites: + +```ts +// First () binds state type explicitly +// Second () allows middleware types to be inferred +const useBearStore = create()((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +})); +``` + +**When to use:** When you need to fix one type parameter while inferring others. + +### Builder Pattern + +Chain methods that progressively narrow types: + +```ts +const schema = z.object({ + name: z.string(), + age: z.number(), +}); +// Type is inferred from the chain +type User = z.infer; +``` + +### Factory with Generics Bound Once + +Capture target/platform type once, let callbacks infer: + +```ts +// Capture Target type once +const slice = createSlice()({ + initialState: { volume: 1 }, + getSnapshot: ({ target }) => ({ volume: target.volume }), + subscribe: ({ target, update }) => { + target.addEventListener('volumechange', update); + return () => target.removeEventListener('volumechange', update); + }, +}); +``` + +--- + +## Parse, Don't Validate + +> "Parsers preserve information in the type system; validators throw it away." — Alexis King + +```ts +// Validator: Returns boolean, discards knowledge +function isNonEmpty(list: string[]): boolean { + return list.length > 0; +} +// After check, TypeScript still thinks it might be empty + +// Parser: Returns refined type, preserves guarantee +function parseNonEmpty(list: T[]): NonEmptyArray | null { + return list.length > 0 ? (list as NonEmptyArray) : null; +} +// After parse, TypeScript knows it's non-empty +``` + +**Why parsing wins:** After parsing, TypeScript remembers the constraint. No redundant checks downstream. + +--- + +## Eliminate Shotgun Parsing + +Validation scattered throughout code signals missing abstraction: + +```ts +// Shotgun parsing: validation everywhere +function processUser(data: unknown) { + if (!data.name) throw new Error('Missing name'); + saveName(data.name); + // ... 100 lines later ... + if (!data.email) throw new Error('Missing email'); // Why here? +} + +// Parse at boundaries, trust types internally +function processUser(user: User) { + saveName(user.name); // Type guarantees existence + saveEmail(user.email); // No defensive checks +} + +// Parsing happens at API boundary +const user = userSchema.parse(request.body); +processUser(user); +``` + +--- + +## Explicit Context Narrowing + +TypeScript's control flow doesn't propagate through function boundaries: + +```ts +// Type narrowing lost +const middleware = ({ ctx, next }) => { + if (!ctx.user) throw new Error('Unauthorized'); + return next(); // ctx.user still User | undefined downstream +}; + +// Explicit return tells TypeScript +const middleware = ({ ctx, next }) => { + if (!ctx.user) throw new Error('Unauthorized'); + return next({ + ctx: { ...ctx, user: ctx.user }, // Explicitly non-null + }); +}; +``` + +**Why explicit returns:** TypeScript can't know that throwing guarantees `ctx.user` exists in `next()`. You must explicitly return the narrowed context. + +--- + +## Avoid Globals for Type Context + +```ts +// Global interface declaration (avoid) +declare global { + interface AppContext { + user: User; + } +} + +// Factory carries type context (prefer) +const t = initTRPC.context<{ user: User }>().create(); +// All procedures derived from t carry this context type +``` + +**Why factories win:** + +- Can have multiple instances with different types +- No global pollution +- Easy to test with different contexts +- TypeScript tracks types through derivation + +--- + +## Design Types First + +> "Without the types and without that being great, there's no point of tRPC." — KATT + +**Process:** + +1. Design types first in TypeScript Playground +2. Verify inference works as expected +3. Backfill runtime implementation + +**Why types-first works:** + +- API design flaws surface early +- Impossible states become visible in types +- IDE autocomplete becomes documentation +- "If it compiles, the API contract is correct" + +--- + +## Export Helper Types + +Every library should export helper types so users can derive types: + +```ts +// Zustand +type State = ExtractState; + +// Jotai +type Value = ExtractAtomValue; + +// XState +type Snapshot = SnapshotFrom; + +// Your library +export type ExtractState = S extends Store ? T : never; +export type InferInput = S extends Schema ? I : never; +``` + +**Rule:** If users need to manually annotate, export a helper type to derive it. + +--- + +## Type Guards Over String Checks + +```ts +// Poor: stringly-typed, no narrowing +if (state.status === 'playing') { ... } + +// Good: type guard with narrowing +if (isPlaying(state)) { + state.currentTime // typed as number +} +``` + +--- + +## Discriminated Unions for State + +```ts +type RequestState = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: T } + | { status: 'error'; error: Error }; + +// Narrowing works automatically +if (state.status === 'success') { + state.data; // typed +} +``` + +--- + +## Generic Constraints + +Good constraints guide inference and provide better errors: + +```ts +// Poor: accepts anything, unhelpful errors +function createStore(initial: T): Store; + +// Good: constrained, clear expectations +function createStore>(initial: T): Store; +``` + +--- + +## Inference Red Flags + +| Pattern | Problem | +| ----------------------------------- | ----------------------------- | +| Frequent explicit generics in usage | Inference not working | +| `unknown` in public API | Forces user casting | +| Deeply nested generics | Inference often fails | +| Required type annotations | Library isn't inference-first | + +--- + +## Testing Type Inference + +Use `expectTypeOf` (vitest) or `tsd` to verify inference: + +```ts +import { expectTypeOf } from 'vitest'; + +test('infers state type', () => { + const store = createStore({ count: 0 }); + expectTypeOf(store.getState()).toEqualTypeOf<{ count: number }>(); +}); +``` + +--- + +## Method Chaining vs Function Composition + +| Approach | Pros | Cons | +| --------------------------------------- | ----------------------------------- | ----------------- | +| Chaining (`z.string().email()`) | Reads naturally, great autocomplete | Larger bundle | +| Composition (`pipe(string(), email())`) | Tree-shakeable | Less discoverable | + +**Bundle comparison:** + +- Zod (chaining): ~15 kB for login form +- Valibot (composition): ~1.4 kB for same form + +--- + +## See Also + +- [Principles](principles.md) — core design principles +- [Anti-Patterns](anti-patterns.md) — TypeScript mistakes to avoid diff --git a/.claude/skills/api/references/voices.md b/.claude/skills/api/references/voices.md new file mode 100644 index 00000000..fbc6a251 --- /dev/null +++ b/.claude/skills/api/references/voices.md @@ -0,0 +1,223 @@ +# Practitioner Voices + +Quick-reference heuristics from DX practitioners. Use as lightweight lenses — don't imitate tone, apply the thinking. + +## Summary Heuristics + +| Voice | Key Question | +| ------------------- | -------------------------------------------------------------- | +| Tanner Linsley | Could this work across frameworks with the same core? | +| Kent C. Dodds | Does the API match how users think? | +| Ryan Carniato | Does state change cause minimal re-work? | +| Adam Wathan | Do constraints guide users to success? | +| Lee Robinson | How fast is first success? | +| Josh Comeau | Does the README build intuition? | +| Evan You | Does it feel like one cohesive product? | +| Base UI (preferred) | Is a11y in the architecture? Does `render` enable composition? | +| Radix | Is a11y in the architecture? | +| Devon Govett | Can behaviors compose via hooks? | + +--- + +## Detailed Perspectives + +### Tanner Linsley (TanStack) + +**Focus:** Composable primitives, core/adapter split, consistent patterns across ecosystems + +- Build framework-agnostic cores with thin framework adapters +- Same mental model whether you're in React, Vue, Solid, or Svelte +- Primitives that compose into larger patterns +- Type inference should flow naturally from usage + +**Applied:** When reviewing, check if the library could support multiple frameworks with the same core logic. + +--- + +### Kent C. Dodds + +**Focus:** User-centered APIs, confidence-driven testing + +> "The more your tests resemble the way your software is used, the more confidence they can give you." + +- APIs should match how users think about the problem +- Testing Library philosophy: test behavior, not implementation +- Colocation — keep related things together + +**Applied:** Does the API match the user's mental model? Can you test it the way it's actually used? + +--- + +### Ryan Carniato (Solid) + +**Focus:** Locality, fine-grained reactivity, avoid unnecessary work + +- Reactivity at the value level, not the component level +- Don't re-run code that doesn't need to re-run +- Locality of behavior — effects close to their triggers +- Explicit over implicit dependencies + +**Applied:** Does state update cause minimal re-computation? Is the dependency graph clear? + +--- + +### Adam Wathan (Tailwind) + +**Focus:** Constraints + consistency, documentation as product + +> "Documentation is the most important thing for the success of basically any open source project." + +- Constraints enable creativity and consistency +- Utility-first: small composable pieces over large abstractions +- Docs are the product's front door +- Sensible defaults with escape hatches + +**Applied:** Are defaults sensible? Is the API constrained enough to guide users toward success? + +--- + +### Lee Robinson (Vercel) + +**Focus:** DX as product, docs + examples are the funnel + +> "DX is about building products developers love to use." + +- Time-to-first-success is a key metric +- Examples are documentation +- Error messages are UI +- Developer experience is user experience + +**Applied:** How fast can someone go from install to working code? Are errors helpful? + +--- + +### Josh Comeau + +**Focus:** Teach via mental models, clarity > completeness early + +- Build intuition before details +- Visual explanations where possible +- Progressive disclosure of complexity +- Joy and delight matter + +**Applied:** Does the README build intuition? Can beginners succeed before learning advanced features? + +--- + +### Evan You (Vue) + +**Focus:** Cohesive defaults, stable mental model across ecosystem + +- Single cohesive vision over committee design +- Progressive enhancement — start simple, add complexity as needed +- Stability and predictability for long-term projects +- The ecosystem should feel like one product + +**Applied:** Does the library feel cohesive? Can users predict behavior from patterns they've already learned? + +--- + +### Base UI / Radix Teams + +**Focus:** Accessibility-first primitives, composition + styling hooks + +- Accessibility is architecture, not a feature +- Headless: ship behavior, not styles +- Compound components over prop explosion +- Data attributes as the styling contract + +**Applied:** Is a11y built into the component model? Can users style without fighting the library? + +--- + +### Devon Govett (React Aria) + +**Focus:** Behavior/state separation, platform-aware accessibility + +- Separate state logic (portable) from DOM behavior (platform-specific) +- ARIA patterns encoded into the architecture +- `mergeProps` for composing behaviors +- Internationalization as a first-class concern + +**Applied:** Is the accessibility implementation based on established patterns? Can behavior hooks compose? + +--- + +## Reference URLs + +URLs for studying best-in-class patterns. Fetch when reviewing similar libraries. + +### State Management + +| Library | Docs | +| -------------- | ---------------------------------------------------------------------------------------- | +| Zustand | https://zustand.docs.pmnd.rs, TypeScript: https://zustand.docs.pmnd.rs/guides/typescript | +| Jotai | https://jotai.org, Core: https://jotai.org/docs/core/atom | +| TanStack Store | https://tanstack.com/store | +| XState | https://stately.ai/docs, TypeScript: https://stately.ai/docs/typescript | +| Nanostores | https://github.com/nanostores/nanostores | +| Valtio | https://valtio.dev | + +### UI Components + +| Library | Docs | +| ----------- | ------------------------------------------------------------------------------------------------------------- | +| Base UI | https://base-ui.com, Styling: https://base-ui.com/react/handbook/styling | +| Radix UI | https://www.radix-ui.com, Composition: https://www.radix-ui.com/primitives/docs/guides/composition | +| React Aria | https://react-spectrum.adobe.com/react-aria, Architecture: https://react-spectrum.adobe.com/architecture.html | +| Ark UI | https://ark-ui.com | +| Headless UI | https://headlessui.com | +| Melt UI | https://melt-ui.com (Svelte) | +| Kobalte | https://kobalte.dev (Solid) | + +### Validation / Data + +| Library | Docs | +| --------------- | ------------------------------------------------------- | +| Zod | https://zod.dev, Errors: https://zod.dev/ERROR_HANDLING | +| Valibot | https://valibot.dev | +| TanStack Query | https://tanstack.com/query | +| tRPC | https://trpc.io | +| TanStack Router | https://tanstack.com/router | + +### Utilities + +| Library | Docs | +| ---------- | ----------------------------- | +| es-toolkit | https://es-toolkit.slash.page | +| Effect | https://effect.website | + +--- + +## Key Pages to Study + +When reviewing a specific pattern, fetch these for comparison: + +| Pattern | Fetch | +| ------------------- | ------------------------------------- | +| Type inference | Zustand TypeScript guide, Zod docs | +| Middleware/plugins | Zustand middleware, XState actors | +| Compound components | Radix primitives, Base UI components | +| Data attributes | Base UI styling handbook | +| `render` prop | Base UI composition guide | +| Framework adapters | TanStack Store, Nanostores | +| Error handling | Zod error handling, Effect docs | +| Accessibility | React Aria architecture, Radix guides | + +--- + +## Documentation Examples + +Best-in-class docs structure to reference: + +- **TanStack Query** — Progressive disclosure, guides before API reference +- **Zod** — Single-page with excellent type examples +- **Radix** — Component pages with anatomy, API, accessibility sections +- **React Aria** — Architecture explanation, hooks composition guides + +--- + +## See Also + +- [Libraries](libraries.md) — code patterns from these libraries +- [Principles](principles.md) — principles these practitioners embody diff --git a/.claude/skills/api/review/agents.md b/.claude/skills/api/review/agents.md new file mode 100644 index 00000000..97837161 --- /dev/null +++ b/.claude/skills/api/review/agents.md @@ -0,0 +1,186 @@ +# Agent Prompts + +Prompts for parallel API review agents. + +## Coordinator + +``` +You are the coordinator. Your job: +1. Read the API code/proposal to review +2. Spawn 4 sub-agents with Task tool +3. Collect their reports +4. Merge into final review + +Tasks: +- Task 1: Types Review +- Task 2: API Surface Review +- Task 3: Extensibility Review +- Task 4: Progressive Disclosure Review + +Wait for all tasks, then synthesize using templates.md format. +``` + +--- + +## Sub-Agent: Types Review + +``` +You are reviewing TypeScript patterns for inference and type safety. + +Load: api/references/typescript.md + +Review for: + +1. **Inference-first** — Can users avoid explicit generics? +2. **Helper types exported** — `ExtractState`, `InferOutput`, etc.? +3. **Type guards provided** — For discriminated unions? +4. **No `unknown` in public API** — Errors and returns typed? +5. **Parsing at boundaries** — Validation centralized, not scattered? +6. **Context narrowing** — Types narrow through middleware? + +Output: + +## Types Review + +### Score: X/10 + +### Issues +[Use CRITICAL/MAJOR/MINOR/NIT format from templates.md] + +### Good Patterns Found +(1-2 examples) + +### Summary +(2-3 sentences) +``` + +--- + +## Sub-Agent: API Surface Review + +``` +You are reviewing API surface design for DX. + +Load: api/references/principles.md + +Review for: + +1. **Config objects** — No boolean traps, no positional args > 2 +2. **Smart defaults** — 80% of users need no options +3. **One way** — Not multiple APIs for same task +4. **Platform patterns** — Uses familiar web APIs +5. **Returns** — Flat vs namespaced appropriate to usage +6. **Naming** — Clear, consistent, predictable + +Output: + +## API Surface Review + +### Score: X/10 + +### Issues +[Use CRITICAL/MAJOR/MINOR/NIT format from templates.md] + +### Good Patterns Found +(1-2 examples) + +### Summary +(2-3 sentences) +``` + +--- + +## Sub-Agent: Extensibility Review + +``` +You are reviewing extensibility architecture. + +Load: +- api/references/extensibility.md +- api/references/principles.md + +Review for: + +1. **Extension model** — Composition vs runtime registration +2. **Middleware** — Ordering explicit, onion model +3. **Builders** — Type accumulation, terminators +4. **Lifecycle** — Init/destroy for resources +5. **Core** — Framework-agnostic where appropriate +6. **Adapters** — Thin, no logic duplication + +Output: + +## Extensibility Review + +### Score: X/10 +### Architecture: [Monolith | Module-based | Atomic | Core/Adapter] + +### Issues +[Use CRITICAL/MAJOR/MINOR/NIT format from templates.md] + +### Good Patterns Found +(1-2 examples) + +### Summary +(2-3 sentences) +``` + +--- + +## Sub-Agent: Progressive Disclosure Review + +``` +You are reviewing progressive disclosure and escape hatches. + +Load: +- api/references/principles.md +- api/references/state.md + +Review for: + +1. **Layering** — Zero-config to expert levels +2. **Escape hatches** — Compose, don't replace defaults +3. **Contracts** — Explicit over implicit requirements +4. **Naming** — Dangerous operations obviously named +5. **Mental model** — Matches domain, predictable + +Output: + +## Progressive Disclosure Review + +### Score: X/10 + +### Issues +[Use CRITICAL/MAJOR/MINOR/NIT format from templates.md] + +### Good Patterns Found +(1-2 examples) + +### Summary +(2-3 sentences) +``` + +--- + +## Domain-Specific Focus + +### State Management + +- Types: Inference, selector types, middleware types +- API: Subscription patterns, update methods +- Extensibility: Module pattern, middleware architecture +- Disclosure: Simple to advanced usage path + +### Framework Adapters + +- Types: Generic preservation across boundary +- API: Consistency with other adapters +- Extensibility: Thinness, no logic duplication +- Disclosure: Works without advanced config + +### UI Components + +For UI-specific reviews, also load: + +- `component` skill — compound patterns, polymorphism +- `aria` skill — accessibility patterns diff --git a/.claude/skills/api/review/checklist.md b/.claude/skills/api/review/checklist.md new file mode 100644 index 00000000..99bbc9ad --- /dev/null +++ b/.claude/skills/api/review/checklist.md @@ -0,0 +1,66 @@ +# Quick Review Checklist + +Single-agent checklist for fast API reviews without forking. + +## Types + +See `references/typescript.md` for patterns, `references/anti-patterns.md` for anti-patterns. + +- [ ] Inference-first (minimal explicit generics)? +- [ ] Helper types exported (`ExtractState`, etc.)? +- [ ] Type guards for discriminated unions? +- [ ] No `unknown` in public API? +- [ ] Generics have appropriate constraints? +- [ ] Parsing at boundaries, not scattered? +- [ ] Context narrowing explicit in middleware? + +## API Surface + +See `references/principles.md` for principles, `references/anti-patterns.md` for anti-patterns. + +- [ ] Config objects for 3+ parameters? +- [ ] No boolean traps? +- [ ] No function overloads? +- [ ] One way to do each thing? +- [ ] Flat returns for independent values? +- [ ] Defaults documented? +- [ ] Immutable inputs (no config mutation)? +- [ ] Platform patterns used (familiar APIs)? + +## Extensibility + +See `references/extensibility.md` for patterns. + +- [ ] Extension through composition, not registration? +- [ ] Middleware ordering explicit (onion model)? +- [ ] Builder chains return new typed objects? +- [ ] Init/destroy lifecycle for resources? +- [ ] Framework-agnostic core (if applicable)? +- [ ] Adapters thin (<50 lines, no logic duplication)? + +## Progressive Disclosure + +See `references/principles.md` for patterns. + +- [ ] Zero-config default works? +- [ ] Complexity grows with use case? +- [ ] Escape hatches compose (don't replace defaults)? +- [ ] Contracts explicit (no hidden requirements)? +- [ ] Dangerous operations obviously named? + +## Packaging + +See `references/anti-patterns.md` Packaging section. + +- [ ] ESM-first? +- [ ] `sideEffects: false`? +- [ ] Shallow subpaths (`pkg/react` not `pkg/react/hooks/store`)? +- [ ] Peer deps correct (not bundled)? +- [ ] Tree-shakeable exports? + +## Related Skills + +For UI component-specific reviews, also check: + +- `component` skill — compound components, polymorphism, styling patterns +- `aria` skill — keyboard, focus, ARIA attributes diff --git a/.claude/skills/api/review/example.md b/.claude/skills/api/review/example.md new file mode 100644 index 00000000..4d9e4a4e --- /dev/null +++ b/.claude/skills/api/review/example.md @@ -0,0 +1,314 @@ +# Example: API Review + +Review of a proposed media player store API. + +--- + +## Target + +```typescript +// Proposed API +function createPlayer( + source: string, + autoplay: boolean, + muted: boolean, + controls: ControlsConfig, + plugins?: Plugin[] +): Player; + +interface Player { + play(): void; + pause(): void; + getState(): PlayerState; + registerPlugin(plugin: Plugin): void; + on(event: string, handler: Function): void; +} +``` + +--- + +# API Review: createPlayer + +## Overall Score: 4/10 + +| Dimension | Score | Critical | Major | Minor | +| ---------------------- | ----- | -------- | ----- | ----- | +| Types | 4/10 | 0 | 2 | 0 | +| API Surface | 3/10 | 1 | 1 | 1 | +| Extensibility | 3/10 | 1 | 1 | 0 | +| Progressive Disclosure | 6/10 | 0 | 1 | 1 | + +--- + +## Critical Issues + +### [CRITICAL] Function uses 5 positional parameters + +**What:** `createPlayer(source, autoplay, muted, controls, plugins)` requires remembering argument order +**Where:** `createPlayer()` function signature +**Why:** Impossible to remember order; adding options requires breaking changes; boolean params particularly confusing (`true, false` means what?) +**Principle:** Config objects for 3+ params (principles.md) +**Fix:** Single config object + +```typescript +// Before +createPlayer('video.mp4', true, false, defaultControls, [analytics]); + +// After +createPlayer({ + src: 'video.mp4', + autoplay: true, + controls: defaultControls, + slices: [analyticsSlice], +}); +``` + +--- + +### [CRITICAL] Runtime plugin registration loses type safety + +**What:** `player.registerPlugin(plugin)` allows adding plugins after creation +**Where:** `Player.registerPlugin()` method +**Why:** TypeScript can't track what capabilities exist; ordering is implicit; can't tree-shake unused plugins +**Principle:** Emergent extensibility through composition (principles.md) +**Fix:** Composition at creation time + +```typescript +// Before +const player = createPlayer(config); +player.registerPlugin(analytics); // Types don't know analytics exists +player.registerPlugin(keyboard); // Order matters but isn't visible + +// After +const player = createPlayer({ + ...config, + slices: [analyticsSlice, keyboardSlice], // Types know exactly what's included +}); +``` + +--- + +## Major Issues + +### [MAJOR] getState() returns full state on every call + +**What:** `getState()` likely returns a new object each time +**Where:** `Player.getState(): PlayerState` +**Why:** React components re-render on every state change, not just relevant changes +**Principle:** Selectors for fine-grained subscriptions (state.md) +**Fix:** Support selector pattern + +```typescript +// Before +const state = player.getState(); // New object every time +const paused = state.paused; // Component re-renders on ANY change + +// After +const paused = usePlayer((s) => s.paused); // Re-render only when paused changes +``` + +--- + +### [MAJOR] Event handler uses untyped string events + +**What:** `on(event: string, handler: Function)` has no type safety +**Where:** `Player.on()` method +**Why:** No autocomplete for event names; handler arguments untyped; typos fail silently +**Principle:** Types as contracts (typescript.md) +**Fix:** Typed event map + +```typescript +// Before +player.on('play', (e) => {}) // 'play' could be typo, e is any +player.on('plaay', (e) => {}) // Silent failure + +// After +interface PlayerEvents { + play: { time: number } + pause: { time: number } + ended: {} +} + +player.on('play', (e) => { // Autocomplete, e is typed + console.log(e.time) +}) +player.on('plaay', ...) // TS Error: 'plaay' not in PlayerEvents +``` + +--- + +### [MAJOR] No escape hatch to underlying element + +**What:** API doesn't expose access to raw video/audio element +**Where:** `Player` interface (missing) +**Why:** Power users can't handle edge cases (custom codecs, WebRTC, canvas capture) +**Principle:** Escape hatches that compose (principles.md) +**Fix:** Explicit escape hatch + +```typescript +// After +interface Player { + // ... primary API ... + + // Escape hatch (named to signal "you're on your own") + get __unsafe__(): { + mediaElement: HTMLMediaElement; + audioContext?: AudioContext; + }; +} +``` + +--- + +## Minor Issues + +| Location | Issue | Principle | Fix | +| -------------------- | ------------------------ | ------------- | --------------------------- | +| `autoplay: boolean` | Boolean params confusing | principles.md | Named in config object | +| `controls: Config` | Generic name | principles.md | Consider `ui` or `skin` | +| `plugins?: Plugin[]` | Plugin vs Slice naming | state.md | Use "slice" if that's model | + +--- + +## Good Patterns Found + +- **Clear method names:** `play()`, `pause()`, `getState()` are intuitive +- **Separation of concerns:** Player vs state distinction is present +- **Optional plugins:** Not required for basic usage + +--- + +## Summary + +This API has structural problems that will cause long-term pain. The two critical issues—positional parameters and runtime plugin registration—should be addressed before any public release. + +**Positional parameters** make the API hard to use and impossible to extend without breaking changes. Converting to a config object is straightforward and enables future options. + +**Runtime plugin registration** loses the type safety and tree-shaking benefits that modern libraries expect. Moving to creation-time composition (like Zustand slices) enables TypeScript to track capabilities and bundlers to eliminate unused code. + +The event system's lack of typing is a significant DX issue but not blocking. Consider typed event maps or a subscription pattern like `subscribe(selector, callback)`. + +**Priority order:** + +1. Convert to config object (blocks everything else) +2. Move plugins to creation-time composition +3. Add typed events +4. Add selector-based subscriptions +5. Add escape hatches + +--- + +
+Full Types Review + +## Types Review + +### Score: 4/10 + +### Issues + +#### [MAJOR] Untyped events + +(See main report) + +#### [MAJOR] Plugin type doesn't carry capabilities + +`Plugin` interface doesn't encode what state/methods the plugin adds, so TypeScript can't know what's available after registration. + +### Good Patterns + +- Player interface is defined +- State type exists (PlayerState) + +### Summary + +The foundation is there but the dynamic parts (events, plugins) bypass the type system entirely. Consider making these static/creation-time. + +
+ +
+Full API Surface Review + +## API Surface Review + +### Score: 3/10 + +### Issues + +#### [CRITICAL] Positional parameters + +(See main report) + +#### [MAJOR] getState() performance + +(See main report) + +#### [MINOR] Boolean parameters + +Two adjacent booleans (`autoplay, muted`) are confusing at call sites. + +### Good Patterns + +- Method names are clear and conventional +- Return type is defined (Player interface) + +### Summary + +The function signature is the main problem. Converting to a config object would immediately improve usability and enable type inference for options. + +
+ +
+Full Extensibility Review + +## Extensibility Review + +### Score: 3/10 + +### Issues + +#### [CRITICAL] Runtime registration + +(See main report) + +#### [MAJOR] No composition model + +Plugins are black boxes. No slice pattern, no middleware composition, no builder chain. + +### Good Patterns + +- Plugin concept exists (just needs different delivery) + +### Summary + +The extensibility model needs a rethink. Look at Zustand slices or tRPC procedures for inspiration—extension through composition at creation time. + +
+ +
+Full Progressive Disclosure Review + +## Progressive Disclosure Review + +### Score: 6/10 + +### Issues + +#### [MAJOR] No escape hatch + +(See main report) + +#### [MINOR] All-or-nothing controls + +`controls: ControlsConfig` is required but users might want headless or partial UI. + +### Good Patterns + +- Basic usage is simple (source, play/pause) +- Plugins are optional + +### Summary + +The layering is reasonable but escape hatches are missing. Power users have no path to lower levels without abandoning the library. + +
diff --git a/.claude/skills/api/review/templates.md b/.claude/skills/api/review/templates.md new file mode 100644 index 00000000..397ab28d --- /dev/null +++ b/.claude/skills/api/review/templates.md @@ -0,0 +1,223 @@ +# Review Templates + +Issue format and report templates for API reviews. + +## Issue Format + +```markdown +### [SEVERITY] Issue title + +**What:** Brief description +**Where:** `path/to/file.ts:42` +**Why:** Impact on users/developers +**Principle:** Which principle violated +**Fix:** Concrete suggestion + +// Before +problematic() + +// After +improved() +``` + +## Severity Levels + +| Level | Meaning | Examples | +| ---------- | ---------------------------- | ---------------------------------------------- | +| `CRITICAL` | Breaks users, blocks release | Wrong types, runtime errors, a11y failures | +| `MAJOR` | Significant DX issue | Poor inference, boolean traps, no tree-shaking | +| `MINOR` | Improvement opportunity | Missing type guards, verbose API | +| `NIT` | Polish, optional | Naming consistency, minor ergonomics | + +--- + +## Issue Examples + +### CRITICAL — Inference failure + +```markdown +### [CRITICAL] Generic forces explicit type annotation + +**What:** Users must provide type parameter manually +**Where:** `createStore.ts:15` +**Why:** Inference should flow from usage +**Principle:** Inference over annotation (principles.md) +**Fix:** Use curried pattern + +// Before +const store = createStore({ count: 0 }) + +// After +const store = createStore({ count: 0 }) +``` + +### MAJOR — Boolean trap + +```markdown +### [MAJOR] Boolean trap in function signature + +**What:** Positional boolean with unclear meaning +**Where:** `createSlider.ts:8` +**Why:** `createSlider(0, 100, true)` — what does `true` mean? +**Principle:** Config objects (principles.md) +**Fix:** Use config object + +// Before +function createSlider(min: number, max: number, vertical: boolean) + +// After +function createSlider(config: { min: number; max: number; vertical?: boolean }) +``` + +### MAJOR — Runtime registration + +```markdown +### [MAJOR] Runtime plugin registration + +**What:** `player.registerPlugin(myPlugin)` pattern +**Where:** `Player.registerPlugin()` method +**Why:** Loses type safety, ordering implicit, can't tree-shake +**Principle:** Emergent extensibility (principles.md) +**Fix:** Composition at creation time + +// Before +const player = createPlayer(config) +player.registerPlugin(analytics) + +// After +const player = createPlayer({ +...config, +slices: [analyticsSlice], +}) +``` + +### MINOR — Missing type guard + +```markdown +### [MINOR] No type guard for discriminated union + +**What:** Users must narrow with string comparison +**Where:** `types.ts:45` +**Why:** Loses type narrowing benefits +**Principle:** Type guards (typescript.md) +**Fix:** Export type guard + +// After +export function isPlaying(state: MediaState): state is PlayingState { +return state.status === 'playing' +} +``` + +--- + +## Merge Report Template + +```markdown +# API Review: [package/file name] + +## Overall Score: X/10 + +| Dimension | Score | Critical | Major | Minor | +| ---------------------- | ----- | -------- | ----- | ----- | +| Types | X/10 | X | X | X | +| API Surface | X/10 | X | X | X | +| Extensibility | X/10 | X | X | X | +| Progressive Disclosure | X/10 | X | X | X | + +## Critical Issues + +[List all CRITICAL issues, full format] + +--- + +## Major Issues + +[List all MAJOR issues, full format] + +--- + +## Minor/Nit Issues + +| Severity | Location | Issue | Fix | +| -------- | ------------- | ------------------- | ----------------- | +| MINOR | `store.ts:45` | No type guard | Add `isPlaying()` | +| NIT | `api.ts:12` | Inconsistent naming | Rename | + +--- + +## Good Patterns Found + +- [What's working well] +- [Worth preserving] + +--- + +## Summary + +[2-3 paragraph assessment: strengths, weaknesses, priority order] + +--- + +## Recommendations + +### Before Release + +1. [Critical fix] + +### Next Release + +1. [Major priority] + +### Future + +1. [Minor priority] + +--- + +
+Full Types Review +[Complete output] +
+ +
+Full API Surface Review +[Complete output] +
+ +
+Full Extensibility Review +[Complete output] +
+ +
+Full Progressive Disclosure Review +[Complete output] +
+``` + +--- + +## PR Review Template + +```markdown +## PR Review: #123 + +### Breaking Changes + +- `createStore` signature changed — [Full review needed] + +### New APIs + +- `useSelector` hook added — [Review new surface] + +### Internal Changes + +- Refactored middleware — [Verify public API unchanged] + +### Checklist + +- [ ] Types still infer correctly +- [ ] No new explicit generics required +- [ ] Defaults documented +- [ ] Breaking changes in changelog +``` diff --git a/.claude/skills/api/review/workflow.md b/.claude/skills/api/review/workflow.md new file mode 100644 index 00000000..94fcf848 --- /dev/null +++ b/.claude/skills/api/review/workflow.md @@ -0,0 +1,85 @@ +# API Review Workflow + +Review APIs and architecture for design quality and developer experience. + +## Process + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Coordinator │ +└─────────────────────┬───────────────────────────────────────┘ + │ + ┌─────────────┼─────────────┬─────────────┐ + ▼ ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ + │ Types │ │ API │ │ Extend │ │ Disclose│ + └─────────┘ └─────────┘ └─────────┘ └─────────┘ + │ │ │ │ + └─────────────┴─────────────┴─────────────┘ + │ + ▼ + ┌─────────────┐ + │ Merge │ + └─────────────┘ +``` + +### 1. Gather Context + +- Single file: `path/to/api.ts` +- Package: `packages/core/src/` +- PR diff: changed API surface + +### 2. Fork Reviews + +Spawn 4 sub-agents. See [agents.md](agents.md) for prompts. + +| Agent | Focus | References | +| ------------- | -------------------------------- | ----------------------------- | +| Types | Inference, generics, exports | `references/typescript.md` | +| API Surface | Config objects, defaults, naming | `references/principles.md` | +| Extensibility | Middleware, builders, adapters | `references/extensibility.md` | +| Disclosure | Layering, escape hatches | `references/principles.md` | + +### 3. Merge Report + +Combine findings using template in [templates.md](templates.md). + +## Quick Review + +For fast reviews without forking, use [checklist.md](checklist.md). + +## Severity Levels + +| Level | Meaning | Action | +| ---------- | -------------------------- | ---------- | +| `CRITICAL` | Breaks inference, unusable | Must fix | +| `MAJOR` | Violates core principles | Should fix | +| `MINOR` | Suboptimal but workable | Consider | +| `NIT` | Enhancement opportunity | Optional | + +## Issue Format + +```markdown +### [SEVERITY] Issue title + +**What:** Brief description +**Where:** `path/to/file.ts:42` +**Why:** Impact on developers +**Principle:** Which principle violated +**Fix:** Concrete suggestion + +// Before +problematic() + +// After +improved() +``` + +## References + +| File | Contents | +| ---------------------------- | ----------------------------- | +| [agents.md](agents.md) | Sub-agent prompts | +| [templates.md](templates.md) | Issue format, report template | +| [checklist.md](checklist.md) | Quick single-agent checklist | +| [example.md](example.md) | Complete example review | diff --git a/.claude/skills/aria/SKILL.md b/.claude/skills/aria/SKILL.md new file mode 100644 index 00000000..86724370 --- /dev/null +++ b/.claude/skills/aria/SKILL.md @@ -0,0 +1,76 @@ +--- +name: aria +description: Review and implement accessibility patterns for UI components following WAI-ARIA and WCAG 2.1. Use when auditing code for a11y issues, implementing accessible controls, adding ARIA attributes, fixing keyboard navigation, handling focus management, building screen reader support, or implementing media player accessibility. Triggers on "accessibility review", "a11y", "ARIA", "keyboard navigation", "screen reader", "focus management", "WCAG", "captions", "live region". +--- + +# ARIA Skill + +## References + +| Pattern | Reference | +| ------------------- | ----------------------------------------------- | +| Keyboard Navigation | [keyboard.md](references/keyboard.md) | +| Focus Management | [focus.md](references/focus.md) | +| ARIA Roles & States | [aria.md](references/aria.md) | +| React Patterns | [react.md](references/react.md) | +| Media Players | [media.md](references/media.md) | +| Anti-Patterns | [anti-patterns.md](references/anti-patterns.md) | + +## Review + +For structured accessibility reviews, load the review workflow: + +| File | Contents | +| ----------------------------------- | --------------------------- | +| [workflow.md](review/workflow.md) | Review process and severity | +| [checklist.md](review/checklist.md) | Comprehensive checklist | +| [templates.md](review/templates.md) | Issue and report formats | + +## Core Principles + +1. **Semantic HTML first** — Use native elements before ARIA +2. **Keyboard accessible** — All interactions work without a mouse +3. **Focus visible** — Clear indication of current focus +4. **Name, Role, Value** — Every control has accessible name, correct role, exposed state +5. **Announce changes** — Dynamic content updates reach screen readers + +## Common Issues (Quick Fixes) + +| Issue | Fix | +| ---------------------- | ------------------------------------- | +| Icon button no name | Add `aria-label` | +| Custom control no role | Add appropriate `role` attribute | +| Focus outline removed | Use `focus-visible` instead | +| Toggle state unclear | Use `aria-pressed` or `aria-expanded` | +| Dynamic content silent | Add live region with `aria-live` | +| Click-only handler | Add `keydown` for Enter/Space | + +## Anti-Patterns + +❌ **Never do these:** + +- Remove focus outlines without replacement +- Use `tabindex > 0` +- Rely solely on color to convey information +- Auto-focus without user intent +- Trap focus unintentionally +- Use ARIA where native HTML suffices +- Change `aria-label` to convey state (use `aria-pressed`) + +## Next Steps + +- For keyboard patterns: [keyboard.md](references/keyboard.md) +- For focus management: [focus.md](references/focus.md) +- For ARIA roles and states: [aria.md](references/aria.md) +- For React-specific patterns: [react.md](references/react.md) +- For media player accessibility: [media.md](references/media.md) +- For common mistakes: [anti-patterns.md](references/anti-patterns.md) +- For comprehensive checklist: [checklist.md](review/checklist.md) + +## Related Skills + +| Need | Use | +| ---------------------- | ----------------- | +| Building UI components | `component` skill | +| API design principles | `api` skill | +| Documentation | `docs` skill | diff --git a/.claude/skills/aria/references/anti-patterns.md b/.claude/skills/aria/references/anti-patterns.md new file mode 100644 index 00000000..b7c1d8bf --- /dev/null +++ b/.claude/skills/aria/references/anti-patterns.md @@ -0,0 +1,252 @@ +# Accessibility Anti-Patterns + +Common mistakes that harm accessibility. For correct patterns, see linked references. + +--- + +## Focus + +### Removing Focus Indicators + +Never remove focus outlines without a visible replacement. + +See [focus.md](focus.md) for focus indicator requirements. + +### Focus Traps Without Escape + +Modal contexts must allow `Escape` to exit. A focus trap with no exit is a keyboard trap (WCAG 2.1.2 violation). + +### Positive Tabindex + +```html + + + +``` + +Use DOM order or `tabindex="0"`. Positive values create unpredictable navigation. + +--- + +## ARIA + +### ARIA on Native Elements + +```html + + + + +
Click
+``` + +Native elements have implicit roles and built-in keyboard handling. See [aria.md](aria.md) First Rule of ARIA. + +### Invalid ARIA Usage + +```html + + + + +Home + + + +``` + +ARIA attributes must match the element's actual behavior. + +### Missing Required Attributes + +```html + +
+ + +
+``` + +See [aria.md](aria.md) for required attributes by role. + +### Duplicate IDs + +```html + + + + + + + +``` + +IDs must be unique. Duplicate IDs break ARIA relationships. + +--- + +## Keyboard + +### Click-Only Handlers + +Interactive elements must respond to keyboard. Use native ` + +
+ + +
+ + +
+``` + +Composite widgets (toolbars, menus, tabs) need roving tabindex. See [keyboard.md](keyboard.md). + +### Conflicting Shortcuts + +Avoid keys used by assistive technology: + +- `Insert` — NVDA/JAWS modifier +- `Caps Lock` — VoiceOver modifier +- Single letters without focus — interfere with browse mode + +Use standard keys: `Enter`, `Space`, `Escape`, `Arrow keys`. + +--- + +## Live Regions + +### Assertive for Non-Critical Updates + +```html + +
Items loaded
+ + +
Items loaded
+``` + +Reserve `assertive` for errors and urgent alerts only. + +### Region Not in DOM on Load + +```html + + + + +
+``` + +Live regions must exist in DOM before content updates. + +### Too Many Regions + +```html + +
Status 1
+
Status 2
+
Status 3
+ + +
+``` + +Use one live region per announcement type. Combine messages if needed. + +--- + +## Content + +### Images Without Alt Text + +```html + + + + +image + + +Video.js logo + + + +``` + +See [aria.md](aria.md) images section. + +### Empty Interactive Elements + +```html + + + + + +``` + +Every interactive element needs an accessible name. + +### Color as Only Indicator + +```html + +Error + + +Error: Invalid input +``` + +Information must not rely on color alone (WCAG 1.4.1). + +--- + +## Motion + +### No Reduced Motion Support + +```css +.element { + animation: bounce 1s infinite; +} + +@media (prefers-reduced-motion: reduce) { + .element { + animation: none; + } +} +``` + +Always respect `prefers-reduced-motion`. + +### Auto-Playing Media + +```html + + + + + + +``` + +Auto-playing audio disrupts screen reader users. Mute by default or require user initiation. + +--- + +## See Also + +- [focus.md](focus.md) — correct focus patterns +- [keyboard.md](keyboard.md) — keyboard navigation +- [aria.md](aria.md) — roles, states, properties +- [checklist.md](../review/checklist.md) — comprehensive review checklist diff --git a/.claude/skills/aria/references/aria.md b/.claude/skills/aria/references/aria.md new file mode 100644 index 00000000..a1bb6fbf --- /dev/null +++ b/.claude/skills/aria/references/aria.md @@ -0,0 +1,370 @@ +# ARIA Patterns + +Roles, states, properties, labeling strategies, and live regions. + +--- + +## First Rule of ARIA + +**Use native HTML elements when possible.** + +```html + +
Submit
+ + + +``` + +ARIA adds semantics but not behavior. Native elements include keyboard handling, form integration, and browser defaults. + +--- + +## Roles by Component + +### Buttons + +| Type | Role | Key Attributes | +|------|------|----------------| +| Action | `button` | — | +| Toggle | `button` | `aria-pressed` | +| Menu trigger | `button` | `aria-haspopup`, `aria-expanded` | + +### Menus + +| Element | Role | Required Attributes | +|---------|------|---------------------| +| Container | `menu` | — | +| Action item | `menuitem` | — | +| Toggle item | `menuitemcheckbox` | `aria-checked` | +| Radio item | `menuitemradio` | `aria-checked` | +| Submenu trigger | `menuitem` | `aria-haspopup`, `aria-expanded` | +| Separator | `separator` | — | + +### Dialogs + +| Type | Role | Required Attributes | +|------|------|---------------------| +| Dialog | `dialog` | `aria-labelledby`, `aria-modal` | +| Alert dialog | `alertdialog` | `aria-labelledby`, `aria-describedby`, `aria-modal` | + +### Lists + +| Element | Role | Required Attributes | +|---------|------|---------------------| +| Listbox | `listbox` | `aria-label` or `aria-labelledby` | +| Option | `option` | `aria-selected` | +| Combobox | `combobox` | `aria-expanded`, `aria-controls` | + +### Tabs + +| Element | Role | Required Attributes | +|---------|------|---------------------| +| Tab list | `tablist` | `aria-label` | +| Tab | `tab` | `aria-selected`, `aria-controls` | +| Tab panel | `tabpanel` | `aria-labelledby` | + +### Sliders + +| Attribute | Required | +|-----------|----------| +| `role="slider"` | ✅ | +| `aria-valuemin` | ✅ | +| `aria-valuemax` | ✅ | +| `aria-valuenow` | ✅ | +| `aria-valuetext` | ✅ (human-readable) | +| `aria-label` | ✅ | +| `aria-orientation` | If not horizontal | + +--- + +## Required Attributes by Role + +Roles have required attributes. Missing them is a **moderate** violation. + +| Role | Required Attributes | +|------|---------------------| +| `checkbox` | `aria-checked` | +| `combobox` | `aria-expanded`, `aria-controls` | +| `heading` | `aria-level` | +| `listbox` | — | +| `option` | `aria-selected` | +| `radio` | `aria-checked` | +| `scrollbar` | `aria-controls`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax` | +| `slider` | `aria-valuenow`, `aria-valuemin`, `aria-valuemax` | +| `spinbutton` | `aria-valuenow`, `aria-valuemin`, `aria-valuemax` | +| `switch` | `aria-checked` | +| `tab` | `aria-selected` | +| `tabpanel` | — | +| `tree` | — | +| `treeitem` | — | + +--- + +## State Attributes + +### Toggle State (`aria-pressed`) + +For buttons that toggle between two states: + +```html + + + +``` + +**Keep label constant.** State conveyed by `aria-pressed`, not by changing the label. + +### Expanded State (`aria-expanded`) + +For elements that show/hide content: + +```html + + +``` + +### Selected State (`aria-selected`) + +Within selection widgets: + +```html +
+
Option 1
+
Option 2
+
+``` + +### Checked State (`aria-checked`) + +For checkboxes and radio items: + +| Value | Meaning | +|-------|---------| +| `true` | Checked | +| `false` | Unchecked | +| `mixed` | Indeterminate (checkbox only) | + +### Current State (`aria-current`) + +Indicates current item in a set: + +| Value | Use Case | +|-------|----------| +| `page` | Current page in navigation | +| `step` | Current step in wizard | +| `location` | Current location in breadcrumb | +| `date` | Current date in calendar | +| `time` | Current time in timeline | +| `true` | Generic current item | + +--- + +## Labeling + +### Priority Order + +1. **Visible label** — `
+ + + +``` + +### Focus Management + +1. Move focus into portal on open +2. Trap focus within portal +3. Return focus to trigger on close +4. Use `aria-controls` to maintain relationship + +--- + +## Skip Links + +Allow keyboard users to bypass repetitive content. + +### Implementation + +```html + + + +
+ ... +
+ +``` + +### Styling + +```css +.skip-link { + position: absolute; + top: -40px; + left: 0; + padding: 8px; + background: #000; + color: #fff; + z-index: 100; +} + +.skip-link:focus { + top: 0; +} +``` + +`tabindex="-1"` on target ensures it receives focus when linked to. diff --git a/.claude/skills/aria/references/keyboard.md b/.claude/skills/aria/references/keyboard.md new file mode 100644 index 00000000..b55ccb9f --- /dev/null +++ b/.claude/skills/aria/references/keyboard.md @@ -0,0 +1,180 @@ +# Keyboard Navigation Patterns + +Keyboard interaction patterns for accessible components. + +--- + +## Activation Keys + +| Element Type | Keys | Notes | +|--------------|------|-------| +| Button | `Enter`, `Space` | Both must work | +| Link | `Enter` | Space scrolls page | +| Checkbox | `Space` | Toggle checked | +| Radio | `Space`, Arrows | Space selects, arrows move | +| Menu item | `Enter`, `Space` | Activates item | + +--- + +## Arrow Key Patterns + +| Widget | Horizontal | Vertical | +|--------|------------|----------| +| Toolbar | ← → | — | +| Menu | — | ↑ ↓ | +| Menubar | ← → | ↑ ↓ (submenus) | +| Tabs | ← → | — | +| Listbox | — | ↑ ↓ | +| Grid | ← → | ↑ ↓ | +| Radio group | ← → or ↑ ↓ | Layout-dependent | +| Slider | ← → or ↑ ↓ | Orientation-dependent | +| Tree | ← → (expand) | ↑ ↓ (navigate) | + +--- + +## Universal Keys + +| Key | Action | +|-----|--------| +| `Tab` | Next focusable element | +| `Shift+Tab` | Previous focusable element | +| `Escape` | Close/dismiss current context | +| `Home` | First item in group | +| `End` | Last item in group | +| `PageUp` | Large step up/back | +| `PageDown` | Large step down/forward | + +--- + +## Roving Tabindex + +Use for composite widgets where only one item is in tab order. + +**Applies to:** Toolbars, tab lists, menu bars, radio groups, listboxes, trees + +**Rules:** +- Container not in tab order +- Active item: `tabindex="0"` +- Other items: `tabindex="-1"` +- Arrow keys move focus AND update tabindex + +```html +
+ + + +
+``` + +--- + +## Widget Keyboard Reference + +### Menu + +| Key | Action | +|-----|--------| +| `↑` `↓` | Navigate items | +| `Enter` `Space` | Activate item | +| `Escape` | Close menu | +| `Home` `End` | First/last item | +| `→` | Open submenu (LTR) | +| `←` | Close submenu (LTR) | +| Alphanumeric | Type-ahead jump | + +### Tabs + +| Key | Action | +|-----|--------| +| `←` `→` | Navigate tabs | +| `Tab` | Move to panel (exit tablist) | +| `Home` `End` | First/last tab | + +### Slider + +| Key | Action | +|-----|--------| +| `←` `↓` | Decrease by step | +| `→` `↑` | Increase by step | +| `PageDown` | Decrease by large step | +| `PageUp` | Increase by large step | +| `Home` | Set to minimum | +| `End` | Set to maximum | + +### Dialog + +| Key | Action | +|-----|--------| +| `Tab` | Cycle through focusable elements (trapped) | +| `Escape` | Close dialog | + +### Listbox + +| Key | Action | +|-----|--------| +| `↑` `↓` | Navigate options | +| `Enter` `Space` | Select option | +| `Home` `End` | First/last option | +| Alphanumeric | Type-ahead jump | + +### Tree + +| Key | Action | +|-----|--------| +| `↑` `↓` | Navigate items | +| `→` | Expand node / move to first child | +| `←` | Collapse node / move to parent | +| `Enter` `Space` | Activate item | +| `Home` `End` | First/last visible item | +| `*` | Expand all siblings | + +--- + +## Non-Semantic Element Handling + +When using `
` or `` as interactive elements: + +```html + +
Click me
+ + +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleClick(); + } + }} +> + Click me +
+ + + +``` + +--- + +## Link Requirements + +Links must have: +- `href` attribute (even if `#` for SPA routing) +- Activates on `Enter` only (not Space) + +```html + +Go somewhere + + +Do something + + +Go to page + + + +``` diff --git a/.claude/skills/aria/references/media.md b/.claude/skills/aria/references/media.md new file mode 100644 index 00000000..15d354ca --- /dev/null +++ b/.claude/skills/aria/references/media.md @@ -0,0 +1,409 @@ +# Media Player Accessibility + +Accessibility patterns specific to video and audio players. Covers controls, keyboard shortcuts, captions, and screen reader support. + +--- + +## Player Container + +The root player element needs proper identification: + +```html +
+ +
+``` + +**Attributes:** +- `role="region"` or `role="application"` (if fully keyboard-managed) +- `aria-label` includes media type and title +- `tabindex="-1"` allows programmatic focus + +--- + +## Keyboard Shortcuts + +### Standard Media Keys + +| Key | Action | Configurable | +|-----|--------|--------------| +| `Space` / `k` | Toggle play/pause | Yes | +| `m` | Toggle mute | Yes | +| `f` | Toggle fullscreen | Yes | +| `c` | Toggle captions | Yes | +| `i` | Toggle picture-in-picture | Yes | +| `←` / `j` | Seek backward | Yes (default: 5-10s) | +| `→` / `l` | Seek forward | Yes (default: 5-10s) | +| `↑` | Volume up | Yes (default: 5-10%) | +| `↓` | Volume down | Yes (default: 5-10%) | +| `Home` | Seek to start | — | +| `End` | Seek to end | — | +| `0-9` | Seek to percentage | — | + +### Key Scope + +Configure whether shortcuts work: +- **Document-wide**: Work anywhere on page (YouTube-style) +- **Player-scoped**: Only when player has focus + +```html + + + +
...
+
...
+ +
+``` + +**Navigation:** +- `Tab` enters/exits toolbar (single stop) +- `←` `→` moves between controls +- `Enter`/`Space` activates control + +--- + +## Control Components + +### Play/Pause Button + +``` +role="button" +aria-pressed="{isPlaying}" +aria-label="Play" +aria-keyshortcuts="k Space" +``` + +**Important:** Use `aria-pressed` for toggle state. Don't change `aria-label` between "Play"/"Pause". + +### Mute Button + +``` +role="button" +aria-pressed="{isMuted}" +aria-label="Mute" +aria-keyshortcuts="m" +``` + +### Time Slider (Seek/Scrubber) + +``` +role="slider" +aria-label="Seek" +aria-valuemin="0" +aria-valuemax="{duration}" +aria-valuenow="{currentTime}" +aria-valuetext="2 minutes 30 seconds of 10 minutes" +aria-orientation="horizontal" +``` + +**Key behavior:** +- `←` `→`: Seek by step (5 seconds) +- `PageUp` `PageDown`: Seek by large step (10%) +- `Home` `End`: Start/end of video +- `Shift+Arrow`: Larger increment + +### Volume Slider + +``` +role="slider" +aria-label="Volume" +aria-valuemin="0" +aria-valuemax="100" +aria-valuenow="{volume}" +aria-valuetext="{volume} percent" +aria-orientation="horizontal" +``` + +### Fullscreen Button + +``` +role="button" +aria-label="Enter fullscreen" | "Exit fullscreen" +aria-keyshortcuts="f" +``` + +**Note:** Label change is acceptable here (not a toggle state). + +### Captions Button + +``` +role="button" +aria-pressed="{captionsEnabled}" +aria-label="Closed captions" +aria-keyshortcuts="c" +``` + +--- + +## Settings Menu + +### Menu Structure + +```html + + + +``` + +### Menu Keyboard Navigation + +| Key | Action | +|-----|--------| +| `↑` `↓` | Navigate items | +| `Enter` `Space` | Select item | +| `Escape` | Close menu | +| `Home` `End` | First/last item | +| Alphanumeric | Type-ahead | + +--- + +## Live Announcements + +### Announcer Component + +Create a dedicated live region for status messages: + +```html +
+ +
+``` + +### Events to Announce + +| Event | Message | Priority | +|-------|---------|----------| +| Play | "Playing" | polite | +| Pause | "Paused" | polite | +| Volume change | "Volume {n} percent" | polite | +| Mute | "Muted" | polite | +| Unmute | "Unmuted" | polite | +| Seek | "Seeking to {time}" | polite | +| Captions on | "Captions on" | polite | +| Captions off | "Captions off" | polite | +| Fullscreen enter | "Entered fullscreen" | polite | +| Fullscreen exit | "Exited fullscreen" | polite | +| Quality change | "Quality: {level}" | polite | +| Playback rate | "Speed: {rate}x" | polite | +| Buffering | "Buffering" | polite | +| Error | "Error: {message}" | **assertive** | + +--- + +## Captions + +### CVAA Compliance Requirements + +Users must be able to customize: +- Font family +- Font size +- Font color +- Background color +- Background opacity +- Edge/outline style +- Window color + +### Caption Settings UI + +```html +
+
Font size
+
Font color
+
Background
+ +
+``` + +### Caption Display Considerations + +- Captions must not overlap controls +- Position should adapt when controls show/hide +- User preferences must persist across sessions +- Support VTT, SRT at minimum + +--- + +## Audio Tracks + +### Track Selection UI + +```html +
+
English
+
Spanish
+
English (Audio Description)
+
+``` + +Audio description tracks should be clearly labeled. + +--- + +## Chapter Navigation + +```html +
+
+ Introduction (0:00) +
+
+ Getting Started (2:30) +
+
+ Advanced Topics (15:00) +
+
+``` + +`aria-current="true"` indicates the currently playing chapter. + +--- + +## Live/Streaming Indicator + +```html + +``` + +For live streams, indicate: +- Whether currently at live edge +- Option to jump to live edge + +--- + +## Focus Management + +### After Fullscreen + +When exiting fullscreen, return focus to: +1. The fullscreen button +2. Or the last focused control + +### Menu Close + +When closing settings menu: +1. Return focus to settings button +2. Keep user in control bar context + +### Control Bar Hide/Show + +When controls auto-hide: +- Keep focus on last active control +- Don't trap focus on hidden elements +- Re-show controls on `Tab` key + +--- + +## Reduced Motion + +Respect `prefers-reduced-motion`: + +```css +@media (prefers-reduced-motion: reduce) { + .player-animation { + animation: none; + transition: none; + } +} +``` + +Consider: +- Disable auto-playing animations +- Reduce or remove control transitions +- Pause decorative motion + +--- + +## Color Contrast + +| Element | Minimum Ratio | +|---------|---------------| +| Control icons/text | 4.5:1 | +| UI components | 3:1 | +| Focus indicators | 3:1 | +| Caption text | 4.5:1 | + +Test against video content backgrounds, not just static backgrounds. + +--- + +## Data Attributes for Styling + +Expose player state for CSS without ARIA pollution: + +| Attribute | Values | +|-----------|--------| +| `data-state` | playing, paused, waiting, ended | +| `data-fullscreen` | present when fullscreen | +| `data-captions` | present when captions visible | +| `data-user-idle` | present when controls should hide | +| `data-muted` | present when muted | +| `data-live` | present for live streams | +| `data-at-live-edge` | present when at live edge | + +```css +[data-state="playing"] .play-icon { display: none; } +[data-state="paused"] .pause-icon { display: none; } +[data-user-idle] .controls { opacity: 0; pointer-events: none; } +``` + +--- + +## WCAG Criteria for Media + +| Criterion | Level | Requirement | +|-----------|-------|-------------| +| 1.2.1 | A | Provide text alternative or audio track | +| 1.2.2 | A | Synchronized captions for prerecorded | +| 1.2.3 | A | Audio description for prerecorded | +| 1.2.5 | AA | Audio description for prerecorded | +| 1.4.2 | A | Audio control (pause, stop, or mute) | +| 2.1.1 | A | All functionality keyboard accessible | +| 2.1.2 | A | No keyboard trap | +| 2.2.2 | A | Pause, stop, hide for moving content | +| 2.3.1 | A | No content flashes more than 3x/second | +| 4.1.2 | A | Name, role, value for all UI components | diff --git a/.claude/skills/aria/references/react.md b/.claude/skills/aria/references/react.md new file mode 100644 index 00000000..672fbb43 --- /dev/null +++ b/.claude/skills/aria/references/react.md @@ -0,0 +1,469 @@ +# React Accessibility Patterns + +React-specific patterns for accessibility. Covers hook architecture, ref management, and framework considerations. + +--- + +## Hook Architecture + +Separate accessibility logic into composable hooks: + +### Layer Separation + +| Layer | Responsibility | Example | +| -------- | -------------- | ------------------------ | +| State | Component data | `useToggleState` | +| Behavior | ARIA + events | `useButton`, `useSlider` | +| Render | DOM output | Component JSX | + +This separation allows: + +- Sharing accessibility logic across components +- Testing behavior independently +- Framework-agnostic core patterns + +### Behavior Hook Pattern + +```typescript +function useButton(props, ref) { + return { + buttonProps: { + role: 'button', + tabIndex: 0, + onKeyDown: handleKeyDown, + onClick: props.onPress, + 'aria-disabled': props.isDisabled, + }, + }; +} +``` + +--- + +## Focus Management Hooks + +### Focus Scope + +Contain focus within a subtree: + +```typescript +function useFocusScope(options: { + contain?: boolean; // Trap focus + restoreFocus?: boolean; // Restore on unmount + autoFocus?: boolean; // Focus first element +}) { + const scopeRef = useRef(null); + const previousFocus = useRef(null); + + // Implementation handles: + // - Finding focusable elements + // - Tab wrapping at boundaries + // - Restoring focus on unmount + + return { scopeRef }; +} +``` + +### Focus Ring Detection + +Detect keyboard vs pointer focus: + +```typescript +function useFocusRing() { + const [isFocusVisible, setFocusVisible] = useState(false); + + // Track input modality globally + // Set true on keyboard events + // Set false on pointer events + + return { + isFocusVisible, + focusProps: { + onFocus: () => { + /* check modality */ + }, + onBlur: () => setFocusVisible(false), + }, + }; +} +``` + +### Roving Tabindex Hook + +```typescript +function useRovingTabindex(items: RefObject[]) { + const [activeIndex, setActiveIndex] = useState(0); + + // Returns props to spread on each item + return items.map((ref, i) => ({ + tabIndex: i === activeIndex ? 0 : -1, + onKeyDown: (e) => { + // Handle arrow keys, Home, End + // Update activeIndex + // Call focus() on new active item + }, + })); +} +``` + +--- + +## Ref Patterns + +### Merging Refs + +When component accepts ref but you also need internal ref: + +```typescript +function useMergedRef(...refs: Ref[]): RefCallback { + return useCallback((value) => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(value); + } else if (ref) { + ref.current = value; + } + }); + }, refs); +} + +// Usage +const Component = forwardRef((props, forwardedRef) => { + const internalRef = useRef(null); + const ref = useMergedRef(forwardedRef, internalRef); + return
; +}); +``` + +### Callback Refs for Dynamic Elements + +```typescript +function useCallbackRef(callback: (node: T | null) => void) { + const ref = useRef(callback); + + useLayoutEffect(() => { + ref.current = callback; + }); + + return useCallback((node: T | null) => { + ref.current(node); + }, []); +} +``` + +--- + +## Event Handling + +### Keyboard Event Normalization + +Handle cross-browser keyboard events: + +```typescript +function useKeyboard(handlers: { onKeyDown?: (e: KeyboardEvent) => void; onKeyUp?: (e: KeyboardEvent) => void }) { + return { + keyboardProps: { + onKeyDown: (e: ReactKeyboardEvent) => { + // Normalize key values + // Handle IME composition + // Call appropriate handler + }, + }, + }; +} +``` + +### Press Events + +Unified press handling for mouse, touch, keyboard: + +```typescript +function usePress(props: { + onPress?: () => void; + onPressStart?: () => void; + onPressEnd?: () => void; + isDisabled?: boolean; +}) { + return { + pressProps: { + onClick: props.onPress, + onKeyDown: (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + props.onPress?.(); + } + }, + }, + }; +} +``` + +--- + +## Announcements + +### Live Region Hook + +```typescript +function useAnnounce() { + const announce = useCallback((message: string, politeness: 'polite' | 'assertive' = 'polite') => { + // Get or create live region + // Clear existing content + // Set new content (triggers announcement) + }, []); + + return { announce }; +} +``` + +### Status Message Pattern + +```typescript +function useFormValidation() { + const { announce } = useAnnounce(); + + const validate = (value) => { + const error = getError(value); + if (error) { + announce(error, 'assertive'); + } + return error; + }; + + return { validate }; +} +``` + +--- + +## SSR Considerations + +### ID Generation + +Generate stable IDs for ARIA relationships: + +```typescript +function useId(prefix?: string): string { + // Use React 18's useId if available + // Otherwise, generate stable ID + // Avoid hydration mismatches +} + +// Usage +function Dialog({ title, children }) { + const titleId = useId('dialog-title'); + + return ( +
+

{title}

+ {children} +
+ ); +} +``` + +### Hydration Safety + +Avoid client-only APIs in initial render: + +```typescript +function useSafeLayoutEffect(effect, deps) { + // useLayoutEffect on client + // useEffect (or skip) on server + const isClient = typeof window !== 'undefined'; + const useIsomorphicEffect = isClient ? useLayoutEffect : useEffect; + useIsomorphicEffect(effect, deps); +} +``` + +--- + +## Portal Accessibility + +### Focus Containment with Portals + +Portaled content (modals, popovers) needs special handling: + +```typescript +function AccessiblePortal({ children, containFocus }) { + return createPortal( + + {children} + , + document.body + ); +} +``` + +### ARIA Relationships Across Portals + +When trigger and content are in different DOM locations: + +```typescript +function Popover({ trigger, content }) { + const triggerId = useId('trigger'); + const contentId = useId('content'); + + return ( + <> + + {isOpen && createPortal( + , + document.body + )} + + ); +} +``` + +--- + +## State Hook Patterns + +### Toggle State + +```typescript +function useToggleState(props: { + defaultSelected?: boolean; + isSelected?: boolean; + onChange?: (isSelected: boolean) => void; +}) { + const [isSelected, setSelected] = useControlledState( + props.isSelected, + props.defaultSelected ?? false, + props.onChange + ); + + return { + isSelected, + toggle: () => setSelected(!isSelected), + setSelected, + }; +} +``` + +### Selection State (Single/Multi) + +```typescript +function useSelectionState(props: { + selectionMode: 'none' | 'single' | 'multiple'; + selectedKeys?: Set; + defaultSelectedKeys?: Set; + onSelectionChange?: (keys: Set) => void; +}) { + // Handles controlled/uncontrolled + // Enforces selection mode rules + // Provides select/deselect/toggle methods +} +``` + +--- + +## Component Prop Patterns + +### Spreading Props Safely + +```typescript +// Separate ARIA/DOM props from component props +function splitProps(props: T, ariaKeys: string[]) { + const ariaProps = {}; + const restProps = {}; + + for (const key in props) { + if (ariaKeys.includes(key) || key.startsWith('aria-')) { + ariaProps[key] = props[key]; + } else { + restProps[key] = props[key]; + } + } + + return [ariaProps, restProps]; +} +``` + +### Render Props for Flexibility + +```typescript +interface ButtonProps { + children: ReactNode | ((state: ButtonState) => ReactNode); +} + +interface ButtonState { + isPressed: boolean; + isFocused: boolean; + isFocusVisible: boolean; + isDisabled: boolean; +} + +// Allows consumers to access state for custom rendering + +``` + +--- + +## Testing Patterns + +### Accessibility Testing Setup + +```typescript +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +test('component has no accessibility violations', async () => { + const { container } = render(); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); +``` + +### Focus Testing + +```typescript +import { fireEvent } from '@testing-library/react'; + +test('arrow keys navigate options', () => { + const { getAllByRole } = render(); + const options = getAllByRole('option'); + + options[0].focus(); + fireEvent.keyDown(options[0], { key: 'ArrowDown' }); + + expect(document.activeElement).toBe(options[1]); +}); +``` + +### Screen Reader Simulation + +```typescript +test('announces state changes', () => { + const { getByRole } = render(); + const button = getByRole('button'); + const liveRegion = getByRole('status'); + + fireEvent.click(button); + + expect(liveRegion).toHaveTextContent('Enabled'); +}); +``` + +--- + +## See Also + +- [component/react.md](../../component/references/react.md) — React component architecture (context, controlled state, render props) diff --git a/.claude/skills/aria/review/checklist.md b/.claude/skills/aria/review/checklist.md new file mode 100644 index 00000000..ae0cf91d --- /dev/null +++ b/.claude/skills/aria/review/checklist.md @@ -0,0 +1,442 @@ +# Accessibility Review Checklist + +Comprehensive checklist for reviewing components. Use for manual audits or as rules for automated linting. + +--- + +## All Interactive Elements + +### Accessible Name (WCAG 4.1.2) + +- [ ] Has visible label, `aria-label`, or `aria-labelledby` +- [ ] Label accurately describes purpose +- [ ] Label is concise (1-3 words for buttons) +- [ ] Icon-only buttons have `aria-label` + +**Detection:** Element with `role` or interactive tag has empty accessible name + +``` +❌ +✅ +``` + +### Keyboard Access (WCAG 2.1.1) + +- [ ] Focusable via Tab (or arrow keys in composite widgets) +- [ ] Responds to expected keys (Enter, Space, arrows) +- [ ] No mouse-only interactions + +**Detection:** `onClick` without `onKeyDown`, or non-button element with click handler + +``` +❌
Click me
+✅ +``` + +### Links (WCAG 4.1.2) + +- [ ] Has `href` attribute +- [ ] Has accessible name (text content or `aria-label`) +- [ ] Uses ` +``` + +### Focus Indicator (WCAG 2.4.7) + +- [ ] Visible focus style when focused via keyboard +- [ ] Focus indicator has 3:1 contrast minimum +- [ ] Not removed via `outline: none` without replacement + +**Detection:** CSS contains `outline: none` or `outline: 0` without `:focus-visible` rule + +``` +❌ :focus { outline: none; } +✅ :focus-visible { outline: 2px solid #005fcc; } +``` + +### Proper Role (WCAG 4.1.2) + +- [ ] Native element used when possible (` + + + + +``` + +## Severity Levels + +| Level | Meaning | Examples | +| ---------- | -------------------------------------- | ---------------------------------------------------------------- | +| `CRITICAL` | Blocks assistive technology users | Missing accessible name, keyboard trap, no focus indicator | +| `MAJOR` | Significant barrier, workarounds exist | Poor focus contrast, missing live region, touch target too small | +| `MINOR` | Suboptimal but functional | Verbose label, missing description, skipped heading level | +| `NIT` | Enhancement opportunity | Could use aria-describedby, label could be shorter | + +--- + +## Issue Examples + +### CRITICAL — Missing accessible name + +```markdown +### [CRITICAL] Icon button has no accessible name + +**What:** Button contains only an SVG icon with no text alternative +**Where:** `src/ui/controls/close-button.ts:15` +**Why:** Screen readers announce "button" with no indication of purpose +**WCAG:** 4.1.2 Name, Role, Value +**Fix:** Add aria-label + + + + + + +``` + +### MAJOR — Missing keyboard handler + +```markdown +### [MAJOR] Custom control only responds to click + +**What:** Slider thumb has onClick but no keyboard support +**Where:** `src/ui/slider/thumb.ts:23` +**Why:** Keyboard users cannot adjust volume +**WCAG:** 2.1.1 Keyboard +**Fix:** Add keydown handler for arrow keys + + +
+ + +
+``` + +### MINOR — Verbose label + +```markdown +### [MINOR] Button label is unnecessarily verbose + +**What:** Label includes redundant information +**Where:** `src/ui/controls/play-button.ts:8` +**Why:** Screen reader users hear repetitive content +**WCAG:** 2.4.6 Headings and Labels +**Fix:** Shorten to essential information + + + +aria-label="Click this button to play the video" + + + +aria-label="Play" +``` + +--- + +## Report Template + +```markdown +# Accessibility Review: [filename or component] + +## Score: X/100 + +| Severity | Count | Points | +| -------- | ----- | ------ | +| Critical | X | -X | +| Major | X | -X | +| Minor | X | -X | +| Nit | X | -X | + +## Critical Issues + +[List all CRITICAL issues using full format above] + +--- + +## Major Issues + +[List all MAJOR issues using full format above] + +--- + +## Minor Issues + +| Severity | Location | Issue | WCAG | Fix | +| -------- | ------------ | ------------------- | ----- | --------------- | +| MINOR | `file.ts:12` | Verbose label | 2.4.6 | Shorten | +| NIT | `file.ts:34` | Missing describedby | 1.3.1 | Add description | + +--- + +## Good Patterns Found + +- [What's working well] +- [Worth preserving] + +--- + +## Summary + +[2-3 paragraph assessment: overall accessibility posture, priority fixes, recommendations] + +--- + +## Recommendations + +### Before Release + +1. [Critical fixes] + +### Next Release + +1. [Major improvements] + +### Future + +1. [Minor enhancements] +``` + +--- + +## PR Review Template + +For reviewing accessibility changes in pull requests: + +```markdown +# PR Accessibility Review: #[number] + +## Changed Files + +| File | Type | Review | +| ------------------ | ------------- | ----------- | +| `src/ui/menu.ts` | New component | Full review | +| `src/ui/button.ts` | Modified | Diff review | + +## New Components + +### src/ui/menu.ts + +[Full review using standard template] + +## Modified Components + +### src/ui/button.ts + +**Changed:** Lines 45-60 + +[Review of changed accessibility surface only] + +## Checklist + +- [ ] New interactive elements have accessible names +- [ ] Keyboard navigation works correctly +- [ ] Focus management handles new UI flows +- [ ] ARIA attributes reflect component state +- [ ] Live regions announce dynamic changes +- [ ] No new accessibility regressions +``` + +--- + +## Console Output Format + +For quick terminal-based reviews: + +``` +═══════════════════════════════════════════════════ +A11Y REVIEW: [filename] +═══════════════════════════════════════════════════ + +CRITICAL (X issues) +─────────────────── +[A11Y] Line X: Issue title + code snippet + Fix: recommended fix + WCAG: criterion + +MAJOR (X issues) +─────────────────── +[A11Y] Line X: Issue title + code snippet + Fix: recommended fix + WCAG: criterion + +MINOR (X issues) +─────────────────── +[A11Y] Line X: Issue title + code snippet + Fix: recommended fix + WCAG: criterion + +═══════════════════════════════════════════════════ +SUMMARY: X critical, X major, X minor +Score: X/100 +═══════════════════════════════════════════════════ +``` diff --git a/.claude/skills/aria/review/workflow.md b/.claude/skills/aria/review/workflow.md new file mode 100644 index 00000000..36d7178d --- /dev/null +++ b/.claude/skills/aria/review/workflow.md @@ -0,0 +1,92 @@ +# Accessibility Review Workflow + +Review components and code for accessibility following WAI-ARIA and WCAG 2.1. + +## Process + +``` +┌─────────────────────────────────────────────────────┐ +│ Gather Context │ +│ Load file(s) or PR diff to review │ +└──────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Load References │ +│ Based on component type (media, menu, form) │ +└──────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Run Checklist │ +│ review/checklist.md by section │ +└──────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Format Report │ +│ Use templates.md for output │ +└─────────────────────────────────────────────────────┘ +``` + +### 1. Gather Context + +Identify what to review: + +- **Single file:** `path/to/component.ts` +- **Package:** `packages/html/src/ui/` +- **PR diff:** Changed accessibility surface + +### 2. Load References + +Based on component type, load relevant references: + +| Component Type | Load | +| ---------------- | ----------------------------------------------- | +| Media player | `references/media.md` | +| Any interactive | `references/keyboard.md`, `references/focus.md` | +| Custom widgets | `references/aria.md` | +| React components | `references/react.md` | +| Common mistakes | `references/anti-patterns.md` | + +### 3. Run Checklist + +Use [checklist.md](checklist.md) systematically: + +1. Start with **All Interactive Elements** section +2. Check component-specific sections (Buttons, Menus, Dialogs, Sliders, etc.) +3. Review Color and Contrast +4. Check Motion and Animation +5. Verify Document Structure if applicable + +### 4. Format Report + +Use [templates.md](templates.md) for consistent output. + +## Quick Review + +For fast reviews without full checklist: + +- [ ] All interactive elements have accessible names +- [ ] Keyboard navigation works (Tab, Enter, Space, Arrows, Escape) +- [ ] Focus indicator visible on all focusable elements +- [ ] Custom controls have appropriate ARIA roles +- [ ] State changes announced (aria-pressed, aria-expanded, live regions) +- [ ] No mouse-only interactions + +## Severity Levels + +See [templates.md](templates.md) for severity definitions and scoring. + +## References + +| File | Contents | +| ---------------------------------------------------------------- | ------------------------------ | +| [checklist.md](checklist.md) | Comprehensive review checklist | +| [templates.md](templates.md) | Issue format, report template | +| [../references/keyboard.md](../references/keyboard.md) | Keyboard navigation patterns | +| [../references/focus.md](../references/focus.md) | Focus management | +| [../references/aria.md](../references/aria.md) | ARIA roles and states | +| [../references/media.md](../references/media.md) | Media player accessibility | +| [../references/react.md](../references/react.md) | React-specific patterns | +| [../references/anti-patterns.md](../references/anti-patterns.md) | Common mistakes | diff --git a/.claude/skills/component/SKILL.md b/.claude/skills/component/SKILL.md new file mode 100644 index 00000000..61761c92 --- /dev/null +++ b/.claude/skills/component/SKILL.md @@ -0,0 +1,215 @@ +--- +name: component +description: >- + Build accessible, headless UI components with modern architecture patterns. + Use when creating component libraries, design systems, or reusable UI primitives. + Handles compound components, state management, accessibility, styling hooks, and API design. + Includes Lit (controllers, ReactiveElement) and React (hooks, context) patterns. +--- + +# Component Architecture Patterns + +Build accessible, headless UI components using proven patterns from Base UI, Radix, and Ark UI. These patterns are **framework-agnostic** — core concepts apply across React, Vue, Svelte, Solid, and vanilla JS. + +**Primary sources:** + +- [Base UI Handbook](https://base-ui.com/react/handbook/overview) +- [Ark UI](https://ark-ui.com/) — Cross-framework implementation +- [Zag.js](https://zagjs.com/) — State machines for UI components + +**Framework-specific:** [react.md](references/react.md) | [lit.md](references/lit.md) + +--- + +## Core Principles + +1. **Headless over styled** — Separate behavior from presentation +2. **Compound over monolithic** — Small composable parts over config-heavy megacomponents +3. **Controlled + uncontrolled** — Support both state ownership models +4. **Accessible by default** — ARIA, keyboard nav, focus management built-in +5. **State via attributes** — Expose state through `data-*` for framework-agnostic styling + +--- + +## Pattern 1: Compound Components + +**What:** Components as related parts sharing state through context, each mapping 1:1 to DOM elements. + +**Why:** + +- Declarative — assemble like building blocks, reorder/omit parts freely +- Each part is an independent styling target +- DOM structure maps directly to ARIA roles + +**Standard hierarchies:** + +| Type | Parts | +| ----------- | ---------------------------------------------------- | +| Popups | Root → Trigger → Portal → Positioner → Popup → Arrow | +| Collections | Root → List → Trigger + Panel | +| Forms | Root → Label → Control → Description → Error | + +**Ref:** [Base UI Composition](https://base-ui.com/react/handbook/composition) + +--- + +## Pattern 2: Controlled & Uncontrolled State + +**What:** Support external state control OR internal state with consistent prop naming. + +**Why:** + +- Flexibility for simple and complex use cases +- Predictable API across components +- Change details enable fine-grained control (cancel changes, track reasons) + +**Convention:** + +| State | Uncontrolled | Controlled | Handler | +| ------- | ---------------- | ---------- | ----------------------------------- | +| Open | `defaultOpen` | `open` | `onOpenChange(open, details)` | +| Value | `defaultValue` | `value` | `onValueChange(value, details)` | +| Checked | `defaultChecked` | `checked` | `onCheckedChange(checked, details)` | + +**Change details:** `{ reason, event, cancel() }` + +**Ref:** [Base UI Customization](https://base-ui.com/react/handbook/customization) + +--- + +## Pattern 3: Prop Getters + +**What:** Functions returning HTML attributes for DOM elements, abstracting logic from rendering. + +**Why:** + +- Portable across frameworks (React, Vue, Svelte, Solid) +- Clean separation of concerns +- Composable via `mergeProps()` + +**Example:** `getTriggerProps()` returns `{ aria-expanded, aria-haspopup, onClick, onKeyDown }` + +**Ref:** [Zag.js](https://zagjs.com/), [Downshift](https://www.downshift-js.com/) + +--- + +## Pattern 4: State via Data Attributes + +**What:** Expose state through `data-*` attributes for CSS targeting. + +**Why:** + +- Framework-agnostic styling +- No JS needed for state-based styles +- Inspectable in DevTools + +**Standard attributes:** + +- `data-open` / `data-closed` — Visibility +- `data-checked` / `data-unchecked` — Toggle state +- `data-highlighted` — Focus within group +- `data-disabled`, `data-valid`, `data-invalid` +- `data-side`, `data-align` — Positioning + +**CSS variables:** `--available-height`, `--anchor-width`, `--transform-origin` + +**Ref:** [Base UI Styling](https://base-ui.com/react/handbook/styling) + +--- + +## Pattern 5: Accessibility + +**What:** ARIA, keyboard navigation, focus management built into architecture. + +**Why:** + +- Accessibility is structural, not decorative +- Users expect standard keyboard interactions +- Consistent patterns reduce errors + +**Key concerns:** + +- **ARIA attributes** — Auto-managed from state +- **Focus trapping** — Modals trap focus within +- **Roving tabindex** — One tabbable item, arrows navigate +- **Virtual focus** — `aria-activedescendant` for long lists +- **Typeahead** — A-Z jumps to matches + +**Ref:** [Base UI Accessibility](https://base-ui.com/react/overview/accessibility), [WAI-ARIA Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) + +For detailed accessibility patterns, load the `aria` skill. + +--- + +## Pattern 6: Floating Positioning + +**What:** Position popups relative to triggers with collision detection. + +**Why:** Handles viewport boundaries, scroll, resize automatically. + +**Config:** `side`, `align`, `sideOffset`, `collision` (flip/shift), `trackAnchor` + +**Ref:** [Floating UI](https://floating-ui.com/) + +--- + +## API Conventions + +| Category | Props | +| ----------- | -------------------------------------------------------------- | +| Interaction | `disabled`, `required`, `readOnly` | +| Collections | `multiple`, `loopFocus`, `orientation` | +| Popups | `modal`, `closeOnEscape`, `closeOnOutsideClick`, `keepMounted` | +| Positioning | `side`, `align`, `sideOffset`, `collision` | + +**Imperative actions:** `actionsRef` exposing `open()`, `close()`, `toggle()` + +See [props.md](references/props.md) for naming conventions. + +--- + +## Reference Files + +| File | Contents | +| ----------------------------------------------- | ------------------------------------ | +| [lit.md](references/lit.md) | Lit controllers, mixins, context | +| [react.md](references/react.md) | React hooks, context, refs | +| [props.md](references/props.md) | Prop naming, conventions, defaults | +| [styling.md](references/styling.md) | Data attributes, CSS variables | +| [animation.md](references/animation.md) | CSS transitions, JS animation libs | +| [polymorphism.md](references/polymorphism.md) | render vs asChild patterns | +| [collection.md](references/collection.md) | Collections, portals, virtualization | +| [anti-patterns.md](references/anti-patterns.md) | Common component mistakes | + +For accessibility patterns (ARIA, keyboard, focus), load the `aria` skill. + +## Review + +For structured component reviews, load the review workflow: + +| File | Contents | +| ----------------------------------- | --------------------------- | +| [workflow.md](review/workflow.md) | Review process and severity | +| [checklist.md](review/checklist.md) | Component review checklist | +| [templates.md](review/templates.md) | Issue and report formats | + +--- + +## Implementation Sources + +| Resource | Use For | +| ------------------------------------------------------------------------------- | --------------------------------- | +| [Base UI Source](https://github.com/mui/base-ui/tree/master/packages/react/src) | React reference implementations | +| [Radix Primitives](https://github.com/radix-ui/primitives) | Alternative approach | +| [Zag.js](https://github.com/chakra-ui/zag) | Framework-agnostic state machines | +| [Floating UI](https://floating-ui.com/docs/getting-started) | Positioning | + +--- + +## Related Skills + +| Need | Use | +| ---------------------- | ------------ | +| Accessibility patterns | `aria` skill | +| API design principles | `api` skill | +| Documentation patterns | `docs` skill | diff --git a/.claude/skills/component/references/animation.md b/.claude/skills/component/references/animation.md new file mode 100644 index 00000000..59d2ccf6 --- /dev/null +++ b/.claude/skills/component/references/animation.md @@ -0,0 +1,238 @@ +# Animation Patterns + +Animate component state changes with CSS or JavaScript libraries. + +**Reference:** [Base UI Animation Handbook](https://base-ui.com/react/handbook/animation) + +--- + +## CSS Transitions (Preferred) + +Use `data-starting-style` / `data-ending-style` for smooth transitions: + +```css +.popup { + transform-origin: var(--transform-origin); + transition: + transform 150ms, + opacity 150ms; +} + +.popup[data-starting-style], +.popup[data-ending-style] { + opacity: 0; + transform: scale(0.9); +} +``` + +**Why transitions over animations:** Transitions can be cancelled midway. If user closes a popup before it finishes opening, it smoothly animates to closed without abrupt changes. + +--- + +## CSS Animations + +Use `data-open` / `data-closed` for keyframe animations: + +```css +.popup[data-open] { + animation: scaleIn 200ms ease-out; +} + +.popup[data-closed] { + animation: scaleOut 200ms ease-in; +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes scaleOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.9); + } +} +``` + +--- + +## JavaScript Animation Libraries + +### Unmounted Components (Dialog, Popover, Menu) + +Components unmounted from DOM when closed need special handling for exit animations. + +**Pattern:** Controlled `open` + `keepMounted` on Portal + AnimatePresence + +```tsx +function AnimatedPopover() { + const [open, setOpen] = useState(false); + + return ( + + Open + + {open && ( + + + + } + > + Content + + + + )} + + + ); +} +``` + +### Kept Mounted Components + +Components with `keepMounted` stay in DOM when closed — use state-based animation: + +```tsx + ( + + )} +> + Content + +``` + +### Manual Unmount Control + +Use `actionsRef` for full lifecycle control: + +```tsx +function ManualUnmount() { + const [open, setOpen] = useState(false); + const actionsRef = useRef(null); + + return ( + + Open + + {open && ( + + { + if (!open) actionsRef.current.unmount(); + }} + /> + } + /> + + )} + + + ); +} +``` + +--- + +## Animation Detection + +Base UI uses `element.getAnimations()` to detect when animations finish before unmounting. + +**Important:** For animations without opacity (e.g., translating drawer), include `opacity: 0.9999` so detection works: + +```tsx + +``` + +--- + +## Height Animation (Accordion) + +Animating `height: auto` requires measurement. + +**Steps:** + +1. On open: measure `scrollHeight`, animate 0 → measured +2. After animation: set to `auto` (allows content resize) +3. On close: set explicit height, animate to 0 + +```css +.accordion-content { + overflow: hidden; + transition: height 200ms ease-out; +} + +.accordion-content[data-open] { + animation: slideDown 200ms ease-out; +} + +@keyframes slideDown { + from { + height: 0; + } + to { + height: var(--accordion-content-height); + } +} +``` + +**Double-rAF trick:** When closing, set explicit height before animating to 0. Browser needs a frame to register height before transitioning. + +> **Reference:** [Radix Collapsible](https://www.radix-ui.com/primitives/docs/components/collapsible) + +--- + +## Reduced Motion + +Respect user preferences: + +```css +@media (prefers-reduced-motion: reduce) { + .popup { + transition: none; + animation: none; + } +} +``` + +--- + +## See Also + +- [Styling](styling.md) — data attributes for state +- [Collection](collection.md) — exit animations in lists diff --git a/.claude/skills/component/references/anti-patterns.md b/.claude/skills/component/references/anti-patterns.md new file mode 100644 index 00000000..5878d13e --- /dev/null +++ b/.claude/skills/component/references/anti-patterns.md @@ -0,0 +1,288 @@ +# Component Anti-Patterns + +Common mistakes when building UI components. + +## Prop Explosion + +```tsx +// BAD: 30+ props on one component + + +// GOOD: Compound components + + + + Title + Close + + +``` + +**Why it fails:** Inflexible, hard to style parts independently, poor TypeScript experience. + +--- + +## Inline Styles for State + +```tsx +// BAD: Forces JS styling, fights theming + + +// GOOD: render prop + + +// GOOD: asChild for simple cases + +``` + +**Why it fails:** Complex generic types slow TypeScript language server and IDE. + +--- + +## Hidden Prop Flow with `asChild` + +```tsx +// BAD: Which props does Button receive? + + + + +// GOOD: Explicit prop handling + ( + + )} +/> +``` + +**Why it fails:** Debugging difficult, behavior non-obvious, silent breakage. + +--- + +## No State Access in Polymorphism + +```tsx +// BAD: asChild can't access internal state + + {/* How to show different icon when checked? */} + + +// GOOD: render function provides state + ( + + {state.checked ? : } + + )} +/> +``` + +**Why it fails:** Can't conditionally render based on component state. + +--- + +## Missing Controlled Support + +```tsx +// BAD: Only uncontrolled + + +// GOOD: Both modes + {/* uncontrolled */} + {/* controlled */} +``` + +**Why it fails:** Users can't integrate with external state management. + +--- + +## Context Without Scoping + +```tsx +// BAD: Nested components collide + + + + {' '} + {/* Reads parent's context! */} + + + + + +// GOOD: Scoped contexts — each Root creates new scope +``` + +**Why it fails:** Nested instances interfere with each other. + +--- + +## No Exit Animation Support + +```tsx +// BAD: Element unmounts immediately +{open && ...} + +// GOOD: CSS exit animation with data attributes + + +// GOOD: JS animation with keepMounted + + + {open && } + + +``` + +**Why it fails:** No opportunity to animate out before removal. + +--- + +## Ignoring SSR + +```tsx +// BAD: document undefined on server +function Portal({ children }) { + return createPortal(children, document.body); +} + +// GOOD: Wait for mount +function Portal({ children }) { + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + if (!mounted) return <>{children}; + return createPortal(children, document.body); +} +``` + +**Why it fails:** `document` doesn't exist during SSR. + +--- + +## Focus Not Managed + +```tsx +// BAD: Focus lost when dialog opens + + ... + + +// GOOD: Focus trapped and restored + + + + ... + + + +``` + +**Why it fails:** Keyboard users can navigate outside modal, focus lost on close. + +--- + +## Non-Standard Attribute Names + +```tsx +// BAD: Inconsistent naming + + +// GOOD: Consistent data attributes + +``` + +**Why it fails:** Inconsistent API, harder to style with CSS selectors. + +--- + +## Forgetting Ref Forwarding + +```tsx +// BAD: Ref can't reach DOM element +const Button = ({ children }) => ; + +// GOOD: Forward ref +const Button = forwardRef(({ children, ...props }, ref) => ( + +)); +``` + +**Why it fails:** Parent components can't access DOM for focus, measurement. + +--- + +## Testing Checklist + +- [ ] No prop explosion (use compound components) +- [ ] State styled via data attributes, not inline +- [ ] No CSS shipped in component package +- [ ] Polymorphism via `render` or `asChild`, not `as` +- [ ] Both controlled and uncontrolled modes +- [ ] Nested instances don't interfere +- [ ] Exit animations possible +- [ ] SSR-safe (no `document` at module scope) +- [ ] Focus managed for modals +- [ ] Refs forwarded to DOM elements + +--- + +## See Also + +- [Accessibility Anti-Patterns](../../aria/references/anti-patterns.md) — a11y mistakes +- [Polymorphism](polymorphism.md) — correct render customization diff --git a/.claude/skills/component/references/collection.md b/.claude/skills/component/references/collection.md new file mode 100644 index 00000000..e1844718 --- /dev/null +++ b/.claude/skills/component/references/collection.md @@ -0,0 +1,165 @@ +# Collection and Portal Patterns + +Patterns for lists, rendering outside DOM hierarchy, and virtualization. + +--- + +## Collection Components + +For lists, menus, selects, and other item-based components. + +### When to Use + +- Listbox, Menu, Select, Combobox +- Any component with selectable/navigable items +- Large lists requiring virtualization + +### Context Responsibilities + +| Concern | What to Track | +| --------- | -------------------------------------- | +| Items | Array of item data or refs | +| Selection | Selected keys (single or multi) | +| Disabled | Keys that can't be selected | +| Focus | Currently focused key for keyboard nav | + +### Render Prop Pattern + +Allow flexible item rendering while maintaining collection behavior: + +```tsx +{(item) => {item.name}} +``` + +**Why render props:** Component controls iteration, caller controls rendering. Enables virtualization, keyboard nav, ARIA without caller knowing internals. + +### Type-Ahead Search + +Allow users to jump to items by typing. + +**Logic flow:** + +1. Buffer printable keystrokes +2. On each keystroke, search items for prefix match (case-insensitive) +3. Focus first matching item +4. Clear buffer after ~500ms of no input + +**Implementation notes:** + +- Only handle single printable characters (`event.key.length === 1`) +- Concatenate to buffer, don't replace +- Use timeout to reset buffer + +> **Reference:** [React Aria useTypeAhead](https://react-spectrum.adobe.com/react-aria/useListBox.html) + +--- + +## Portal Pattern + +Render content outside its DOM parent for proper layering. + +### When to Use + +- Dialogs, modals, sheets +- Dropdown menus, popovers, tooltips +- Any overlay that needs to escape parent CSS context + +### Why Portals + +| Problem | Portal Solution | +| ----------------------------- | --------------------------- | +| Parent has `overflow: hidden` | Render at body, no clipping | +| Parent has low z-index | Control stacking at root | +| Parent has `transform` | Escape stacking context | + +### Implementation Considerations + +**SSR safety:** + +- Don't access `document.body` at module scope +- Render inline on server, portal after hydration +- Check `typeof document !== 'undefined'` + +**Context preservation:** + +- Framework context must flow through portal +- Provider wraps portal _source_, not target +- Most frameworks handle this automatically + +**Custom container:** + +```tsx + + + +``` + +### Z-Index Management + +**Approach:** Increment z-index for each nested portal layer. + +- Track nesting depth via context +- Each portal reads parent depth, adds 1 +- Apply `z-index: depth * 100` (or similar scale) + +> **Reference:** [Radix Portal](https://www.radix-ui.com/primitives/docs/utilities/portal) + +--- + +## Virtualization + +Render only visible items for large lists. + +### When to Use + +- Lists > 100 items +- Complex item rendering +- Mobile/low-power devices + +### Core Concept + +**Calculate visible window:** + +``` +startIndex = floor(scrollTop / itemHeight) - overscan +endIndex = ceil((scrollTop + containerHeight) / itemHeight) + overscan +``` + +**Render only `items.slice(startIndex, endIndex)`** + +### Implementation Requirements + +| Requirement | Purpose | +| ----------------------------- | ----------------------------- | +| Fixed or measured item height | Calculate positions | +| Container with fixed height | Define viewport | +| Scroll listener | Update visible range | +| Absolute positioning | Place items at correct offset | + +### Positioning + +```css +.list-container { + height: calc(var(--item-count) * var(--item-height)); + position: relative; +} + +.list-item { + position: absolute; + top: calc(var(--index) * var(--item-height)); + height: var(--item-height); +} +``` + +### Variable Height Items + +For variable heights, measure items and cache heights. More complex — consider using a library. + +> **Reference:** [TanStack Virtual](https://tanstack.com/virtual/latest) + +--- + +## See Also + +- [Focus Management](../../aria/references/focus.md) — keyboard navigation in collections +- [Animation](animation.md) — exit animations for items diff --git a/.claude/skills/component/references/lit.md b/.claude/skills/component/references/lit.md new file mode 100644 index 00000000..03f1f4d4 --- /dev/null +++ b/.claude/skills/component/references/lit.md @@ -0,0 +1,161 @@ +# Lit Component Patterns + +Lit-specific patterns for Video.js web components. + +## Package Requirements + +```ts +import { ContextConsumer, ContextProvider } from '@lit/context'; +import { ReactiveElement } from '@lit/reactive-element'; +``` + +- Use `@lit/reactive-element` and `@lit/context` +- **Never** import from `lit` package + +## Platform Bindings + +- Package-specific bindings live in `{package}/lit/` (e.g., `@videojs/store/lit`) +- Main web component library: `@videojs/html` + +--- + +## Component Types + +### Skins (Container Elements) + +- Extend `ReactiveElement` +- May use shadow DOM for style encapsulation +- Provide store to descendants via context +- Register with `customElements.define()` + +### Primitives (Control Elements) + +- Extend `ReactiveElement` +- **No shadow DOM** — style via light DOM +- **No slots** +- **No render()** — manipulate host element directly +- Controllers provide all behavior + +--- + +## Controllers + +Controllers are the primary composability mechanism. Use controllers, not hooks or behavior mixins. + +All store-related controllers live in `@videojs/store/lit`. See that package for available controllers and their APIs. + +### Controller Pattern + +```ts +class MyElement extends ReactiveElement { + #paused = new SelectorController(this, context, (s) => s.paused); + #play = new RequestController(this, context, 'play'); +} +``` + +### Architecture + +- Accept `StoreSource` — either direct store OR context +- `StoreAccessor` resolves source internally +- Register via `host.addController(this)` +- Lifecycle: `hostConnected()`, `hostDisconnected()` + +--- + +## StoreAccessor + +Internal utility that resolves a store from either a direct instance or context. This enables controllers to accept a `StoreSource` parameter — users can pass either: + +1. **Direct store** — For testing or when store is already available +2. **Context** — For production use where store is provided by an ancestor + +```ts +// Direct store — value available immediately +const selector = new SelectorController(this, store, (s) => s.paused); + +// Context — value available after context resolves +const selector = new SelectorController(this, storeContext, (s) => s.paused); +``` + +Controllers handle both cases transparently. The `StoreAccessor`: + +- Returns store immediately if passed directly +- Waits for context resolution if passed a context +- Fires `onAvailable` callback when store becomes available (for subscription setup) + +--- + +## Host Type Pattern + +Always export an explicit host type for controllers and mixins: + +```ts +export type SelectorControllerHost = ReactiveControllerHost & HTMLElement; +export type ProviderMixinHost = ReactiveElement & EventTarget; +``` + +- Never use bare `ReactiveControllerHost` +- Allows future extension without breaking consumers +- Self-documents required host capabilities + +--- + +## Mixins + +Mixins are for **store provision only**, not behavior. Behavior goes in controllers. + +### createStoreProviderMixin + +Creates a mixin that provides a store via context: + +```ts +const { StoreProviderMixin } = createStore({ slices: [playbackSlice] }); + +class MyPlayer extends StoreProviderMixin(ReactiveElement) { + // Store provided to all descendants +} +``` + +- Creates store on first access (lazy) +- Provides via Lit Context Protocol +- Destroys store on disconnect (if owned) + +--- + +## Context Protocol + +- `ContextProvider` — skin/root provides store to descendants +- `ContextConsumer` — controllers consume store via context +- Context passed as `StoreSource` to controller constructors + +```ts +// Provider (in skin) +#provider = new ContextProvider(this, { context, initialValue: this.store }); + +// Consumer (in controller) +#consumer = new ContextConsumer(host, { context, subscribe: false }); +``` + +--- + +## Element Registration + +Use the standard custom elements registry: + +```ts +customElements.define('vjs-play-button', PlayButtonElement); +``` + +For elements that need a store mixin: + +```ts +customElements.define('vjs-player', StoreMixin(PlayerElement)); +``` + +--- + +## See Also + +- `@videojs/store/lit` — Controller and mixin implementations +- `@videojs/html` — Web component library +- [react.md](react.md) — React-specific patterns (parallel reference) diff --git a/.claude/skills/component/references/polymorphism.md b/.claude/skills/component/references/polymorphism.md new file mode 100644 index 00000000..00c4908e --- /dev/null +++ b/.claude/skills/component/references/polymorphism.md @@ -0,0 +1,134 @@ +# Polymorphism Patterns + +Patterns for rendering component behavior on custom elements. + +## Overview + +Polymorphism allows users to customize which element a component renders as. Two main approaches: + +| Pattern | Library | Approach | +| ------------- | ------- | ---------------------------- | +| `render` prop | Base UI | Explicit function or element | +| `asChild` | Radix | Clone child element | + +**Recommendation: Prefer `render` prop** for explicit state access and clearer prop flow. + +--- + +## `render` Pattern (Preferred) + +### Element Form — Simple Cases + +```tsx +// Renders MyButton with Dialog.Trigger behavior +}>Open dialog +``` + +### Function Form — State Access + +```tsx +// Access internal state for conditional rendering + {state.checked ? : }} +/> +``` + +--- + +## `asChild` Pattern + +### Usage + +```tsx + + Open dialog + +``` + +--- + +## Why `render` > `asChild` + +| Concern | `render` | `asChild` | +| ------------------ | ------------------------------------- | ----------------------------------------- | +| **Prop flow** | Explicit — you spread props visibly | Hidden — `cloneElement` merges implicitly | +| **State access** | Function form exposes component state | No state access | +| **TypeScript** | Predictable inference | Can slow IDE autocomplete | +| **Debugging** | Traceable prop flow | Magic makes tracing difficult | +| **React guidance** | Aligns with React docs | Uses `cloneElement` (React warns against) | + +### The Problem with `asChild` + +React's documentation warns that `cloneElement` "is uncommon and can lead to fragile code" and makes "it hard to tell how the data flows through your app." + +`asChild` hides complexity rather than eliminating it: + +```tsx +// asChild — implicit prop injection + + {/* Which props does Button receive? */} + + +// render — explicit prop handling +}> + Open + +``` + +With `asChild`, the child component must: + +1. Spread all props it receives +2. Forward refs correctly +3. Handle event handler merging + +Nothing enforces these requirements at compile time — breakage is silent. + +--- + +## Prop Merging + +Both patterns need to merge props carefully: + +| Type | Behavior | +| -------------- | ------------------- | +| Event handlers | Chain — both called | +| `className` | Concatenate | +| `style` | Shallow merge | +| Other props | Consumer overrides | + +> **Reference:** [Base UI mergeProps](https://github.com/mui/base-ui/blob/master/packages/react/src/merge-props/mergeProps.ts) + +--- + +## Avoiding the `as` Prop + +The `as` prop (polymorphic components) has TypeScript performance issues: + +```tsx +// BAD: slow TypeScript, poor autocomplete + + +// GOOD: use render prop instead + +``` + +The `as` prop requires complex generic types that slow down the TypeScript language server. + +--- + +## When to Use Each + +| Scenario | Pattern | +| --------------------- | ---------------------------- | +| Simple element swap | `asChild` acceptable | +| State-based rendering | `render` (function form) | +| Complex prop merging | `render` (explicit control) | +| Debugging issues | `render` (visible prop flow) | +| Maximum type safety | `render` | + +--- + +## See Also + +- [Progressive Disclosure](../../api-design/principles/progressive-disclosure.md) — layered complexity +- [Anti-Patterns](anti-patterns.md) — polymorphism pitfalls diff --git a/.claude/skills/component/references/props.md b/.claude/skills/component/references/props.md new file mode 100644 index 00000000..3b17abc6 --- /dev/null +++ b/.claude/skills/component/references/props.md @@ -0,0 +1,235 @@ +# API Design Conventions + +Prop naming, defaults, and documentation patterns for consistent component APIs. + +**Reference implementations:** [Base UI](https://base-ui.com/), [Radix](https://www.radix-ui.com/), [Ark UI](https://ark-ui.com/) + +--- + +## Prop Naming + +### Boolean Props + +Use positive adjectives, avoid `is`/`has` prefixes: + +| ✅ Good | ❌ Avoid | +|---------|----------| +| `disabled` | `isDisabled` | +| `required` | `isRequired` | +| `open` | `isOpen` | +| `loading` | `isLoading` | + +--- + +### State Props + +| State | Uncontrolled | Controlled | Handler | +|-------|--------------|------------|---------| +| Open | `defaultOpen` | `open` | `onOpenChange` | +| Value | `defaultValue` | `value` | `onValueChange` | +| Checked | `defaultChecked` | `checked` | `onCheckedChange` | +| Selected | `defaultSelected` | `selected` | `onSelectedChange` | + +--- + +### Event Handlers + +Pattern: `on` + Noun + Verb + +| ✅ Good | ❌ Avoid | +|---------|----------| +| `onOpenChange` | `handleOpen`, `setOpen` | +| `onValueChange` | `onChange` (too generic) | +| `onSelect` | `onItemSelected` | +| `onDismiss` | `onClose` (ambiguous) | + +--- + +## Standard Props by Category + +### Interaction + +```typescript +disabled?: boolean; +required?: boolean; +readOnly?: boolean; +autoFocus?: boolean; +``` + +### Collections + +```typescript +multiple?: boolean; // Allow multiple selection +loopFocus?: boolean; // Arrow keys loop at ends +orientation?: 'horizontal' | 'vertical'; +typeahead?: boolean; // A-Z navigation +``` + +### Popups + +```typescript +modal?: boolean | 'trap-focus'; +closeOnEscape?: boolean; +closeOnOutsideClick?: boolean; +keepMounted?: boolean; // Keep in DOM when closed +``` + +### Positioning + +```typescript +side?: 'top' | 'bottom' | 'left' | 'right'; +align?: 'start' | 'center' | 'end'; +sideOffset?: number; +alignOffset?: number; +collision?: 'flip' | 'shift' | 'none'; +``` + +--- + +## Change Event Details + +**What:** Rich context passed to change handlers. + +**Why:** Enables conditional logic — prevent changes, track analytics, debug. + +```typescript +interface ChangeDetails { + reason: string; // 'click' | 'keyboard' | 'blur' | 'escape' | 'outside-click' + event?: Event; // Original DOM event + cancel(): void; // Prevent internal state change +} +``` + +**Usage:** `onOpenChange={(open, details) => { if (details.reason === 'outside-click') details.cancel(); }}` + +--- + +## Imperative Actions + +**What:** Expose actions via `actionsRef` for programmatic control. + +**Common actions:** +- `open()`, `close()`, `toggle()` — Popups +- `focus()` — Focus management +- `scrollToIndex(i)` — Virtualized lists + +**Pattern:** `` → `ref.current.open()` + +--- + +## Render Delegation + +### The `render` Prop + +**What:** Replace default element while preserving behavior. + +**Forms:** +- Element: `render={}` — Props merged onto element +- Function: `render={(props, state) => ...}` — Full control + +### `className` / `style` as Function + +**What:** State-aware styling without external state. + +**Pattern:** `className={(state) => state.checked ? 'on' : 'off'}` + +--- + +## Defaults + +### Sensible Defaults (80% case) + +| Prop | Default | Rationale | +|------|---------|-----------| +| `side` | `'bottom'` | Most common popup position | +| `align` | `'center'` | Visually balanced | +| `sideOffset` | `8` | Standard spacing | +| `closeOnEscape` | `true` | Expected behavior | +| `loopFocus` | `true` | Better keyboard UX | + +### Require Explicit Opt-in + +| Prop | Default | Rationale | +|------|---------|-----------| +| `autoFocus` | `false` | Can be disorienting | +| `modal` | `false` | Has side effects (scroll lock) | +| `keepMounted` | `false` | Performance | + +--- + +## Escape Hatches + +| Need | Solution | +|------|----------| +| DOM access | Forward refs to root element | +| Custom attributes | Spread `...props` | +| Custom portal target | `container` prop on Portal | +| Override handlers | Spread after internal handlers | + +--- + +## Documentation Pattern + +### Props Table + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `open` | `boolean` | — | Controlled open state | +| `defaultOpen` | `boolean` | `false` | Initial open state | +| `onOpenChange` | `(open, details) => void` | — | Called on state change | + +### Data Attributes Table + +| Attribute | When Present | +|-----------|--------------| +| `data-open` | Component is open | +| `data-disabled` | Component is disabled | + +### Anatomy Section + +Show component structure with all parts. + +### Examples Section + +Basic usage, controlled, with custom trigger, etc. + +--- + +## Versioning + +| Change | Breaking? | +|--------|-----------| +| Add optional prop | No | +| Change default value | **Yes** | +| Remove prop | **Yes** | +| Add required prop | **Yes** | + +### Deprecation Pattern + +1. Add new prop alongside old +2. Log warning when old prop used +3. Remove old prop in next major + +--- + +## Checklist for New Components + +- [ ] Boolean props use positive adjectives +- [ ] State props follow `value`/`defaultValue`/`onValueChange` +- [ ] Change handlers receive `details` with `reason` and `cancel()` +- [ ] Ref forwarded to root DOM element +- [ ] `render` prop for element polymorphism +- [ ] `className` accepts function +- [ ] State exposed via `data-*` attributes +- [ ] Additional HTML attributes spread +- [ ] Types exported (`Component.Props`, `Component.State`) + +--- + +## References + +| Library | API Style | +|---------|-----------| +| [Base UI API](https://base-ui.com/react/components/dialog) | Canonical reference | +| [Radix API](https://www.radix-ui.com/primitives/docs/components/dialog) | Alternative conventions | +| [Ark UI API](https://ark-ui.com/react/docs/components/dialog) | Cross-framework | diff --git a/.claude/skills/component/references/react.md b/.claude/skills/component/references/react.md new file mode 100644 index 00000000..79a6769a --- /dev/null +++ b/.claude/skills/component/references/react.md @@ -0,0 +1,316 @@ +# React Component Patterns + +React-specific implementation details for compound components. For framework-agnostic patterns, see [SKILL.md](../SKILL.md). + +--- + +## Context Architecture + +**What:** Compound components share state via React Context without prop drilling. + +**Why:** + +- Implicit state sharing between parts (Root → Trigger → Content) +- Clean consumer API — no manual wiring +- Nested contexts for multi-level components (Accordion → Item → Trigger) + +**Pattern:** + +- Create context with `undefined` default +- Consumer hook throws if used outside provider +- Root provides state, children consume + +**Ref:** [Base UI Dialog Source](https://github.com/mui/base-ui/tree/master/packages/react/src/dialog) + +--- + +## Essential Hooks + +### `useControlledState` + +**What:** Unifies controlled/uncontrolled state patterns. + +**Why:** Single implementation handles both modes with consistent API. + +**Behavior:** + +- If `value` provided → controlled (external state) +- If only `defaultValue` → uncontrolled (internal state) +- Calls `onChange` in both modes + +**Ref:** [Radix useControllableState](https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx) + +--- + +### `useId` + +**What:** Generate unique IDs for ARIA relationships. + +**Why:** Labels, descriptions, and controls need matching IDs for accessibility. + +**Note:** Built into React 18+. For earlier versions, use `@reach/auto-id`. + +--- + +### `useFocusTrap` + +**What:** Trap focus within a container (modal dialogs). + +**Why:** Modal accessibility requires focus stays within dialog until closed. + +**Behavior:** + +- Tab at last element → first element +- Shift+Tab at first → last element +- Returns focus to trigger on close + +**Ref:** [focus-trap](https://github.com/focus-trap/focus-trap) library + +--- + +### `useRovingFocus` + +**What:** Arrow key navigation within groups with single Tab stop. + +**Why:** Standard keyboard pattern for menus, tablists, toolbars. + +**Behavior:** + +- Only focused item has `tabIndex={0}` +- Others have `tabIndex={-1}` +- Arrows move focus, optionally loops + +**Ref:** [Radix RovingFocus](https://github.com/radix-ui/primitives/tree/main/packages/react/roving-focus) + +--- + +### `useFloating` + +**What:** Position floating elements relative to anchors. + +**Why:** Popups need collision detection, scroll tracking, arrow positioning. + +**Use:** Wrap `@floating-ui/react` with component-specific defaults. + +**Ref:** [Floating UI React](https://floating-ui.com/docs/react) + +--- + +## Ref Patterns + +### Forward Refs on All Parts + +**What:** Every compound component part forwards refs to its DOM element. + +**Why:** Consumers need DOM access for focus management, measurements, animations. + +**Pattern:** `forwardRef((props, ref) => ...)` + +--- + +### `useImperativeHandle` for Actions + +**What:** Expose component actions through ref. + +**Why:** Programmatic control — `dialogRef.current.open()` + +**Pattern:** + +```tsx +interface Actions { + open(): void; + close(): void; +} +useImperativeHandle(actionsRef, () => ({ open, close })); +``` + +**Ref:** [React useImperativeHandle](https://react.dev/reference/react/useImperativeHandle) + +--- + +### `useMergeRefs` / `composeRefs` + +**What:** Combine multiple refs pointing to same element. + +**Why:** Compound components often need both: + +- Internal ref (for positioning, focus management, measurements) +- Forwarded ref (for consumer access) +- Floating UI ref (for anchor positioning) + +**Use cases:** + +- Trigger needs internal ref + forwarded ref + floating anchor ref +- Popup needs internal ref + forwarded ref + floating ref +- Any part using `useFloating` alongside `forwardRef` + +**Ref:** [Floating UI useMergeRefs](https://floating-ui.com/docs/react#usemergerefs), [Radix composeRefs](https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/composeRefs.tsx) + +--- + +## Render Prop Implementation + +**What:** The `render` prop replaces default element with custom element or component. + +**Key pieces:** + +- Accept `ReactElement` or `(props, state) => ReactElement` +- Use `cloneElement` for element form +- Use `mergeProps` to combine internal + external props + +**`mergeProps` behavior:** + +- Event handlers — chain (both called) +- className — concatenate +- style — shallow merge +- Other props — override + +**Ref:** [Base UI useRender](https://github.com/mui/base-ui/blob/master/packages/react/src/use-render/useRender.ts) + +--- + +## Render Delegation + +**What:** Replace default rendered element while preserving component behavior. + +**Why:** + +- Element polymorphism (button → link) +- Integrate with existing component libraries +- Conditional rendering based on internal state + +### Approaches + +| Pattern | Library | Usage | +| -------------- | ------- | --------------------------------------------------------------- | +| `render` prop | Base UI | `render={}` or `render={(props, state) => ...}` | +| `asChild` prop | Radix | `Link` | +| `as` prop | Various | `; + +// GOOD +const Button = forwardRef(({ children, ...props }, ref) => ( + +)); +``` + +### Polymorphism + +- [ ] Uses `render` prop or `asChild`, not `as` prop +- [ ] `render` function receives props and state +- [ ] State accessible for conditional rendering + +**Detection:** Component has `as` prop + +```tsx +// BAD: TypeScript performance issues + +``` + +--- + +## Data Attributes & Styling + +### State via Data Attributes + +- [ ] State exposed via `data-*` attributes +- [ ] Enables CSS-only styling without JS + +**Detection:** Inline styles for state, no data attributes + +| Attribute | When Present | +| ------------------ | --------------------------- | +| `data-open` | Component is open | +| `data-closed` | Component is closed | +| `data-checked` | Toggle is checked | +| `data-disabled` | Component is disabled | +| `data-highlighted` | Item has focus within group | +| `data-side` | Popup position side | +| `data-align` | Popup alignment | + +```tsx +// BAD + +\`; +} +} + +### See Also + +- [Related Controller](/api/controllers/related) +- [Lit Integration Guide](/guides/lit) +``` + +--- + +## Checklist + +When writing API reference: + +- [ ] Brief description at top +- [ ] Import statement shown +- [ ] Basic example immediately after +- [ ] All parameters documented +- [ ] All options with types and defaults +- [ ] Return value documented +- [ ] Errors/throws documented if applicable +- [ ] Multiple examples (basic → advanced) +- [ ] See Also section with related items +- [ ] Types linked or inline diff --git a/.claude/skills/docs/templates/component-page.md b/.claude/skills/docs/templates/component-page.md new file mode 100644 index 00000000..7ded32f5 --- /dev/null +++ b/.claude/skills/docs/templates/component-page.md @@ -0,0 +1,419 @@ +# Component Page Template + +Use this template for documenting UI components. + +--- + +## Template + +```markdown +## ComponentName + +Brief description of what the component does. + + + + + + + + + + +### Features + +- Feature one +- Feature two +- Feature three +- Keyboard accessible + +### Installation + +npm install @videojs/dom + +### Anatomy + +Import and assemble the parts: + +import { ComponentName } from '@videojs/dom'; + + + + + + + + +### API Reference + +#### Root + +Container element. Renders a `
`. + +##### Props + +| Prop | Type | Default | Description | +| --------------- | ------------------------- | ------- | ------------------- | +| `value` | `number` | — | Controlled value | +| `defaultValue` | `number` | `0` | Initial value | +| `disabled` | `boolean` | `false` | Disable interaction | +| `onValueChange` | `(value: number) => void` | — | Called on change | + +##### Data Attributes + +| Attribute | Description | +| --------------- | --------------------- | +| `data-disabled` | Present when disabled | +| `data-state` | `'idle' \| 'active'` | + +##### CSS Variables + +| Variable | Default | Description | +| ------------------ | ------- | -------------- | +| `--component-size` | `100%` | Container size | + +#### PartA + +Description. Renders a `
`. + +##### Props + +| Prop | Type | Default | Description | +| ----------- | -------- | ------- | ----------- | +| `className` | `string` | — | CSS class | + +##### Data Attributes + +| Attribute | Description | +| ------------ | ------------- | +| `data-state` | Current state | + +#### PartB + +Description. Renders a `
`. + +[Continue for each part...] + +### Examples + +#### Basic + + + + + +#### Controlled + +function ControlledExample() { +const [value, setValue] = useState(50); + +return ( + + + +); +} + +#### Disabled + + + + + +#### Custom Styling + + + + + +.custom-component { +--component-size: 200px; +} + +.custom-component[data-state='active'] { +border-color: blue; +} + +#### With Other Components + + + + + + + +### Accessibility + +Follows [WAI-ARIA Pattern Name](https://www.w3.org/WAI/ARIA/apg/patterns/...). + +#### Keyboard Interactions + +| Key | Action | +| ----------- | ---------- | +| `Enter` | Activate | +| `Space` | Activate | +| `ArrowUp` | Increase | +| `ArrowDown` | Decrease | +| `Home` | Minimum | +| `End` | Maximum | +| `Tab` | Move focus | + +#### ARIA Attributes + +| Attribute | Value | +| --------------- | --------------- | +| `role` | `slider` | +| `aria-valuenow` | Current value | +| `aria-valuemin` | Minimum value | +| `aria-valuemax` | Maximum value | +| `aria-label` | Accessible name | + +### See Also + +- [RelatedComponent](/components/related) +- [Styling Guide](/handbook/styling) +- [Accessibility Guide](/handbook/accessibility) +``` + +--- + +## Anatomy Diagram + +Show component structure visually: + +``` +┌─ Root ─────────────────────────────────┐ +│ │ +│ ┌─ Track ──────────────────────────┐ │ +│ │ │ │ +│ │ ┌─ Fill ─────────┐ │ │ +│ │ │████████████████│ │ │ +│ │ └────────────────┘ │ │ +│ │ │ │ +│ └──────────────────────────────────┘ │ +│ ○ Thumb │ +│ │ +└────────────────────────────────────────┘ +``` + +--- + +## Framework Variations + +Include tabs for each framework: + +````markdown +### Framework Examples + + + +```tsx +import { Slider } from '@videojs/react'; + +function VolumeSlider() { +const [volume, setVolume] = useState(1); + +return ( + + + + + + +); +} + +```` + + +```vue + + + +```` + + + +```svelte + + + + + + + + + +``` + + +``` +```` + +--- + +## Styling Section + +Always include styling examples: + +```markdown +### Styling + +#### With CSS + +.slider[data-dragging] { +cursor: grabbing; +} + +.slider-thumb:focus-visible { +outline: 2px solid blue; +} + +#### With Tailwind + + + + + + + + +#### CSS Variables Reference + +| Variable | Default | Description | +| --------------------- | -------------- | ---------------- | +| `--slider-track-bg` | `#e5e5e5` | Track background | +| `--slider-fill-bg` | `currentColor` | Fill color | +| `--slider-thumb-size` | `16px` | Thumb diameter | +``` + +--- + +## Web Component Template (Lit) + +For `@videojs/html` components built with Lit: + +```markdown +## element-name + +Brief description. + +### Registration + +import { ElementName } from '@videojs/html/path'; + +// Default registration +ElementName.define(); + +// Custom tag name +ElementName.define('custom-name'); + +// With custom mixin +import { createStore } from '@videojs/store/lit'; +import { extendConfig } from '@videojs/html/skins/frosted'; + +const { StoreMixin } = createStore( +extendConfig({ slices: [customSlice] }) +); + +ElementName.define('custom-name', StoreMixin); + +### HTML Usage + + + + + +### Attributes + +| Attribute | Type | Default | Description | +| --------- | -------- | ------- | ------------ | +| `src` | `string` | — | Media source | + +### Slots + +| Slot | Description | +| --------- | -------------------------------------- | +| (default) | Media element (`