diff --git a/.agents/skills/vercel-react-best-practices/AGENTS.md b/.agents/skills/vercel-react-best-practices/AGENTS.md
new file mode 100644
index 000000000..db951abe7
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/AGENTS.md
@@ -0,0 +1,2934 @@
+# React Best Practices
+
+**Version 1.0.0**
+Vercel Engineering
+January 2026
+
+> **Note:**
+> This document is mainly for agents and LLMs to follow when maintaining,
+> generating, or refactoring React and Next.js codebases. Humans
+> may also find it useful, but guidance here is optimized for automation
+> and consistency by AI-assisted workflows.
+
+---
+
+## 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.
+
+---
+
+## Table of Contents
+
+1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
+ - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)
+ - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)
+ - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)
+ - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)
+ - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)
+2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
+ - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
+ - 2.2 [Conditional Module Loading](#22-conditional-module-loading)
+ - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
+ - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
+ - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
+3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
+ - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
+ - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
+ - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
+ - 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries)
+ - 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition)
+ - 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache)
+ - 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations)
+4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
+ - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
+ - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
+ - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
+ - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
+5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
+ - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
+ - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
+ - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
+ - 5.4 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#54-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
+ - 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components)
+ - 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies)
+ - 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers)
+ - 5.8 [Subscribe to Derived State](#58-subscribe-to-derived-state)
+ - 5.9 [Use Functional setState Updates](#59-use-functional-setstate-updates)
+ - 5.10 [Use Lazy State Initialization](#510-use-lazy-state-initialization)
+ - 5.11 [Use Transitions for Non-Urgent Updates](#511-use-transitions-for-non-urgent-updates)
+ - 5.12 [Use useRef for Transient Values](#512-use-useref-for-transient-values)
+6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
+ - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
+ - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
+ - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
+ - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
+ - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
+ - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
+ - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
+ - 6.8 [Use Explicit Conditional Rendering](#68-use-explicit-conditional-rendering)
+ - 6.9 [Use useTransition Over Manual Loading States](#69-use-usetransition-over-manual-loading-states)
+7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
+ - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
+ - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
+ - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
+ - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
+ - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
+ - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
+ - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)
+ - 7.8 [Early Return from Functions](#78-early-return-from-functions)
+ - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)
+ - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort)
+ - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)
+ - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)
+8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
+ - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
+ - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
+ - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
+
+---
+
+## 1. Eliminating Waterfalls
+
+**Impact: CRITICAL**
+
+Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
+
+### 1.1 Defer Await Until Needed
+
+**Impact: HIGH (avoids blocking unused code paths)**
+
+Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
+
+**Incorrect: blocks both branches**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ const userData = await fetchUserData(userId)
+
+ if (skipProcessing) {
+ // Returns immediately but still waited for userData
+ return { skipped: true }
+ }
+
+ // Only this branch uses userData
+ return processUserData(userData)
+}
+```
+
+**Correct: only blocks when needed**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ if (skipProcessing) {
+ // Returns immediately without waiting
+ return { skipped: true }
+ }
+
+ // Fetch only when needed
+ const userData = await fetchUserData(userId)
+ return processUserData(userData)
+}
+```
+
+**Another example: early return optimization**
+
+```typescript
+// Incorrect: always fetches permissions
+async function updateResource(resourceId: string, userId: string) {
+ const permissions = await fetchPermissions(userId)
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+
+// Correct: fetches only when needed
+async function updateResource(resourceId: string, userId: string) {
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ const permissions = await fetchPermissions(userId)
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+```
+
+This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
+
+### 1.2 Dependency-Based Parallelization
+
+**Impact: CRITICAL (2-10× improvement)**
+
+For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
+
+**Incorrect: profile waits for config unnecessarily**
+
+```typescript
+const [user, config] = await Promise.all([
+ fetchUser(),
+ fetchConfig()
+])
+const profile = await fetchProfile(user.id)
+```
+
+**Correct: config and profile run in parallel**
+
+```typescript
+import { all } from 'better-all'
+
+const { user, config, profile } = await all({
+ async user() { return fetchUser() },
+ async config() { return fetchConfig() },
+ async profile() {
+ return fetchProfile((await this.$.user).id)
+ }
+})
+```
+
+**Alternative without extra dependencies:**
+
+```typescript
+const userPromise = fetchUser()
+const profilePromise = userPromise.then(user => fetchProfile(user.id))
+
+const [user, config, profile] = await Promise.all([
+ userPromise,
+ fetchConfig(),
+ profilePromise
+])
+```
+
+We can also create all the promises first, and do `Promise.all()` at the end.
+
+Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
+
+### 1.3 Prevent Waterfall Chains in API Routes
+
+**Impact: CRITICAL (2-10× improvement)**
+
+In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
+
+**Incorrect: config waits for auth, data waits for both**
+
+```typescript
+export async function GET(request: Request) {
+ const session = await auth()
+ const config = await fetchConfig()
+ const data = await fetchData(session.user.id)
+ return Response.json({ data, config })
+}
+```
+
+**Correct: auth and config start immediately**
+
+```typescript
+export async function GET(request: Request) {
+ const sessionPromise = auth()
+ const configPromise = fetchConfig()
+ const session = await sessionPromise
+ const [config, data] = await Promise.all([
+ configPromise,
+ fetchData(session.user.id)
+ ])
+ return Response.json({ data, config })
+}
+```
+
+For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
+
+### 1.4 Promise.all() for Independent Operations
+
+**Impact: CRITICAL (2-10× improvement)**
+
+When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
+
+**Incorrect: sequential execution, 3 round trips**
+
+```typescript
+const user = await fetchUser()
+const posts = await fetchPosts()
+const comments = await fetchComments()
+```
+
+**Correct: parallel execution, 1 round trip**
+
+```typescript
+const [user, posts, comments] = await Promise.all([
+ fetchUser(),
+ fetchPosts(),
+ fetchComments()
+])
+```
+
+### 1.5 Strategic Suspense Boundaries
+
+**Impact: HIGH (faster initial paint)**
+
+Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
+
+**Incorrect: wrapper blocked by data fetching**
+
+```tsx
+async function Page() {
+ const data = await fetchData() // Blocks entire page
+
+ return (
+
+
Sidebar
+
Header
+
+
+
+
Footer
+
+ )
+}
+```
+
+The entire layout waits for data even though only the middle section needs it.
+
+**Correct: wrapper shows immediately, data streams in**
+
+```tsx
+function Page() {
+ return (
+
+
Sidebar
+
Header
+
+ }>
+
+
+
+
Footer
+
+ )
+}
+
+async function DataDisplay() {
+ const data = await fetchData() // Only blocks this component
+ return
{data.content}
+}
+```
+
+Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
+
+**Alternative: share promise across components**
+
+```tsx
+function Page() {
+ // Start fetch immediately, but don't await
+ const dataPromise = fetchData()
+
+ return (
+
+}
+```
+
+Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
+
+### 5.2 Defer State Reads to Usage Point
+
+**Impact: MEDIUM (avoids unnecessary subscriptions)**
+
+Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
+
+**Incorrect: subscribes to all searchParams changes**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const searchParams = useSearchParams()
+
+ const handleShare = () => {
+ const ref = searchParams.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
+}
+```
+
+**Correct: reads on demand, no subscription**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const handleShare = () => {
+ const params = new URLSearchParams(window.location.search)
+ const ref = params.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
+}
+```
+
+### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
+
+**Impact: LOW-MEDIUM (wasted computation on every render)**
+
+When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
+
+Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
+
+**Incorrect:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = useMemo(() => {
+ return user.isLoading || notifications.isLoading
+ }, [user.isLoading, notifications.isLoading])
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+**Correct:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = user.isLoading || notifications.isLoading
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+### 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+
+**Impact: MEDIUM (restores memoization by using a constant for default value)**
+
+When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
+
+To address this issue, extract the default value into a constant.
+
+**Incorrect: `onClick` has different values on every rerender**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+**Correct: stable default value**
+
+```tsx
+const NOOP = () => {};
+
+const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+### 5.5 Extract to Memoized Components
+
+**Impact: MEDIUM (enables early returns)**
+
+Extract expensive work into memoized components to enable early returns before computation.
+
+**Incorrect: computes avatar even when loading**
+
+```tsx
+function Profile({ user, loading }: Props) {
+ const avatar = useMemo(() => {
+ const id = computeAvatarId(user)
+ return
+ }, [user])
+
+ if (loading) return
+ return
{avatar}
+}
+```
+
+**Correct: skips computation when loading**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
+ const id = useMemo(() => computeAvatarId(user), [user])
+ return
+})
+
+function Profile({ user, loading }: Props) {
+ if (loading) return
+ return (
+
+
+
+ )
+}
+```
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
+
+### 5.6 Narrow Effect Dependencies
+
+**Impact: LOW (minimizes effect re-runs)**
+
+Specify primitive dependencies instead of objects to minimize effect re-runs.
+
+**Incorrect: re-runs on any user field change**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user])
+```
+
+**Correct: re-runs only when id changes**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user.id])
+```
+
+**For derived state, compute outside effect:**
+
+```tsx
+// Incorrect: runs on width=767, 766, 765...
+useEffect(() => {
+ if (width < 768) {
+ enableMobileMode()
+ }
+}, [width])
+
+// Correct: runs only on boolean transition
+const isMobile = width < 768
+useEffect(() => {
+ if (isMobile) {
+ enableMobileMode()
+ }
+}, [isMobile])
+```
+
+### 5.7 Put Interaction Logic in Event Handlers
+
+**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
+
+If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
+
+**Incorrect: event modeled as state + effect**
+
+```tsx
+function Form() {
+ const [submitted, setSubmitted] = useState(false)
+ const theme = useContext(ThemeContext)
+
+ useEffect(() => {
+ if (submitted) {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+ }, [submitted, theme])
+
+ return
+}
+```
+
+**Correct: do it in the handler**
+
+```tsx
+function Form() {
+ const theme = useContext(ThemeContext)
+
+ function handleSubmit() {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+
+ return
+}
+```
+
+Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
+
+### 5.8 Subscribe to Derived State
+
+**Impact: MEDIUM (reduces re-render frequency)**
+
+Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
+
+**Incorrect: re-renders on every pixel change**
+
+```tsx
+function Sidebar() {
+ const width = useWindowWidth() // updates continuously
+ const isMobile = width < 768
+ return
+}
+```
+
+**Correct: re-renders only when boolean changes**
+
+```tsx
+function Sidebar() {
+ const isMobile = useMediaQuery('(max-width: 767px)')
+ return
+}
+```
+
+### 5.9 Use Functional setState Updates
+
+**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
+
+When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
+
+**Incorrect: requires state as dependency**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Callback must depend on items, recreated on every items change
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems([...items, ...newItems])
+ }, [items]) // ❌ items dependency causes recreations
+
+ // Risk of stale closure if dependency is forgotten
+ const removeItem = useCallback((id: string) => {
+ setItems(items.filter(item => item.id !== id))
+ }, []) // ❌ Missing items dependency - will use stale items!
+
+ return
+}
+```
+
+The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
+
+**Correct: stable callbacks, no stale closures**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Stable callback, never recreated
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems(curr => [...curr, ...newItems])
+ }, []) // ✅ No dependencies needed
+
+ // Always uses latest state, no stale closure risk
+ const removeItem = useCallback((id: string) => {
+ setItems(curr => curr.filter(item => item.id !== id))
+ }, []) // ✅ Safe and stable
+
+ return
+}
+```
+
+**Benefits:**
+
+1. **Stable callback references** - Callbacks don't need to be recreated when state changes
+
+2. **No stale closures** - Always operates on the latest state value
+
+3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
+
+4. **Prevents bugs** - Eliminates the most common source of React closure bugs
+
+**When to use functional updates:**
+
+- Any setState that depends on the current state value
+
+- Inside useCallback/useMemo when state is needed
+
+- Event handlers that reference state
+
+- Async operations that update state
+
+**When direct updates are fine:**
+
+- Setting state to a static value: `setCount(0)`
+
+- Setting state from props/arguments only: `setName(newName)`
+
+- State doesn't depend on previous value
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
+
+### 5.10 Use Lazy State Initialization
+
+**Impact: MEDIUM (wasted computation on every render)**
+
+Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
+
+**Incorrect: runs on every render**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs on EVERY render, even after initialization
+ const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ // When query changes, buildSearchIndex runs again unnecessarily
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs on every render
+ const [settings, setSettings] = useState(
+ JSON.parse(localStorage.getItem('settings') || '{}')
+ )
+
+ return
+}
+```
+
+**Correct: runs only once**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs ONLY on initial render
+ const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs only on initial render
+ const [settings, setSettings] = useState(() => {
+ const stored = localStorage.getItem('settings')
+ return stored ? JSON.parse(stored) : {}
+ })
+
+ return
+}
+```
+
+Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
+
+For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
+
+### 5.11 Use Transitions for Non-Urgent Updates
+
+**Impact: MEDIUM (maintains UI responsiveness)**
+
+Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
+
+**Incorrect: blocks UI on every scroll**
+
+```tsx
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => setScrollY(window.scrollY)
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+**Correct: non-blocking updates**
+
+```tsx
+import { startTransition } from 'react'
+
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => {
+ startTransition(() => setScrollY(window.scrollY))
+ }
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+### 5.12 Use useRef for Transient Values
+
+**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
+
+When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
+
+**Incorrect: renders every update**
+
+```tsx
+function Tracker() {
+ const [lastX, setLastX] = useState(0)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => setLastX(e.clientX)
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+**Correct: no re-render for tracking**
+
+```tsx
+function Tracker() {
+ const lastXRef = useRef(0)
+ const dotRef = useRef(null)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ lastXRef.current = e.clientX
+ const node = dotRef.current
+ if (node) {
+ node.style.transform = `translateX(${e.clientX}px)`
+ }
+ }
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+---
+
+## 6. Rendering Performance
+
+**Impact: MEDIUM**
+
+Optimizing the rendering process reduces the work the browser needs to do.
+
+### 6.1 Animate SVG Wrapper Instead of SVG Element
+
+**Impact: LOW (enables hardware acceleration)**
+
+Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `
+ )
+}
+```
+
+This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
+
+### 6.4 Optimize SVG Precision
+
+**Impact: LOW (reduces file size)**
+
+Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
+
+**Incorrect: excessive precision**
+
+```svg
+
+```
+
+**Correct: 1 decimal place**
+
+```svg
+
+```
+
+**Automate with SVGO:**
+
+```bash
+npx svgo --precision=1 --multipass icon.svg
+```
+
+### 6.5 Prevent Hydration Mismatch Without Flickering
+
+**Impact: MEDIUM (avoids visual flicker and hydration errors)**
+
+When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
+
+**Incorrect: breaks SSR**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ // localStorage is not available on server - throws error
+ const theme = localStorage.getItem('theme') || 'light'
+
+ return (
+
+ )
+}
+```
+
+Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
+
+**Correct: no flicker, no hydration mismatch**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
+}
+```
+
+The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
+
+This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
+
+### 6.6 Suppress Expected Hydration Mismatches
+
+**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
+
+In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
+
+**Incorrect: known mismatch warnings**
+
+```tsx
+function Timestamp() {
+ return {new Date().toLocaleString()}
+}
+```
+
+**Correct: suppress expected mismatch only**
+
+```tsx
+function Timestamp() {
+ return (
+
+ {new Date().toLocaleString()}
+
+ )
+}
+```
+
+### 6.7 Use Activity Component for Show/Hide
+
+**Impact: MEDIUM (preserves state/DOM)**
+
+Use React's `` to preserve state/DOM for expensive components that frequently toggle visibility.
+
+**Usage:**
+
+```tsx
+import { Activity } from 'react'
+
+function Dropdown({ isOpen }: Props) {
+ return (
+
+
+
+ )
+}
+```
+
+Avoids expensive re-renders and state loss.
+
+### 6.8 Use Explicit Conditional Rendering
+
+**Impact: LOW (prevents rendering 0 or NaN)**
+
+Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
+
+**Incorrect: renders "0" when count is 0**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count && {count}}
+
+ )
+}
+
+// When count = 0, renders:
0
+// When count = 5, renders:
5
+```
+
+**Correct: renders nothing when count is 0**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count > 0 ? {count} : null}
+
+ )
+}
+
+// When count = 0, renders:
+// When count = 5, renders:
5
+```
+
+### 6.9 Use useTransition Over Manual Loading States
+
+**Impact: LOW (reduces re-renders and improves code clarity)**
+
+Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
+
+**Incorrect: manual loading state**
+
+```tsx
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSearch = async (value: string) => {
+ setIsLoading(true)
+ setQuery(value)
+ const data = await fetchResults(value)
+ setResults(data)
+ setIsLoading(false)
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isLoading && }
+
+ >
+ )
+}
+```
+
+**Correct: useTransition with built-in pending state**
+
+```tsx
+import { useTransition, useState } from 'react'
+
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isPending, startTransition] = useTransition()
+
+ const handleSearch = (value: string) => {
+ setQuery(value) // Update input immediately
+
+ startTransition(async () => {
+ // Fetch and update results
+ const data = await fetchResults(value)
+ setResults(data)
+ })
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isPending && }
+
+ >
+ )
+}
+```
+
+**Benefits:**
+
+- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
+
+- **Error resilience**: Pending state correctly resets even if the transition throws
+
+- **Better responsiveness**: Keeps the UI responsive during updates
+
+- **Interrupt handling**: New transitions automatically cancel pending ones
+
+Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
+
+---
+
+## 7. JavaScript Performance
+
+**Impact: LOW-MEDIUM**
+
+Micro-optimizations for hot paths can add up to meaningful improvements.
+
+### 7.1 Avoid Layout Thrashing
+
+**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
+
+Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
+
+**This is OK: browser batches style changes**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Each line invalidates style, but browser batches the recalculation
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+}
+```
+
+**Incorrect: interleaved reads and writes force reflows**
+
+```typescript
+function layoutThrashing(element: HTMLElement) {
+ element.style.width = '100px'
+ const width = element.offsetWidth // Forces reflow
+ element.style.height = '200px'
+ const height = element.offsetHeight // Forces another reflow
+}
+```
+
+**Correct: batch writes, then read once**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Batch all writes together
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+
+ // Read after all writes are done (single reflow)
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+
+**Correct: batch reads, then writes**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ element.classList.add('highlighted-box')
+
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+
+**Better: use CSS classes**
+
+**React example:**
+
+```tsx
+// Incorrect: interleaving style changes with layout queries
+function Box({ isHighlighted }: { isHighlighted: boolean }) {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ if (ref.current && isHighlighted) {
+ ref.current.style.width = '100px'
+ const width = ref.current.offsetWidth // Forces layout
+ ref.current.style.height = '200px'
+ }
+ }, [isHighlighted])
+
+ return
+}
+```
+
+**Why this matters in React:**
+
+1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
+
+2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
+
+**Browser support: fallback for older browsers**
+
+```typescript
+// Fallback for older browsers
+const sorted = [...items].sort((a, b) => a.value - b.value)
+```
+
+`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
+
+**Other immutable array methods:**
+
+- `.toSorted()` - immutable sort
+
+- `.toReversed()` - immutable reverse
+
+- `.toSpliced()` - immutable splice
+
+- `.with()` - immutable element replacement
+
+---
+
+## 8. Advanced Patterns
+
+**Impact: LOW**
+
+Advanced patterns for specific cases that require careful implementation.
+
+### 8.1 Initialize App Once, Not Per Mount
+
+**Impact: LOW-MEDIUM (avoids duplicate init in development)**
+
+Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
+
+**Incorrect: runs twice in dev, re-runs on remount**
+
+```tsx
+function Comp() {
+ useEffect(() => {
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+**Correct: once per app load**
+
+```tsx
+let didInit = false
+
+function Comp() {
+ useEffect(() => {
+ if (didInit) return
+ didInit = true
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
+
+### 8.2 Store Event Handlers in Refs
+
+**Impact: LOW (stable subscriptions)**
+
+Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
+
+**Incorrect: re-subscribes on every render**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ useEffect(() => {
+ window.addEventListener(event, handler)
+ return () => window.removeEventListener(event, handler)
+ }, [event, handler])
+}
+```
+
+**Correct: stable subscription**
+
+```tsx
+import { useEffectEvent } from 'react'
+
+function useWindowEvent(event: string, handler: (e) => void) {
+ const onEvent = useEffectEvent(handler)
+
+ useEffect(() => {
+ window.addEventListener(event, onEvent)
+ return () => window.removeEventListener(event, onEvent)
+ }, [event])
+}
+```
+
+**Alternative: use `useEffectEvent` if you're on latest React:**
+
+`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
+
+### 8.3 useEffectEvent for Stable Callback Refs
+
+**Impact: LOW (prevents effect re-runs)**
+
+Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
+
+**Incorrect: effect re-runs on every callback change**
+
+```tsx
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearch(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query, onSearch])
+}
+```
+
+**Correct: using React's useEffectEvent**
+
+```tsx
+import { useEffectEvent } from 'react';
+
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+ const onSearchEvent = useEffectEvent(onSearch)
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearchEvent(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query])
+}
+```
+
+---
+
+## References
+
+1. [https://react.dev](https://react.dev)
+2. [https://nextjs.org](https://nextjs.org)
+3. [https://swr.vercel.app](https://swr.vercel.app)
+4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
+5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
+6. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
+7. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
diff --git a/.agents/skills/vercel-react-best-practices/SKILL.md b/.agents/skills/vercel-react-best-practices/SKILL.md
new file mode 100644
index 000000000..1ad7750e5
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/SKILL.md
@@ -0,0 +1,136 @@
+---
+name: vercel-react-best-practices
+description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
+license: MIT
+metadata:
+ author: vercel
+ version: "1.0.0"
+---
+
+# Vercel React Best Practices
+
+Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 57 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
+
+## When to Apply
+
+Reference these guidelines when:
+- Writing new React components or Next.js pages
+- Implementing data fetching (client or server-side)
+- Reviewing code for performance issues
+- Refactoring existing React/Next.js code
+- Optimizing bundle size or load times
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Prefix |
+|----------|----------|--------|--------|
+| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
+| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
+| 3 | Server-Side Performance | HIGH | `server-` |
+| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
+| 5 | Re-render Optimization | MEDIUM | `rerender-` |
+| 6 | Rendering Performance | MEDIUM | `rendering-` |
+| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
+| 8 | Advanced Patterns | LOW | `advanced-` |
+
+## Quick Reference
+
+### 1. Eliminating Waterfalls (CRITICAL)
+
+- `async-defer-await` - Move await into branches where actually used
+- `async-parallel` - Use Promise.all() for independent operations
+- `async-dependencies` - Use better-all for partial dependencies
+- `async-api-routes` - Start promises early, await late in API routes
+- `async-suspense-boundaries` - Use Suspense to stream content
+
+### 2. Bundle Size Optimization (CRITICAL)
+
+- `bundle-barrel-imports` - Import directly, avoid barrel files
+- `bundle-dynamic-imports` - Use next/dynamic for heavy components
+- `bundle-defer-third-party` - Load analytics/logging after hydration
+- `bundle-conditional` - Load modules only when feature is activated
+- `bundle-preload` - Preload on hover/focus for perceived speed
+
+### 3. Server-Side Performance (HIGH)
+
+- `server-auth-actions` - Authenticate server actions like API routes
+- `server-cache-react` - Use React.cache() for per-request deduplication
+- `server-cache-lru` - Use LRU cache for cross-request caching
+- `server-dedup-props` - Avoid duplicate serialization in RSC props
+- `server-serialization` - Minimize data passed to client components
+- `server-parallel-fetching` - Restructure components to parallelize fetches
+- `server-after-nonblocking` - Use after() for non-blocking operations
+
+### 4. Client-Side Data Fetching (MEDIUM-HIGH)
+
+- `client-swr-dedup` - Use SWR for automatic request deduplication
+- `client-event-listeners` - Deduplicate global event listeners
+- `client-passive-event-listeners` - Use passive listeners for scroll
+- `client-localstorage-schema` - Version and minimize localStorage data
+
+### 5. Re-render Optimization (MEDIUM)
+
+- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
+- `rerender-memo` - Extract expensive work into memoized components
+- `rerender-memo-with-default-value` - Hoist default non-primitive props
+- `rerender-dependencies` - Use primitive dependencies in effects
+- `rerender-derived-state` - Subscribe to derived booleans, not raw values
+- `rerender-derived-state-no-effect` - Derive state during render, not effects
+- `rerender-functional-setstate` - Use functional setState for stable callbacks
+- `rerender-lazy-state-init` - Pass function to useState for expensive values
+- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
+- `rerender-move-effect-to-event` - Put interaction logic in event handlers
+- `rerender-transitions` - Use startTransition for non-urgent updates
+- `rerender-use-ref-transient-values` - Use refs for transient frequent values
+
+### 6. Rendering Performance (MEDIUM)
+
+- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
+- `rendering-content-visibility` - Use content-visibility for long lists
+- `rendering-hoist-jsx` - Extract static JSX outside components
+- `rendering-svg-precision` - Reduce SVG coordinate precision
+- `rendering-hydration-no-flicker` - Use inline script for client-only data
+- `rendering-hydration-suppress-warning` - Suppress expected mismatches
+- `rendering-activity` - Use Activity component for show/hide
+- `rendering-conditional-render` - Use ternary, not && for conditionals
+- `rendering-usetransition-loading` - Prefer useTransition for loading state
+
+### 7. JavaScript Performance (LOW-MEDIUM)
+
+- `js-batch-dom-css` - Group CSS changes via classes or cssText
+- `js-index-maps` - Build Map for repeated lookups
+- `js-cache-property-access` - Cache object properties in loops
+- `js-cache-function-results` - Cache function results in module-level Map
+- `js-cache-storage` - Cache localStorage/sessionStorage reads
+- `js-combine-iterations` - Combine multiple filter/map into one loop
+- `js-length-check-first` - Check array length before expensive comparison
+- `js-early-exit` - Return early from functions
+- `js-hoist-regexp` - Hoist RegExp creation outside loops
+- `js-min-max-loop` - Use loop for min/max instead of sort
+- `js-set-map-lookups` - Use Set/Map for O(1) lookups
+- `js-tosorted-immutable` - Use toSorted() for immutability
+
+### 8. Advanced Patterns (LOW)
+
+- `advanced-event-handler-refs` - Store event handlers in refs
+- `advanced-init-once` - Initialize app once per app load
+- `advanced-use-latest` - useLatest for stable callback refs
+
+## How to Use
+
+Read individual rule files for detailed explanations and code examples:
+
+```
+rules/async-parallel.md
+rules/bundle-barrel-imports.md
+```
+
+Each rule file contains:
+- Brief explanation of why it matters
+- Incorrect code example with explanation
+- Correct code example with explanation
+- Additional context and references
+
+## Full Compiled Document
+
+For the complete guide with all rules expanded: `AGENTS.md`
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md b/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md
new file mode 100644
index 000000000..97e7ade24
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md
@@ -0,0 +1,55 @@
+---
+title: Store Event Handlers in Refs
+impact: LOW
+impactDescription: stable subscriptions
+tags: advanced, hooks, refs, event-handlers, optimization
+---
+
+## Store Event Handlers in Refs
+
+Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
+
+**Incorrect (re-subscribes on every render):**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ useEffect(() => {
+ window.addEventListener(event, handler)
+ return () => window.removeEventListener(event, handler)
+ }, [event, handler])
+}
+```
+
+**Correct (stable subscription):**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ const handlerRef = useRef(handler)
+ useEffect(() => {
+ handlerRef.current = handler
+ }, [handler])
+
+ useEffect(() => {
+ const listener = (e) => handlerRef.current(e)
+ window.addEventListener(event, listener)
+ return () => window.removeEventListener(event, listener)
+ }, [event])
+}
+```
+
+**Alternative: use `useEffectEvent` if you're on latest React:**
+
+```tsx
+import { useEffectEvent } from 'react'
+
+function useWindowEvent(event: string, handler: (e) => void) {
+ const onEvent = useEffectEvent(handler)
+
+ useEffect(() => {
+ window.addEventListener(event, onEvent)
+ return () => window.removeEventListener(event, onEvent)
+ }, [event])
+}
+```
+
+`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md b/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md
new file mode 100644
index 000000000..73ee38e5e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md
@@ -0,0 +1,42 @@
+---
+title: Initialize App Once, Not Per Mount
+impact: LOW-MEDIUM
+impactDescription: avoids duplicate init in development
+tags: initialization, useEffect, app-startup, side-effects
+---
+
+## Initialize App Once, Not Per Mount
+
+Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
+
+**Incorrect (runs twice in dev, re-runs on remount):**
+
+```tsx
+function Comp() {
+ useEffect(() => {
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+**Correct (once per app load):**
+
+```tsx
+let didInit = false
+
+function Comp() {
+ useEffect(() => {
+ if (didInit) return
+ didInit = true
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md b/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md
new file mode 100644
index 000000000..9c7cb5016
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md
@@ -0,0 +1,39 @@
+---
+title: useEffectEvent for Stable Callback Refs
+impact: LOW
+impactDescription: prevents effect re-runs
+tags: advanced, hooks, useEffectEvent, refs, optimization
+---
+
+## useEffectEvent for Stable Callback Refs
+
+Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
+
+**Incorrect (effect re-runs on every callback change):**
+
+```tsx
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearch(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query, onSearch])
+}
+```
+
+**Correct (using React's useEffectEvent):**
+
+```tsx
+import { useEffectEvent } from 'react';
+
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+ const onSearchEvent = useEffectEvent(onSearch)
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearchEvent(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query])
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md b/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md
new file mode 100644
index 000000000..6feda1ef0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md
@@ -0,0 +1,38 @@
+---
+title: Prevent Waterfall Chains in API Routes
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: api-routes, server-actions, waterfalls, parallelization
+---
+
+## Prevent Waterfall Chains in API Routes
+
+In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
+
+**Incorrect (config waits for auth, data waits for both):**
+
+```typescript
+export async function GET(request: Request) {
+ const session = await auth()
+ const config = await fetchConfig()
+ const data = await fetchData(session.user.id)
+ return Response.json({ data, config })
+}
+```
+
+**Correct (auth and config start immediately):**
+
+```typescript
+export async function GET(request: Request) {
+ const sessionPromise = auth()
+ const configPromise = fetchConfig()
+ const session = await sessionPromise
+ const [config, data] = await Promise.all([
+ configPromise,
+ fetchData(session.user.id)
+ ])
+ return Response.json({ data, config })
+}
+```
+
+For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md b/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md
new file mode 100644
index 000000000..ea7082a36
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md
@@ -0,0 +1,80 @@
+---
+title: Defer Await Until Needed
+impact: HIGH
+impactDescription: avoids blocking unused code paths
+tags: async, await, conditional, optimization
+---
+
+## Defer Await Until Needed
+
+Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
+
+**Incorrect (blocks both branches):**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ const userData = await fetchUserData(userId)
+
+ if (skipProcessing) {
+ // Returns immediately but still waited for userData
+ return { skipped: true }
+ }
+
+ // Only this branch uses userData
+ return processUserData(userData)
+}
+```
+
+**Correct (only blocks when needed):**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ if (skipProcessing) {
+ // Returns immediately without waiting
+ return { skipped: true }
+ }
+
+ // Fetch only when needed
+ const userData = await fetchUserData(userId)
+ return processUserData(userData)
+}
+```
+
+**Another example (early return optimization):**
+
+```typescript
+// Incorrect: always fetches permissions
+async function updateResource(resourceId: string, userId: string) {
+ const permissions = await fetchPermissions(userId)
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+
+// Correct: fetches only when needed
+async function updateResource(resourceId: string, userId: string) {
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ const permissions = await fetchPermissions(userId)
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+```
+
+This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md b/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md
new file mode 100644
index 000000000..0484ebab9
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md
@@ -0,0 +1,51 @@
+---
+title: Dependency-Based Parallelization
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: async, parallelization, dependencies, better-all
+---
+
+## Dependency-Based Parallelization
+
+For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
+
+**Incorrect (profile waits for config unnecessarily):**
+
+```typescript
+const [user, config] = await Promise.all([
+ fetchUser(),
+ fetchConfig()
+])
+const profile = await fetchProfile(user.id)
+```
+
+**Correct (config and profile run in parallel):**
+
+```typescript
+import { all } from 'better-all'
+
+const { user, config, profile } = await all({
+ async user() { return fetchUser() },
+ async config() { return fetchConfig() },
+ async profile() {
+ return fetchProfile((await this.$.user).id)
+ }
+})
+```
+
+**Alternative without extra dependencies:**
+
+We can also create all the promises first, and do `Promise.all()` at the end.
+
+```typescript
+const userPromise = fetchUser()
+const profilePromise = userPromise.then(user => fetchProfile(user.id))
+
+const [user, config, profile] = await Promise.all([
+ userPromise,
+ fetchConfig(),
+ profilePromise
+])
+```
+
+Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-parallel.md b/.agents/skills/vercel-react-best-practices/rules/async-parallel.md
new file mode 100644
index 000000000..64133f6c3
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-parallel.md
@@ -0,0 +1,28 @@
+---
+title: Promise.all() for Independent Operations
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: async, parallelization, promises, waterfalls
+---
+
+## Promise.all() for Independent Operations
+
+When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
+
+**Incorrect (sequential execution, 3 round trips):**
+
+```typescript
+const user = await fetchUser()
+const posts = await fetchPosts()
+const comments = await fetchComments()
+```
+
+**Correct (parallel execution, 1 round trip):**
+
+```typescript
+const [user, posts, comments] = await Promise.all([
+ fetchUser(),
+ fetchPosts(),
+ fetchComments()
+])
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md b/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md
new file mode 100644
index 000000000..1fbc05b04
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md
@@ -0,0 +1,99 @@
+---
+title: Strategic Suspense Boundaries
+impact: HIGH
+impactDescription: faster initial paint
+tags: async, suspense, streaming, layout-shift
+---
+
+## Strategic Suspense Boundaries
+
+Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
+
+**Incorrect (wrapper blocked by data fetching):**
+
+```tsx
+async function Page() {
+ const data = await fetchData() // Blocks entire page
+
+ return (
+
+
Sidebar
+
Header
+
+
+
+
Footer
+
+ )
+}
+```
+
+The entire layout waits for data even though only the middle section needs it.
+
+**Correct (wrapper shows immediately, data streams in):**
+
+```tsx
+function Page() {
+ return (
+
+
Sidebar
+
Header
+
+ }>
+
+
+
+
Footer
+
+ )
+}
+
+async function DataDisplay() {
+ const data = await fetchData() // Only blocks this component
+ return
{data.content}
+}
+```
+
+Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
+
+**Alternative (share promise across components):**
+
+```tsx
+function Page() {
+ // Start fetch immediately, but don't await
+ const dataPromise = fetchData()
+
+ return (
+
+ )
+}
+```
+
+Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
+
+See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md b/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md
new file mode 100644
index 000000000..180f8ac8f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md
@@ -0,0 +1,80 @@
+---
+title: Cache Repeated Function Calls
+impact: MEDIUM
+impactDescription: avoid redundant computation
+tags: javascript, cache, memoization, performance
+---
+
+## Cache Repeated Function Calls
+
+Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
+
+**Incorrect (redundant computation):**
+
+```typescript
+function ProjectList({ projects }: { projects: Project[] }) {
+ return (
+
+ {projects.map(project => {
+ // slugify() called 100+ times for same project names
+ const slug = slugify(project.name)
+
+ return
+ })}
+
+ )
+}
+```
+
+This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md b/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md
new file mode 100644
index 000000000..7e866f585
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md
@@ -0,0 +1,40 @@
+---
+title: Use Explicit Conditional Rendering
+impact: LOW
+impactDescription: prevents rendering 0 or NaN
+tags: rendering, conditional, jsx, falsy-values
+---
+
+## Use Explicit Conditional Rendering
+
+Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
+
+**Incorrect (renders "0" when count is 0):**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count && {count}}
+
+ )
+}
+
+// When count = 0, renders:
0
+// When count = 5, renders:
5
+```
+
+**Correct (renders nothing when count is 0):**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count > 0 ? {count} : null}
+
+ )
+}
+
+// When count = 0, renders:
+// When count = 5, renders:
+ )
+}
+```
+
+This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md
new file mode 100644
index 000000000..5cf0e79b6
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md
@@ -0,0 +1,82 @@
+---
+title: Prevent Hydration Mismatch Without Flickering
+impact: MEDIUM
+impactDescription: avoids visual flicker and hydration errors
+tags: rendering, ssr, hydration, localStorage, flicker
+---
+
+## Prevent Hydration Mismatch Without Flickering
+
+When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
+
+**Incorrect (breaks SSR):**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ // localStorage is not available on server - throws error
+ const theme = localStorage.getItem('theme') || 'light'
+
+ return (
+
+ )
+}
+```
+
+Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
+
+**Correct (no flicker, no hydration mismatch):**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
+}
+```
+
+The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
+
+This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md
new file mode 100644
index 000000000..24ba2513a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md
@@ -0,0 +1,30 @@
+---
+title: Suppress Expected Hydration Mismatches
+impact: LOW-MEDIUM
+impactDescription: avoids noisy hydration warnings for known differences
+tags: rendering, hydration, ssr, nextjs
+---
+
+## Suppress Expected Hydration Mismatches
+
+In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
+
+**Incorrect (known mismatch warnings):**
+
+```tsx
+function Timestamp() {
+ return {new Date().toLocaleString()}
+}
+```
+
+**Correct (suppress expected mismatch only):**
+
+```tsx
+function Timestamp() {
+ return (
+
+ {new Date().toLocaleString()}
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md b/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md
new file mode 100644
index 000000000..6d7712860
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md
@@ -0,0 +1,28 @@
+---
+title: Optimize SVG Precision
+impact: LOW
+impactDescription: reduces file size
+tags: rendering, svg, optimization, svgo
+---
+
+## Optimize SVG Precision
+
+Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
+
+**Incorrect (excessive precision):**
+
+```svg
+
+```
+
+**Correct (1 decimal place):**
+
+```svg
+
+```
+
+**Automate with SVGO:**
+
+```bash
+npx svgo --precision=1 --multipass icon.svg
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md b/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md
new file mode 100644
index 000000000..0c1b0b98e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md
@@ -0,0 +1,75 @@
+---
+title: Use useTransition Over Manual Loading States
+impact: LOW
+impactDescription: reduces re-renders and improves code clarity
+tags: rendering, transitions, useTransition, loading, state
+---
+
+## Use useTransition Over Manual Loading States
+
+Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
+
+**Incorrect (manual loading state):**
+
+```tsx
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSearch = async (value: string) => {
+ setIsLoading(true)
+ setQuery(value)
+ const data = await fetchResults(value)
+ setResults(data)
+ setIsLoading(false)
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isLoading && }
+
+ >
+ )
+}
+```
+
+**Correct (useTransition with built-in pending state):**
+
+```tsx
+import { useTransition, useState } from 'react'
+
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isPending, startTransition] = useTransition()
+
+ const handleSearch = (value: string) => {
+ setQuery(value) // Update input immediately
+
+ startTransition(async () => {
+ // Fetch and update results
+ const data = await fetchResults(value)
+ setResults(data)
+ })
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isPending && }
+
+ >
+ )
+}
+```
+
+**Benefits:**
+
+- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
+- **Error resilience**: Pending state correctly resets even if the transition throws
+- **Better responsiveness**: Keeps the UI responsive during updates
+- **Interrupt handling**: New transitions automatically cancel pending ones
+
+Reference: [useTransition](https://react.dev/reference/react/useTransition)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md b/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md
new file mode 100644
index 000000000..e867c95f0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md
@@ -0,0 +1,39 @@
+---
+title: Defer State Reads to Usage Point
+impact: MEDIUM
+impactDescription: avoids unnecessary subscriptions
+tags: rerender, searchParams, localStorage, optimization
+---
+
+## Defer State Reads to Usage Point
+
+Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
+
+**Incorrect (subscribes to all searchParams changes):**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const searchParams = useSearchParams()
+
+ const handleShare = () => {
+ const ref = searchParams.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
+}
+```
+
+**Correct (reads on demand, no subscription):**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const handleShare = () => {
+ const params = new URLSearchParams(window.location.search)
+ const ref = params.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md b/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md
new file mode 100644
index 000000000..47a4d9268
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md
@@ -0,0 +1,45 @@
+---
+title: Narrow Effect Dependencies
+impact: LOW
+impactDescription: minimizes effect re-runs
+tags: rerender, useEffect, dependencies, optimization
+---
+
+## Narrow Effect Dependencies
+
+Specify primitive dependencies instead of objects to minimize effect re-runs.
+
+**Incorrect (re-runs on any user field change):**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user])
+```
+
+**Correct (re-runs only when id changes):**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user.id])
+```
+
+**For derived state, compute outside effect:**
+
+```tsx
+// Incorrect: runs on width=767, 766, 765...
+useEffect(() => {
+ if (width < 768) {
+ enableMobileMode()
+ }
+}, [width])
+
+// Correct: runs only on boolean transition
+const isMobile = width < 768
+useEffect(() => {
+ if (isMobile) {
+ enableMobileMode()
+ }
+}, [isMobile])
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
new file mode 100644
index 000000000..3d9fe4050
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
@@ -0,0 +1,40 @@
+---
+title: Calculate Derived State During Rendering
+impact: MEDIUM
+impactDescription: avoids redundant renders and state drift
+tags: rerender, derived-state, useEffect, state
+---
+
+## Calculate Derived State During Rendering
+
+If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
+
+**Incorrect (redundant state and effect):**
+
+```tsx
+function Form() {
+ const [firstName, setFirstName] = useState('First')
+ const [lastName, setLastName] = useState('Last')
+ const [fullName, setFullName] = useState('')
+
+ useEffect(() => {
+ setFullName(firstName + ' ' + lastName)
+ }, [firstName, lastName])
+
+ return
+}
+```
+
+References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md
new file mode 100644
index 000000000..e5c899f6c
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md
@@ -0,0 +1,29 @@
+---
+title: Subscribe to Derived State
+impact: MEDIUM
+impactDescription: reduces re-render frequency
+tags: rerender, derived-state, media-query, optimization
+---
+
+## Subscribe to Derived State
+
+Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
+
+**Incorrect (re-renders on every pixel change):**
+
+```tsx
+function Sidebar() {
+ const width = useWindowWidth() // updates continuously
+ const isMobile = width < 768
+ return
+}
+```
+
+**Correct (re-renders only when boolean changes):**
+
+```tsx
+function Sidebar() {
+ const isMobile = useMediaQuery('(max-width: 767px)')
+ return
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md b/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md
new file mode 100644
index 000000000..b004ef45e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md
@@ -0,0 +1,74 @@
+---
+title: Use Functional setState Updates
+impact: MEDIUM
+impactDescription: prevents stale closures and unnecessary callback recreations
+tags: react, hooks, useState, useCallback, callbacks, closures
+---
+
+## Use Functional setState Updates
+
+When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
+
+**Incorrect (requires state as dependency):**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Callback must depend on items, recreated on every items change
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems([...items, ...newItems])
+ }, [items]) // ❌ items dependency causes recreations
+
+ // Risk of stale closure if dependency is forgotten
+ const removeItem = useCallback((id: string) => {
+ setItems(items.filter(item => item.id !== id))
+ }, []) // ❌ Missing items dependency - will use stale items!
+
+ return
+}
+```
+
+The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
+
+**Correct (stable callbacks, no stale closures):**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Stable callback, never recreated
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems(curr => [...curr, ...newItems])
+ }, []) // ✅ No dependencies needed
+
+ // Always uses latest state, no stale closure risk
+ const removeItem = useCallback((id: string) => {
+ setItems(curr => curr.filter(item => item.id !== id))
+ }, []) // ✅ Safe and stable
+
+ return
+}
+```
+
+**Benefits:**
+
+1. **Stable callback references** - Callbacks don't need to be recreated when state changes
+2. **No stale closures** - Always operates on the latest state value
+3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
+4. **Prevents bugs** - Eliminates the most common source of React closure bugs
+
+**When to use functional updates:**
+
+- Any setState that depends on the current state value
+- Inside useCallback/useMemo when state is needed
+- Event handlers that reference state
+- Async operations that update state
+
+**When direct updates are fine:**
+
+- Setting state to a static value: `setCount(0)`
+- Setting state from props/arguments only: `setName(newName)`
+- State doesn't depend on previous value
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md b/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md
new file mode 100644
index 000000000..4ecb350fb
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md
@@ -0,0 +1,58 @@
+---
+title: Use Lazy State Initialization
+impact: MEDIUM
+impactDescription: wasted computation on every render
+tags: react, hooks, useState, performance, initialization
+---
+
+## Use Lazy State Initialization
+
+Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
+
+**Incorrect (runs on every render):**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs on EVERY render, even after initialization
+ const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ // When query changes, buildSearchIndex runs again unnecessarily
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs on every render
+ const [settings, setSettings] = useState(
+ JSON.parse(localStorage.getItem('settings') || '{}')
+ )
+
+ return
+}
+```
+
+**Correct (runs only once):**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs ONLY on initial render
+ const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs only on initial render
+ const [settings, setSettings] = useState(() => {
+ const stored = localStorage.getItem('settings')
+ return stored ? JSON.parse(stored) : {}
+ })
+
+ return
+}
+```
+
+Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
+
+For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md b/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md
new file mode 100644
index 000000000..635704918
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md
@@ -0,0 +1,38 @@
+---
+
+title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+impact: MEDIUM
+impactDescription: restores memoization by using a constant for default value
+tags: rerender, memo, optimization
+
+---
+
+## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+
+When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
+
+To address this issue, extract the default value into a constant.
+
+**Incorrect (`onClick` has different values on every rerender):**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+**Correct (stable default value):**
+
+```tsx
+const NOOP = () => {};
+
+const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md b/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md
new file mode 100644
index 000000000..f8982ab61
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md
@@ -0,0 +1,44 @@
+---
+title: Extract to Memoized Components
+impact: MEDIUM
+impactDescription: enables early returns
+tags: rerender, memo, useMemo, optimization
+---
+
+## Extract to Memoized Components
+
+Extract expensive work into memoized components to enable early returns before computation.
+
+**Incorrect (computes avatar even when loading):**
+
+```tsx
+function Profile({ user, loading }: Props) {
+ const avatar = useMemo(() => {
+ const id = computeAvatarId(user)
+ return
+ }, [user])
+
+ if (loading) return
+ return
{avatar}
+}
+```
+
+**Correct (skips computation when loading):**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
+ const id = useMemo(() => computeAvatarId(user), [user])
+ return
+})
+
+function Profile({ user, loading }: Props) {
+ if (loading) return
+ return (
+
+
+
+ )
+}
+```
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md b/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md
new file mode 100644
index 000000000..dd58a1af0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md
@@ -0,0 +1,45 @@
+---
+title: Put Interaction Logic in Event Handlers
+impact: MEDIUM
+impactDescription: avoids effect re-runs and duplicate side effects
+tags: rerender, useEffect, events, side-effects, dependencies
+---
+
+## Put Interaction Logic in Event Handlers
+
+If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
+
+**Incorrect (event modeled as state + effect):**
+
+```tsx
+function Form() {
+ const [submitted, setSubmitted] = useState(false)
+ const theme = useContext(ThemeContext)
+
+ useEffect(() => {
+ if (submitted) {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+ }, [submitted, theme])
+
+ return
+}
+```
+
+**Correct (do it in the handler):**
+
+```tsx
+function Form() {
+ const theme = useContext(ThemeContext)
+
+ function handleSubmit() {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+
+ return
+}
+```
+
+Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md b/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md
new file mode 100644
index 000000000..59dfab0f3
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md
@@ -0,0 +1,35 @@
+---
+title: Do not wrap a simple expression with a primitive result type in useMemo
+impact: LOW-MEDIUM
+impactDescription: wasted computation on every render
+tags: rerender, useMemo, optimization
+---
+
+## Do not wrap a simple expression with a primitive result type in useMemo
+
+When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
+Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
+
+**Incorrect:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = useMemo(() => {
+ return user.isLoading || notifications.isLoading
+ }, [user.isLoading, notifications.isLoading])
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+**Correct:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = user.isLoading || notifications.isLoading
+
+ if (isLoading) return
+ // return some markup
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md b/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md
new file mode 100644
index 000000000..d99f43f76
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md
@@ -0,0 +1,40 @@
+---
+title: Use Transitions for Non-Urgent Updates
+impact: MEDIUM
+impactDescription: maintains UI responsiveness
+tags: rerender, transitions, startTransition, performance
+---
+
+## Use Transitions for Non-Urgent Updates
+
+Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
+
+**Incorrect (blocks UI on every scroll):**
+
+```tsx
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => setScrollY(window.scrollY)
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+**Correct (non-blocking updates):**
+
+```tsx
+import { startTransition } from 'react'
+
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => {
+ startTransition(() => setScrollY(window.scrollY))
+ }
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md b/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md
new file mode 100644
index 000000000..cf04b81f8
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md
@@ -0,0 +1,73 @@
+---
+title: Use useRef for Transient Values
+impact: MEDIUM
+impactDescription: avoids unnecessary re-renders on frequent updates
+tags: rerender, useref, state, performance
+---
+
+## Use useRef for Transient Values
+
+When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
+
+**Incorrect (renders every update):**
+
+```tsx
+function Tracker() {
+ const [lastX, setLastX] = useState(0)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => setLastX(e.clientX)
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+**Correct (no re-render for tracking):**
+
+```tsx
+function Tracker() {
+ const lastXRef = useRef(0)
+ const dotRef = useRef(null)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ lastXRef.current = e.clientX
+ const node = dotRef.current
+ if (node) {
+ node.style.transform = `translateX(${e.clientX}px)`
+ }
+ }
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md b/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md
new file mode 100644
index 000000000..e8f5b260f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md
@@ -0,0 +1,73 @@
+---
+title: Use after() for Non-Blocking Operations
+impact: MEDIUM
+impactDescription: faster response times
+tags: server, async, logging, analytics, side-effects
+---
+
+## Use after() for Non-Blocking Operations
+
+Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
+
+**Incorrect (blocks response):**
+
+```tsx
+import { logUserAction } from '@/app/utils'
+
+export async function POST(request: Request) {
+ // Perform mutation
+ await updateDatabase(request)
+
+ // Logging blocks the response
+ const userAgent = request.headers.get('user-agent') || 'unknown'
+ await logUserAction({ userAgent })
+
+ return new Response(JSON.stringify({ status: 'success' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+```
+
+**Correct (non-blocking):**
+
+```tsx
+import { after } from 'next/server'
+import { headers, cookies } from 'next/headers'
+import { logUserAction } from '@/app/utils'
+
+export async function POST(request: Request) {
+ // Perform mutation
+ await updateDatabase(request)
+
+ // Log after response is sent
+ after(async () => {
+ const userAgent = (await headers()).get('user-agent') || 'unknown'
+ const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
+
+ logUserAction({ sessionCookie, userAgent })
+ })
+
+ return new Response(JSON.stringify({ status: 'success' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+```
+
+The response is sent immediately while logging happens in the background.
+
+**Common use cases:**
+
+- Analytics tracking
+- Audit logging
+- Sending notifications
+- Cache invalidation
+- Cleanup tasks
+
+**Important notes:**
+
+- `after()` runs even if the response fails or redirects
+- Works in Server Actions, Route Handlers, and Server Components
+
+Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md b/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md
new file mode 100644
index 000000000..ee82c0442
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md
@@ -0,0 +1,96 @@
+---
+title: Authenticate Server Actions Like API Routes
+impact: CRITICAL
+impactDescription: prevents unauthorized access to server mutations
+tags: server, server-actions, authentication, security, authorization
+---
+
+## Authenticate Server Actions Like API Routes
+
+**Impact: CRITICAL (prevents unauthorized access to server mutations)**
+
+Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
+
+Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
+
+**Incorrect (no authentication check):**
+
+```typescript
+'use server'
+
+export async function deleteUser(userId: string) {
+ // Anyone can call this! No auth check
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**Correct (authentication inside the action):**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { unauthorized } from '@/lib/errors'
+
+export async function deleteUser(userId: string) {
+ // Always check auth inside the action
+ const session = await verifySession()
+
+ if (!session) {
+ throw unauthorized('Must be logged in')
+ }
+
+ // Check authorization too
+ if (session.user.role !== 'admin' && session.user.id !== userId) {
+ throw unauthorized('Cannot delete other users')
+ }
+
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**With input validation:**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { z } from 'zod'
+
+const updateProfileSchema = z.object({
+ userId: z.string().uuid(),
+ name: z.string().min(1).max(100),
+ email: z.string().email()
+})
+
+export async function updateProfile(data: unknown) {
+ // Validate input first
+ const validated = updateProfileSchema.parse(data)
+
+ // Then authenticate
+ const session = await verifySession()
+ if (!session) {
+ throw new Error('Unauthorized')
+ }
+
+ // Then authorize
+ if (session.user.id !== validated.userId) {
+ throw new Error('Can only update own profile')
+ }
+
+ // Finally perform the mutation
+ await db.user.update({
+ where: { id: validated.userId },
+ data: {
+ name: validated.name,
+ email: validated.email
+ }
+ })
+
+ return { success: true }
+}
+```
+
+Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md b/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md
new file mode 100644
index 000000000..ef6938aa5
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md
@@ -0,0 +1,41 @@
+---
+title: Cross-Request LRU Caching
+impact: HIGH
+impactDescription: caches across requests
+tags: server, cache, lru, cross-request
+---
+
+## Cross-Request LRU Caching
+
+`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
+
+**Implementation:**
+
+```typescript
+import { LRUCache } from 'lru-cache'
+
+const cache = new LRUCache({
+ max: 1000,
+ ttl: 5 * 60 * 1000 // 5 minutes
+})
+
+export async function getUser(id: string) {
+ const cached = cache.get(id)
+ if (cached) return cached
+
+ const user = await db.user.findUnique({ where: { id } })
+ cache.set(id, user)
+ return user
+}
+
+// Request 1: DB query, result cached
+// Request 2: cache hit, no DB query
+```
+
+Use when sequential user actions hit multiple endpoints needing the same data within seconds.
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
+
+**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
+
+Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md b/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md
new file mode 100644
index 000000000..87c9ca331
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md
@@ -0,0 +1,76 @@
+---
+title: Per-Request Deduplication with React.cache()
+impact: MEDIUM
+impactDescription: deduplicates within request
+tags: server, cache, react-cache, deduplication
+---
+
+## Per-Request Deduplication with React.cache()
+
+Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
+
+**Usage:**
+
+```typescript
+import { cache } from 'react'
+
+export const getCurrentUser = cache(async () => {
+ const session = await auth()
+ if (!session?.user?.id) return null
+ return await db.user.findUnique({
+ where: { id: session.user.id }
+ })
+})
+```
+
+Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
+
+**Avoid inline objects as arguments:**
+
+`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
+
+**Incorrect (always cache miss):**
+
+```typescript
+const getUser = cache(async (params: { uid: number }) => {
+ return await db.user.findUnique({ where: { id: params.uid } })
+})
+
+// Each call creates new object, never hits cache
+getUser({ uid: 1 })
+getUser({ uid: 1 }) // Cache miss, runs query again
+```
+
+**Correct (cache hit):**
+
+```typescript
+const getUser = cache(async (uid: number) => {
+ return await db.user.findUnique({ where: { id: uid } })
+})
+
+// Primitive args use value equality
+getUser(1)
+getUser(1) // Cache hit, returns cached result
+```
+
+If you must pass objects, pass the same reference:
+
+```typescript
+const params = { uid: 1 }
+getUser(params) // Query runs
+getUser(params) // Cache hit (same reference)
+```
+
+**Next.js-Specific Note:**
+
+In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
+
+- Database queries (Prisma, Drizzle, etc.)
+- Heavy computations
+- Authentication checks
+- File system operations
+- Any non-fetch async work
+
+Use `React.cache()` to deduplicate these operations across your component tree.
+
+Reference: [React.cache documentation](https://react.dev/reference/react/cache)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md b/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md
new file mode 100644
index 000000000..fb24a2562
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md
@@ -0,0 +1,65 @@
+---
+title: Avoid Duplicate Serialization in RSC Props
+impact: LOW
+impactDescription: reduces network payload by avoiding duplicate serialization
+tags: server, rsc, serialization, props, client-components
+---
+
+## Avoid Duplicate Serialization in RSC Props
+
+**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
+
+RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
+
+**Incorrect (duplicates array):**
+
+```tsx
+// RSC: sends 6 strings (2 arrays × 3 items)
+
+```
+
+**Correct (sends 3 strings):**
+
+```tsx
+// RSC: send once
+
+
+// Client: transform there
+'use client'
+const sorted = useMemo(() => [...usernames].sort(), [usernames])
+```
+
+**Nested deduplication behavior:**
+
+Deduplication works recursively. Impact varies by data type:
+
+- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
+- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
+
+```tsx
+// string[] - duplicates everything
+usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
+
+// object[] - duplicates array structure only
+users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
+```
+
+**Operations breaking deduplication (create new references):**
+
+- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
+- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
+
+**More examples:**
+
+```tsx
+// ❌ Bad
+ u.active)} />
+
+
+// ✅ Good
+
+
+// Do filtering/destructuring in client
+```
+
+**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md b/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md
new file mode 100644
index 000000000..1affc835a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md
@@ -0,0 +1,83 @@
+---
+title: Parallel Data Fetching with Component Composition
+impact: CRITICAL
+impactDescription: eliminates server-side waterfalls
+tags: server, rsc, parallel-fetching, composition
+---
+
+## Parallel Data Fetching with Component Composition
+
+React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
+
+**Incorrect (Sidebar waits for Page's fetch to complete):**
+
+```tsx
+export default async function Page() {
+ const header = await fetchHeader()
+ return (
+
+ )
+}
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-serialization.md b/.agents/skills/vercel-react-best-practices/rules/server-serialization.md
new file mode 100644
index 000000000..39c5c4164
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-serialization.md
@@ -0,0 +1,38 @@
+---
+title: Minimize Serialization at RSC Boundaries
+impact: HIGH
+impactDescription: reduces data transfer size
+tags: server, rsc, serialization, props
+---
+
+## Minimize Serialization at RSC Boundaries
+
+The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
+
+**Incorrect (serializes all 50 fields):**
+
+```tsx
+async function Page() {
+ const user = await fetchUser() // 50 fields
+ return
+}
+
+'use client'
+function Profile({ user }: { user: User }) {
+ return
{user.name}
// uses 1 field
+}
+```
+
+**Correct (serializes only 1 field):**
+
+```tsx
+async function Page() {
+ const user = await fetchUser()
+ return
+}
+
+'use client'
+function Profile({ name }: { name: string }) {
+ return
{name}
+}
+```
diff --git a/.all-contributorsrc b/.all-contributorsrc
index f57081a2f..ff7338bdb 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -628,6 +628,24 @@
"contributions": [
"bug"
]
+ },
+ {
+ "login": "ZakaryaCH",
+ "name": "ZakaryaCH",
+ "avatar_url": "https://avatars.githubusercontent.com/u/45012800?v=4",
+ "profile": "https://github.com/ZakaryaCH",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "jboeijenga",
+ "name": "Jasper Boeijenga",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1516438?v=4",
+ "profile": "https://github.com/jboeijenga",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/.astro/content-assets.mjs b/.astro/content-assets.mjs
deleted file mode 100644
index 2b8b8234b..000000000
--- a/.astro/content-assets.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export default new Map();
\ No newline at end of file
diff --git a/.astro/content-modules.mjs b/.astro/content-modules.mjs
deleted file mode 100644
index 2b8b8234b..000000000
--- a/.astro/content-modules.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export default new Map();
\ No newline at end of file
diff --git a/.astro/content.d.ts b/.astro/content.d.ts
deleted file mode 100644
index c0082cc81..000000000
--- a/.astro/content.d.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-declare module 'astro:content' {
- export interface RenderResult {
- Content: import('astro/runtime/server/index.js').AstroComponentFactory;
- headings: import('astro').MarkdownHeading[];
- remarkPluginFrontmatter: Record;
- }
- interface Render {
- '.md': Promise;
- }
-
- export interface RenderedContent {
- html: string;
- metadata?: {
- imagePaths: Array;
- [key: string]: unknown;
- };
- }
-}
-
-declare module 'astro:content' {
- type Flatten = T extends { [K: string]: infer U } ? U : never;
-
- export type CollectionKey = keyof AnyEntryMap;
- export type CollectionEntry = Flatten;
-
- export type ContentCollectionKey = keyof ContentEntryMap;
- export type DataCollectionKey = keyof DataEntryMap;
-
- type AllValuesOf = T extends any ? T[keyof T] : never;
- type ValidContentEntrySlug = AllValuesOf<
- ContentEntryMap[C]
- >['slug'];
-
- export type ReferenceDataEntry<
- C extends CollectionKey,
- E extends keyof DataEntryMap[C] = string,
- > = {
- collection: C;
- id: E;
- };
- export type ReferenceContentEntry<
- C extends keyof ContentEntryMap,
- E extends ValidContentEntrySlug | (string & {}) = string,
- > = {
- collection: C;
- slug: E;
- };
- export type ReferenceLiveEntry = {
- collection: C;
- id: string;
- };
-
- /** @deprecated Use `getEntry` instead. */
- export function getEntryBySlug<
- C extends keyof ContentEntryMap,
- E extends ValidContentEntrySlug | (string & {}),
- >(
- collection: C,
- // Note that this has to accept a regular string too, for SSR
- entrySlug: E,
- ): E extends ValidContentEntrySlug
- ? Promise>
- : Promise | undefined>;
-
- /** @deprecated Use `getEntry` instead. */
- export function getDataEntryById(
- collection: C,
- entryId: E,
- ): Promise>;
-
- export function getCollection>(
- collection: C,
- filter?: (entry: CollectionEntry) => entry is E,
- ): Promise;
- export function getCollection(
- collection: C,
- filter?: (entry: CollectionEntry) => unknown,
- ): Promise[]>;
-
- export function getLiveCollection(
- collection: C,
- filter?: LiveLoaderCollectionFilterType,
- ): Promise<
- import('astro').LiveDataCollectionResult, LiveLoaderErrorType>
- >;
-
- export function getEntry<
- C extends keyof ContentEntryMap,
- E extends ValidContentEntrySlug | (string & {}),
- >(
- entry: ReferenceContentEntry,
- ): E extends ValidContentEntrySlug
- ? Promise>
- : Promise | undefined>;
- export function getEntry<
- C extends keyof DataEntryMap,
- E extends keyof DataEntryMap[C] | (string & {}),
- >(
- entry: ReferenceDataEntry,
- ): E extends keyof DataEntryMap[C]
- ? Promise
- : Promise | undefined>;
- export function getEntry<
- C extends keyof ContentEntryMap,
- E extends ValidContentEntrySlug | (string & {}),
- >(
- collection: C,
- slug: E,
- ): E extends ValidContentEntrySlug
- ? Promise>
- : Promise | undefined>;
- export function getEntry<
- C extends keyof DataEntryMap,
- E extends keyof DataEntryMap[C] | (string & {}),
- >(
- collection: C,
- id: E,
- ): E extends keyof DataEntryMap[C]
- ? string extends keyof DataEntryMap[C]
- ? Promise | undefined
- : Promise
- : Promise | undefined>;
- export function getLiveEntry(
- collection: C,
- filter: string | LiveLoaderEntryFilterType,
- ): Promise, LiveLoaderErrorType>>;
-
- /** Resolve an array of entry references from the same collection */
- export function getEntries(
- entries: ReferenceContentEntry>[],
- ): Promise[]>;
- export function getEntries(
- entries: ReferenceDataEntry[],
- ): Promise[]>;
-
- export function render(
- entry: AnyEntryMap[C][string],
- ): Promise;
-
- export function reference(
- collection: C,
- ): import('astro/zod').ZodEffects<
- import('astro/zod').ZodString,
- C extends keyof ContentEntryMap
- ? ReferenceContentEntry>
- : ReferenceDataEntry
- >;
- // Allow generic `string` to avoid excessive type errors in the config
- // if `dev` is not running to update as you edit.
- // Invalid collection names will be caught at build time.
- export function reference(
- collection: C,
- ): import('astro/zod').ZodEffects;
-
- type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T;
- type InferEntrySchema = import('astro/zod').infer<
- ReturnTypeOrOriginal['schema']>
- >;
-
- type ContentEntryMap = {
-
- };
-
- type DataEntryMap = {
-
- };
-
- type AnyEntryMap = ContentEntryMap & DataEntryMap;
-
- type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader<
- infer TData,
- infer TEntryFilter,
- infer TCollectionFilter,
- infer TError
- >
- ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
- : { data: never; entryFilter: never; collectionFilter: never; error: never };
- type ExtractDataType = ExtractLoaderTypes['data'];
- type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter'];
- type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter'];
- type ExtractErrorType = ExtractLoaderTypes['error'];
-
- type LiveLoaderDataType =
- LiveContentConfig['collections'][C]['schema'] extends undefined
- ? ExtractDataType
- : import('astro/zod').infer<
- Exclude
- >;
- type LiveLoaderEntryFilterType =
- ExtractEntryFilterType;
- type LiveLoaderCollectionFilterType =
- ExtractCollectionFilterType;
- type LiveLoaderErrorType = ExtractErrorType<
- LiveContentConfig['collections'][C]['loader']
- >;
-
- export type ContentConfig = typeof import("../src/content.config.mjs");
- export type LiveContentConfig = never;
-}
diff --git a/.astro/data-store.json b/.astro/data-store.json
deleted file mode 100644
index 5036073c0..000000000
--- a/.astro/data-store.json
+++ /dev/null
@@ -1 +0,0 @@
-[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.15.3","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false}}"]
\ No newline at end of file
diff --git a/.astro/settings.json b/.astro/settings.json
deleted file mode 100644
index 0afe29342..000000000
--- a/.astro/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "_variables": {
- "lastUpdateCheck": 1763653789860
- }
-}
\ No newline at end of file
diff --git a/.astro/types.d.ts b/.astro/types.d.ts
deleted file mode 100644
index f964fe0cf..000000000
--- a/.astro/types.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/.changeset/config.json b/.changeset/config.json
index 8639ae567..46c604cd8 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -4,8 +4,12 @@
"commit": false,
"fixed": [],
"linked": [],
- "access": "restricted",
+ "access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
- "ignore": []
+ "privatePackages": {
+ "version": false,
+ "tag": false
+ },
+ "ignore": ["@eventcatalog/dsl-playground"]
}
diff --git a/.claude/agents/code-review-uncommitted.md b/.claude/agents/code-review-uncommitted.md
new file mode 100644
index 000000000..4f90ff5b9
--- /dev/null
+++ b/.claude/agents/code-review-uncommitted.md
@@ -0,0 +1,103 @@
+---
+name: code-review-uncommitted
+description: "Use this agent when you want to review uncommitted changes in your working directory to ensure they follow project conventions, are maintainable, and could be simplified without changing behavior. This agent should be called before committing code to catch issues early.\\n\\nExamples:\\n\\n\\nContext: The user has just finished writing a new feature and wants to ensure quality before committing.\\nuser: \"I've finished implementing the new event filtering feature, can you review my changes?\"\\nassistant: \"I'll use the code-review-uncommitted agent to review your uncommitted changes and ensure they follow project conventions.\"\\n\\n\\n\\n\\nContext: The user wants a pre-commit review of their work.\\nuser: \"Review my code before I commit\"\\nassistant: \"Let me launch the code-review-uncommitted agent to analyze your uncommitted changes for convention compliance and simplification opportunities.\"\\n\\n\\n\\n\\nContext: The user has been coding for a while and wants to check their work proactively.\\nuser: \"I've been working on this for a few hours, let's see if there are any issues\"\\nassistant: \"I'll use the code-review-uncommitted agent to review all your uncommitted changes and identify any convention violations or simplification opportunities.\"\\n\\n"
+model: opus
+color: purple
+---
+
+You are an expert code reviewer with deep expertise in software maintainability, clean code principles, and long-term codebase health. Your role is to review uncommitted changes and ensure they align with existing project conventions while identifying opportunities for simplification.
+
+## Your Primary Objectives
+
+1. **Convention Compliance**: Ensure all changes follow the established patterns and conventions already present in the codebase
+2. **Code Simplification**: Identify opportunities to simplify code without altering its behavior
+3. **Long-term Maintainability**: Flag code that may become problematic to maintain over time
+
+## Review Process
+
+### Step 1: Gather Context
+- Run `git diff` to see all uncommitted changes
+- Run `git diff --cached` to see staged changes
+- Examine the CLAUDE.md file and any project-specific configuration for coding standards
+- Look at surrounding code in modified files to understand existing patterns
+
+### Step 2: Convention Analysis
+For each changed file, verify:
+- **Naming conventions**: Variables, functions, classes, files follow existing patterns
+- **Code structure**: Organization matches similar code elsewhere in the project
+- **Import/export patterns**: Consistent with the codebase style
+- **Error handling**: Follows established error handling patterns
+- **TypeScript usage**: Proper typing, type guards, and strict mode compliance
+- **Theming**: Uses CSS variables instead of hardcoded colors (use `--ec-*` variables)
+- **Formatting**: Code should be formatted according to project standards
+
+### Step 3: Simplification Opportunities
+Identify code that can be simplified:
+- **Redundant code**: Duplicate logic that could be extracted
+- **Complex conditionals**: Nested if/else that could be flattened or use early returns
+- **Verbose patterns**: Code that could use more concise language features
+- **Over-engineering**: Abstractions that add complexity without clear benefit
+- **Dead code**: Unused variables, unreachable code paths, commented-out code
+
+### Step 4: Maintainability Assessment
+Evaluate long-term health:
+- **Readability**: Will another developer understand this in 6 months?
+- **Testability**: Is the code structured for easy testing?
+- **Coupling**: Are dependencies appropriate and minimal?
+- **Single Responsibility**: Does each function/component do one thing well?
+- **Documentation**: Are complex logic sections adequately commented?
+
+## Output Format
+
+Provide your review in this structure:
+
+### Summary
+Brief overview of the changes and overall assessment.
+
+### Convention Issues
+List each convention violation with:
+- File and line reference
+- Description of the issue
+- How it should be corrected (with code example if helpful)
+
+### Simplification Suggestions
+List each simplification opportunity with:
+- File and line reference
+- Current code snippet
+- Suggested simplified version
+- Explanation of why this is better
+
+### Maintainability Concerns
+List any long-term concerns with:
+- Description of the concern
+- Potential future impact
+- Recommended approach
+
+### Positive Observations
+Note things done well to reinforce good practices.
+
+## Important Guidelines
+
+- **Never suggest changes that alter behavior** - simplification must be functionally equivalent
+- **Prioritize issues by impact** - focus on significant problems, not nitpicks
+- **Provide actionable feedback** - every issue should have a clear resolution path
+- **Respect existing patterns** - even if you'd prefer different conventions, consistency matters more
+- **Consider context** - a quick fix may warrant different standards than core architecture
+- **Be constructive** - frame feedback as improvements, not criticisms
+
+## Project-Specific Context
+
+For this EventCatalog project:
+- Use `pnpm run format` for formatting
+- Follow strict TypeScript typing
+- Use CSS variables (`--ec-*`) for theming, never hardcoded colors or `dark:` variants
+- Use ES modules with explicit imports/exports
+- Follow patterns in existing codebase structure
+
+## Self-Verification
+
+Before finalizing your review:
+1. Confirm you've checked all uncommitted changes
+2. Verify each suggestion maintains identical behavior
+3. Ensure recommendations align with existing codebase conventions
+4. Check that feedback is specific and actionable
diff --git a/.claude/agents/docs.md b/.claude/agents/docs.md
new file mode 100644
index 000000000..c52d01b0a
--- /dev/null
+++ b/.claude/agents/docs.md
@@ -0,0 +1,105 @@
+---
+name: docs-updater
+description: Use this agent when the user asks to write, update, or generate documentation for a feature or code change. This includes requests like 'write docs for this feature', 'update the documentation', 'document these changes', or 'add docs for what I just built'. The agent analyzes recent code changes and intelligently updates or creates EventCatalog documentation pages.\n\nExamples:\n\n\nContext: User has just implemented a new visualization feature for EventCatalog.\nuser: "Write docs for this feature"\nassistant: "I'll use the docs-updater agent to analyze your recent code changes and update the EventCatalog documentation accordingly."\n\nSince the user wants documentation written for their recent feature work, use the Task tool to launch the docs-updater agent which will compare the current code against the previous commit, identify the changes, and update or create relevant documentation pages.\n\n\n\n\nContext: User has added a new component to the EventCatalog codebase.\nuser: "Can you document the changes I made?"\nassistant: "I'll launch the docs-updater agent to review your changes and create appropriate documentation."\n\nThe user is asking for documentation of their changes. Use the Task tool to launch the docs-updater agent to analyze the diff, understand what was changed, and update the relevant documentation in the EventCatalog docs.\n\n\n\n\nContext: User has refactored an existing feature and needs docs updated.\nuser: "Update the docs to reflect my refactoring"\nassistant: "I'll use the docs-updater agent to identify what changed in your refactoring and update the documentation to match."\n\nSince the user has refactored code and needs documentation updated, use the Task tool to launch the docs-updater agent which will detect the changes and modify existing documentation pages to reflect the new implementation.\n\n
+model: sonnet
+color: cyan
+---
+
+You are an expert technical documentation specialist with deep knowledge of EventCatalog's architecture, documentation patterns, and writing style. Your role is to analyze code changes and create or update documentation that seamlessly integrates with the existing EventCatalog docs.
+
+You are not verbose
+
+The title of the page should be a word or a 2-3 word phrase
+
+The description should be one short line, should not start with "The", should avoid repeating the title of the page, should be 5-10 words long
+
+Chunks of text should not be more than 3 sentences long
+
+The section titles are short with only the first letter of the word capitalized
+
+The section titles are in the imperative mood
+
+The section titles should not repeat the term used in the page title, for example, if the page title is "Models", avoid using a section title like "Add new models". This might be unavoidable in some cases, but try to avoid it.
+
+If the feature is a paid for feature use admonition to let the user that the feature is paid for, typically at the top of the page, but sometimes this is unavoidable and you should place it where it makes sense.
+
+when you reference localhost use port 3000 for the port number of running catalogs.
+
+never use em-dash.
+
+## Your Core Responsibilities
+
+1. **Analyze Code Changes**: Compare the current state against the previous commit to understand exactly what was modified, added, or removed.
+
+2. **Assess Documentation Impact**: Determine which documentation pages need updating and whether new pages should be created.
+
+3. **Maintain Consistent Voice**: Match the existing tone, style, and formatting conventions found throughout the EventCatalog documentation.
+
+4. **Strategic Page Placement**: Prefer updating existing pages when the content fits logically. Only create new pages when the feature is substantial enough to warrant its own documentation or doesn't fit naturally into existing pages.
+
+## Workflow
+
+### Step 1: Understand the Changes
+- Use `git diff HEAD~1` to see what changed in the most recent commit
+- If more context is needed, examine additional commits with `git log --oneline -10` and `git diff ..HEAD`
+- Read the changed files thoroughly to understand the feature's purpose and implementation
+
+### Step 2: Survey Existing Documentation
+- Explore the `/website` directory to understand the documentation structure
+- Read existing documentation pages to absorb the writing style, tone, and formatting patterns
+- Identify pages that cover related topics where new content might fit
+- Note the markdown conventions, heading structures, and code example patterns used
+
+### Step 3: Plan Documentation Updates
+- List which existing pages should be updated and why
+- Determine if a new page is necessary (only for substantial, standalone features)
+- Outline what content needs to be added or modified
+
+### Step 4: Write Documentation
+- Match the existing documentation's:
+ - Tone (professional but approachable, clear and concise)
+ - Heading hierarchy and structure
+ - Code example formatting
+ - Use of admonitions, tips, or warnings
+- Include practical code examples when relevant
+- Explain both the 'what' and the 'why' of features
+- Link to related documentation pages when appropriate
+
+### Step 5: Validate Changes
+- Ensure new content flows naturally with existing content
+- Verify all code examples are accurate and follow project conventions
+- Check that formatting is consistent with other pages
+
+## Documentation Location Guidelines
+
+- **Component documentation**: Look for existing component docs and add to them
+- **Configuration options**: Update relevant configuration documentation
+- **New features**: Consider if they extend an existing feature (update that page) or are entirely new (may warrant new page)
+- **Bug fixes**: Usually don't require documentation unless they change behavior
+- **API changes**: Update API reference documentation
+
+## IMAGES
+
+- If you need to add an image just use the italic _PLACE_HOLDER_IMAGE_ for the image.
+
+## Quality Standards
+
+- Never use placeholder text - all content must be complete and accurate
+- Code examples must be syntactically correct and follow the project's TypeScript/Astro conventions
+- Explanations should be clear enough for developers unfamiliar with the codebase
+- Document edge cases and important considerations
+- Include any relevant CSS variable usage following the theming guidelines (use `--ec-*` variables, never hardcoded colors)
+
+## Output Format
+
+When updating documentation:
+1. Clearly state which files you're modifying and why
+2. Show the relevant changes in context
+3. Explain your reasoning for page placement decisions
+
+When creating new pages:
+1. Explain why a new page is warranted
+2. Describe where in the documentation structure it belongs
+3. Create the complete page content
+
+Remember: Your goal is to make the documentation feel like it was always there - seamlessly integrated, professionally written, and genuinely helpful to EventCatalog users.
diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md
new file mode 100644
index 000000000..09faabf88
--- /dev/null
+++ b/.claude/commands/commit.md
@@ -0,0 +1,45 @@
+---
+description: Create a semantic commit (EventCatalog conventions), auto-branch if needed
+allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git diff --staged:*), Bash(git branch:*), Bash(git rev-parse:*), Bash(git switch:*), Bash(git checkout:*), Bash(git add:*), Bash(git commit:*), Bash(git restore:*)
+---
+
+You are in a git repository that uses EventCatalog-style semantic commit messages.
+
+Goal:
+- Create a single commit with message format: `(): ` where scope is optional.
+- Message must be lowercase.
+- Allowed types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `misc`.
+- Pick a scope only if it clearly applies (short, kebab-case). Examples: `core`, `content-docs`, `theme-classic`.
+
+User hint (may be empty): $ARGUMENTS
+
+Workflow:
+1) Run:
+ - `git status`
+ - `git diff`
+ - `git diff --staged`
+2) If there are both unrelated changes and the user hint clearly targets one thing, keep the commit focused:
+ - stage only the relevant files/lines
+ - leave unrelated changes unstaged
+3) Decide the semantic commit message:
+ - Choose the best `type` from the allowed list based on the actual diff
+ - Choose `scope` only if unambiguous
+ - Write a short present-tense, imperative `subject` in lowercase (no trailing period)
+4) Branch handling:
+ - Determine current branch with git
+ - If on `main` (or `master`) OR in detached HEAD, create and switch to a new branch
+ - Branch name format: `/-`
+ - lowercase, kebab-case
+ - example: `feat/core-add-schema-registry-sidebar`
+ - if no scope: `fix-update-docs-links`
+5) Before committing:
+ - Show the exact commit message you plan to use
+ - Show the exact branch name (if creating one)
+ - Then proceed without asking follow-up questions
+6) Stage changes (only what belongs in this commit) using `git add ...`.
+7) Create the commit using the exact semantic message.
+
+Constraints:
+- Do not include “wip”.
+- Do not invent changes not present in the diff.
+- If there are no changes to commit, say so and stop.
diff --git a/.claude/commands/issue.md b/.claude/commands/issue.md
new file mode 100644
index 000000000..97ad827fa
--- /dev/null
+++ b/.claude/commands/issue.md
@@ -0,0 +1,39 @@
+---
+description: Create a GitHub issue in this project
+allowed-tools: Bash(gh issue create:*), Bash(gh label list:*)
+---
+
+You are creating a GitHub issue for the EventCatalog project using the GitHub CLI (`gh`).
+
+Goal:
+- Create a clear, concise GitHub issue based on the user's request.
+- Keep it simple and to the point.
+
+User request: $ARGUMENTS
+
+Workflow:
+1) Parse the user's request to understand:
+ - What the issue is about
+ - Whether it's a bug, feature request, enhancement, or documentation need
+2) Fetch available labels with `gh label list` to see what labels exist.
+3) Create the issue with `gh issue create`:
+ - Write a clear, concise title (imperative mood, lowercase)
+ - Write a focused body that captures the requirement without fluff
+ - Add appropriate labels based on issue type:
+ - Bug: `bug`
+ - Feature: `enhancement`
+ - Documentation: `documentation`
+ - Add other relevant labels if they exist and apply
+
+Issue format:
+- Title: Short, descriptive, imperative (e.g., "add support for X", "fix Y when Z")
+- Body:
+ - Brief description of what is needed
+ - Context if necessary (1-2 sentences max)
+ - No verbose templates or unnecessary sections
+
+Constraints:
+- Do not add sections that aren't needed (no "steps to reproduce" for features, etc.)
+- Keep the body under 200 words unless complexity requires more
+- Do not ask follow-up questions - use your judgment based on the request
+- After creating, output the issue URL
diff --git a/.claude/commands/pr-description.md b/.claude/commands/pr-description.md
new file mode 100644
index 000000000..69ede283d
--- /dev/null
+++ b/.claude/commands/pr-description.md
@@ -0,0 +1,107 @@
+---
+description: Generate a PR description markdown file summarizing branch changes
+allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git branch:*), Bash(git rev-parse:*), Bash(git merge-base:*), Bash(git show:*), Bash(mkdir:*), Read, Write, Glob
+---
+
+You are a helpful assistant that generates clear, user-friendly PR descriptions.
+
+Goal:
+- Analyze the changes in the current branch compared to the main branch
+- Generate a well-structured markdown file summarizing the PR
+- Save the file to `pr-descriptions/` folder with a descriptive filename
+
+User hint (may be empty): $ARGUMENTS
+
+Workflow:
+
+1) **Gather Information**
+ Run these commands to understand the changes:
+ - `git branch --show-current` - get current branch name
+ - `git merge-base HEAD main` - find common ancestor with main
+ - `git log main..HEAD --oneline` - list commits in this branch
+ - `git diff main...HEAD --stat` - file change summary
+ - `git diff main...HEAD` - full diff for analysis
+
+2) **Analyze the Changes**
+ Review all changes and identify:
+ - The primary purpose/feature of this PR
+ - Key files and components modified
+ - Any new dependencies or configurations
+ - Potential breaking changes (API changes, removed features, renamed exports, config changes)
+ - Testing considerations
+
+3) **Create the pr-descriptions Directory**
+ - Run `mkdir -p pr-descriptions` to ensure the folder exists
+
+4) **Generate the PR Description File**
+ Create a markdown file with this structure:
+
+ ```markdown
+ # PR: [Brief Title Based on Changes]
+
+ **Branch:** `[branch-name]`
+ **Date:** [YYYY-MM-DD]
+ **Commits:** [number of commits]
+
+ ## What This PR Does
+
+ [2-4 sentences describing the purpose and goals of this PR. Focus on the "why" - what problem does this solve or what feature does it add?]
+
+ ## Changes Overview
+
+ ### Files Changed
+ - [List key files/directories modified with brief notes]
+
+ ### Key Changes
+ - [Bullet points of the main changes, written for humans to understand]
+ - [Focus on what changed, not line-by-line diffs]
+
+ ## How It Works
+
+ [Explain the implementation approach. How does the new code work? What patterns or approaches were used? Include relevant technical details that reviewers should understand.]
+
+ ## Breaking Changes
+
+ [List any breaking changes, or state "None" if there are no breaking changes]
+
+ - **[Change]**: [Description of what broke and how to migrate]
+
+ ## Testing
+
+ - [ ] Unit tests added/updated
+ - [ ] Manual testing performed
+ - [ ] [Other relevant testing notes]
+
+ ## Screenshots/Examples
+
+ [If applicable, note where screenshots could be added or include code examples]
+
+ ## Checklist
+
+ - [ ] Code follows project conventions
+ - [ ] Documentation updated (if needed)
+ - [ ] No console errors or warnings introduced
+ - [ ] Reviewed for security implications
+
+ ## Additional Notes
+
+ [Any other context, related issues, follow-up work needed, or notes for reviewers]
+ ```
+
+5) **Filename Convention**
+ Save the file as: `pr-descriptions/[branch-name]-[YYYY-MM-DD].md`
+ - Replace `/` in branch names with `-`
+ - Example: `pr-descriptions/feat-core-add-mcp-server-2025-01-12.md`
+
+6) **Output**
+ After creating the file:
+ - Show the full path to the generated file
+ - Display a brief summary of what was documented
+
+Constraints:
+- Write in clear, simple language that non-technical stakeholders can understand
+- Be honest about breaking changes - don't hide them
+- If there are no changes compared to main, say so and stop
+- Focus on substance over formatting - the content matters more than looking pretty
+- Use present tense ("Adds feature" not "Added feature")
+- Keep bullet points concise but informative
diff --git a/.claude/skills/frontend-design/SKILL.md b/.claude/skills/frontend-design/SKILL.md
new file mode 100644
index 000000000..600b6db41
--- /dev/null
+++ b/.claude/skills/frontend-design/SKILL.md
@@ -0,0 +1,42 @@
+---
+name: frontend-design
+description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
+license: Complete terms in LICENSE.txt
+---
+
+This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
+
+The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
+
+## Design Thinking
+
+Before coding, understand the context and commit to a BOLD aesthetic direction:
+- **Purpose**: What problem does this interface solve? Who uses it?
+- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
+- **Constraints**: Technical requirements (framework, performance, accessibility).
+- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
+
+**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
+
+Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
+- Production-grade and functional
+- Visually striking and memorable
+- Cohesive with a clear aesthetic point-of-view
+- Meticulously refined in every detail
+
+## Frontend Aesthetics Guidelines
+
+Focus on:
+- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
+- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
+- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
+- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
+- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
+
+NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
+
+Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
+
+**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
+
+Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
\ No newline at end of file
diff --git a/.claude/skills/skill-creator/SKILL.md b/.claude/skills/skill-creator/SKILL.md
new file mode 100644
index 000000000..b3b699497
--- /dev/null
+++ b/.claude/skills/skill-creator/SKILL.md
@@ -0,0 +1,357 @@
+---
+name: skill-creator
+description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
+license: Complete terms in LICENSE.txt
+---
+
+# Skill Creator
+
+This skill provides guidance for creating effective skills.
+
+## About Skills
+
+Skills are modular, self-contained packages that extend Claude's capabilities by providing
+specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
+domains or tasks—they transform Claude from a general-purpose agent into a specialized agent
+equipped with procedural knowledge that no model can fully possess.
+
+### What Skills Provide
+
+1. Specialized workflows - Multi-step procedures for specific domains
+2. Tool integrations - Instructions for working with specific file formats or APIs
+3. Domain expertise - Company-specific knowledge, schemas, business logic
+4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
+
+## Core Principles
+
+### Concise is Key
+
+The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
+
+**Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?"
+
+Prefer concise examples over verbose explanations.
+
+### Set Appropriate Degrees of Freedom
+
+Match the level of specificity to the task's fragility and variability:
+
+**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
+
+**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
+
+**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
+
+Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
+
+### Anatomy of a Skill
+
+Every skill consists of a required SKILL.md file and optional bundled resources:
+
+```
+skill-name/
+├── SKILL.md (required)
+│ ├── YAML frontmatter metadata (required)
+│ │ ├── name: (required)
+│ │ ├── description: (required)
+│ │ └── compatibility: (optional, rarely needed)
+│ └── Markdown instructions (required)
+└── Bundled Resources (optional)
+ ├── scripts/ - Executable code (Python/Bash/etc.)
+ ├── references/ - Documentation intended to be loaded into context as needed
+ └── assets/ - Files used in output (templates, icons, fonts, etc.)
+```
+
+#### SKILL.md (required)
+
+Every SKILL.md consists of:
+
+- **Frontmatter** (YAML): Contains `name` and `description` fields (required), plus optional fields like `license`, `metadata`, and `compatibility`. Only `name` and `description` are read by Claude to determine when the skill triggers, so be clear and comprehensive about what the skill is and when it should be used. The `compatibility` field is for noting environment requirements (target product, system packages, etc.) but most skills don't need it.
+- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
+
+#### Bundled Resources (optional)
+
+##### Scripts (`scripts/`)
+
+Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
+
+- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
+- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
+- **Benefits**: Token efficient, deterministic, may be executed without loading into context
+- **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments
+
+##### References (`references/`)
+
+Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking.
+
+- **When to include**: For documentation that Claude should reference while working
+- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications
+- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
+- **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed
+- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md
+- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
+
+##### Assets (`assets/`)
+
+Files not intended to be loaded into context, but rather used within the output Claude produces.
+
+- **When to include**: When the skill needs files that will be used in the final output
+- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography
+- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
+- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context
+
+#### What to Not Include in a Skill
+
+A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
+
+- README.md
+- INSTALLATION_GUIDE.md
+- QUICK_REFERENCE.md
+- CHANGELOG.md
+- etc.
+
+The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
+
+### Progressive Disclosure Design Principle
+
+Skills use a three-level loading system to manage context efficiently:
+
+1. **Metadata (name + description)** - Always in context (~100 words)
+2. **SKILL.md body** - When skill triggers (<5k words)
+3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window)
+
+#### Progressive Disclosure Patterns
+
+Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
+
+**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
+
+**Pattern 1: High-level guide with references**
+
+```markdown
+# PDF Processing
+
+## Quick start
+
+Extract text with pdfplumber:
+[code example]
+
+## Advanced features
+
+- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
+- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
+- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
+```
+
+Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
+
+**Pattern 2: Domain-specific organization**
+
+For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
+
+```
+bigquery-skill/
+├── SKILL.md (overview and navigation)
+└── reference/
+ ├── finance.md (revenue, billing metrics)
+ ├── sales.md (opportunities, pipeline)
+ ├── product.md (API usage, features)
+ └── marketing.md (campaigns, attribution)
+```
+
+When a user asks about sales metrics, Claude only reads sales.md.
+
+Similarly, for skills supporting multiple frameworks or variants, organize by variant:
+
+```
+cloud-deploy/
+├── SKILL.md (workflow + provider selection)
+└── references/
+ ├── aws.md (AWS deployment patterns)
+ ├── gcp.md (GCP deployment patterns)
+ └── azure.md (Azure deployment patterns)
+```
+
+When the user chooses AWS, Claude only reads aws.md.
+
+**Pattern 3: Conditional details**
+
+Show basic content, link to advanced content:
+
+```markdown
+# DOCX Processing
+
+## Creating documents
+
+Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
+
+## Editing documents
+
+For simple edits, modify the XML directly.
+
+**For tracked changes**: See [REDLINING.md](REDLINING.md)
+**For OOXML details**: See [OOXML.md](OOXML.md)
+```
+
+Claude reads REDLINING.md or OOXML.md only when the user needs those features.
+
+**Important guidelines:**
+
+- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
+- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing.
+
+## Skill Creation Process
+
+Skill creation involves these steps:
+
+1. Understand the skill with concrete examples
+2. Plan reusable skill contents (scripts, references, assets)
+3. Initialize the skill (run init_skill.py)
+4. Edit the skill (implement resources and write SKILL.md)
+5. Package the skill (run package_skill.py)
+6. Iterate based on real usage
+
+Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
+
+### Step 1: Understanding the Skill with Concrete Examples
+
+Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.
+
+To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.
+
+For example, when building an image-editor skill, relevant questions include:
+
+- "What functionality should the image-editor skill support? Editing, rotating, anything else?"
+- "Can you give some examples of how this skill would be used?"
+- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?"
+- "What would a user say that should trigger this skill?"
+
+To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.
+
+Conclude this step when there is a clear sense of the functionality the skill should support.
+
+### Step 2: Planning the Reusable Skill Contents
+
+To turn concrete examples into an effective skill, analyze each example by:
+
+1. Considering how to execute on the example from scratch
+2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
+
+Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
+
+1. Rotating a PDF requires re-writing the same code each time
+2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
+
+Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
+
+1. Writing a frontend webapp requires the same boilerplate HTML/React each time
+2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
+
+Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
+
+1. Querying BigQuery requires re-discovering the table schemas and relationships each time
+2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
+
+To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
+
+### Step 3: Initializing the Skill
+
+At this point, it is time to actually create the skill.
+
+Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
+
+When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
+
+Usage:
+
+```bash
+scripts/init_skill.py --path
+```
+
+The script:
+
+- Creates the skill directory at the specified path
+- Generates a SKILL.md template with proper frontmatter and TODO placeholders
+- Creates example resource directories: `scripts/`, `references/`, and `assets/`
+- Adds example files in each directory that can be customized or deleted
+
+After initialization, customize or remove the generated SKILL.md and example files as needed.
+
+### Step 4: Edit the Skill
+
+When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively.
+
+#### Learn Proven Design Patterns
+
+Consult these helpful guides based on your skill's needs:
+
+- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic
+- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns
+
+These files contain established best practices for effective skill design.
+
+#### Start with Reusable Skill Contents
+
+To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
+
+Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
+
+Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.
+
+#### Update SKILL.md
+
+**Writing Guidelines:** Always use imperative/infinitive form.
+
+##### Frontmatter
+
+Write the YAML frontmatter with `name` and `description`:
+
+- `name`: The skill name
+- `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill.
+ - Include both what the Skill does and specific triggers/contexts for when to use it.
+ - Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude.
+ - Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
+
+Do not include any other fields in YAML frontmatter.
+
+##### Body
+
+Write instructions for using the skill and its bundled resources.
+
+### Step 5: Packaging a Skill
+
+Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
+
+```bash
+scripts/package_skill.py
+```
+
+Optional output directory specification:
+
+```bash
+scripts/package_skill.py ./dist
+```
+
+The packaging script will:
+
+1. **Validate** the skill automatically, checking:
+
+ - YAML frontmatter format and required fields
+ - Skill naming conventions and directory structure
+ - Description completeness and quality
+ - File organization and resource references
+
+2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.
+
+If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
+
+### Step 6: Iterate
+
+After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.
+
+**Iteration workflow:**
+
+1. Use the skill on real tasks
+2. Notice struggles or inefficiencies
+3. Identify how SKILL.md or bundled resources should be updated
+4. Implement changes and test again
\ No newline at end of file
diff --git a/.claude/skills/skill-creator/references/output-patterns.md b/.claude/skills/skill-creator/references/output-patterns.md
new file mode 100644
index 000000000..073ddda5f
--- /dev/null
+++ b/.claude/skills/skill-creator/references/output-patterns.md
@@ -0,0 +1,82 @@
+# Output Patterns
+
+Use these patterns when skills need to produce consistent, high-quality output.
+
+## Template Pattern
+
+Provide templates for output format. Match the level of strictness to your needs.
+
+**For strict requirements (like API responses or data formats):**
+
+```markdown
+## Report structure
+
+ALWAYS use this exact template structure:
+
+# [Analysis Title]
+
+## Executive summary
+[One-paragraph overview of key findings]
+
+## Key findings
+- Finding 1 with supporting data
+- Finding 2 with supporting data
+- Finding 3 with supporting data
+
+## Recommendations
+1. Specific actionable recommendation
+2. Specific actionable recommendation
+```
+
+**For flexible guidance (when adaptation is useful):**
+
+```markdown
+## Report structure
+
+Here is a sensible default format, but use your best judgment:
+
+# [Analysis Title]
+
+## Executive summary
+[Overview]
+
+## Key findings
+[Adapt sections based on what you discover]
+
+## Recommendations
+[Tailor to the specific context]
+
+Adjust sections as needed for the specific analysis type.
+```
+
+## Examples Pattern
+
+For skills where output quality depends on seeing examples, provide input/output pairs:
+
+```markdown
+## Commit message format
+
+Generate commit messages following these examples:
+
+**Example 1:**
+Input: Added user authentication with JWT tokens
+Output:
+```
+feat(auth): implement JWT-based authentication
+
+Add login endpoint and token validation middleware
+```
+
+**Example 2:**
+Input: Fixed bug where dates displayed incorrectly in reports
+Output:
+```
+fix(reports): correct date formatting in timezone conversion
+
+Use UTC timestamps consistently across report generation
+```
+
+Follow this style: type(scope): brief description, then detailed explanation.
+```
+
+Examples help Claude understand the desired style and level of detail more clearly than descriptions alone.
diff --git a/.claude/skills/skill-creator/references/workflows.md b/.claude/skills/skill-creator/references/workflows.md
new file mode 100644
index 000000000..a350c3cc8
--- /dev/null
+++ b/.claude/skills/skill-creator/references/workflows.md
@@ -0,0 +1,28 @@
+# Workflow Patterns
+
+## Sequential Workflows
+
+For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md:
+
+```markdown
+Filling a PDF form involves these steps:
+
+1. Analyze the form (run analyze_form.py)
+2. Create field mapping (edit fields.json)
+3. Validate mapping (run validate_fields.py)
+4. Fill the form (run fill_form.py)
+5. Verify output (run verify_output.py)
+```
+
+## Conditional Workflows
+
+For tasks with branching logic, guide Claude through decision points:
+
+```markdown
+1. Determine the modification type:
+ **Creating new content?** → Follow "Creation workflow" below
+ **Editing existing content?** → Follow "Editing workflow" below
+
+2. Creation workflow: [steps]
+3. Editing workflow: [steps]
+```
\ No newline at end of file
diff --git a/.claude/skills/skill-creator/scripts/init_skill.py b/.claude/skills/skill-creator/scripts/init_skill.py
new file mode 100644
index 000000000..c544fc725
--- /dev/null
+++ b/.claude/skills/skill-creator/scripts/init_skill.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+"""
+Skill Initializer - Creates a new skill from template
+
+Usage:
+ init_skill.py --path
+
+Examples:
+ init_skill.py my-new-skill --path skills/public
+ init_skill.py my-api-helper --path skills/private
+ init_skill.py custom-skill --path /custom/location
+"""
+
+import sys
+from pathlib import Path
+
+
+SKILL_TEMPLATE = """---
+name: {skill_name}
+description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
+---
+
+# {skill_title}
+
+## Overview
+
+[TODO: 1-2 sentences explaining what this skill enables]
+
+## Structuring This Skill
+
+[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
+
+**1. Workflow-Based** (best for sequential processes)
+- Works well when there are clear step-by-step procedures
+- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing"
+- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2...
+
+**2. Task-Based** (best for tool collections)
+- Works well when the skill offers different operations/capabilities
+- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text"
+- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2...
+
+**3. Reference/Guidelines** (best for standards or specifications)
+- Works well for brand guidelines, coding standards, or requirements
+- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features"
+- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage...
+
+**4. Capabilities-Based** (best for integrated systems)
+- Works well when the skill provides multiple interrelated features
+- Example: Product Management with "Core Capabilities" → numbered capability list
+- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature...
+
+Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
+
+Delete this entire "Structuring This Skill" section when done - it's just guidance.]
+
+## [TODO: Replace with the first main section based on chosen structure]
+
+[TODO: Add content here. See examples in existing skills:
+- Code samples for technical skills
+- Decision trees for complex workflows
+- Concrete examples with realistic user requests
+- References to scripts/templates/references as needed]
+
+## Resources
+
+This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
+
+### scripts/
+Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
+
+**Examples from other skills:**
+- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
+- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
+
+**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
+
+**Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments.
+
+### references/
+Documentation and reference material intended to be loaded into context to inform Claude's process and thinking.
+
+**Examples from other skills:**
+- Product management: `communication.md`, `context_building.md` - detailed workflow guides
+- BigQuery: API reference documentation and query examples
+- Finance: Schema documentation, company policies
+
+**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working.
+
+### assets/
+Files not intended to be loaded into context, but rather used within the output Claude produces.
+
+**Examples from other skills:**
+- Brand styling: PowerPoint template files (.pptx), logo files
+- Frontend builder: HTML/React boilerplate project directories
+- Typography: Font files (.ttf, .woff2)
+
+**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
+
+---
+
+**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
+"""
+
+EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
+"""
+Example helper script for {skill_name}
+
+This is a placeholder script that can be executed directly.
+Replace with actual implementation or delete if not needed.
+
+Example real scripts from other skills:
+- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
+- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
+"""
+
+def main():
+ print("This is an example script for {skill_name}")
+ # TODO: Add actual script logic here
+ # This could be data processing, file conversion, API calls, etc.
+
+if __name__ == "__main__":
+ main()
+'''
+
+EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
+
+This is a placeholder for detailed reference documentation.
+Replace with actual reference content or delete if not needed.
+
+Example real reference docs from other skills:
+- product-management/references/communication.md - Comprehensive guide for status updates
+- product-management/references/context_building.md - Deep-dive on gathering context
+- bigquery/references/ - API references and query examples
+
+## When Reference Docs Are Useful
+
+Reference docs are ideal for:
+- Comprehensive API documentation
+- Detailed workflow guides
+- Complex multi-step processes
+- Information too lengthy for main SKILL.md
+- Content that's only needed for specific use cases
+
+## Structure Suggestions
+
+### API Reference Example
+- Overview
+- Authentication
+- Endpoints with examples
+- Error codes
+- Rate limits
+
+### Workflow Guide Example
+- Prerequisites
+- Step-by-step instructions
+- Common patterns
+- Troubleshooting
+- Best practices
+"""
+
+EXAMPLE_ASSET = """# Example Asset File
+
+This placeholder represents where asset files would be stored.
+Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
+
+Asset files are NOT intended to be loaded into context, but rather used within
+the output Claude produces.
+
+Example asset files from other skills:
+- Brand guidelines: logo.png, slides_template.pptx
+- Frontend builder: hello-world/ directory with HTML/React boilerplate
+- Typography: custom-font.ttf, font-family.woff2
+- Data: sample_data.csv, test_dataset.json
+
+## Common Asset Types
+
+- Templates: .pptx, .docx, boilerplate directories
+- Images: .png, .jpg, .svg, .gif
+- Fonts: .ttf, .otf, .woff, .woff2
+- Boilerplate code: Project directories, starter files
+- Icons: .ico, .svg
+- Data files: .csv, .json, .xml, .yaml
+
+Note: This is a text placeholder. Actual assets can be any file type.
+"""
+
+
+def title_case_skill_name(skill_name):
+ """Convert hyphenated skill name to Title Case for display."""
+ return ' '.join(word.capitalize() for word in skill_name.split('-'))
+
+
+def init_skill(skill_name, path):
+ """
+ Initialize a new skill directory with template SKILL.md.
+
+ Args:
+ skill_name: Name of the skill
+ path: Path where the skill directory should be created
+
+ Returns:
+ Path to created skill directory, or None if error
+ """
+ # Determine skill directory path
+ skill_dir = Path(path).resolve() / skill_name
+
+ # Check if directory already exists
+ if skill_dir.exists():
+ print(f"❌ Error: Skill directory already exists: {skill_dir}")
+ return None
+
+ # Create skill directory
+ try:
+ skill_dir.mkdir(parents=True, exist_ok=False)
+ print(f"✅ Created skill directory: {skill_dir}")
+ except Exception as e:
+ print(f"❌ Error creating directory: {e}")
+ return None
+
+ # Create SKILL.md from template
+ skill_title = title_case_skill_name(skill_name)
+ skill_content = SKILL_TEMPLATE.format(
+ skill_name=skill_name,
+ skill_title=skill_title
+ )
+
+ skill_md_path = skill_dir / 'SKILL.md'
+ try:
+ skill_md_path.write_text(skill_content)
+ print("✅ Created SKILL.md")
+ except Exception as e:
+ print(f"❌ Error creating SKILL.md: {e}")
+ return None
+
+ # Create resource directories with example files
+ try:
+ # Create scripts/ directory with example script
+ scripts_dir = skill_dir / 'scripts'
+ scripts_dir.mkdir(exist_ok=True)
+ example_script = scripts_dir / 'example.py'
+ example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
+ example_script.chmod(0o755)
+ print("✅ Created scripts/example.py")
+
+ # Create references/ directory with example reference doc
+ references_dir = skill_dir / 'references'
+ references_dir.mkdir(exist_ok=True)
+ example_reference = references_dir / 'api_reference.md'
+ example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
+ print("✅ Created references/api_reference.md")
+
+ # Create assets/ directory with example asset placeholder
+ assets_dir = skill_dir / 'assets'
+ assets_dir.mkdir(exist_ok=True)
+ example_asset = assets_dir / 'example_asset.txt'
+ example_asset.write_text(EXAMPLE_ASSET)
+ print("✅ Created assets/example_asset.txt")
+ except Exception as e:
+ print(f"❌ Error creating resource directories: {e}")
+ return None
+
+ # Print next steps
+ print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}")
+ print("\nNext steps:")
+ print("1. Edit SKILL.md to complete the TODO items and update the description")
+ print("2. Customize or delete the example files in scripts/, references/, and assets/")
+ print("3. Run the validator when ready to check the skill structure")
+
+ return skill_dir
+
+
+def main():
+ if len(sys.argv) < 4 or sys.argv[2] != '--path':
+ print("Usage: init_skill.py --path ")
+ print("\nSkill name requirements:")
+ print(" - Kebab-case identifier (e.g., 'my-data-analyzer')")
+ print(" - Lowercase letters, digits, and hyphens only")
+ print(" - Max 64 characters")
+ print(" - Must match directory name exactly")
+ print("\nExamples:")
+ print(" init_skill.py my-new-skill --path skills/public")
+ print(" init_skill.py my-api-helper --path skills/private")
+ print(" init_skill.py custom-skill --path /custom/location")
+ sys.exit(1)
+
+ skill_name = sys.argv[1]
+ path = sys.argv[3]
+
+ print(f"🚀 Initializing skill: {skill_name}")
+ print(f" Location: {path}")
+ print()
+
+ result = init_skill(skill_name, path)
+
+ if result:
+ sys.exit(0)
+ else:
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.claude/skills/skill-creator/scripts/package_skill.py b/.claude/skills/skill-creator/scripts/package_skill.py
new file mode 100644
index 000000000..5cd36cb16
--- /dev/null
+++ b/.claude/skills/skill-creator/scripts/package_skill.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+"""
+Skill Packager - Creates a distributable .skill file of a skill folder
+
+Usage:
+ python utils/package_skill.py [output-directory]
+
+Example:
+ python utils/package_skill.py skills/public/my-skill
+ python utils/package_skill.py skills/public/my-skill ./dist
+"""
+
+import sys
+import zipfile
+from pathlib import Path
+from quick_validate import validate_skill
+
+
+def package_skill(skill_path, output_dir=None):
+ """
+ Package a skill folder into a .skill file.
+
+ Args:
+ skill_path: Path to the skill folder
+ output_dir: Optional output directory for the .skill file (defaults to current directory)
+
+ Returns:
+ Path to the created .skill file, or None if error
+ """
+ skill_path = Path(skill_path).resolve()
+
+ # Validate skill folder exists
+ if not skill_path.exists():
+ print(f"❌ Error: Skill folder not found: {skill_path}")
+ return None
+
+ if not skill_path.is_dir():
+ print(f"❌ Error: Path is not a directory: {skill_path}")
+ return None
+
+ # Validate SKILL.md exists
+ skill_md = skill_path / "SKILL.md"
+ if not skill_md.exists():
+ print(f"❌ Error: SKILL.md not found in {skill_path}")
+ return None
+
+ # Run validation before packaging
+ print("🔍 Validating skill...")
+ valid, message = validate_skill(skill_path)
+ if not valid:
+ print(f"❌ Validation failed: {message}")
+ print(" Please fix the validation errors before packaging.")
+ return None
+ print(f"✅ {message}\n")
+
+ # Determine output location
+ skill_name = skill_path.name
+ if output_dir:
+ output_path = Path(output_dir).resolve()
+ output_path.mkdir(parents=True, exist_ok=True)
+ else:
+ output_path = Path.cwd()
+
+ skill_filename = output_path / f"{skill_name}.skill"
+
+ # Create the .skill file (zip format)
+ try:
+ with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
+ # Walk through the skill directory
+ for file_path in skill_path.rglob('*'):
+ if file_path.is_file():
+ # Calculate the relative path within the zip
+ arcname = file_path.relative_to(skill_path.parent)
+ zipf.write(file_path, arcname)
+ print(f" Added: {arcname}")
+
+ print(f"\n✅ Successfully packaged skill to: {skill_filename}")
+ return skill_filename
+
+ except Exception as e:
+ print(f"❌ Error creating .skill file: {e}")
+ return None
+
+
+def main():
+ if len(sys.argv) < 2:
+ print("Usage: python utils/package_skill.py [output-directory]")
+ print("\nExample:")
+ print(" python utils/package_skill.py skills/public/my-skill")
+ print(" python utils/package_skill.py skills/public/my-skill ./dist")
+ sys.exit(1)
+
+ skill_path = sys.argv[1]
+ output_dir = sys.argv[2] if len(sys.argv) > 2 else None
+
+ print(f"📦 Packaging skill: {skill_path}")
+ if output_dir:
+ print(f" Output directory: {output_dir}")
+ print()
+
+ result = package_skill(skill_path, output_dir)
+
+ if result:
+ sys.exit(0)
+ else:
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.claude/skills/skill-creator/scripts/quick_validate.py b/.claude/skills/skill-creator/scripts/quick_validate.py
new file mode 100644
index 000000000..ed8e1dddc
--- /dev/null
+++ b/.claude/skills/skill-creator/scripts/quick_validate.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+"""
+Quick validation script for skills - minimal version
+"""
+
+import sys
+import os
+import re
+import yaml
+from pathlib import Path
+
+def validate_skill(skill_path):
+ """Basic validation of a skill"""
+ skill_path = Path(skill_path)
+
+ # Check SKILL.md exists
+ skill_md = skill_path / 'SKILL.md'
+ if not skill_md.exists():
+ return False, "SKILL.md not found"
+
+ # Read and validate frontmatter
+ content = skill_md.read_text()
+ if not content.startswith('---'):
+ return False, "No YAML frontmatter found"
+
+ # Extract frontmatter
+ match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
+ if not match:
+ return False, "Invalid frontmatter format"
+
+ frontmatter_text = match.group(1)
+
+ # Parse YAML frontmatter
+ try:
+ frontmatter = yaml.safe_load(frontmatter_text)
+ if not isinstance(frontmatter, dict):
+ return False, "Frontmatter must be a YAML dictionary"
+ except yaml.YAMLError as e:
+ return False, f"Invalid YAML in frontmatter: {e}"
+
+ # Define allowed properties
+ ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata', 'compatibility'}
+
+ # Check for unexpected properties (excluding nested keys under metadata)
+ unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
+ if unexpected_keys:
+ return False, (
+ f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. "
+ f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}"
+ )
+
+ # Check required fields
+ if 'name' not in frontmatter:
+ return False, "Missing 'name' in frontmatter"
+ if 'description' not in frontmatter:
+ return False, "Missing 'description' in frontmatter"
+
+ # Extract name for validation
+ name = frontmatter.get('name', '')
+ if not isinstance(name, str):
+ return False, f"Name must be a string, got {type(name).__name__}"
+ name = name.strip()
+ if name:
+ # Check naming convention (kebab-case: lowercase with hyphens)
+ if not re.match(r'^[a-z0-9-]+$', name):
+ return False, f"Name '{name}' should be kebab-case (lowercase letters, digits, and hyphens only)"
+ if name.startswith('-') or name.endswith('-') or '--' in name:
+ return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
+ # Check name length (max 64 characters per spec)
+ if len(name) > 64:
+ return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters."
+
+ # Extract and validate description
+ description = frontmatter.get('description', '')
+ if not isinstance(description, str):
+ return False, f"Description must be a string, got {type(description).__name__}"
+ description = description.strip()
+ if description:
+ # Check for angle brackets
+ if '<' in description or '>' in description:
+ return False, "Description cannot contain angle brackets (< or >)"
+ # Check description length (max 1024 characters per spec)
+ if len(description) > 1024:
+ return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters."
+
+ # Validate compatibility field if present (optional)
+ compatibility = frontmatter.get('compatibility', '')
+ if compatibility:
+ if not isinstance(compatibility, str):
+ return False, f"Compatibility must be a string, got {type(compatibility).__name__}"
+ if len(compatibility) > 500:
+ return False, f"Compatibility is too long ({len(compatibility)} characters). Maximum is 500 characters."
+
+ return True, "Skill is valid!"
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("Usage: python quick_validate.py ")
+ sys.exit(1)
+
+ valid, message = validate_skill(sys.argv[1])
+ print(message)
+ sys.exit(0 if valid else 1)
\ No newline at end of file
diff --git a/.claude/skills/spec-change/SKILL.md b/.claude/skills/spec-change/SKILL.md
new file mode 100644
index 000000000..8451a70f4
--- /dev/null
+++ b/.claude/skills/spec-change/SKILL.md
@@ -0,0 +1,87 @@
+---
+name: spec-change
+description: Update the EventCatalog DSL specification across all locations — Langium grammar, specification markdown docs, playground examples, and tests. Use when making any change to DSL syntax, adding new DSL features, modifying grammar rules, or updating DSL documentation. Triggers on requests like "add X to the DSL", "update the grammar", "change DSL syntax for Y", or "spec change".
+---
+
+# DSL Specification Change
+
+Apply a DSL specification change across all required locations. See [references/spec-files.md](references/spec-files.md) for all file paths.
+
+## Workflow
+
+### 1. Understand the change
+- Parse the user's request to identify the DSL feature being added/modified
+- Identify affected grammar rules, spec files, and examples
+
+### 2. Update the Langium grammar
+- Read `packages/language-server/src/ec.langium`
+- Locate and update the relevant grammar rules
+- Follow Langium conventions
+
+### 3. Add/update tests
+- Add tests in `packages/language-server/src/test/` (or `__tests__/`)
+- Cover: valid syntax, invalid syntax, edge cases, backwards compatibility
+- Use vitest. Test pattern:
+
+```typescript
+describe('Feature Name', () => {
+ it('should parse valid syntax', () => {
+ const dsl = `...valid DSL...`;
+ const result = parseDsl(dsl);
+ expect(result.parseErrors).toHaveLength(0);
+ });
+
+ it('should reject invalid syntax', () => {
+ const dsl = `...invalid DSL...`;
+ const result = parseDsl(dsl);
+ expect(result.parseErrors.length).toBeGreaterThan(0);
+ });
+});
+```
+
+- Run: `pnpm --filter @eventcatalog/language-server run test --run`
+
+### 4. Update specification markdown
+- See [references/spec-files.md](references/spec-files.md) for the full file list
+- Update examples, EBNF sections, syntax tables, and reference sections in all affected files
+
+### 5. Update playground examples and completions
+- Read `packages/playground/src/examples.ts`
+- Update ALL relevant examples to demonstrate the new syntax consistently
+- Add new example if the feature warrants one
+- Read `packages/playground/src/monaco/ec-completion.ts`
+- Update annotation suggestions and keyword completions to match the grammar changes
+
+### 6. Update SDK DSL functions and tests
+- Check `packages/sdk/src/dsl/` for functions that generate or parse DSL syntax
+- Check `packages/sdk/src/test/dsl.test.ts` for DSL-related tests
+- Ensure any DSL output or parsing reflects the grammar changes made above
+
+### 7. Verify the Langium grammar matches the spec
+- After any spec change, read `packages/language-server/src/ec.langium` and confirm the grammar is consistent with what the spec now describes
+- If they diverge, flag it to the user — the grammar may need updating too
+- Run: `pnpm --filter @eventcatalog/language-server run test --run`
+
+### 8. Format and verify
+- Run `pnpm run format`
+- Run tests again to confirm everything passes
+
+## Rules
+- **Grammar first** — always update grammar before markdown/examples
+- **Test coverage** — every grammar change MUST have tests
+- **Be thorough** — update ALL affected files, not just some
+- **Consistency** — ensure syntax is consistent across ALL playground examples
+- **EBNF sync** — keep EBNF notation in spec markdown in sync with Langium grammar
+- **Note breaking changes** — flag if changes break existing syntax
+
+## Output Format
+1. State what's changing and why
+2. Show grammar changes
+3. Show test additions/updates and run results
+4. List all spec files updated
+5. Show playground example updates
+6. Confirm formatting completed
+
+**IMPORTANT:** If you discover issues or inconsistencies in the specification that are outside the scope of the current change, do NOT fix them without first verifying with the user. Only make changes directly related to the requested spec change.
+
+Ask clarifying questions if you need to know more details.
diff --git a/.claude/skills/spec-change/references/spec-files.md b/.claude/skills/spec-change/references/spec-files.md
new file mode 100644
index 000000000..e016c6cea
--- /dev/null
+++ b/.claude/skills/spec-change/references/spec-files.md
@@ -0,0 +1,49 @@
+# DSL Specification File Locations
+
+## Grammar
+- `packages/language-server/src/ec.langium` — Langium grammar definition
+
+## Tests
+- `packages/language-server/src/test/` or `packages/language-server/__tests__/` — Test directory
+- Framework: vitest
+- Run: `pnpm --filter @eventcatalog/language-server run test --run`
+
+## Specification Markdown
+| File | Content |
+|------|---------|
+| `00-overview.md` | High-level overview |
+| `01-domain.md` | Domain syntax |
+| `02-service.md` | Service syntax |
+| `03-event.md` | Event syntax |
+| `04-command.md` | Command syntax |
+| `05-query.md` | Query syntax |
+| `06-channel.md` | Channel syntax |
+| `07-entity.md` | Entity syntax |
+| `08-container.md` | Container syntax |
+| `09-data-product.md` | Data product syntax |
+| `14-relationships.md` | Relationship/pointer syntax |
+| `15-versioning.md` | Version syntax |
+| `17-examples.md` | Example updates |
+| `18-grammar.md` | Grammar reference (EBNF) |
+| `19-visualizer.md` | Visualizer syntax |
+
+Path: `packages/language-server/specification/`
+
+## SDK DSL Functions
+- `packages/sdk/src/dsl/` — DSL generation/parsing functions
+- `packages/sdk/src/test/dsl.test.ts` — DSL-related tests
+
+## Playground Examples
+- `packages/playground/src/examples.ts`
+
+## Playground Monaco Completions
+- `packages/playground/src/monaco/ec-completion.ts` — Autocomplete suggestions for annotations and keywords in the playground editor
+
+## Language Server Completions
+- `packages/language-server/src/ec-completion-provider.ts` — Autocomplete suggestions for the language server (KNOWN_ANNOTATIONS, keyword sets)
+
+## DSL Conventions
+- Version syntax: `@` prefix (e.g., `resource@1.0.0`)
+- Lists: comma-separated (e.g., `to ChannelA, ChannelB`)
+- Optional blocks: `{...}` only when needed
+- Prefer inline syntax over blocks
diff --git a/.claude/skills/vercel-react-best-practices b/.claude/skills/vercel-react-best-practices
new file mode 120000
index 000000000..e567923b3
--- /dev/null
+++ b/.claude/skills/vercel-react-best-practices
@@ -0,0 +1 @@
+../../.agents/skills/vercel-react-best-practices
\ No newline at end of file
diff --git a/.claude/skills/visualiser-performance/SKILL.md b/.claude/skills/visualiser-performance/SKILL.md
new file mode 100644
index 000000000..c78c8e321
--- /dev/null
+++ b/.claude/skills/visualiser-performance/SKILL.md
@@ -0,0 +1,142 @@
+---
+name: visualiser-performance
+description: React Flow performance rules and review checklist for the @eventcatalog/visualiser package. Automatically applies when making changes to any file under packages/visualiser/. Use this skill to audit, review, or implement visualiser code with performance in mind.
+globs:
+ - packages/visualiser/**/*.tsx
+ - packages/visualiser/**/*.ts
+---
+
+# Visualiser Performance Rules
+
+When modifying any code in `packages/visualiser/`, follow these rules to avoid React Flow performance regressions. A single unoptimized line can cause all nodes to re-render on every drag tick, dropping FPS from 60 to 2.
+
+## Rule 1: Never pass unstable references to `` props
+
+All props on `` must be referentially stable:
+
+- **Objects/arrays**: Define outside the component or wrap in `useMemo` with stable deps
+- **Functions**: Wrap in `useCallback` with stable deps
+- **NEVER** pass anonymous functions (`onClick={() => {}}`) or inline objects directly
+
+```tsx
+// BAD - anonymous function causes ALL nodes to re-render on every state change
+ {}} />
+
+// GOOD
+const handleNodeClick = useCallback(() => {}, []);
+
+```
+
+`nodeTypes` and `edgeTypes` must be memoized with `useMemo(() => ..., [])` or defined outside the component. These are currently correct in `NodeGraph.tsx`.
+
+## Rule 2: Never depend on `nodes`/`edges` arrays for structural data
+
+The `nodes` and `edges` arrays from `useNodesState`/`useEdgesState` get new references on every position change (drag). If you put them in a `useMemo`/`useEffect` dependency array, that code runs on every drag tick.
+
+**Pattern: Use stable structural keys**
+
+When you only care about which nodes exist (not their positions), derive a stable key using `useRef`:
+
+```tsx
+// Stable key - only changes when nodes are added/removed
+const nodeIdsKeyRef = useRef("");
+const computedKey = nodes.map((n) => n.id).join(",");
+if (computedKey !== nodeIdsKeyRef.current) {
+ nodeIdsKeyRef.current = computedKey;
+}
+const nodeIdsKey = nodeIdsKeyRef.current;
+
+// Now use nodeIdsKey instead of nodes in deps
+const searchNodes = useMemo(() => nodes, [nodeIdsKey]);
+```
+
+For edges, include source/target in the key:
+```tsx
+const edgeKey = edges.map((e) => `${e.source}-${e.target}`).join(",");
+```
+
+**Never do this:**
+```tsx
+// BAD - runs on every drag tick
+useEffect(() => { /* expensive work */ }, [nodes, edges]);
+
+// BAD - filter runs on every position change
+const selected = useMemo(() => nodes.filter(n => n.selected), [nodes]);
+```
+
+## Rule 3: Always wrap custom nodes and edges in `memo()`
+
+Every custom node and edge component MUST be wrapped in `React.memo`. This is the single most impactful optimization — it prevents node content from re-rendering during drag even if parent state changes.
+
+```tsx
+// GOOD
+export default memo(function MyNode(props: NodeProps) {
+ return
...
;
+});
+```
+
+All current node components (`ServiceNode`, `EventNode`, `CommandNode`, `QueryNode`, `ChannelNode`, `DataNode`, `ViewNode`, `ActorNode`, `NoteNode`, `ExternalSystem`, `Custom`, `Entity`, `Step`, `Domain`, `Flow`, `DataProduct`, `User`) are correctly wrapped.
+
+All edge components (`AnimatedMessageEdge`, `MultilineEdgeLabel`, `FlowEdge`) are correctly wrapped.
+
+**Do not break this pattern when adding new node or edge types.**
+
+## Rule 4: Memoize heavy sub-components inside nodes
+
+If a node renders complex sub-components (data grids, forms, SVG animations), wrap those in `memo()` too. This prevents the inner content from re-rendering even when the node itself re-renders.
+
+```tsx
+// Sub-components with static or rarely-changing props should be memoized
+const GlowHandle = memo(function GlowHandle({ side }: { side: "left" | "right" }) {
+ return ;
+});
+```
+
+Currently memoized sub-components: `GlowHandle` (in ServiceNode, EventNode, CommandNode, QueryNode), `MiniEnvelope`, `ServiceMessageFlow` (in ServiceNode).
+
+## Rule 5: Avoid `useStore` selectors that return new references
+
+If using ReactFlow's `useStore` (or any Zustand store), never return arrays/objects that get recreated on every state change:
+
+```tsx
+// BAD - new array reference on every state update
+const selected = useStore(state => state.nodes.filter(n => n.selected));
+
+// GOOD - extract primitive values, or use useShallow
+const selectedIds = useStore(
+ state => state.nodes.filter(n => n.selected).map(n => n.id)
+);
+// With useShallow for object/array returns
+import { useShallow } from 'zustand/react/shallow';
+const [a, b] = useStore(useShallow(state => [state.a, state.b]));
+```
+
+## Checklist for PR review
+
+When reviewing visualiser changes, verify:
+
+- [ ] No anonymous functions or inline objects passed to `` props
+- [ ] No `useMemo`/`useEffect`/`useCallback` with `nodes` or `edges` in deps (use structural keys instead)
+- [ ] New custom nodes/edges are wrapped in `memo()`
+- [ ] Heavy sub-components inside nodes are wrapped in `memo()`
+- [ ] No `useStore` selectors returning unstable references
+- [ ] `nodeTypes`/`edgeTypes` remain memoized with empty deps
+
+## Key files
+
+| File | What to check |
+|------|--------------|
+| `src/components/NodeGraph.tsx` | ReactFlow props, structural keys, legend computation |
+| `src/components/StepWalkthrough.tsx` | Effect dependencies use stable keys |
+| `src/components/VisualiserSearch.tsx` | Search filtering uses stable node snapshot |
+| `src/components/FocusMode/FocusModeContent.tsx` | Focus graph calculation deps |
+| `src/nodes/*/` | All node components wrapped in memo() |
+| `src/edges/*/` | All edge components wrapped in memo() |
+
+## Reference
+
+Based on: "The Ultimate Guide to Optimize React Flow Project Performance" by Lukasz Jazwa. Key benchmarks from that article (100 nodes):
+- Anonymous function on ReactFlow prop: 60 FPS -> 10 FPS (default), 2 FPS (heavy)
+- Node depending on full nodes array via useStore: 60 FPS -> 12 FPS
+- Adding React.memo to nodes: recovers to 50-60 FPS even with non-optimal parent
+- Memoizing heavy node content: recovers to 60 FPS stable
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 1e0b6cd75..000000000
--- a/.dockerignore
+++ /dev/null
@@ -1,7 +0,0 @@
-node_modules/
-.changeset/
-.github/
-.vscode/
-images/
-
-**/dist/
diff --git a/.github/workflows/notify-discord.yml b/.github/workflows/notify-discord.yml
new file mode 100644
index 000000000..112ed5e57
--- /dev/null
+++ b/.github/workflows/notify-discord.yml
@@ -0,0 +1,53 @@
+name: Discord Release Notification
+
+on:
+ workflow_dispatch: # Allow manual triggering for testing
+ release:
+ types:
+ - published
+
+jobs:
+ notify:
+ runs-on: ubuntu-latest
+ # Run on manual trigger OR when a core package release is published
+ if: >
+ github.event_name == 'workflow_dispatch' ||
+ (github.event_name == 'release' &&
+ startsWith(github.event.release.tag_name, '@eventcatalog/core@'))
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Get latest release info (manual trigger only)
+ if: github.event_name == 'workflow_dispatch'
+ id: release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ # Fetch the latest @eventcatalog/core release specifically
+ RELEASE_JSON=$(gh api repos/${{ github.repository }}/releases \
+ --jq '[.[] | select(.tag_name | startswith("@eventcatalog/core@"))] | first')
+
+ # Extract fields
+ echo "tag=$(echo "$RELEASE_JSON" | jq -r '.tag_name')" >> $GITHUB_OUTPUT
+ echo "name=$(echo "$RELEASE_JSON" | jq -r '.name')" >> $GITHUB_OUTPUT
+ echo "url=$(echo "$RELEASE_JSON" | jq -r '.html_url')" >> $GITHUB_OUTPUT
+
+ # Body needs special handling for multiline
+ echo "body<> $GITHUB_OUTPUT
+ echo "$RELEASE_JSON" | jq -r '.body' >> $GITHUB_OUTPUT
+ echo "EOF" >> $GITHUB_OUTPUT
+
+ - name: Send Discord notification
+ run: node packages/core/scripts/notify-discord-release.js
+ env:
+ DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
+ RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || steps.release.outputs.tag }}
+ RELEASE_NAME: ${{ github.event_name == 'release' && github.event.release.name || steps.release.outputs.name }}
+ RELEASE_BODY: ${{ github.event_name == 'release' && github.event.release.body || steps.release.outputs.body }}
+ RELEASE_URL: ${{ github.event_name == 'release' && github.event.release.html_url || steps.release.outputs.url }}
diff --git a/.github/workflows/redeploy-eventcatalog-examples.yml b/.github/workflows/redeploy-eventcatalog-examples.yml
new file mode 100644
index 000000000..ba613af80
--- /dev/null
+++ b/.github/workflows/redeploy-eventcatalog-examples.yml
@@ -0,0 +1,47 @@
+name: Redeploy EventCatalog Examples on core release
+on:
+ workflow_run:
+ workflows: ["Release"]
+ types:
+ - completed
+jobs:
+ redeploy-finance:
+ runs-on: ubuntu-latest
+ if: >
+ github.event.workflow_run.conclusion == 'success' &&
+ startsWith(github.event.workflow_run.head_commit.message, 'Version Packages')
+ steps:
+ - name: Redeploy EventCatalog Finance Example Catalog
+ run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL"
+ env:
+ VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_FINANCE_EXAMPLE_CATALOG_URL }}
+ redeploy-healthcare:
+ runs-on: ubuntu-latest
+ if: >
+ github.event.workflow_run.conclusion == 'success' &&
+ startsWith(github.event.workflow_run.head_commit.message, 'Version Packages')
+ steps:
+ - name: Redeploy EventCatalog Healthcare Example Catalog
+ run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL"
+ env:
+ VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_HEALTHCARE_EXAMPLE_CATALOG_URL }}
+ redeploy-demo:
+ runs-on: ubuntu-latest
+ if: >
+ github.event.workflow_run.conclusion == 'success' &&
+ startsWith(github.event.workflow_run.head_commit.message, 'Version Packages')
+ steps:
+ - name: Redeploy EventCatalog Demo (FlowMart) Example Catalog
+ run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL"
+ env:
+ VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_DEMO_FLOWMART_CATALOG_URL }}
+ redeploy-saas:
+ runs-on: ubuntu-latest
+ if: >
+ github.event.workflow_run.conclusion == 'success' &&
+ startsWith(github.event.workflow_run.head_commit.message, 'Version Packages')
+ steps:
+ - name: Redeploy EventCatalog Demo (SaaS) Example Catalog
+ run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL"
+ env:
+ VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_SAAS_EXAMPLE_CATALOG_URL }}
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5b8ff674e..2150974cd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -26,12 +26,16 @@ jobs:
with:
node-version: 24.x
cache: 'pnpm'
+ registry-url: 'https://registry.npmjs.org'
- name: Install deps
run: pnpm i
+ - name: Build all packages
+ run: pnpm run build:bin
+
- name: Create Release Pull Request or Publish to npm
- uses: changesets/action@master
+ uses: changesets/action@v1
with:
publish: pnpm run release
env:
diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml
index dd6531e0f..7863ab448 100644
--- a/.github/workflows/verify-build.yml
+++ b/.github/workflows/verify-build.yml
@@ -8,8 +8,8 @@ on:
- main
jobs:
- build-cli:
- name: Build cli
+ build-and-test:
+ name: Build and Test All Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -22,24 +22,54 @@ jobs:
cache: 'pnpm'
- name: Installation
run: pnpm i
- - name: Test
+ - name: Turbo Cache
+ uses: actions/cache@v4
+ with:
+ path: .turbo
+ key: turbo-${{ runner.os }}-${{ github.sha }}
+ restore-keys: |
+ turbo-${{ runner.os }}-
+ - name: Test All Packages
run: pnpm run test:ci
- - name: Build cli
+ - name: Build All Packages
run: pnpm run build:bin
- - name: Package
+ - name: Package SDK
+ run: pnpm pack
+ working-directory: packages/sdk
+ - name: Package CLI
+ run: pnpm pack
+ working-directory: packages/cli
+ - name: Package Core
+ run: pnpm pack
+ working-directory: packages/core
+ - name: Package Visualiser
+ run: pnpm pack
+ working-directory: packages/visualiser
+ - name: Package Language Server
run: pnpm pack
- - name: Rename package artifact
- run: mv *.tgz package.tgz # Rename to a consistent name
- - name: Upload package artifact
+ working-directory: packages/language-server
+ - name: Rename package artifacts
+ run: |
+ mv packages/sdk/*.tgz sdk-package.tgz
+ mv packages/cli/*.tgz cli-package.tgz
+ mv packages/core/*.tgz core-package.tgz
+ mv packages/visualiser/*.tgz visualiser-package.tgz
+ mv packages/language-server/*.tgz language-server-package.tgz
+ - name: Upload package artifacts
uses: actions/upload-artifact@v4
with:
- name: eventcatalog-core-package-artifact
- path: package.tgz
+ name: eventcatalog-packages-artifact
+ path: |
+ sdk-package.tgz
+ cli-package.tgz
+ core-package.tgz
+ visualiser-package.tgz
+ language-server-package.tgz
build-eventcatalog:
name: Build EventCatalog
timeout-minutes: 10
- needs: build-cli
+ needs: build-and-test
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
@@ -50,13 +80,18 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 24.x
- - name: Download package artifact
+ - name: Download package artifacts
uses: actions/download-artifact@v4
with:
- name: eventcatalog-core-package-artifact
- - name: Install @eventcatalog-core
+ name: eventcatalog-packages-artifact
+ - name: Install @eventcatalog packages
working-directory: examples/default/
- run: npm install ../../package.tgz
+ run: npm install ../../sdk-package.tgz ../../core-package.tgz
- name: Build EventCatalog
working-directory: examples/default/
run: npx eventcatalog build
+ - name: Check build size
+ if: matrix.os == 'ubuntu-latest'
+ run: node packages/core/scripts/check-build-size.js
+ env:
+ SIZE_THRESHOLD: 5
diff --git a/.gitignore b/.gitignore
index 326a6a93e..4e815c283 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
# build output
dist/
+# turborepo
+.turbo/
+
# dependencies
node_modules/
@@ -23,8 +26,9 @@ pnpm-debug.log*
# EventCatalog files
**/.eventcatalog-core/
/examples/large-catalog/*
-eventcatalog/pnpm-lock.yaml
-eventcatalog/public/pagefind
+packages/core/eventcatalog/pnpm-lock.yaml
+packages/core/eventcatalog/public/pagefind
+/packages/core/eventcatalog/src/pages/api/[...auth].ts
.vscode/*
@@ -37,10 +41,31 @@ git-push.sh
src/__tests__/example-catalog-dependencies/dependencies
-eventcatalog/public/ai
+**/__tests__/catalog/
+
+packages/core/eventcatalog/public/ai
examples/default/public/ai
+examples/default/eventcatalog.chat.js
**/[...auth].ts
+.astro/
+
dev-scripts/
-examples/e-commerce
\ No newline at end of file
+examples/e-commerce
+
+.claude/agents/eventcatalog-blog-writer.md
+pr-descriptions/
+.claude/commands/ship.md
+
+
+PRD.md
+progress.txt
+ralph-once.sh
+terms/
+
+notes/*
+PERFORMANCE-ANALYSIS.md
+
+
+.agents/skills/copy*
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..42c37b4b4
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,47 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+This repository is a `pnpm` + Turborepo monorepo. Packages live in `packages/*`:
+- `packages/core`: main EventCatalog app (Astro + React) and local scripts.
+- `packages/cli`, `packages/sdk`, `packages/linter`, `packages/language-server`, `packages/visualiser`, `packages/create-eventcatalog`.
+- `packages/playground`: local DSL playground app.
+
+Examples and integration checks live in `examples/` (notably `examples/default`). Shared release metadata is in `.changeset/`.
+
+## Build, Test, and Development Commands
+Run from repo root unless noted:
+- `pnpm i`: install workspace dependencies.
+- `pnpm build:bin`: build distributable artifacts across packages.
+- `pnpm build`: run full Turbo build graph.
+- `pnpm test`: run package tests.
+- `pnpm test:ci`: CI-style test run used by GitHub Actions.
+- `pnpm format` / `pnpm format:diff`: format or check formatting.
+- `pnpm start:catalog`: run local catalog via `@eventcatalog/core`.
+- `pnpm --filter @eventcatalog/ run
diff --git a/eventcatalog/src/components/Grids/DomainGrid.tsx b/eventcatalog/src/components/Grids/DomainGrid.tsx
deleted file mode 100644
index e71a18fc2..000000000
--- a/eventcatalog/src/components/Grids/DomainGrid.tsx
+++ /dev/null
@@ -1,390 +0,0 @@
-import { useState, useMemo, useEffect } from 'react';
-import {
- ServerIcon,
- EnvelopeIcon,
- RectangleGroupIcon,
- Squares2X2Icon,
- QueueListIcon,
- CircleStackIcon,
-} from '@heroicons/react/24/outline';
-import { buildUrlWithParams, buildUrl } from '@utils/url-builder';
-import type { CollectionEntry } from 'astro:content';
-import { type CollectionMessageTypes } from '@types';
-import { getCollectionStyles } from './utils';
-import { SearchBar } from './components';
-import { BoxIcon } from 'lucide-react';
-
-export interface ExtendedDomain extends CollectionEntry<'domains'> {
- sends: CollectionEntry[];
- receives: CollectionEntry[];
- services: CollectionEntry<'services'>[];
- domains: CollectionEntry<'domains'>[];
-}
-
-interface DomainGridProps {
- domains: ExtendedDomain[];
- embeded: boolean;
-}
-
-export default function DomainGrid({ domains, embeded }: DomainGridProps) {
- const [searchQuery, setSearchQuery] = useState('');
- const [isMultiColumn, setIsMultiColumn] = useState(false);
-
- useEffect(() => {
- if (typeof window !== 'undefined') {
- const saved = localStorage.getItem('EventCatalog:ArchitectureColumnLayout');
- if (saved !== null) {
- setIsMultiColumn(saved === 'multi');
- }
- }
- }, []);
-
- const toggleColumnLayout = () => {
- const newValue = !isMultiColumn;
- setIsMultiColumn(newValue);
- if (typeof window !== 'undefined') {
- localStorage.setItem('EventCatalog:ArchitectureColumnLayout', newValue ? 'multi' : 'single');
- }
- };
-
- const filteredDomains = useMemo(() => {
- let result = [...domains];
-
- // Filter by search query
- if (searchQuery) {
- const query = searchQuery.toLowerCase();
- result = result.filter(
- (domain) =>
- domain.data.name?.toLowerCase().includes(query) ||
- domain.data.summary?.toLowerCase().includes(query) ||
- domain.data.services?.some((service: any) => service.data.name.toLowerCase().includes(query)) ||
- domain.sends?.some((message: any) => message.data.name.toLowerCase().includes(query)) ||
- domain.receives?.some((message: any) => message.data.name.toLowerCase().includes(query))
- );
- }
-
- // Sort by name by default
- result.sort((a, b) => (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id));
-
- return result;
- }, [domains, searchQuery]);
-
- return (
-
- {/* Breadcrumb */}
-
-
-
-
-
-
- Domains ({filteredDomains.length})
-
-
Browse and manage domains in your event-driven architecture
- {urlParams?.domainName ? `Messages in ${urlParams.serviceName}` : 'All Messages'}
-
-
-
- {urlParams?.domainName
- ? `Browse messages in the ${urlParams.serviceName} service`
- : 'Browse and discover messages in your event-driven architecture'}
-
- {urlParams?.domainId
- ? `Browse services and messages in the ${urlParams.domainId} domain`
- : 'Browse and discover services in your event-driven architecture'}
-
e.stopPropagation()}
- >
- {pagefindLoadError ? (
- // Show indexing required message when Pagefind fails to load - full modal content
-
-
-
-
Search Index Not Found
-
- Your EventCatalog needs to be built to generate the search index. This enables fast searching across all
- your domains, services, events, and documentation.
-
-
-
-
-
-
- Build Your Catalog
-
-
- npm run build
-
-
This will generate your catalog and create the search index
-
-
-
-
-
-
Need to update search results?
-
- Run npm run build again after
- making changes to your catalog content.
-
- {/* Show stats and exact match toggle - only when there are results or search term */}
- {currentSearch.trim() && (filteredResults.length > 0 || isLoading) && (
-
-
- {filteredResults.length} results for "{currentSearch}"
- {isLoading && Loading...}
-