* feat(inbox): skip AI extraction for multi-page PDFs (#553) Bedrock churns for minutes on multi-page PDFs (sales reports, bank statements, contracts) and returns nothing useful. Above 3 pages we now skip extraction entirely and mark the row with extraction_skipped=true; the document still lands in the inbox and can be attached or converted manually. Same gate applies to the /items/:id/attach path. Client can also opt out via skip_extraction=true (skip_reason=client_opt_out). The InvoiceInboxWorkspace renders an "Inte AI-tolkad" badge for skipped rows, distinct from the "Felaktig" failure state (status='error'). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(ui): introduce DataList + DropdownMenu primitives, roll out across list pages DataList replaces the per-row Card pattern across Granskning, Transactions, Invoices, Supplier invoices, and Pending. One bordered container with hairline rows matches the flat-with-hairlines aesthetic in CLAUDE.md — no shadows, no state-tinted borders, secondary token for selected/hover. DropdownMenu fills the gap for row-level action menus on TransactionInboxCard, TransactionHistoryList, and the page-level action menus on /transactions and /pending. Replaces ad-hoc Popover + buttons constructions. Migrates list pages and the transaction inbox/history components onto the new primitives. No behavior change beyond the visual unification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: add agent skills + gnubok domain skills, gitignore compliance reports .agents/skills/ + skills-lock.json + symlinks under .claude/skills/ check in the vercel-labs/agent-skills set pinned by the local skill manager (deploy-to-vercel, vercel-cli-with-tokens, react-best-practices, composition-patterns, react-native-skills, react-view-transitions, web-design-guidelines). Keeps the team on the same versions. .claude/skills/industry/ + .claude/skills/modifier/ are hand-authored vertical and entity-modifier skills for the specialized accountant agent — industries (konsult-it, e-handel, bygg-hantverk, reklambyra, saas-ai) and entity overlays (holding-ab, single-shareholder-ab-fmb, mixed- verksamhet). Project-owned content; lives in the repo by design. Also gitignores .compliance-reports/ — those are large generated SARIF/dossier artifacts from the compliance scanner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(transactions): unify inbox/history chrome, drop swipe flow The transactions page mixed two-tier filtering, a swipe-view detour, and per-row Card chrome that didn't carry its weight. This pass collapses those into a single editorial-list surface and removes the unused swipe path entirely. User-visible changes: - Mode toggle (Att bokföra / Alla transaktioner) moved from a Tabs row under the header into a dropdown to the right of a unified search bar. Search now persists when switching modes. - Removed the swipe categorization view ("Gå igenom alla") and its trigger button. The 800-line SwipeCategorizationView component is deleted; suggestion-fetching shrinks to what the template picker still consumes. - Inbox rows now show one primary action: invoice/supplier-invoice match shortcut when auto-detected, else "Bokför". A new visible Link2 icon button opens the customer or supplier invoice picker manually (chosen by amount sign). Delete becomes a plain trash button — no overflow menu since it only ever held one item. - Bulk action bar swaps "Markera som privat" for "Ta bort" with a single combined confirmation. - Built SupplierInvoicePicker mirroring InvoicePicker so expense transactions can be matched to supplier invoices from the inbox. Wired through /api/transactions/{id}/match-supplier-invoice. - Template picker dialog renamed to "Bokför transaktion"; "Bokför manuellt…" and "Matcha med faktura…" promoted from muted ghost buttons at the bottom to outline buttons at the top, above the template list. - Breathing room: row padding py-3 → py-4, primary text text-sm → text-base, amount text-base, button heights h-8 → h-9, trailing gap-2 → gap-3 (in the DataList primitive itself, so every list benefits slightly). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: align package-lock.json with merged package.json The merge resolution took origin/main's package-lock.json (which dropped pdf-lib) but kept our package.json (which still requires pdf-lib for the invoice-inbox extension's PDFDocument import). `npm ci` rejected the mismatch. Regenerate the lock from the merged package.json so both files agree. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: regenerate package-lock.json with npm@10 for CI compat Local npm@11 produced a lock that npm@10 (CI) rejected with "Missing: @swc/helpers@0.5.21". Regenerated with npm@10 --package-lock-only so CI can install. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): supplier-invoice status-leak guard + drop dead prop Two follow-ups from the merge-risk audit: - SupplierInvoicePicker now mirrors InvoicePicker's status-leak guard: if a supplier invoice is still 'approved'/'overdue' but already has a payment voucher attached (journal_entry_id on supplier_invoice_payments), hide it. Closes a UX race window between payment and status flip. Partially-paid invoices still pass through. - Drop the unused onMarkPrivate prop on TransactionInboxCard and the matching handleMarkPrivate wrapper in the parent. Both became dead when the swipe-categorisation flow was removed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
321 lines
12 KiB
Markdown
321 lines
12 KiB
Markdown
---
|
|
name: vercel-react-view-transitions
|
|
description: Guide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions view transitions, `startViewTransition`, `ViewTransition`, transition types, or asks about animating between UI states in React without third-party animation libraries.
|
|
license: MIT
|
|
metadata:
|
|
author: vercel
|
|
version: "1.0.0"
|
|
---
|
|
|
|
# React View Transitions
|
|
|
|
Animate between UI states using the browser's native `document.startViewTransition`. Declare *what* with `<ViewTransition>`, trigger *when* with `startTransition` / `useDeferredValue` / `Suspense`, control *how* with CSS classes. Unsupported browsers skip animations gracefully.
|
|
|
|
## When to Animate
|
|
|
|
Every `<ViewTransition>` should communicate a spatial relationship or continuity. If you can't articulate what it communicates, don't add it.
|
|
|
|
Implement **all** applicable patterns from this list, in this order:
|
|
|
|
| Priority | Pattern | What it communicates |
|
|
|----------|---------|---------------------|
|
|
| 1 | **Shared element** (`name`) | "Same thing — going deeper" |
|
|
| 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" |
|
|
|
|
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.
|
|
|
|
### Choosing Animation Style
|
|
|
|
| Context | Animation | Why |
|
|
|---------|-----------|-----|
|
|
| Hierarchical navigation (list → detail) | Type-keyed `nav-forward` / `nav-back` | Communicates spatial depth |
|
|
| Lateral navigation (tab-to-tab) | Bare `<ViewTransition>` (fade) or `default="none"` | No depth to communicate |
|
|
| Suspense reveal | `enter`/`exit` string props | Content arriving |
|
|
| Revalidation / background refresh | `default="none"` | Silent — no animation needed |
|
|
|
|
Reserve directional slides for hierarchical navigation (list → detail) and ordered sequences (prev/next photo, carousel, paginated results). For ordered sequences, the direction communicates position: "next" slides from right, "previous" from left. Lateral/unordered navigation (tab-to-tab) should not use directional slides — it falsely implies spatial depth.
|
|
|
|
---
|
|
|
|
## Availability
|
|
|
|
- **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 on unsupported browsers.
|
|
|
|
---
|
|
|
|
## Implementation Workflow
|
|
|
|
When adding view transitions to an existing app, **follow `references/implementation.md` step by step.** Start with the audit — do not skip it. Copy the CSS recipes from `references/css-recipes.md` into the global stylesheet — do not write your own animation CSS.
|
|
|
|
---
|
|
|
|
## Core Concepts
|
|
|
|
### The `<ViewTransition>` Component
|
|
|
|
```jsx
|
|
import { ViewTransition } from 'react';
|
|
|
|
<ViewTransition>
|
|
<Component />
|
|
</ViewTransition>
|
|
```
|
|
|
|
React auto-assigns a unique `view-transition-name` and calls `document.startViewTransition` behind the scenes. Never call `startViewTransition` yourself.
|
|
|
|
### Animation Triggers
|
|
|
|
| Trigger | When it fires |
|
|
|---------|--------------|
|
|
| **enter** | `<ViewTransition>` first inserted during a Transition |
|
|
| **exit** | `<ViewTransition>` first removed during a Transition |
|
|
| **update** | DOM mutations inside a `<ViewTransition>`. 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
|
|
|
|
`<ViewTransition>` only activates enter/exit if it appears **before any DOM nodes**:
|
|
|
|
```jsx
|
|
// Works
|
|
<ViewTransition enter="auto" exit="auto">
|
|
<div>Content</div>
|
|
</ViewTransition>
|
|
|
|
// Broken — div wraps the VT, suppressing enter/exit
|
|
<div>
|
|
<ViewTransition enter="auto" exit="auto">
|
|
<div>Content</div>
|
|
</ViewTransition>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|
|
<ViewTransition default="none" enter="slide-in" exit="slide-out" share="morph" />
|
|
```
|
|
|
|
If `default` is `"none"`, all triggers are off unless explicitly listed.
|
|
|
|
### CSS Pseudo-Elements
|
|
|
|
- `::view-transition-old(.class)` — outgoing snapshot
|
|
- `::view-transition-new(.class)` — incoming snapshot
|
|
- `::view-transition-group(.class)` — container
|
|
- `::view-transition-image-pair(.class)` — old + new pair
|
|
|
|
See `references/css-recipes.md` for ready-to-use animation recipes.
|
|
|
|
---
|
|
|
|
## Transition 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(() => {
|
|
addTransitionType('nav-forward');
|
|
addTransitionType('select-item');
|
|
router.push('/detail/1');
|
|
});
|
|
```
|
|
|
|
Pass an object to map types to CSS classes. Works on `enter`, `exit`, **and** `share`:
|
|
|
|
```jsx
|
|
<ViewTransition
|
|
enter={{ 'nav-forward': 'slide-from-right', 'nav-back': 'slide-from-left', default: 'none' }}
|
|
exit={{ 'nav-forward': 'slide-to-left', 'nav-back': 'slide-to-right', default: 'none' }}
|
|
share={{ 'nav-forward': 'morph-forward', 'nav-back': 'morph-back', default: 'morph' }}
|
|
default="none"
|
|
>
|
|
<Page />
|
|
</ViewTransition>
|
|
```
|
|
|
|
`enter` and `exit` don't have to be symmetric. For example, fade in but slide out directionally:
|
|
|
|
```jsx
|
|
<ViewTransition
|
|
enter={{ 'nav-forward': 'fade-in', 'nav-back': 'fade-in', default: 'none' }}
|
|
exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
|
|
default="none"
|
|
>
|
|
```
|
|
|
|
**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 (
|
|
<ViewTransition
|
|
enter={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
|
|
exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
|
|
default="none"
|
|
>
|
|
{children}
|
|
</ViewTransition>
|
|
);
|
|
}
|
|
```
|
|
|
|
### `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.
|
|
|
|
### 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 Transitions
|
|
|
|
Same `name` on two VTs — one unmounting, one mounting — creates a shared element morph:
|
|
|
|
```jsx
|
|
<ViewTransition name="hero-image">
|
|
<img src="/thumb.jpg" onClick={() => startTransition(() => onSelect())} />
|
|
</ViewTransition>
|
|
|
|
// On the other view — same name
|
|
<ViewTransition name="hero-image">
|
|
<img src="/full.jpg" />
|
|
</ViewTransition>
|
|
```
|
|
|
|
- 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.
|
|
- Never use a fade-out exit on pages with shared morphs — use a directional slide instead.
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Enter/Exit
|
|
|
|
```jsx
|
|
{show && (
|
|
<ViewTransition enter="fade-in" exit="fade-out"><Panel /></ViewTransition>
|
|
)}
|
|
```
|
|
|
|
### List Reorder
|
|
|
|
```jsx
|
|
{items.map(item => (
|
|
<ViewTransition key={item.id}><ItemCard item={item} /></ViewTransition>
|
|
))}
|
|
```
|
|
|
|
Trigger inside `startTransition`. Avoid wrapper `<div>`s between list and VT.
|
|
|
|
### 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 (e.g., an image that morphs into a detail view), use two nested `<ViewTransition>` boundaries:
|
|
|
|
```jsx
|
|
{items.map(item => (
|
|
<ViewTransition key={item.id}> {/* list identity */}
|
|
<Link href={`/items/${item.id}`}>
|
|
<ViewTransition name={`item-image-${item.id}`} share="morph"> {/* shared element */}
|
|
<Image src={item.image} />
|
|
</ViewTransition>
|
|
<p>{item.name}</p>
|
|
</Link>
|
|
</ViewTransition>
|
|
))}
|
|
```
|
|
|
|
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`
|
|
|
|
```jsx
|
|
<ViewTransition key={searchParams.toString()} enter="slide-up" default="none">
|
|
<ResultsGrid />
|
|
</ViewTransition>
|
|
```
|
|
|
|
**Caution:** If wrapping `<Suspense>`, changing `key` remounts the boundary and refetches.
|
|
|
|
### Suspense Fallback to Content
|
|
|
|
Simple cross-fade:
|
|
```jsx
|
|
<ViewTransition>
|
|
<Suspense fallback={<Skeleton />}><Content /></Suspense>
|
|
</ViewTransition>
|
|
```
|
|
|
|
Directional reveal:
|
|
```jsx
|
|
<Suspense fallback={<ViewTransition exit="slide-down"><Skeleton /></ViewTransition>}>
|
|
<ViewTransition enter="slide-up" default="none"><Content /></ViewTransition>
|
|
</Suspense>
|
|
```
|
|
|
|
For more patterns, see `references/patterns.md`.
|
|
|
|
---
|
|
|
|
## How Multiple VTs Interact
|
|
|
|
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"` Liberally
|
|
|
|
Without it, every VT fires the browser cross-fade on **every** transition — Suspense resolves, `useDeferredValue` updates, background revalidations. Always use `default="none"` and explicitly enable only desired triggers.
|
|
|
|
### Two Patterns Coexist
|
|
|
|
**Pattern A — Directional slides:** Type-keyed VT on each page, fires during navigation.
|
|
**Pattern B — Suspense reveals:** Simple string props, fires when data loads (no type).
|
|
|
|
They coexist because they fire at different moments. `default="none"` on both prevents cross-interference. Always pair `enter` with `exit`. Place directional VTs in page components, not layouts.
|
|
|
|
### 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.
|
|
|
|
---
|
|
|
|
## Next.js Integration
|
|
|
|
For Next.js setup (`experimental.viewTransition` flag, `transitionTypes` prop on `next/link`, App Router patterns, Server Components), see `references/nextjs.md`.
|
|
|
|
---
|
|
|
|
## Accessibility
|
|
|
|
Always add the reduced motion CSS from `references/css-recipes.md` to your global stylesheet.
|
|
|
|
---
|
|
|
|
## Reference Files
|
|
|
|
- **`references/implementation.md`** — Step-by-step implementation workflow.
|
|
- **`references/patterns.md`** — Patterns, animation timing, events API, troubleshooting.
|
|
- **`references/css-recipes.md`** — Ready-to-use CSS animation recipes.
|
|
- **`references/nextjs.md`** — Next.js App Router patterns and Server Component details.
|
|
|
|
## Full Compiled Document
|
|
|
|
For the complete guide with all reference files expanded: `AGENTS.md`
|