diff --git a/plugins/dev3000/.agents/skills/d3k/SKILL.md b/plugins/dev3000/.agents/skills/d3k/SKILL.md index 1439e9ad..fffa5d6c 100644 --- a/plugins/dev3000/.agents/skills/d3k/SKILL.md +++ b/plugins/dev3000/.agents/skills/d3k/SKILL.md @@ -19,15 +19,29 @@ When this skill triggers, operate d3k. Do not merely tell the user how to run it Run from the project root. -1. Check for an existing project runtime: +1. Check canonical Portless readiness before starting any app process: + +```bash +d3k portless status --json +``` + +If `"setupRequired": true`, run: + +```bash +d3k portless setup +``` + +On macOS this opens the system administrator authorization dialog. Wait for the user to approve it, then rerun `d3k portless status --json`. Do not start d3k until `"canonical": true`, `"serviceInstalled": true`, and `"setupRequired": false`. If authorization is declined or unavailable, stop and explain the blocker. Never silently substitute direct localhost. + +2. Check for an existing project runtime: ```bash d3k status --json ``` -If it reports `"running": true`, reuse it. Do not start a second dev server or browser. +Reuse it only if it reports `"running": true`, `"ready": true`, `"routing": "portless"`, `"browserConnected": true`, and a genuinely port-free `https://...localhost` `appUrl`. If an active session reports `"routing": "direct"`, stop that retained d3k session and restart after Portless is ready. Do not start a second dev server or browser. -2. If d3k is not installed, install it: +3. If d3k is not installed, install it: ```bash bun install -g dev3000 @@ -35,10 +49,10 @@ bun install -g dev3000 Use `npm install -g dev3000` only when Bun is unavailable. -3. Start d3k with the agent's shell/process tool as a retained background or yielded session: +4. Start d3k with the agent's shell/process tool as a retained background or yielded session (non-TUI by default): ```bash -d3k --no-agent --no-tui -t +d3k -t ``` Do not wait for this long-running command to exit. Keep its process/session handle so you can monitor or stop it later. Prefer the execution tool's background/session support over shelling with `&`. @@ -46,18 +60,18 @@ Do not wait for this long-running command to exit. Keep its process/session hand If the target URL is already known, pass it so the managed browser opens there: ```bash -d3k --no-agent --no-tui -t --app-url "" +d3k -t --app-url "" ``` Let d3k auto-detect the package manager, dev command, and port. Add `--command`, `--script`, or `--port` only when detection is wrong or the user specified them. -4. Poll until the runtime is ready: +5. Poll until the runtime is ready: ```bash d3k status --json ``` -A successful status response is the readiness boundary. Prefer the reported Portless `appUrl`; the underlying app port may change between runs. If startup fails, inspect the retained process output and `d3k logs --type server`; do not launch a separate dev server. +A successful status response is the readiness boundary only when it reports `"ready": true`, `"routing": "portless"`, `"browserConnected": true`, and a port-free `https://...localhost` `appUrl`. The underlying app port may change between runs. If startup fails, inspect the retained process output and `d3k logs --type server`; do not launch a separate dev server. ## User-Driven Testing @@ -125,4 +139,4 @@ Use `--headless` only for CI or when explicitly requested. Use `--servers-only` - Keep d3k alive across edits and retests. - Preserve the project-stable Chrome profile unless the user explicitly asks for a fresh profile. - Leave the runtime running when handing a headed browser to the user; stop it only when asked or when the task requires a clean restart. -- Portless routing is the default. Use `--no-portless` or `PORTLESS=0` only when direct localhost routing is explicitly required. +- Canonical Portless HTTPS routing is required by default. Never add `--no-portless` or set `PORTLESS=0` unless the user explicitly requests direct localhost routing. diff --git a/plugins/dev3000/agent/skills/d3k/SKILL.md b/plugins/dev3000/agent/skills/d3k/SKILL.md index 511c3ec0..43118331 100644 --- a/plugins/dev3000/agent/skills/d3k/SKILL.md +++ b/plugins/dev3000/agent/skills/d3k/SKILL.md @@ -17,15 +17,29 @@ When this skill triggers, operate d3k. Do not merely tell the user how to run it Run from the project root. -1. Check for an existing project runtime: +1. Check canonical Portless readiness before starting any app process: + +```bash +d3k portless status --json +``` + +If `"setupRequired": true`, run: + +```bash +d3k portless setup +``` + +On macOS this opens the system administrator authorization dialog. Wait for the user to approve it, then rerun `d3k portless status --json`. Do not start d3k until `"canonical": true`, `"serviceInstalled": true`, and `"setupRequired": false`. If authorization is declined or unavailable, stop and explain the blocker. Never silently substitute direct localhost. + +2. Check for an existing project runtime: ```bash d3k status --json ``` -If it reports `"running": true`, reuse it. Do not start a second dev server or browser. +Reuse it only if it reports `"running": true`, `"ready": true`, `"routing": "portless"`, `"browserConnected": true`, and a genuinely port-free `https://...localhost` `appUrl`. If an active session reports `"routing": "direct"`, stop that retained d3k session and restart after Portless is ready. Do not start a second dev server or browser. -2. If d3k is not installed, install it: +3. If d3k is not installed, install it: ```bash bun install -g dev3000 @@ -33,10 +47,10 @@ bun install -g dev3000 Use `npm install -g dev3000` only when Bun is unavailable. -3. Start d3k with the agent's shell/process tool as a retained background or yielded session: +4. Start d3k with the agent's shell/process tool as a retained background or yielded session (non-TUI by default): ```bash -d3k --no-agent --no-tui -t +d3k -t ``` Do not wait for this long-running command to exit. Keep its process/session handle so you can monitor or stop it later. Prefer the execution tool's background/session support over shelling with `&`. @@ -44,18 +58,18 @@ Do not wait for this long-running command to exit. Keep its process/session hand If the target URL is already known, pass it so the managed browser opens there: ```bash -d3k --no-agent --no-tui -t --app-url "" +d3k -t --app-url "" ``` Let d3k auto-detect the package manager, dev command, and port. Add `--command`, `--script`, or `--port` only when detection is wrong or the user specified them. -4. Poll until the runtime is ready: +5. Poll until the runtime is ready: ```bash d3k status --json ``` -A successful status response is the readiness boundary. Prefer the reported Portless `appUrl`; the underlying app port may change between runs. If startup fails, inspect the retained process output and `d3k logs --type server`; do not launch a separate dev server. +A successful status response is the readiness boundary only when it reports `"ready": true`, `"routing": "portless"`, `"browserConnected": true`, and a port-free `https://...localhost` `appUrl`. The underlying app port may change between runs. If startup fails, inspect the retained process output and `d3k logs --type server`; do not launch a separate dev server. ## User-Driven Testing @@ -123,4 +137,4 @@ Use `--headless` only for CI or when explicitly requested. Use `--servers-only` - Keep d3k alive across edits and retests. - Preserve the project-stable Chrome profile unless the user explicitly asks for a fresh profile. - Leave the runtime running when handing a headed browser to the user; stop it only when asked or when the task requires a clean restart. -- Portless routing is the default. Use `--no-portless` or `PORTLESS=0` only when direct localhost routing is explicitly required. +- Canonical Portless HTTPS routing is required by default. Never add `--no-portless` or set `PORTLESS=0` unless the user explicitly requests direct localhost routing. diff --git a/plugins/dev3000/skills-lock.json b/plugins/dev3000/skills-lock.json index f7d3f718..fda32d3b 100644 --- a/plugins/dev3000/skills-lock.json +++ b/plugins/dev3000/skills-lock.json @@ -5,7 +5,7 @@ "source": "vercel-labs/dev3000", "sourceType": "github", "skillPath": "skills/d3k/SKILL.md", - "computedHash": "6462ff3b28aaec3ba02f8419e4a6faace3ccb69165bf0da8f55a7da93393e2d3" + "computedHash": "b8604d8dcb3136f9d04a3a0ec02962380d0260c7b7ffc321ca90e8d5569e2b8d" } } } diff --git a/plugins/emulate/.agents/skills/emulate/SKILL.md b/plugins/emulate/.agents/skills/emulate/SKILL.md index 81a5e999..79053831 100644 --- a/plugins/emulate/.agents/skills/emulate/SKILL.md +++ b/plugins/emulate/.agents/skills/emulate/SKILL.md @@ -386,7 +386,7 @@ const kvAdapter: PersistenceAdapter = { } ``` -State is loaded on cold start and saved after every mutating request (POST, PUT, PATCH, DELETE). Saves are serialized to prevent race conditions. +State is loaded on cold start and saved after every mutating request (POST, PUT, PATCH, DELETE). Saves are serialized to prevent race conditions. Generated GitHub App identities require `initialize` to atomically create the initial value or return the value another instance created first. ## Architecture diff --git a/plugins/emulate/.agents/skills/github/SKILL.md b/plugins/emulate/.agents/skills/github/SKILL.md index 6a9322a6..9df4dcf0 100644 --- a/plugins/emulate/.agents/skills/github/SKILL.md +++ b/plugins/emulate/.agents/skills/github/SKILL.md @@ -53,6 +53,8 @@ const privateKey = github.generatedSecrets.find( The key remains stable across `github.reset()`. Explicit keys are not included in `generatedSecrets`. +The Next.js and Nuxt adapters also generate omitted keys. Retain the returned handler and call its server-only `generatedSecrets()` method. With persistence configured, the identity survives cold starts. Keep snapshots private because they contain the signing key, and require custom persistence backends to implement atomic `initialize()` semantics. + For the CLI, omit `private_key` only when requesting a private delivery file: ```bash diff --git a/plugins/emulate/.agents/skills/next/SKILL.md b/plugins/emulate/.agents/skills/next/SKILL.md index ce6ac745..b5e0b7ef 100644 --- a/plugins/emulate/.agents/skills/next/SKILL.md +++ b/plugins/emulate/.agents/skills/next/SKILL.md @@ -123,6 +123,8 @@ import { filePersistence } from '@emulators/core' persistence: filePersistence('.emulate/state.json'), ``` +GitHub App seeds may omit `private_key`. Retain the handler and call server-only `generatedSecrets()`; explicit keys are excluded. Keep persisted snapshots private and implement `initialize` atomically. + ### How Persistence Works - **Cold start**: The adapter loads state from the persistence adapter. If found, it restores the full Store and token map (skipping seed). If not found, it seeds from config and saves the initial state. @@ -140,7 +142,7 @@ persistence: filePersistence('.emulate/state.json'), ## Limitations - Requires the Node.js runtime (not Edge) since emulators use `crypto.randomBytes` -- Concurrent serverless instances writing to the same persistence adapter use last-write-wins semantics (acceptable for dev/preview traffic) +- Concurrent mutations use last-write-wins semantics. Generated identities require `initialize` to select the initial snapshot atomically across cold starts. ## Config Reference @@ -172,7 +174,8 @@ Wraps a Next.js config to include emulator font files in the serverless output t interface PersistenceAdapter { load(): Promise save(data: string): Promise + initialize?(data: string): Promise } ``` -The built-in `filePersistence(path)` from `@emulators/core` provides a file-based adapter for local development. +`initialize` must atomically create the initial value or return the value another instance created first. Implement it with compare-and-set semantics such as Redis `SET NX`. The built-in `filePersistence(path)` from `@emulators/core` provides this behavior for local development. diff --git a/plugins/emulate/agent/skills/emulate/SKILL.md b/plugins/emulate/agent/skills/emulate/SKILL.md index fb5b8ab5..f90620bc 100644 --- a/plugins/emulate/agent/skills/emulate/SKILL.md +++ b/plugins/emulate/agent/skills/emulate/SKILL.md @@ -383,7 +383,7 @@ const kvAdapter: PersistenceAdapter = { } ``` -State is loaded on cold start and saved after every mutating request (POST, PUT, PATCH, DELETE). Saves are serialized to prevent race conditions. +State is loaded on cold start and saved after every mutating request (POST, PUT, PATCH, DELETE). Saves are serialized to prevent race conditions. Generated GitHub App identities require `initialize` to atomically create the initial value or return the value another instance created first. ## Architecture diff --git a/plugins/emulate/agent/skills/github/SKILL.md b/plugins/emulate/agent/skills/github/SKILL.md index 2f789777..f0f82db2 100644 --- a/plugins/emulate/agent/skills/github/SKILL.md +++ b/plugins/emulate/agent/skills/github/SKILL.md @@ -50,6 +50,8 @@ const privateKey = github.generatedSecrets.find( The key remains stable across `github.reset()`. Explicit keys are not included in `generatedSecrets`. +The Next.js and Nuxt adapters also generate omitted keys. Retain the returned handler and call its server-only `generatedSecrets()` method. With persistence configured, the identity survives cold starts. Keep snapshots private because they contain the signing key, and require custom persistence backends to implement atomic `initialize()` semantics. + For the CLI, omit `private_key` only when requesting a private delivery file: ```bash diff --git a/plugins/emulate/agent/skills/next/SKILL.md b/plugins/emulate/agent/skills/next/SKILL.md index 87215212..aecbfcb6 100644 --- a/plugins/emulate/agent/skills/next/SKILL.md +++ b/plugins/emulate/agent/skills/next/SKILL.md @@ -120,6 +120,8 @@ import { filePersistence } from '@emulators/core' persistence: filePersistence('.emulate/state.json'), ``` +GitHub App seeds may omit `private_key`. Retain the handler and call server-only `generatedSecrets()`; explicit keys are excluded. Keep persisted snapshots private and implement `initialize` atomically. + ### How Persistence Works - **Cold start**: The adapter loads state from the persistence adapter. If found, it restores the full Store and token map (skipping seed). If not found, it seeds from config and saves the initial state. @@ -137,7 +139,7 @@ persistence: filePersistence('.emulate/state.json'), ## Limitations - Requires the Node.js runtime (not Edge) since emulators use `crypto.randomBytes` -- Concurrent serverless instances writing to the same persistence adapter use last-write-wins semantics (acceptable for dev/preview traffic) +- Concurrent mutations use last-write-wins semantics. Generated identities require `initialize` to select the initial snapshot atomically across cold starts. ## Config Reference @@ -169,7 +171,8 @@ Wraps a Next.js config to include emulator font files in the serverless output t interface PersistenceAdapter { load(): Promise save(data: string): Promise + initialize?(data: string): Promise } ``` -The built-in `filePersistence(path)` from `@emulators/core` provides a file-based adapter for local development. +`initialize` must atomically create the initial value or return the value another instance created first. Implement it with compare-and-set semantics such as Redis `SET NX`. The built-in `filePersistence(path)` from `@emulators/core` provides this behavior for local development. diff --git a/plugins/emulate/skills-lock.json b/plugins/emulate/skills-lock.json index 3fedfa86..6a5effef 100644 --- a/plugins/emulate/skills-lock.json +++ b/plugins/emulate/skills-lock.json @@ -17,13 +17,13 @@ "source": "vercel-labs/emulate", "sourceType": "github", "skillPath": "skills/emulate/SKILL.md", - "computedHash": "9f490bb96968516335972dc0df78fcd7b75025aab169a6bea24c2f9f4e63987e" + "computedHash": "e0a7a8e5a53676aa84fe1fd8bfdd9b59e78e48a681937560586593d2a8497bc4" }, "github": { "source": "vercel-labs/emulate", "sourceType": "github", "skillPath": "skills/github/SKILL.md", - "computedHash": "ae3920ffad035acdd71b0764015bc9401223197788092efb0be6a5edb7210fa6" + "computedHash": "04cf1a03b3a1b88315d37216079ee43f2f4b430e2f6e1ca05f5f3b4c253c62a8" }, "google": { "source": "vercel-labs/emulate", @@ -47,7 +47,7 @@ "source": "vercel-labs/emulate", "sourceType": "github", "skillPath": "skills/next/SKILL.md", - "computedHash": "105094c96ab29d6e081554c77bf30fe4393a81d91ddca385f215e09899af8a48" + "computedHash": "77d33a4cb9f9903f337c0e90b28104f53d4a552537273198e4c64ec8266bc3ee" }, "resend": { "source": "vercel-labs/emulate", @@ -65,13 +65,13 @@ "source": "vercel-labs/emulate", "sourceType": "github", "skillPath": "skills/stripe/SKILL.md", - "computedHash": "d2e822c16e3e7698b719e9dd06e21ed1019cc37a62e29c1b51718da838ee106d" + "computedHash": "b429538af7b5c52e8350970d767c02e4c53c3f70baf8af4b7503674d5b334198" }, "vercel": { "source": "vercel-labs/emulate", "sourceType": "github", "skillPath": "skills/vercel/SKILL.md", - "computedHash": "3cd45b301312b61ce4663a1d8ec1f55daed567470cd62cee77f65a43a9d56905" + "computedHash": "75f252a2d7e42886373f960bbe2c4ca95309cdf7425b6e8cfb0cf980f31132e9" } } } diff --git a/plugins/portless/.agents/skills/portless/SKILL.md b/plugins/portless/.agents/skills/portless/SKILL.md index 1e528ee0..a8aed422 100644 --- a/plugins/portless/.agents/skills/portless/SKILL.md +++ b/plugins/portless/.agents/skills/portless/SKILL.md @@ -155,6 +155,10 @@ Set `PORTLESS=0` to run the command directly without the proxy: PORTLESS=0 pnpm dev # Bypasses proxy, uses default port ``` +When a proxied command is stopped with Ctrl+C, portless waits for its process tree to exit. A second +Ctrl+C forwards another interrupt, and remaining descendants are terminated after a short grace +period. + ## How It Works 1. `portless proxy start` starts an HTTPS reverse proxy on port 443 as a background daemon. Auto-elevates with sudo on macOS/Linux; falls back to port 1355 if sudo is unavailable. Use `--no-tls` for plain HTTP on port 80. Configurable with `-p` / `--port` or the `PORTLESS_PORT` env var. The proxy also auto-starts when you run an app. @@ -411,7 +415,7 @@ portless hosts sync # Adds current routes to /etc/hosts portless hosts clean # Remove entries later ``` -Auto-syncs `/etc/hosts` for route hostnames by default. Set `PORTLESS_SYNC_HOSTS=0` to disable. +Auto-syncs `/etc/hosts` for route hostnames by default. Set `PORTLESS_SYNC_HOSTS=0` to disable. If a route hostname will not resolve, the command that registered it warns and points you to `portless hosts sync`. ### Browser shows certificate warning with --https diff --git a/plugins/portless/agent/skills/portless/SKILL.md b/plugins/portless/agent/skills/portless/SKILL.md index dda0dda7..2beba3d2 100644 --- a/plugins/portless/agent/skills/portless/SKILL.md +++ b/plugins/portless/agent/skills/portless/SKILL.md @@ -153,6 +153,10 @@ Set `PORTLESS=0` to run the command directly without the proxy: PORTLESS=0 pnpm dev # Bypasses proxy, uses default port ``` +When a proxied command is stopped with Ctrl+C, portless waits for its process tree to exit. A second +Ctrl+C forwards another interrupt, and remaining descendants are terminated after a short grace +period. + ## How It Works 1. `portless proxy start` starts an HTTPS reverse proxy on port 443 as a background daemon. Auto-elevates with sudo on macOS/Linux; falls back to port 1355 if sudo is unavailable. Use `--no-tls` for plain HTTP on port 80. Configurable with `-p` / `--port` or the `PORTLESS_PORT` env var. The proxy also auto-starts when you run an app. @@ -409,7 +413,7 @@ portless hosts sync # Adds current routes to /etc/hosts portless hosts clean # Remove entries later ``` -Auto-syncs `/etc/hosts` for route hostnames by default. Set `PORTLESS_SYNC_HOSTS=0` to disable. +Auto-syncs `/etc/hosts` for route hostnames by default. Set `PORTLESS_SYNC_HOSTS=0` to disable. If a route hostname will not resolve, the command that registered it warns and points you to `portless hosts sync`. ### Browser shows certificate warning with --https diff --git a/plugins/portless/skills-lock.json b/plugins/portless/skills-lock.json index 2f82146a..f6edef59 100644 --- a/plugins/portless/skills-lock.json +++ b/plugins/portless/skills-lock.json @@ -5,7 +5,7 @@ "source": "vercel-labs/portless", "sourceType": "github", "skillPath": "skills/portless/SKILL.md", - "computedHash": "4dd2cfba1475a35878d3936d4ce80047b800f7d7f65c9c6ddccb4638651d66a5" + "computedHash": "3554b1c4b77a327e87dfa1fafe8fec5136c19f5adfb9803a412aab7fd6c16e05" } } } diff --git a/plugins/react-native/.agents/skills/vercel-react-native-skills/metadata.json b/plugins/react-native/.agents/skills/vercel-react-native-skills/metadata.json deleted file mode 100644 index 600eb5bc..00000000 --- a/plugins/react-native/.agents/skills/vercel-react-native-skills/metadata.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "1.0.0", - "organization": "Engineering", - "date": "January 2026", - "abstract": "Comprehensive performance optimization guide for React Native applications, designed for AI agents and LLMs. Contains 35+ rules across 13 categories, prioritized by impact from critical (core rendering, list performance) to incremental (fonts, imports). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.", - "references": [ - "https://react.dev", - "https://reactnative.dev", - "https://docs.swmansion.com/react-native-reanimated", - "https://docs.swmansion.com/react-native-gesture-handler", - "https://docs.expo.dev", - "https://legendapp.com/open-source/legend-list", - "https://github.com/nandorojo/galeria", - "https://zeego.dev" - ] -} diff --git a/plugins/react-native/agent/skills/vercel-react-native-skills/metadata.json b/plugins/react-native/agent/skills/vercel-react-native-skills/metadata.json deleted file mode 100644 index 600eb5bc..00000000 --- a/plugins/react-native/agent/skills/vercel-react-native-skills/metadata.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "1.0.0", - "organization": "Engineering", - "date": "January 2026", - "abstract": "Comprehensive performance optimization guide for React Native applications, designed for AI agents and LLMs. Contains 35+ rules across 13 categories, prioritized by impact from critical (core rendering, list performance) to incremental (fonts, imports). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.", - "references": [ - "https://react.dev", - "https://reactnative.dev", - "https://docs.swmansion.com/react-native-reanimated", - "https://docs.swmansion.com/react-native-gesture-handler", - "https://docs.expo.dev", - "https://legendapp.com/open-source/legend-list", - "https://github.com/nandorojo/galeria", - "https://zeego.dev" - ] -} diff --git a/plugins/react-native/skills-lock.json b/plugins/react-native/skills-lock.json index 14674bd4..8545c572 100644 --- a/plugins/react-native/skills-lock.json +++ b/plugins/react-native/skills-lock.json @@ -5,7 +5,7 @@ "source": "vercel-labs/agent-skills", "sourceType": "github", "skillPath": "skills/react-native-skills/SKILL.md", - "computedHash": "41d24eafa7c3d82e270439808f7cfbc4d51aeb2d14f2809a2267c16275784d06" + "computedHash": "2e9088a7333666d8c2833b8ff58bd51b955501c42b4c7244f72b4cbf22dafcc4" } } } diff --git a/plugins/react/.agents/skills/vercel-composition-patterns/metadata.json b/plugins/react/.agents/skills/vercel-composition-patterns/metadata.json deleted file mode 100644 index 3470b744..00000000 --- a/plugins/react/.agents/skills/vercel-composition-patterns/metadata.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "1.0.0", - "organization": "Engineering", - "date": "January 2026", - "abstract": "Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.", - "references": [ - "https://react.dev", - "https://react.dev/learn/passing-data-deeply-with-context", - "https://react.dev/reference/react/use" - ] -} diff --git a/plugins/react/.agents/skills/vercel-react-best-practices/metadata.json b/plugins/react/.agents/skills/vercel-react-best-practices/metadata.json deleted file mode 100644 index 3bec38b1..00000000 --- a/plugins/react/.agents/skills/vercel-react-best-practices/metadata.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": "1.0.0", - "organization": "Vercel Engineering", - "date": "January 2026", - "abstract": "Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.", - "references": [ - "https://react.dev", - "https://nextjs.org", - "https://swr.vercel.app", - "https://github.com/shuding/better-all", - "https://github.com/isaacs/node-lru-cache", - "https://vercel.com/blog/how-we-optimized-package-imports-in-next-js", - "https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast" - ] -} diff --git a/plugins/react/.agents/skills/vercel-react-view-transitions/AGENTS.md b/plugins/react/.agents/skills/vercel-react-view-transitions/AGENTS.md index 8e213614..ad47782c 100644 --- a/plugins/react/.agents/skills/vercel-react-view-transitions/AGENTS.md +++ b/plugins/react/.agents/skills/vercel-react-view-transitions/AGENTS.md @@ -2,47 +2,11 @@ **Version 1.0.0** Vercel Engineering -March 2026 > **Note:** -> This document is mainly for agents and LLMs to follow when implementing -> view transitions in React applications. Humans may also find it useful, -> but guidance here is optimized for automation and consistency by -> AI-assisted workflows. - ---- - -## Abstract - -Guide for implementing smooth, native-feeling animations using React's View Transition API. Covers the `` component, `addTransitionType`, CSS view transition pseudo-elements, shared element transitions, Suspense reveals, list reorder, directional navigation, and Next.js integration. Includes a step-by-step implementation workflow, ready-to-use CSS animation recipes, and common mistake warnings. - ---- - -## Table of Contents - -1. [Core Reference](#when-to-animate) - - [When to Animate](#when-to-animate) - - [Availability](#availability) - - [Core Concepts](#core-concepts) - - [Styling with View Transition Classes](#styling-with-view-transition-classes) - - [Transition Types](#transition-types) - - [Shared Element Transitions](#shared-element-transitions) - - [Common Patterns](#common-patterns) - - [How Multiple VTs Interact](#how-multiple-vts-interact) - - [Next.js Integration](#nextjs-integration) - - [Accessibility](#accessibility) -2. [Implementation Workflow](#implementation-workflow) - - [Step 1: Audit the App](#step-1-audit-the-app) - - [Step 2: Add CSS Recipes](#step-2-add-css-recipes) - - [Step 3: Isolate Persistent Elements](#step-3-isolate-persistent-elements) - - [Step 4: Add Directional Page Transitions](#step-4-add-directional-page-transitions) - - [Step 5: Add Suspense Reveals](#step-5-add-suspense-reveals) - - [Step 6: Add Shared Element Transitions](#step-6-add-shared-element-transitions) - - [Step 7: Verify Each Navigation Path](#step-7-verify-each-navigation-path) - - [Common Mistakes](#common-mistakes) -3. [Patterns and Guidelines](#patterns-and-guidelines) -4. [CSS Animation Recipes](#css-animation-recipes) -5. [View Transitions in Next.js](#view-transitions-in-nextjs) +> This standalone guide is compiled from `SKILL.md` and its reference files +> for agents that consume a single `AGENTS.md` document. Edit the source +> files, not this compiled copy. --- @@ -60,7 +24,7 @@ Implement **all** applicable patterns from this list, in this order: | 2 | **Suspense reveal** | "Data loaded" | | 3 | **List identity** (per-item `key`) | "Same items, new arrangement" | | 4 | **State change** (`enter`/`exit`) | "Something appeared/disappeared" | -| 5 | **Route change** (layout-level) | "Going to a new place" | +| 5 | **Route change** (page-level) | "Going to a new place" | This is an implementation order, not a "pick one" list. Implement every pattern that fits the app. Only skip a pattern if the app has no use case for it. @@ -81,7 +45,13 @@ Reserve directional slides for hierarchical navigation (list → detail) and ord - **Next.js:** Do **not** install `react@canary` — the App Router already bundles React canary internally. `ViewTransition` works out of the box. `npm ls react` may show a stable-looking version; this is expected. - **Without Next.js:** Install `react@canary react-dom@canary` (`ViewTransition` is not in stable React). -- Browser support: Chromium 111+, Firefox 144+, Safari 18.2+. Graceful degradation. +- Browser support: Chromium 125+ (React needs the v2 object form of `startViewTransition`), Firefox 144+, Safari 18.2+. Graceful degradation on unsupported browsers. + +--- + +## Implementation Workflow + +When adding view transitions to an existing app, **follow [references/implementation.md](references/implementation.md) step by step.** Start with the audit — do not skip it. Use [references/css-recipes.md](references/css-recipes.md) for the applicable CSS and adapt it to the app. --- @@ -103,29 +73,37 @@ React auto-assigns a unique `view-transition-name` and calls `document.startView | Trigger | When it fires | |---------|--------------| -| **enter** | VT first inserted during a Transition | -| **exit** | VT first removed during a Transition | -| **update** | DOM mutations inside a VT. With nested VTs, mutation applies to the innermost one | -| **share** | Named VT unmounts and another with same `name` mounts in same Transition | +| **enter** | `` first inserted during a Transition | +| **exit** | `` first removed during a Transition | +| **update** | DOM mutations inside a ``, or the boundary itself changing size/position due to an immediate sibling. With nested VTs, mutation applies to the innermost one | +| **share** | Named VT unmounts and another with same `name` mounts in the same Transition | Only `startTransition`, `useDeferredValue`, or `Suspense` activate VTs. Regular `setState` does not animate. ### Critical Placement Rule -VT only activates enter/exit if it appears **before any DOM nodes**: +`` only activates enter/exit if it appears **before any DOM nodes**: ```jsx // Works -
Content
+ +
Content
+
-// Broken — div wraps the VT -
Content
+// Broken — div wraps the VT, suppressing enter/exit +
+ +
Content
+
+
``` --- ## Styling with View Transition Classes +### Props + Values: `"auto"` (browser cross-fade), `"none"` (disabled), `"class-name"` (custom CSS), or `{ [type]: value }` for type-specific animations. ```jsx @@ -141,11 +119,13 @@ If `default` is `"none"`, all triggers are off unless explicitly listed. - `::view-transition-group(.class)` — container - `::view-transition-image-pair(.class)` — old + new pair +See [references/css-recipes.md](references/css-recipes.md) for ready-to-use animation recipes. + --- ## Transition Types -Tag transitions with `addTransitionType` so VTs can pick different animations. Call it multiple times to stack types — different VTs in the tree react to different types: +Tag transitions with `addTransitionType` so VTs can pick different animations based on context. Call it multiple times to stack types — different VTs in the tree react to different types: ```jsx startTransition(() => { @@ -155,7 +135,7 @@ startTransition(() => { }); ``` -Map types to CSS classes. Works on `enter`, `exit`, **and** `share`: +Pass an object to map types to CSS classes. Works on `enter`, `exit`, **and** `share`: ```jsx ``` -**TypeScript:** `ViewTransitionClassPerType` requires a `default` key. +**TypeScript:** `ViewTransitionClassPerType` requires a `default` key in the object. + +For apps with multiple pages, extract the type-keyed VT into a reusable wrapper: + +```jsx +export function DirectionalTransition({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` ### `router.back()` and Browser Back Button -`router.back()` and the browser's back/forward buttons do **not** trigger view transitions (`popstate` is synchronous, incompatible with `startViewTransition`). Use `router.push()` with an explicit URL instead. +`router.back()` and the browser's back/forward buttons carry **no transition types**, so type-keyed animations (directional slides) resolve to their `default` and don't play — untyped shared-element morphs still apply. For typed animations, use `router.push()` with an explicit URL. ### Types and Suspense Types are available during navigation but **not** during subsequent Suspense reveals (separate transitions, no type). Use type maps for page-level enter/exit; use simple string props for Suspense reveals. +### Shared Element Readiness + +A shared element transition can pair elements only when both the old and new views are rendered in the same Transition. If incoming content suspends, only its fallback exists for that update; the resolved content appears in a later Suspense transition and can be animated separately. + --- ## Shared Element Transitions @@ -199,15 +199,16 @@ Same `name` on two VTs — one unmounting, one mounting — creates a shared ele startTransition(() => onSelect())} /> -// Other view — same name +// On the other view — same name ``` -- Only one VT with a given `name` can be mounted at a time — use unique names. Watch for reusable components: if a component with a named VT is rendered in both a modal/popover *and* a page, both mount simultaneously and break the morph. Either make the name conditional (via a prop) or move the named VT out of the shared component into the specific consumer. -- `share` takes precedence over `enter`/`exit`. Think through each navigation path: when no pair forms, `enter`/`exit` fires instead. Consider whether the element needs a fallback animation for those paths. -- Never use fade-out exit on pages with shared morphs — use directional slide. +- Only one VT with a given `name` can be mounted at a time — use unique names (`photo-${id}`). Watch for reusable components: if a component with a named VT is rendered in both a modal/popover *and* a page, both mount simultaneously and break the morph. Either make the name conditional (via a prop) or move the named VT out of the shared component into the specific consumer. +- `share` takes precedence over `enter`/`exit`. Think through each navigation path: when no matching pair forms (e.g., the target page doesn't have the same name), `enter`/`exit` fires instead. Consider whether the element needs a fallback animation for those paths. +- Two ways a wired-up morph silently never fires: (1) `default="none"` with no explicit `share` prop — share resolves to none; (2) type-keyed `share` where the navigation never adds the type — a plain link click resolves the map's `default`. Every link that should morph must add the type (`transitionTypes` on `next/link`, or `addTransitionType`). +- Never use a fade-out exit on pages with shared morphs — use a directional slide instead. --- @@ -231,9 +232,13 @@ Same `name` on two VTs — one unmounting, one mounting — creates a shared ele Trigger inside `startTransition`. Avoid wrapper `
`s between list and VT. +### Layout Displacement Morph + +Only content inside an activated boundary animates position — everything else teleports to its new layout spot. Wrap the sibling content below a growing/shrinking list in a bare `` so it glides instead of jumping. See [Layout Displacement Morph](references/patterns.md#layout-displacement-morph). + ### Composing Shared Elements with List Identity -Shared elements and list identity are independent concerns — don't confuse one for the other. When a list item contains a shared element, use two nested `` boundaries: +Shared elements and list identity are independent concerns — don't confuse one for the other. When a list item contains a shared element (e.g., an image that morphs into a detail view), use two nested `` boundaries: ```jsx {items.map(item => ( @@ -248,7 +253,7 @@ Shared elements and list identity are independent concerns — don't confuse one ))} ``` -The outer VT handles list reorder/enter. The inner VT handles cross-route shared element morph. Missing either layer means that animation silently doesn't happen. +The outer VT handles list reorder/enter animations. The inner VT handles the cross-route shared element morph. Missing either layer means that animation silently doesn't happen. ### Force Re-Enter with `key` @@ -258,7 +263,7 @@ The outer VT handles list reorder/enter. The inner VT handles cross-route shared ``` -**Caution:** Wrapping `` with key remounts the boundary and refetches. +**Caution:** If wrapping ``, changing `key` remounts the boundary and refetches. ### Suspense Fallback to Content @@ -276,15 +281,19 @@ Directional reveal: ``` +For more patterns, see [references/patterns.md](references/patterns.md). + --- ## How Multiple VTs Interact -Every VT matching the trigger fires simultaneously in a single `document.startViewTransition`. VTs in **different** transitions don't compete. +Every VT matching the trigger fires simultaneously in a single `document.startViewTransition`. VTs in **different** transitions (navigation vs later Suspense resolve) don't compete. + +### Use `default="none"` Deliberately -### Use `default="none"` Liberally +Without it, every VT fires the browser cross-fade on **every** transition — Suspense resolves, `useDeferredValue` updates, background revalidations. Use `default="none"` on named/shared elements and type-keyed page VTs. -Without it, every VT fires the browser cross-fade on **every** transition. Always use `default="none"` and explicitly enable only desired triggers. +But it also turns off `update` (layout/reflow morphs) and `share` (a named pair with no explicit `share` prop never morphs). Keyed list items and displaced siblings *want* update — leave them bare or set `update="auto"`. ### Two Patterns Coexist @@ -295,47 +304,48 @@ They coexist because they fire at different moments. `default="none"` on both pr ### Nested VT Limitation -When a parent VT exits, nested VTs inside it do **not** fire their own enter/exit — only the outermost VT animates. Per-item staggered animations during page navigation are not possible today. See [react#36135](https://github.com/facebook/react/pull/36135) for an experimental opt-in fix. +When a parent VT mounts/unmounts **as one unit** with nested VTs inside it, the nested ones do not fire their own enter/exit — only the outermost VT animates. (A child VT mounted inside a *persistent* parent VT fires enter/exit normally.) Per-item staggered animations during page navigation are not currently available in Next.js; see [troubleshooting](references/troubleshooting.md) for the upstream experimental status. --- ## Next.js Integration -See the [View Transitions in Next.js](#view-transitions-in-nextjs) section below. +For Next.js integration (`transitionTypes` on `next/link` and `useRouter`, App Router patterns, Server Components), see [references/nextjs.md](references/nextjs.md). --- ## Accessibility -Always add reduced motion CSS to your global stylesheet: +Always add the reduced motion CSS from [references/css-recipes.md](references/css-recipes.md#reduced-motion) to your global stylesheet. -```css -@media (prefers-reduced-motion: reduce) { - ::view-transition-old(*), - ::view-transition-new(*), - ::view-transition-group(*) { - animation-duration: 0s !important; - animation-delay: 0s !important; - } -} -``` +--- + +## Reference Files + +- **[references/implementation.md](references/implementation.md)** — Step-by-step implementation workflow. +- **[references/patterns.md](references/patterns.md)** — Patterns, animation timing, and events API. +- **[references/troubleshooting.md](references/troubleshooting.md)** — Symptom-driven debugging and runtime limitations. +- **[references/css-recipes.md](references/css-recipes.md)** — Ready-to-use CSS animation recipes. +- **[references/nextjs.md](references/nextjs.md)** — Next.js App Router patterns and Server Component details. --- # Implementation Workflow -**Follow these steps in order.** Start with the audit — do not skip it. Copy the CSS recipes from the CSS Recipes section below — do not write your own animation CSS. +Follow these steps in order when adding view transitions to an app. Each step builds on the previous one. + +Use the official [React `` reference](https://react.dev/reference/react/ViewTransition) and [Next.js guide](https://nextjs.org/docs/app/guides/view-transitions) for API behavior. This file focuses on audit order, integration decisions, and verification. ## Step 1: Audit the App Before writing any code, scan the codebase thoroughly. Search for: -- **Every `` and `router.push`** — open every file that contains one -- **Every `` boundary** — check what its fallback renders -- **Every page/route component** — each needs a VT placement decision -- **Persistent elements** (headers, navbars, sidebars) — need `viewTransitionName` isolation -- **Shared visual elements** on both source and target views -- **Skeleton-to-content control pairs** — if a fallback renders a control that also exists in the real content, both need a matching `viewTransitionName` +- **Every `` and `router.push`** — these are your navigation triggers. Open every file that contains one. +- **Every `` boundary** — each one is a candidate for a reveal animation. Check what its fallback renders. +- **Every page/route component** — list them all. Each page needs a VT placement decision. +- **Persistent elements** — headers, navbars, sidebars, sticky controls that stay on screen across navigations. These need `viewTransitionName` isolation. +- **Shared visual elements** — images, cards, or avatars that appear on both a source and target view (e.g., a thumbnail in a list and the same image on a detail page). +- **Skeleton-to-content control pairs** — if a Suspense fallback renders a control (search input, tab bar) that also exists in the real content, both need a matching `viewTransitionName`. Then classify every navigation and produce a navigation map: @@ -349,29 +359,30 @@ Then classify every navigation and produce a navigation map: | (Suspense) | (content loads) | — | slide-up reveal | ``` -For each shared element (`name` prop), note where a pair forms and where it doesn't — this determines whether you need `enter`/`exit` as a fallback alongside `share`. +For each shared element (`name` prop), note every navigation where a pair forms and where it doesn't — this determines whether you need `enter`/`exit` as a fallback alongside `share`. ## Step 2: Add CSS Recipes -Copy the **complete** CSS recipe set from the CSS Animation Recipes section below into your global stylesheet. Don't write your own — the recipes handle staggered timing, motion blur, and reduced motion. +Choose the animation pattern from the audit and this skill's guidance, then copy only the applicable sections from [css-recipes.md](css-recipes.md). Always include reduced motion. Add live-root, persistent-element, backdrop, or floating-element rules only when the audit found those surfaces. + +Customize timing after the structure works. Keep ordinary crossfades opacity-only; scope blur to a specific shared morph when it is intentional. ## Step 3: Isolate Persistent Elements +For every persistent element identified in Step 1, add a `viewTransitionName` style to pull it out of the page content's transition snapshot: + ```jsx
...
``` -```css -::view-transition-group(site-header) { - animation: none; - z-index: 100; -} -``` +Then add the [Persistent Element Isolation](css-recipes.md#persistent-element-isolation) CSS (prevents the element from animating during page transitions). If the element uses `backdrop-blur` or `backdrop-filter`, use the [Backdrop-Blur Workaround](css-recipes.md#backdrop-blur-workaround) instead. -For `backdrop-blur`/`backdrop-filter`, use the backdrop-blur workaround instead. +If a Suspense fallback mirrors a persistent control (e.g., a skeleton search input), give both the real control and the skeleton the same `viewTransitionName` so they morph in place. ## Step 4: Add Directional Page Transitions +For hierarchical navigations identified in Step 1, tag the navigation direction using `addTransitionType` inside `startTransition`: + ```jsx startTransition(() => { addTransitionType('nav-forward'); @@ -379,19 +390,29 @@ startTransition(() => { }); ``` -Wrap each **page component** (not layout) in a type-keyed VT: +Then wrap each **page component** (not layout) in a type-keyed ``: ```jsx
...page content...
``` -Extract into a reusable component so every page doesn't repeat the type map: +The `nav-forward` and `nav-back` CSS classes from [Directional Navigation](css-recipes.md#directional-navigation) produce horizontal slides. For simpler apps where directional motion isn't needed, a bare `` wrapper with `enter="fade-in"` / `exit="fade-out"` works too. + +Extract this into a reusable component so every page doesn't repeat the verbose type map: ```jsx export function DirectionalTransition({ children }: { children: React.ReactNode }) { @@ -407,83 +428,89 @@ export function DirectionalTransition({ children }: { children: React.ReactNode } ``` -**Rules:** Always pair `enter` with `exit`. Always include `default: "none"`. Place in page components, not layouts. Only use directional slides for hierarchical navigation or ordered sequences (prev/next). +This also becomes the single place to adjust if you add new transition types later. + +**Rules:** +- Always pair `enter` with `exit` — without an exit animation, the old page disappears instantly while the new one animates in. +- Always include `default: "none"` in type map objects and `default="none"` on the component — otherwise it fires on every transition. +- Place the directional `` in each page component, not in a layout. Layouts persist across navigations and never trigger enter/exit. +- Only use directional slides for hierarchical navigation or ordered sequences (prev/next). Lateral/sibling navigation (tab-to-tab) should use a bare `` (cross-fade) or `default="none"`. ## Step 5: Add Suspense Reveals +For every `` boundary identified in Step 1, wrap the fallback and content in separate ``s: + ```jsx -}> - + + + + } +> + + +
``` -Use `default="none"` on content VT. Use simple string props (not type maps) — Suspense resolves have no type. +This example uses `slide-down` / `slide-up` for directional vertical motion. For a simpler reveal, a bare `` around the `` gives a cross-fade with zero configuration. Choose based on the spatial meaning described in the main skill. + +**Rules:** +- Always use `default="none"` on the content `` to prevent re-animation on revalidation or unrelated transitions. +- Use simple string props (not type maps) on Suspense ``s — Suspense resolves fire as separate transitions with no type, so type-keyed props won't match. +- A fallback/content `share` pair morphs between snapshots. Use it only when that interpolation is desired and does not distort layout or geometry. +- If the same element appears in **both** the fallback and the content (a title, a heading), it flickers on reveal — an opacity dip. Render it **outside** the `` boundary (or pin it), so it isn't in both. See [Suspense reveal flicker](patterns.md#suspense-reveal-flicker). ## Step 6: Add Shared Element Transitions +For every shared visual element identified in Step 1, add matching named `` wrappers on both the source and target views: + ```jsx -// Source view +// On the source view (e.g., list/grid page) -// Target view — same name +// On the target view (e.g., detail page) — same name ``` -When list items contain shared elements, compose both patterns — two independent layers: - -```jsx -{items.map(item => ( - {/* list identity */} - - {/* shared element */} - - - - -))} -``` +The `share="morph"` class uses the [Shared Element Morph](css-recipes.md#shared-element-morph) recipe (controlled duration + motion blur). For a simpler cross-fade, use `share="auto"` (browser default). -The outer VT handles list reorder/enter. The inner VT handles cross-route shared element morph. Missing either layer means that animation silently doesn't happen. +When list items contain shared elements, compose both patterns with two nested `` layers — an outer keyed VT for list identity and an inner named VT for the cross-route pair. See [Composing Shared Elements with List Identity](../SKILL.md#composing-shared-elements-with-list-identity). -**Rules:** Names must be globally unique. Add `default="none"` on list-side shared elements. +**Rules:** +- Names must be globally unique — use prefixes like `photo-${id}`. +- Add `default="none"` on list-side shared elements to prevent per-item cross-fades on filter/search updates. +- The target must be **in the DOM at navigation time** for the pair to form. If it's behind a Suspense fallback (not rendered yet), no pair forms and it won't morph. It works when the target is present at the snapshot — render it above the data boundary, or have its data **cached/prefetched** so it resolves in time. ## Step 7: Verify Each Navigation Path -Walk through every row in the navigation map from Step 1: +Walk through every row in the navigation map from Step 1 and confirm: -- Does the VT mount/unmount, or stay mounted (same-route)? +- Does the VT mount/unmount on this navigation, or does it stay mounted (same-route)? - For named VTs: does a shared pair form? If not, does `enter`/`exit` provide a fallback? - Does `default="none"` block an animation you actually want? -- Do persistent elements stay static? +- Do persistent elements stay static (not sliding with page content)? - Do Suspense reveals animate independently from directional navigations? ---- - -## Common Mistakes - -- **Bare VT without `default="none"`** — fires cross-fade on every transition -- **Directional VT in a layout** — layouts persist, enter/exit won't fire on route changes -- **Fade-out exit with shared morphs** — conflicts with morph, use directional slide -- **Writing custom animation CSS** — use the recipes -- **Missing `default: "none"` in type-keyed objects** — TypeScript requires it, fallback is `"auto"` -- **Type maps on Suspense reveals** — Suspense resolves have no type, use string props -- **Raw `viewTransitionName` CSS to trigger animations** — React only starts view transitions when `` components are in the tree. Bare `viewTransitionName` is for isolating elements, not triggering animations. -- **`update` trigger for same-route navigations** — nested VTs steal the mutation from the parent. Use `key` + `name` + `share` instead. -- **Named VT in a reusable component** — if a component with a named VT is rendered in both a modal/popover *and* a page, both mount simultaneously and break the morph. Make the name conditional or move it to the specific consumer. -- **`router.back()` for back navigation** — `router.back()` triggers synchronous `popstate`, incompatible with view transitions. Use `router.push()` with an explicit URL. +If any path produces no animation or competing animations, use the symptom-driven [troubleshooting guide](troubleshooting.md). -For Next.js-specific steps, see the Next.js section below. +For Next.js-specific implementation steps (`transitionTypes` on ``, prefetch behavior, and same-route dynamic segments), see [nextjs.md](nextjs.md). --- # Patterns and Guidelines +Use the official [React `` reference](https://react.dev/reference/react/ViewTransition) for API mechanics. This file collects reusable implementation patterns and failure modes from production apps. + ## Searchable Grid with `useDeferredValue` +`useDeferredValue` makes filter updates a transition, activating ``: + ```tsx 'use client'; @@ -506,10 +533,20 @@ export default function SearchableGrid({ itemsPromise }) { } ``` -Per-item named VTs in deferred lists trigger cross-fades on every keystroke. Fix with `default="none"`. +Per-item `` inside a deferred list triggers cross-fades on every keystroke. Fix with `default="none"`: + +```tsx +{filteredItems.map(item => ( + + + +))} +``` ## Card Expand/Collapse with `startTransition` +Toggle between grid and detail view with shared element morph: + ```tsx 'use client'; @@ -549,58 +586,168 @@ export default function ItemGrid({ items }) { } ``` +## Type-Safe Transition Helpers + +Use `as const` arrays and derived types to prevent ID clashes: + +```tsx +const transitionTypes = ['default', 'transition-to-detail', 'transition-to-list'] as const; +const animationTypes = ['auto', 'none', 'animate-slide-from-left', 'animate-slide-from-right'] as const; + +type TransitionType = (typeof transitionTypes)[number]; +type AnimationType = (typeof animationTypes)[number]; +type TransitionMap = { default: AnimationType } & Partial, AnimationType>>; + +export function HorizontalTransition({ children, enter, exit }: { + children: React.ReactNode; + enter: TransitionMap; + exit: TransitionMap; +}) { + return {children}; +} +``` + ## Cross-Fade Without Remount -Omit `key` to trigger update (cross-fade) instead of exit + enter. Avoids Suspense remount: +Omit `key` to trigger an update (cross-fade) instead of exit + enter. Avoids Suspense remount/refetch: ```jsx - + + + ``` +Use `key` when content identity changes (state resets). Omit for cross-fades (tabs, panels, carousel). + ## Isolate Elements from Parent Animations -Persistent elements get captured in page's transition snapshot. Fix with `viewTransitionName`: +Pull an element out of the animated `root` snapshot by giving it its own `view-transition-name`. **`view-transition-name: none` is a no-op** — it's the CSS default, so the element stays in `root` (a common flicker bug). Use a real, unique name, then neutralize with `` (no CSS) or CSS (needed for `z-index`/`display` control — see [css-recipes.md](css-recipes.md#persistent-element-isolation)). + +- **Persistent chrome** (nav, sidebar, player bar): `