start-vibing-stacks 2.18.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing-stacks",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "description": "AI-powered multi-stack dev workflow for Claude Code. Supports PHP, Node.js, Python and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +1,14 @@
1
1
  ---
2
2
  name: preline-ui
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "Preline UI — semantic-token component system on top of Tailwind v4 (stable Jan 2025). 220+ design tokens covering background/foreground/primary/secondary/muted/destructive/border/surface/layer + per-component groups (navbar, sidebar, card, dropdown, select, overlay, tooltip, popover, scrollbar). 840+ free copy-paste blocks (hero, testimonials, pricing, dashboards, forms). Mandatory `HSStaticMethods.autoInit()` after client-side navigation in Next.js — without it, dropdowns/modals/accordions break. Theme generator CLI, custom palettes via `@theme inline { … }`, light + dark via `data-theme` + `.dark`. Chart tokens require HEX (no oklch in -hex). Pairs with React 19 (refs as props, document metadata). Invoke when using Preline components, creating themes, or customising tokens."
4
5
  ---
5
6
 
6
- # Preline UI — Component & Theme System for TailwindCSS 4
7
+ # Preline UI — Component & Theme System for Tailwind v4 (2026)
7
8
 
8
- **ALWAYS invoke when using Preline components, creating themes, or customizing design tokens.**
9
+ **ALWAYS invoke when using Preline components, creating themes, or customising design tokens.**
10
+
11
+ > Pairs with `tailwind-patterns` v2 (Tailwind v4 `@theme`, Oxide engine, OKLCH) and `react-standards` v2 (LABELS/STYLES const pattern). For shadcn-style primitives with Radix/Base UI underneath, see `shadcn-ui` v2 — pick **one** component system per project, don't mix Preline and shadcn.
9
12
 
10
13
  ## What is Preline
11
14
 
@@ -1,12 +1,23 @@
1
1
  ---
2
2
  name: react-patterns
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "Modern React 19 component architecture (stable Dec 5 2024; 19.1 in 2025). Covers the new hooks (`useActionState`, `useOptimistic`, `useFormStatus`, `use()` for promises and Context), refs-as-props (no more `forwardRef`), document metadata hoisting (write `<title>` and `<meta>` anywhere), Actions for async mutations, Server Components vs Client Components, compound + generic components, custom hooks, state-management decision matrix (useState → useReducer → Context → Zustand → server cache), Error Boundaries, performance via memo/useMemo/useCallback, and Suspense boundaries. Invoke for any new component, hook, state design, or React-architecture decision."
4
5
  ---
5
6
 
6
- # React Patterns — Modern Component Architecture
7
+ # React 19 Patterns — Modern Component Architecture
7
8
 
8
9
  **ALWAYS invoke when writing React components, hooks, or state management.**
9
10
 
11
+ ## React 19 essentials (stable Dec 5, 2024 → 19.1 in 2025)
12
+
13
+ - **Actions** — async functions passed straight to forms; `useActionState` manages pending/error/optimistic state automatically
14
+ - **`use()`** — read promises and Context **conditionally** during render (paired with Suspense + Error Boundaries)
15
+ - **Refs as props** — pass `ref` like any other prop. **`forwardRef` is no longer needed** for new components
16
+ - **Document metadata hoisting** — `<title>`, `<meta>`, `<link>` get auto-hoisted into `<head>` from anywhere
17
+ - **Form `action` prop** — submits via the action, auto-resets uncontrolled inputs on success
18
+ - **Stylesheet management** — `<link rel="stylesheet">` in components is awaited before the Suspense boundary reveals
19
+ - **Suspense sibling pre-warming** — fallback shows immediately and queues sibling requests in parallel
20
+
10
21
  ## Component Patterns
11
22
 
12
23
  ### Compound Components
@@ -109,52 +120,125 @@ const Heavy = lazy(() => import('./HeavyComponent'));
109
120
  <Suspense fallback={<Loading />}><Heavy /></Suspense>
110
121
  ```
111
122
 
112
- ## React 19 Hooks
123
+ ## React 19 — Actions & form-state hooks
124
+
125
+ ### `useActionState` — pending + error + result for one form
113
126
 
114
127
  ```tsx
115
- // useActionState — form submission state
116
128
  import { useActionState } from 'react';
117
129
 
118
130
  function LoginForm() {
119
131
  const [state, formAction, isPending] = useActionState(
120
- async (prev, formData: FormData) => {
132
+ async (_prev, formData: FormData) => {
121
133
  const result = await login(formData);
122
134
  if (result.error) return { error: result.error };
123
135
  redirect('/dashboard');
124
136
  },
125
- { error: null }
137
+ { error: null as string | null }
126
138
  );
127
139
 
128
140
  return (
129
141
  <form action={formAction}>
130
- <input name="email" type="email" />
142
+ <input name="email" type="email" required />
131
143
  {state.error && <p className="text-destructive">{state.error}</p>}
132
- <button disabled={isPending}>
133
- {isPending ? 'Signing in...' : 'Sign In'}
134
- </button>
144
+ <button disabled={isPending}>{isPending ? 'Signing in…' : 'Sign In'}</button>
135
145
  </form>
136
146
  );
137
147
  }
148
+ ```
149
+
150
+ ### `useFormStatus` — child of a form reads its parent's submit state
151
+
152
+ ```tsx
153
+ import { useFormStatus } from 'react-dom';
154
+
155
+ function SubmitButton() {
156
+ const { pending } = useFormStatus(); // ← reads the enclosing <form action={…}>
157
+ return <button disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>;
158
+ }
159
+ ```
138
160
 
139
- // useOptimisticinstant UI feedback
161
+ Drop `<SubmitButton />` inside any `<form action={…}>` no prop drilling, no context.
162
+
163
+ ### `useOptimistic` — instant UI feedback that auto-reverts on error
164
+
165
+ ```tsx
140
166
  import { useOptimistic } from 'react';
141
167
 
142
168
  function TodoList({ todos }: { todos: Todo[] }) {
143
169
  const [optimisticTodos, addOptimistic] = useOptimistic(
144
170
  todos,
145
- (state, newTodo: Todo) => [...state, newTodo]
171
+ (current, newTodo: Todo) => [...current, newTodo]
146
172
  );
147
173
 
148
- const addTodo = async (formData: FormData) => {
149
- const todo = { id: crypto.randomUUID(), title: formData.get('title') as string, done: false };
150
- addOptimistic(todo); // Instant UI
151
- await saveTodo(todo); // Server sync
152
- };
174
+ async function add(formData: FormData) {
175
+ const todo = { id: crypto.randomUUID(), title: String(formData.get('title')), done: false };
176
+ addOptimistic(todo); // instant
177
+ await saveTodo(todo); // server confirms (or throws → revert is automatic on rerender)
178
+ }
153
179
 
154
- return <ul>{optimisticTodos.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
180
+ return (
181
+ <>
182
+ <form action={add}><input name="title" /></form>
183
+ <ul>{optimisticTodos.map(t => <li key={t.id}>{t.title}</li>)}</ul>
184
+ </>
185
+ );
155
186
  }
156
187
  ```
157
188
 
189
+ ### `use()` — read a promise or Context conditionally
190
+
191
+ ```tsx
192
+ import { use, Suspense } from 'react';
193
+
194
+ function UserName({ userPromise }: { userPromise: Promise<User> }) {
195
+ const user = use(userPromise); // suspends until resolved
196
+ return <span>{user.name}</span>;
197
+ }
198
+
199
+ // Parent
200
+ <Suspense fallback={<Skeleton />}>
201
+ <UserName userPromise={fetchUser(id)} />
202
+ </Suspense>
203
+ ```
204
+
205
+ `use()` is the only hook allowed inside conditionals — perfect for "render after this resource resolves" without restructuring components.
206
+
207
+ ## React 19 — refs as props (no `forwardRef`)
208
+
209
+ ```tsx
210
+ // React 19 — `ref` is just a prop
211
+ function Input({ ref, className, ...props }: React.InputHTMLAttributes<HTMLInputElement> & {
212
+ ref?: React.Ref<HTMLInputElement>;
213
+ }) {
214
+ return <input ref={ref} className={cn('h-10 px-3 …', className)} {...props} />;
215
+ }
216
+
217
+ // Usage — no different from before
218
+ const myRef = useRef<HTMLInputElement>(null);
219
+ <Input ref={myRef} />
220
+ ```
221
+
222
+ `forwardRef` keeps working — but new components should ditch it.
223
+
224
+ ## React 19 — document metadata anywhere
225
+
226
+ ```tsx
227
+ function Article({ post }: { post: Post }) {
228
+ return (
229
+ <article>
230
+ <title>{post.title} — My Site</title> {/* hoisted into <head> */}
231
+ <meta name="description" content={post.summary} />
232
+ <link rel="canonical" href={`/blog/${post.slug}`} />
233
+ <h1>{post.title}</h1>
234
+ {/* … */}
235
+ </article>
236
+ );
237
+ }
238
+ ```
239
+
240
+ No `next/head` / `react-helmet` needed — React handles it. Server-rendered metadata appears in the initial HTML (good for SEO).
241
+
158
242
  ## State Management Selection
159
243
 
160
244
  | Complexity | Solution | When |
@@ -219,11 +303,26 @@ class ErrorBoundary extends Component<
219
303
 
220
304
  ## FORBIDDEN
221
305
 
222
- 1. **Class components** function components only (except ErrorBoundary)
223
- 2. **Prop drilling** — use context or composition
224
- 3. **Inline objects/functions in JSX**causes re-renders
225
- 4. **useEffect for derived state** use useMemo
226
- 5. **Mutating state directly** always setState/dispatch
227
- 6. **Index as key** use stable unique ID (`uuid`, `id`)
228
- 7. **Premature optimization** profile first with React DevTools
229
- 8. **useEffect for everything** prefer server components for data
306
+ | Pattern | Why |
307
+ |---|---|
308
+ | `forwardRef` in new components | Refs are props in React 19 drop the wrapper |
309
+ | `useEffect` to fetch data | Use Server Components, `use()` + Suspense, or TanStack Query |
310
+ | `useEffect` for derived state | Compute it in render or via `useMemo` |
311
+ | `next/head` / `react-helmet` in R19 | Use plain `<title>` / `<meta>` — React hoists |
312
+ | Class components | Function components only (Error Boundaries are the rare exception) |
313
+ | Prop drilling > 2 levels | Composition or `Context` |
314
+ | Inline objects/functions in hot paths | Stable refs via `useMemo` / `useCallback` (only when profiler shows it matters) |
315
+ | Direct state mutation | Always set new state — React relies on referential change |
316
+ | Index as `key` | Use stable unique IDs (`crypto.randomUUID()`, server IDs) |
317
+ | Premature memoisation | Profile with React DevTools first |
318
+ | Manual `isPending` + `error` `useState` for forms | Use `useActionState` |
319
+ | Custom optimistic-state code | `useOptimistic` + Action |
320
+
321
+ ## See Also
322
+
323
+ - `react-standards` v2 — LABELS / STYLES const pattern
324
+ - `react-ui-patterns` v2 — loading/error/empty + TanStack Query v5
325
+ - `tailwind-patterns` v2 — `@theme`, container queries, OKLCH
326
+ - `shadcn-ui` v2 — primitives without `forwardRef`, `data-slot` styling
327
+ - `zod-validation` v2 — Zod 4 + RHF for form Actions
328
+ - `_shared/skills/playwright-automation` v2 — E2E coverage including R19 forms
@@ -1,14 +1,18 @@
1
1
  ---
2
2
  name: react-standards
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "Project conventions for React 19 + Tailwind v4 codebases. Mandates the LABELS / STYLES const pattern (defined at file top, before hooks, with `as const` for stable references — prevents re-renders, centralises styles, supports i18n drop-in), controlled debug logging (no raw console.log), semantic Tailwind tokens (no raw colours), separate-file SVG icons, mandatory loading/empty states, modal data-flow contract (`onUpdated` callback), and third-party chart integration (no useRef DOM mutation). Pairs with `tailwind-patterns` v2 for tokens and `react-ui-patterns` v2 for state choreography. Invoke at the start of any new React project or component file."
4
5
  ---
5
6
 
6
- # React 19+ Standards
7
+ # React 19 + Tailwind v4 — Project Standards
8
+
9
+ > Conventions that apply to **every** component in the codebase. Pairs with `react-patterns` (architecture), `tailwind-patterns` (tokens), `react-ui-patterns` (state), and `shadcn-ui` (primitives).
7
10
 
8
11
  ## Version Requirements
9
12
 
10
- - **ReactJS >= 19** — MANDATORY
11
- - **TailwindCSS >= 4** — MANDATORY
13
+ - **React 19.0** (stable Dec 5, 2024) — MANDATORY for new projects
14
+ - **TailwindCSS 4.0** (stable Jan 22, 2025) — MANDATORY (CSS-first `@theme`, Oxide engine)
15
+ - **TypeScript ≥ 5.6** strict mode
12
16
 
13
17
  ## Label Constants Pattern
14
18
 
@@ -279,3 +283,12 @@ const processedData = useMemo(() => {
279
283
  - Conditional rendering: `data && <Component />`
280
284
  - `useMemo` for expensive computations
281
285
  - Loading states before rendering charts
286
+
287
+ ## See Also
288
+
289
+ - `react-patterns` v2 — React 19 hooks, `use()`, refs as props
290
+ - `tailwind-patterns` v2 — `@theme`, Oxide engine, container queries, OKLCH
291
+ - `shadcn-ui` v2 — primitives without `forwardRef`, `data-slot` slots
292
+ - `react-ui-patterns` v2 — loading/error/empty + TanStack Query v5
293
+ - `zod-validation` v2 — Zod 4 schemas for forms + env vars
294
+ - `_shared/skills/ui-ux-audit` v2 — WCAG 2.2 AA validation
@@ -1,12 +1,26 @@
1
1
  ---
2
2
  name: react-ui-patterns
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "React 19 + TanStack Query v5 UI patterns for production async UI: loading/error/empty state hierarchy, skeleton vs spinner choice, ErrorState/EmptyState/LoadingState reusable components, button states with disabled+loading indicator, RHF + Zod 4 forms, optimistic updates via React 19 `useOptimistic` (built-in, simpler than TanStack `onMutate`/rollback) AND TanStack Query v5 onMutate pattern (when you need cache surgery), Suspense + useSuspenseQuery boundaries, signal threading for cancellation. TanStack Query v5 renamed `loading` → `pending` (note isPending vs isFetching vs isLoading). Invoke when handling any async UI state, form, mutation, or user feedback."
4
5
  ---
5
6
 
6
- # React UI Patterns — Loading, Errors, Empty States & Forms
7
+ # React UI Patterns — Loading, Errors, Empty States & Forms (R19 + TanStack v5)
7
8
 
8
9
  **ALWAYS invoke when handling async UI states, forms, or user feedback.**
9
10
 
11
+ ## Stack snapshot (2026)
12
+
13
+ | Concern | Pick |
14
+ |---|---|
15
+ | Server cache + refetch + dedup | **TanStack Query v5** (`useQuery`, `useMutation`, `useSuspenseQuery`) |
16
+ | Form state + validation | **react-hook-form** + **`@hookform/resolvers/zod`** (Zod 4) |
17
+ | Optimistic UI for **submit-then-confirm** flows | React 19 **`useOptimistic`** (no cache surgery needed) |
18
+ | Optimistic UI when you must mutate the cache | TanStack `onMutate` + `setQueryData` rollback |
19
+ | Suspense / streaming data | `useSuspenseQuery` + `<Suspense>` |
20
+ | Toasts | **Sonner** (`toast.success`, `toast.error`) |
21
+
22
+ > **TanStack Query v5 renamed `loading` → `pending`.** Use `isPending` (no data yet, no error), `isFetching` (any fetch in flight, including refetch), `isLoading` (legacy alias for `isPending && isFetching`). The render-tree decision below is unchanged.
23
+
10
24
  ## Core Principles
11
25
 
12
26
  1. **Never show stale UI** — loading only when actually loading
@@ -33,18 +47,40 @@ Has data?
33
47
  ```
34
48
 
35
49
  ```tsx
36
- // ✅ CORRECT — only loading when no data
37
- const { data, isLoading, error, refetch } = useQuery(...);
50
+ // ✅ CORRECT — TanStack v5: isPending = no data yet
51
+ const { data, isPending, error, refetch } = useQuery({
52
+ queryKey: ['items'],
53
+ queryFn: ({ signal }) => fetchItems({ signal }), // thread signal for cancellation
54
+ });
38
55
 
39
- if (error) return <ErrorState error={error} onRetry={refetch} />;
40
- if (isLoading && !data) return <Skeleton />;
56
+ if (error) return <ErrorState error={error} onRetry={refetch} />;
57
+ if (isPending) return <Skeleton />;
41
58
  if (!data?.items.length) return <EmptyState />;
42
59
  return <ItemList items={data.items} />;
43
60
 
44
- // ❌ WRONG — flashes spinner on refetch when cached data exists
61
+ // ❌ WRONG (v3 idiom) — flashes spinner on refetch when cached data exists
45
62
  if (isLoading) return <Spinner />;
46
63
  ```
47
64
 
65
+ ### Suspense alternative
66
+
67
+ ```tsx
68
+ // useSuspenseQuery — cleaner when you wrap with <Suspense> + <ErrorBoundary>
69
+ function ItemList() {
70
+ const { data } = useSuspenseQuery({
71
+ queryKey: ['items'],
72
+ queryFn: ({ signal }) => fetchItems({ signal }),
73
+ });
74
+ return <ul>{data.items.map(i => <li key={i.id}>{i.title}</li>)}</ul>;
75
+ }
76
+
77
+ <ErrorBoundary fallback={<ErrorState />}>
78
+ <Suspense fallback={<Skeleton />}>
79
+ <ItemList />
80
+ </Suspense>
81
+ </ErrorBoundary>
82
+ ```
83
+
48
84
  ### Skeleton vs Spinner
49
85
 
50
86
  | Use Skeleton | Use Spinner |
@@ -276,49 +312,83 @@ export default function CreateUser() {
276
312
  }
277
313
  ```
278
314
 
279
- ## Optimistic Updates (TanStack Query)
315
+ ## Optimistic Updates pick by use case
316
+
317
+ ### Option A — React 19 `useOptimistic` (simpler, NEW DEFAULT for forms)
318
+
319
+ When the optimistic update is **local to a component** and tied to a form Action, this is the cleanest option — no manual rollback, no cache surgery.
320
+
321
+ ```tsx
322
+ "use client";
323
+ import { useOptimistic } from "react";
324
+
325
+ function TodoList({ todos, addTodo }: { todos: Todo[]; addTodo: (t: Todo) => Promise<void> }) {
326
+ const [optimisticTodos, addOptimistic] = useOptimistic(
327
+ todos,
328
+ (current, newTodo: Todo) => [...current, newTodo],
329
+ );
330
+
331
+ async function action(formData: FormData) {
332
+ const todo = { id: crypto.randomUUID(), title: String(formData.get("title")), done: false };
333
+ addOptimistic(todo); // instant
334
+ await addTodo(todo); // server confirms — on error React reverts on next render
335
+ }
336
+
337
+ return (
338
+ <>
339
+ <form action={action}>
340
+ <input name="title" required />
341
+ <button>Add</button>
342
+ </form>
343
+ <ul>{optimisticTodos.map(t => <li key={t.id}>{t.title}</li>)}</ul>
344
+ </>
345
+ );
346
+ }
347
+ ```
348
+
349
+ ### Option B — TanStack Query `onMutate` + cache rollback
350
+
351
+ Reach for this when the mutation has to update a **shared server cache** that other components also read (toggling a favourite that appears in three lists, for instance).
280
352
 
281
353
  ```tsx
282
- import { useMutation, useQueryClient } from '@tanstack/react-query';
354
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
283
355
 
284
356
  function ToggleFavorite({ item }: { item: Item }) {
285
- const queryClient = useQueryClient();
357
+ const qc = useQueryClient();
286
358
 
287
359
  const mutation = useMutation({
288
360
  mutationFn: () =>
289
- fetch(`/api/items/${item.id}/favorite`, { method: 'POST' }).then((res) => {
290
- if (!res.ok) throw new Error('Failed');
291
- return res.json();
361
+ fetch(`/api/items/${item.id}/favorite`, { method: "POST" }).then(r => {
362
+ if (!r.ok) throw new Error("Failed");
363
+ return r.json();
292
364
  }),
293
365
  onMutate: async () => {
294
- await queryClient.cancelQueries({ queryKey: ['items'] });
295
- const previous = queryClient.getQueryData<Item[]>(['items']);
296
-
297
- queryClient.setQueryData<Item[]>(['items'], (old) =>
298
- old?.map((i) =>
299
- i.id === item.id ? { ...i, isFavorite: !i.isFavorite } : i
300
- )
366
+ await qc.cancelQueries({ queryKey: ["items"] });
367
+ const previous = qc.getQueryData<Item[]>(["items"]);
368
+ qc.setQueryData<Item[]>(["items"], (old) =>
369
+ old?.map(i => i.id === item.id ? { ...i, isFavorite: !i.isFavorite } : i),
301
370
  );
302
-
303
371
  return { previous };
304
372
  },
305
373
  onError: (_err, _vars, context) => {
306
- queryClient.setQueryData(['items'], context?.previous);
307
- toast.error('Failed to update');
374
+ qc.setQueryData(["items"], context?.previous); // rollback
375
+ toast.error("Failed to update");
308
376
  },
309
377
  onSettled: () => {
310
- queryClient.invalidateQueries({ queryKey: ['items'] });
378
+ qc.invalidateQueries({ queryKey: ["items"] }); // truth from server
311
379
  },
312
380
  });
313
381
 
314
382
  return (
315
- <button onClick={() => mutation.mutate()} className="text-xl">
316
- {item.isFavorite ? '❤️' : '🤍'}
383
+ <button onClick={() => mutation.mutate()} className="text-xl" aria-label="Toggle favorite">
384
+ {item.isFavorite ? "❤️" : "🤍"}
317
385
  </button>
318
386
  );
319
387
  }
320
388
  ```
321
389
 
390
+ > Don't combine both for the same flow — `useOptimistic` for forms, TanStack `onMutate` for cross-cache updates. Mixing them creates invisible double rollbacks.
391
+
322
392
  ## Checklist — Before Shipping Any UI Component
323
393
 
324
394
  - [ ] Error state handled and shown to user
@@ -332,8 +402,13 @@ function ToggleFavorite({ item }: { item: Item }) {
332
402
 
333
403
  ## FORBIDDEN
334
404
 
335
- 1. **`if (loading) return <Spinner />`** — check `loading && !data` instead
336
- 2. **Silent catch** — always toast/display errors to user
337
- 3. **No empty state** every list needs one
338
- 4. **Clickable button during submit** always `disabled={isPending}` or `disabled={isSubmitting}`
339
- 5. **Console.log-only errors** user must see feedback
405
+ | Anti-pattern | Fix |
406
+ |---|---|
407
+ | `if (isLoading) return <Spinner />` (v3 idiom) | TanStack v5: check `isPending` (no data yet), keep cached data visible during refetch |
408
+ | Silent `catch (e) { console.log(e) }` | Always surface via toast / inline error |
409
+ | List without an empty state | Every collection needs `<EmptyState />` |
410
+ | Clickable button while a mutation is pending | `disabled={mutation.isPending}` + spinner |
411
+ | `console.log`-only errors | User must see feedback (toast / banner / inline) |
412
+ | `useOptimistic` AND TanStack `onMutate` for the same flow | Pick one — mixing produces double rollbacks |
413
+ | `mutationFn` that doesn't thread the `signal` | Add `({ signal }) => fetch(url, { signal })` so unmount cancels in-flight |
414
+ | `forwardRef` wrappers around shadcn primitives | React 19 — refs are plain props |
@@ -1,97 +1,325 @@
1
1
  ---
2
2
  name: shadcn-ui
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "shadcn/ui CLI v4 (March 2026) for React 19 + Tailwind v4. Components updated to remove forwardRef (refs as props in R19), every primitive carries a data-slot attribute for targeted styling, HSL → OKLCH tokens, size-* utility replaces w-h pairs, default style deprecated in favor of new-york. CLI v4 brings shadcn/skills (AI agent context for component registry), --preset (shareable design-system bundle), --dry-run / --diff / --view (inspect changes before applying), --template scaffolds (Next.js, Vite, Laravel, React Router, Astro, TanStack Start), --base flag (Radix vs Base UI primitives), and `shadcn info` / `shadcn docs` commands for agents. Includes components.json schema, registry-item.json, blocks. Invoke when adding, customising, theming, or scaffolding shadcn components."
4
5
  ---
5
6
 
6
- # shadcn/ui — Component Library Patterns
7
+ # shadcn/ui — CLI v4 + React 19 + Tailwind v4 (2026)
7
8
 
8
- **ALWAYS invoke when adding or modifying shadcn/ui components.**
9
+ **ALWAYS invoke when adding, customising, theming, or scaffolding shadcn components.**
9
10
 
10
- ## Installation
11
+ > shadcn/ui is a copy-paste component system, not an npm dependency — components live **in your repo**, get owned and modified there. The CLI keeps them in sync with the upstream registry without hiding the source.
12
+
13
+ ## What's new in 2026 (CLI v4 — March 2026)
14
+
15
+ | Change | Impact |
16
+ |---|---|
17
+ | **Tailwind v4 + React 19 first-class** | All components updated; v3+R18 still works (non-breaking) |
18
+ | **`forwardRef` removed** | React 19 treats `ref` as a prop — components are simpler |
19
+ | **`data-slot="…"` on every primitive** | Style any sub-element from outside without overriding source |
20
+ | **HSL → OKLCH** colour tokens | Smoother gradients, perceptual uniformity |
21
+ | **`size-*` utility** | `size-9` instead of `w-9 h-9` |
22
+ | **`default` style deprecated** | Use **`new-york`** (the new default in `components.json`) |
23
+ | **`shadcn/skills`** | AI-agent registry context — agents discover components via the CLI |
24
+ | **`--preset` flag** | Pack colours/theme/fonts/radius into a shareable code string |
25
+ | **`--dry-run` / `--diff` / `--view`** | Inspect registry changes before writing files |
26
+ | **`--template`** | Scaffolds for Next.js, Vite, Laravel, React Router, Astro, TanStack Start |
27
+ | **`--base`** | Choose between **Radix** and **Base UI** primitives |
28
+ | **`shadcn info` / `shadcn docs`** | Context commands for agents and humans |
29
+
30
+ ## Initialisation
11
31
 
12
32
  ```bash
13
- npx shadcn@latest init
14
- npx shadcn@latest add button card dialog input
33
+ # Pick a template (CLI v4 — March 2026)
34
+ npx shadcn@latest init --template next-app
35
+ # Other templates: vite, laravel, react-router, astro, tanstack-start
36
+
37
+ # Add components — non-interactive, idempotent
38
+ npx shadcn@latest add button card dialog input form
39
+
40
+ # Inspect before writing (new in v4)
41
+ npx shadcn@latest add button --dry-run --diff
42
+
43
+ # Use Base UI primitives instead of Radix
44
+ npx shadcn@latest add dialog --base baseui
15
45
  ```
16
46
 
17
- ## Theming
47
+ ## `components.json` (Tailwind v4 shape)
48
+
49
+ ```json
50
+ {
51
+ "$schema": "https://ui.shadcn.com/schema.json",
52
+ "style": "new-york",
53
+ "rsc": true,
54
+ "tsx": true,
55
+ "tailwind": {
56
+ "config": "",
57
+ "css": "src/app/globals.css",
58
+ "baseColor": "neutral",
59
+ "cssVariables": true,
60
+ "prefix": ""
61
+ },
62
+ "aliases": {
63
+ "components": "@/components",
64
+ "utils": "@/lib/utils",
65
+ "ui": "@/components/ui",
66
+ "lib": "@/lib",
67
+ "hooks": "@/hooks"
68
+ },
69
+ "iconLibrary": "lucide"
70
+ }
71
+ ```
72
+
73
+ > For Tailwind v4 the `tailwind.config` field stays empty — there's no JS config file in v4.
74
+
75
+ ## Theming with OKLCH
18
76
 
19
77
  ```css
20
- /* globals.css — CSS variables for theming */
21
- @layer base {
22
- :root {
23
- --background: 0 0% 100%;
24
- --foreground: 222.2 84% 4.9%;
25
- --primary: 222.2 47.4% 11.2%;
26
- --primary-foreground: 210 40% 98%;
27
- --muted: 210 40% 96.1%;
28
- --muted-foreground: 215.4 16.3% 46.9%;
29
- --border: 214.3 31.8% 91.4%;
30
- --ring: 222.2 84% 4.9%;
31
- --radius: 0.5rem;
32
- }
33
- .dark {
34
- --background: 222.2 84% 4.9%;
35
- --foreground: 210 40% 98%;
36
- --primary: 210 40% 98%;
37
- --primary-foreground: 222.2 47.4% 11.2%;
38
- }
78
+ /* src/app/globals.css */
79
+ @import "tailwindcss";
80
+
81
+ @theme inline {
82
+ /* Light theme — OKLCH (replaces v1 HSL tokens) */
83
+ --color-background: oklch(1.000 0 0);
84
+ --color-foreground: oklch(0.145 0 0);
85
+
86
+ --color-primary: oklch(0.205 0 0);
87
+ --color-primary-foreground: oklch(0.985 0 0);
88
+
89
+ --color-secondary: oklch(0.970 0 0);
90
+ --color-secondary-foreground: oklch(0.205 0 0);
91
+
92
+ --color-muted: oklch(0.970 0 0);
93
+ --color-muted-foreground: oklch(0.556 0 0);
94
+
95
+ --color-destructive: oklch(0.577 0.245 27.325);
96
+ --color-destructive-foreground: oklch(0.985 0 0);
97
+
98
+ --color-border: oklch(0.922 0 0);
99
+ --color-ring: oklch(0.708 0 0);
100
+
101
+ --radius: 0.625rem; /* slightly larger than v1's 0.5rem */
102
+ }
103
+
104
+ @theme dark inline {
105
+ --color-background: oklch(0.145 0 0);
106
+ --color-foreground: oklch(0.985 0 0);
107
+ --color-primary: oklch(0.985 0 0);
108
+ --color-primary-foreground: oklch(0.205 0 0);
109
+ /* … rest of dark overrides */
110
+ }
111
+ ```
112
+
113
+ OKLCH ranges (rough guide): lightness 0–1, chroma 0–~0.4, hue 0–360°. Pick lightness on the same axis for siblings — gradients stay perceptually smooth.
114
+
115
+ ## Components without `forwardRef`
116
+
117
+ ```tsx
118
+ // React 19 — refs are props, no forwardRef needed
119
+ import * as React from "react";
120
+ import { cn } from "@/lib/utils";
121
+
122
+ function Button({
123
+ ref, // ← just a normal prop
124
+ className,
125
+ variant = "default",
126
+ size = "default",
127
+ ...props
128
+ }: React.ButtonHTMLAttributes<HTMLButtonElement> & {
129
+ ref?: React.Ref<HTMLButtonElement>;
130
+ variant?: "default" | "outline" | "ghost" | "destructive";
131
+ size?: "default" | "sm" | "lg" | "icon";
132
+ }) {
133
+ return (
134
+ <button
135
+ ref={ref}
136
+ data-slot="button" // ← every primitive carries data-slot
137
+ className={cn(buttonVariants({ variant, size }), className)}
138
+ {...props}
139
+ />
140
+ );
39
141
  }
142
+
143
+ export { Button };
144
+ ```
145
+
146
+ `forwardRef` still works — but the new components ship without it.
147
+
148
+ ## `data-slot` — style sub-parts from outside
149
+
150
+ ```tsx
151
+ // Style the icon inside a Button without touching the Button source
152
+ <Button className="[&_[data-slot=icon]]:size-4 [&_[data-slot=icon]]:text-primary">
153
+ <CheckIcon data-slot="icon" />
154
+ Confirm
155
+ </Button>
156
+
157
+ // Style the trigger of a Dialog from a parent
158
+ <Dialog>
159
+ <DialogTrigger className="[&[data-slot=trigger]]:rounded-full" asChild>
160
+ <Button>Open</Button>
161
+ </DialogTrigger>
162
+ </Dialog>
40
163
  ```
41
164
 
42
- ## Component Customization
165
+ This replaces the old "expose every internal class through props" pattern — the contract is the slot name, not the className.
166
+
167
+ ## Customisation — extend, don't fork
43
168
 
44
169
  ```tsx
45
170
  // CORRECT — extend via className + variants
46
- import { Button } from '@/components/ui/button';
47
- <Button variant="outline" size="lg" className="rounded-full">Custom</Button>
171
+ <Button variant="outline" size="lg" className="rounded-full">
172
+ Custom
173
+ </Button>
48
174
 
49
- // WRONGmodify component source for one-off styles
175
+ // CORRECTwrap a shadcn primitive when you need a project-specific variant
176
+ function PageHeading({ children, className, ...rest }: ComponentProps<"h1">) {
177
+ return (
178
+ <h1 data-slot="page-heading" className={cn("text-2xl font-semibold tracking-tight", className)} {...rest}>
179
+ {children}
180
+ </h1>
181
+ );
182
+ }
183
+
184
+ // WRONG — modifying the shadcn source for one-off styles
185
+ // → upgrade path breaks; intent gets lost
50
186
  ```
51
187
 
52
- ## Composition Pattern
188
+ ## Composition
53
189
 
54
190
  ```tsx
55
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
56
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
191
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
192
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
57
193
 
58
- // Compose primitives, don't create monolithic components
59
194
  <Dialog>
60
195
  <DialogTrigger asChild>
61
196
  <Button variant="outline">Open</Button>
62
197
  </DialogTrigger>
63
198
  <DialogContent>
64
199
  <DialogHeader>
65
- <DialogTitle>Title</DialogTitle>
200
+ <DialogTitle>Edit profile</DialogTitle>
66
201
  </DialogHeader>
67
- <Card><CardContent>...</CardContent></Card>
202
+ <Card>
203
+ <CardHeader>
204
+ <CardTitle>Account</CardTitle>
205
+ <CardDescription>Update your information.</CardDescription>
206
+ </CardHeader>
207
+ <CardContent>{/* form fields */}</CardContent>
208
+ </Card>
68
209
  </DialogContent>
69
210
  </Dialog>
70
211
  ```
71
212
 
72
- ## Form Pattern (with Zod)
213
+ ## Forms — shadcn Form + React Hook Form + Zod 4
73
214
 
74
215
  ```tsx
75
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
76
- import { useForm } from 'react-hook-form';
77
- import { zodResolver } from '@hookform/resolvers/zod';
78
-
79
- const form = useForm<z.infer<typeof schema>>({ resolver: zodResolver(schema) });
80
-
81
- <Form {...form}>
82
- <FormField control={form.control} name="email" render={({ field }) => (
83
- <FormItem>
84
- <FormLabel>Email</FormLabel>
85
- <FormControl><Input {...field} /></FormControl>
86
- <FormMessage />
87
- </FormItem>
88
- )} />
89
- </Form>
216
+ "use client";
217
+ import { z } from "zod";
218
+ import { useForm } from "react-hook-form";
219
+ import { zodResolver } from "@hookform/resolvers/zod";
220
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
221
+ import { Input } from "@/components/ui/input";
222
+ import { Button } from "@/components/ui/button";
223
+
224
+ const Schema = z.object({
225
+ email: z.email("Enter a valid email"), // Zod 4 top-level
226
+ password: z.string().min(8, "At least 8 characters"),
227
+ });
228
+
229
+ type FormValues = z.infer<typeof Schema>;
230
+
231
+ export function LoginForm() {
232
+ const form = useForm<FormValues>({
233
+ resolver: zodResolver(Schema),
234
+ defaultValues: { email: "", password: "" },
235
+ });
236
+
237
+ function onSubmit(values: FormValues) { /* call your API */ }
238
+
239
+ return (
240
+ <Form {...form}>
241
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
242
+ <FormField control={form.control} name="email" render={({ field }) => (
243
+ <FormItem>
244
+ <FormLabel>Email</FormLabel>
245
+ <FormControl><Input type="email" autoComplete="email" {...field} /></FormControl>
246
+ <FormMessage />
247
+ </FormItem>
248
+ )}/>
249
+ <FormField control={form.control} name="password" render={({ field }) => (
250
+ <FormItem>
251
+ <FormLabel>Password</FormLabel>
252
+ <FormControl><Input type="password" autoComplete="current-password" {...field} /></FormControl>
253
+ <FormMessage />
254
+ </FormItem>
255
+ )}/>
256
+ <Button type="submit" className="w-full">Sign in</Button>
257
+ </form>
258
+ </Form>
259
+ );
260
+ }
261
+ ```
262
+
263
+ ## Blocks — copy-paste full UI sections
264
+
265
+ ```bash
266
+ # List blocks (auth, dashboards, charts, sidebars, calendars, …)
267
+ npx shadcn@latest add --list-blocks
268
+
269
+ # Add a specific block
270
+ npx shadcn@latest add login-04
271
+ ```
272
+
273
+ Blocks are full sections (login pages, dashboard shells, billing screens) registered as `registry:block` in the catalog. They land in `components/` and you own them after that.
274
+
275
+ ## Custom registries
276
+
277
+ ```json
278
+ // registry-item.json — share components across your team / org
279
+ {
280
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
281
+ "name": "data-table",
282
+ "type": "registry:component",
283
+ "dependencies": ["@tanstack/react-table"],
284
+ "registryDependencies": ["button", "input"],
285
+ "files": [{ "path": "components/data-table.tsx", "type": "registry:component" }]
286
+ }
287
+ ```
288
+
289
+ Host the JSON anywhere (GitHub Pages, Vercel) and consume:
290
+
291
+ ```bash
292
+ npx shadcn@latest add https://your-registry.com/r/data-table.json
293
+ ```
294
+
295
+ ## CLI v4 — workflow commands
296
+
297
+ ```bash
298
+ shadcn info # show project + dependency context (great for AI agents)
299
+ shadcn docs <component> # open docs for a specific component
300
+ shadcn add button --dry-run --view # show what would change, don't write
301
+ shadcn add button --diff # show diff against current files
302
+ shadcn add --preset <hash> # bootstrap an entire design-system from a preset hash
90
303
  ```
91
304
 
92
305
  ## FORBIDDEN
93
306
 
94
- 1. **Modifying component source for one-off needs** — use className/variants
95
- 2. **Installing without init** — always `npx shadcn@latest init` first
96
- 3. **Ignoring dark mode** all components must work in both themes
97
- 4. **Skipping accessibility** shadcn components have built-in a11y, don't break it
307
+ | Don't | Why |
308
+ |---|---|
309
+ | Modify component source for one-off styles | Use `className` + `variant` + slots |
310
+ | Skip `init` | The CLI needs `components.json` for paths/aliases |
311
+ | Stay on the deprecated `default` style | Use `new-york` (new CLI default) |
312
+ | Use `forwardRef` in new R19 components | Refs are plain props now |
313
+ | Add `w-10 h-10` instead of `size-10` | Tailwind v4 `size-*` is the recommended utility |
314
+ | HSL tokens for new themes | OKLCH gives smoother gradients & a11y-friendly mixing |
315
+ | Add a component then `npm install` it as a package | shadcn lives **in your repo**, never as a runtime dependency |
316
+ | Break built-in a11y (aria, focus, escape handlers) | Radix/Base UI primitives wire it correctly out of the box |
317
+
318
+ ## See Also
319
+
320
+ - `tailwind-patterns` v2 — `@theme`, container queries, OKLCH
321
+ - `react-patterns` v2 — refs as props in R19, `useActionState`, `useOptimistic`
322
+ - `react-standards` v2 — STYLES/LABELS const pattern that pairs with shadcn slots
323
+ - `zod-validation` v2 — Zod 4 + `@hookform/resolvers/zod`
324
+ - `react-ui-patterns` v2 — loading/error/empty states using shadcn primitives
325
+ - `_shared/skills/ui-ux-audit` v2 — WCAG 2.2 AA on shadcn components
@@ -1,21 +1,37 @@
1
1
  ---
2
2
  name: tailwind-patterns
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "Tailwind CSS v4 (stable Jan 22 2025) CSS-first design system with the Rust-based Oxide engine — 3.78× faster full builds, 8.8× faster incremental, 100× faster on no-CSS-change runs. CSS-first config via `@theme` directive (no `tailwind.config.js`), automatic content detection, native container queries (`@container` + `@md:`), OKLCH color space (perceptual uniformity), modern CSS features (cascade layers, `@property`, `color-mix()`), `@import \"tailwindcss\"` single line. Covers the v3 → v4 migration, semantic-token architecture, mobile-first + container-query responsive strategy, dark mode via `@theme dark`, and the 4px spacing scale. Invoke whenever writing Tailwind classes, theming a project, or designing responsive layouts."
4
5
  ---
5
6
 
6
- # TailwindCSS 4 — CSS-First Design System
7
+ # Tailwind CSS v4 — CSS-First Design System (2026)
7
8
 
8
9
  **ALWAYS invoke when writing Tailwind classes, theming, or responsive layouts.**
9
10
 
10
- ## What Changed (v3 → v4)
11
+ ## v4 Status (2026)
11
12
 
12
- | v3 (Legacy) | v4 (Current) |
13
+ - **Tailwind v4.0 stable**: January 22, 2025
14
+ - **Oxide engine** (Rust) — full builds **3.78× faster** (378ms → 100ms), incremental rebuilds **8.8× faster** (44ms → 5ms), no-CSS-change rebuilds **100× faster** (35ms → 192µs)
15
+ - **No `tailwind.config.js`** — config lives in CSS via `@theme`
16
+ - **Automatic content detection** — no `content: [...]` paths to maintain
17
+ - **Native container queries** — no plugin
18
+ - **OKLCH** color space everywhere (perceptual uniformity, smoother gradients)
19
+ - Built-in support for cascade layers, `@property`, `color-mix()`, modern CSS
20
+
21
+ ## v3 → v4 cheatsheet
22
+
23
+ | v3 (legacy) | v4 (current) |
13
24
  |---|---|
14
- | `tailwind.config.js` | CSS `@theme` directive |
15
- | PostCSS plugin | Oxide engine (10x faster) |
16
- | `@tailwind base/components/utilities` | `@import "tailwindcss"` |
17
- | Colors in JS config | Colors as CSS variables |
18
- | `@apply` everywhere | Components > `@apply` |
25
+ | `tailwind.config.js` (JS) | `@theme { … }` block (CSS) |
26
+ | `@tailwind base/components/utilities` | `@import "tailwindcss"` (single line) |
27
+ | `content: ["./src/**/*.tsx"]` | Auto-detected (no setup) |
28
+ | Colors in JS object | CSS variables (`--color-primary`) |
29
+ | HSL or hex tokens | **OKLCH** (recommended) |
30
+ | `@tailwindcss/container-queries` plugin | Native (`@container`, `@md:flex-row`) |
31
+ | `dark:` only | `dark:` **and** `@theme dark { … }` for token switching |
32
+ | `@apply` for everything | React components + tokens; reserve `@apply` for legacy CSS |
33
+
34
+ Migration is non-breaking for existing v3 codebases — both can coexist. Run `npx @tailwindcss/upgrade@latest` to automate the bulk.
19
35
 
20
36
  ## Setup (v4)
21
37
 
@@ -262,17 +278,60 @@ Section gaps: space-y-6 md:space-y-8
262
278
  Card padding: p-4 md:p-6
263
279
  ```
264
280
 
281
+ ## Modern CSS — leverage what v4 unlocks
282
+
283
+ ```css
284
+ /* color-mix() — derive variants from semantic tokens */
285
+ .btn-primary-soft {
286
+ background: color-mix(in oklch, var(--color-primary) 12%, transparent);
287
+ }
288
+
289
+ /* @property — animatable custom properties */
290
+ @property --gradient-angle {
291
+ syntax: '<angle>';
292
+ inherits: false;
293
+ initial-value: 0deg;
294
+ }
295
+
296
+ /* Cascade layers — predictable specificity (Tailwind already uses these) */
297
+ @layer components {
298
+ .card-hero { /* your custom layer */ }
299
+ }
300
+ ```
301
+
302
+ ## `size-*` utility (use over `w-N h-N`)
303
+
304
+ ```tsx
305
+ {/* OK — single utility, less repetition */}
306
+ <img className="size-10 rounded-full" />
307
+ <button className="size-9 grid place-items-center">
308
+
309
+ {/* Avoid the duplicate */}
310
+ <img className="w-10 h-10 rounded-full" />
311
+ ```
312
+
265
313
  ## FORBIDDEN
266
314
 
267
315
  | ❌ Don't | ✅ Do |
268
316
  |---|---|
269
317
  | `tailwind.config.js` in v4 | `@theme` in CSS |
270
- | `@apply` for everything | React components |
271
- | `!important` | Fix specificity |
318
+ | `@apply` for everything | React components + token classes |
319
+ | `!important` | Fix specificity (or use cascade layers) |
272
320
  | `style={{ color: 'red' }}` | `text-destructive` |
273
- | Arbitrary values for tokens | Define in `@theme` |
274
- | `bg-white dark:bg-gray-900` | `bg-background` (semantic) |
275
- | `@tailwind base` | `@import "tailwindcss"` (v4) |
276
- | Dynamic class strings | Static complete strings (purge-safe) |
277
- | `text-[#FF5733]` inline | `--color-accent` in `@theme` |
321
+ | Arbitrary values for design tokens (`text-[#2563eb]`) | Define `--color-*` in `@theme` |
322
+ | `bg-white dark:bg-gray-900` | `bg-background` (semantic + `@theme dark`) |
323
+ | `@tailwind base/components/utilities` | `@import "tailwindcss"` |
324
+ | Dynamic class strings (`bg-${color}-500`) | Static complete strings (Oxide can't see derived ones) |
325
+ | HSL tokens for new themes | OKLCH (perceptual uniformity, smoother gradients) |
326
+ | `w-10 h-10` pairs | `size-10` |
327
+ | `forwardRef`-style ref-as-prop indirection | Refs are just props in React 19 |
328
+ | `content: [...]` arrays | Auto-detection in v4 |
278
329
  | Inconsistent spacing | Follow 4px scale |
330
+
331
+ ## See Also
332
+
333
+ - `react-standards` — STYLES const pattern using these tokens
334
+ - `shadcn-ui` — components built on top of Tailwind v4 + OKLCH + `data-slot`
335
+ - `preline-ui` — alternative token system on top of Tailwind v4
336
+ - `react-ui-patterns` — loading/error/empty states using semantic tokens
337
+ - `_shared/skills/ui-ux-audit` — WCAG 2.2 AA contrast checks against semantic tokens
@@ -1,42 +1,97 @@
1
1
  ---
2
2
  name: zod-validation
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "Zod 4 (stable Aug 2025) runtime validation for TypeScript boundaries — forms, API responses, env vars, server actions. Zod 4 brings 14× faster string parsing, 7× array, 6.5× object, 57% smaller bundle, 2× faster TS compile, 100× fewer tsc instantiations. Major API shifts: top-level format validators (`z.email()`, `z.url()`, `z.uuid()`) for tree-shaking; unified `error` parameter replaces `required_error` / `invalid_type_error` / `errorMap`. Codemod available: `npx @zod/codemod`. Covers schema design, RHF + zodResolver integration, API response validation, split server/client env schemas (security-critical for `NEXT_PUBLIC_*`), reusable schemas, transforms, and Next.js Server Actions. Mentions Valibot/ArkType when bundle/inference cost matters. Invoke at any trust boundary."
4
5
  ---
5
6
 
6
- # Zod Validation — Runtime Type Safety
7
+ # Zod 4 — Runtime Type Safety (2026)
7
8
 
8
- **ALWAYS use Zod for form validation, API responses, and data boundaries.**
9
+ **ALWAYS use Zod 4 for form validation, API responses, env vars, server actions.**
9
10
 
10
- ## Why Zod
11
+ ## Why Zod 4 (stable since August 2025)
11
12
 
12
- - Runtime validation (TypeScript types disappear at runtime)
13
- - Auto-infer TypeScript types from schemas
14
- - Works with React Hook Form, tRPC, Next.js Server Actions
15
- - Single source of truth: schema type validation
13
+ - **14× faster** string parsing, **7×** array, **6.5×** object
14
+ - **57% smaller** core bundle
15
+ - **~2× faster** TS compilation on large schemas; **100× fewer** tsc instantiations
16
+ - Ecosystem locked in: tRPC, Drizzle, React Hook Form, OpenAPI generators
17
+ - 15M+ weekly downloads — the de-facto standard
18
+
19
+ ### When to consider an alternative
20
+
21
+ | Situation | Tool |
22
+ |---|---|
23
+ | **Bundle size** is critical (edge functions, embeds) | **Valibot** — sub-1KB simple schemas |
24
+ | You want **faster TS check** in giant codebases | **Valibot** (~18× faster type-check vs Zod 4) |
25
+ | You like TS-syntax string schemas | **ArkType** |
26
+ | Mainstream ecosystem + maturity | **Zod 4** ✅ |
27
+
28
+ ## Migration v3 → v4
29
+
30
+ ```bash
31
+ npx @zod/codemod --transform v3-to-v4 ./src
32
+ ```
33
+
34
+ The two breaking changes you'll see most:
35
+
36
+ ### 1. Top-level format validators (tree-shaking)
37
+
38
+ ```tsx
39
+ // v3 (works in v4 but not tree-shakable)
40
+ z.string().email().min(5);
41
+
42
+ // v4 — recommended
43
+ z.email();
44
+ z.url();
45
+ z.uuid();
46
+ z.iso.datetime(); // ISO-8601
47
+ z.iso.date();
48
+ z.cuid2();
49
+ z.ipv4();
50
+ z.ipv6();
51
+ z.base64();
52
+ z.jwt();
53
+ ```
54
+
55
+ ### 2. Unified `error` parameter
56
+
57
+ ```tsx
58
+ // v3 — three different parameters
59
+ z.string({
60
+ required_error: "Required",
61
+ invalid_type_error: "Must be a string",
62
+ errorMap: ({ code }) => ({ message: code === "too_small" ? "Too short" : "Invalid" }),
63
+ });
64
+
65
+ // v4 — one parameter, string or function
66
+ z.string({
67
+ error: (issue) => issue.code === "too_small" ? "Too short" : "Required",
68
+ });
69
+ // Or just a string:
70
+ z.string({ error: "Name is required" });
71
+ ```
16
72
 
17
73
  ## Core Patterns
18
74
 
19
- ### Schema Definition
75
+ ### Schema definition
20
76
 
21
77
  ```tsx
22
78
  import { z } from 'zod';
23
79
 
24
- // Define schema
25
80
  const UserSchema = z.object({
26
- name: z.string().min(2, 'Name must be at least 2 characters'),
27
- email: z.string().email('Invalid email address'),
28
- age: z.number().min(18, 'Must be 18+').max(120),
29
- role: z.enum(['admin', 'user', 'moderator']),
30
- bio: z.string().max(500).optional(),
31
- tags: z.array(z.string()).min(1, 'At least one tag required'),
81
+ name: z.string().min(2, "Name must be at least 2 characters"),
82
+ email: z.email({ error: "Enter a valid email" }), // v4 top-level
83
+ age: z.number().min(18, "Must be 18+").max(120),
84
+ role: z.enum(["admin", "user", "moderator"]),
85
+ bio: z.string().max(500).optional(),
86
+ tags: z.array(z.string()).min(1, "At least one tag required"),
32
87
  metadata: z.record(z.string(), z.unknown()).optional(),
33
88
  });
34
89
 
35
- // Infer type from schema (SINGLE SOURCE OF TRUTH)
90
+ // SINGLE SOURCE OF TRUTH schema type
36
91
  type User = z.infer<typeof UserSchema>;
37
92
  ```
38
93
 
39
- ### Form Validation (React Hook Form + Zod)
94
+ ### Form Validation (React Hook Form + Zod 4)
40
95
 
41
96
  ```tsx
42
97
  import { useForm } from 'react-hook-form';
@@ -44,7 +99,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
44
99
 
45
100
  const CreateUserSchema = z.object({
46
101
  name: z.string().min(2),
47
- email: z.string().email(),
102
+ email: z.email(), // v4 top-level
48
103
  password: z.string().min(8, 'Password must be at least 8 characters'),
49
104
  confirmPassword: z.string(),
50
105
  }).refine((data) => data.password === data.confirmPassword, {
@@ -150,24 +205,47 @@ export const clientEnv = ClientEnvSchema.parse({
150
205
 
151
206
  **Rule:** If a variable contains a key, secret, token, or password, it MUST be in `ServerEnvSchema` without `NEXT_PUBLIC_` prefix.
152
207
 
153
- ### Reusable Schemas
208
+ ### Reusable Schemas (Zod 4 top-level)
154
209
 
155
210
  ```tsx
156
- // Shared between frontend and backend
157
- // schemas/user.ts
158
-
159
- export const EmailSchema = z.string().email().toLowerCase().trim();
211
+ // schemas/common.ts — shared between frontend and backend
212
+ export const EmailSchema = z.email().transform(v => v.toLowerCase().trim());
160
213
  export const PasswordSchema = z.string().min(8).max(128);
161
- export const UUIDSchema = z.string().uuid();
214
+ export const UUIDSchema = z.uuid();
215
+ export const URLSchema = z.url();
216
+ export const ISODateSchema = z.iso.datetime();
217
+
162
218
  export const PaginationSchema = z.object({
163
- page: z.coerce.number().min(1).default(1),
219
+ page: z.coerce.number().min(1).default(1),
164
220
  limit: z.coerce.number().min(1).max(100).default(20),
165
221
  });
166
222
 
167
223
  export const DateRangeSchema = z.object({
168
224
  from: z.coerce.date(),
169
- to: z.coerce.date(),
170
- }).refine((d) => d.to > d.from, 'End date must be after start date');
225
+ to: z.coerce.date(),
226
+ }).refine(d => d.to > d.from, "End date must be after start date");
227
+ ```
228
+
229
+ ### Discriminated unions (polymorphic payloads)
230
+
231
+ ```tsx
232
+ const PaymentSchema = z.discriminatedUnion("type", [
233
+ z.object({ type: z.literal("card"), last4: z.string().length(4) }),
234
+ z.object({ type: z.literal("pix"), key: z.string() }),
235
+ z.object({ type: z.literal("boleto"), barcode: z.string() }),
236
+ ]);
237
+ type Payment = z.infer<typeof PaymentSchema>;
238
+ ```
239
+
240
+ ### Branded types — distinguish IDs at the type level
241
+
242
+ ```tsx
243
+ const UserIdSchema = z.uuid().brand<"UserId">();
244
+ type UserId = z.infer<typeof UserIdSchema>;
245
+
246
+ function getUser(id: UserId) { /* … */ }
247
+ getUser("not a uuid"); // ❌ type error
248
+ getUser(UserIdSchema.parse(input)); // ✅
171
249
  ```
172
250
 
173
251
  ### Transform & Preprocess
@@ -253,20 +331,64 @@ export function LeadForm() {
253
331
  }
254
332
  ```
255
333
 
334
+ ## Use with React 19 Actions + `useActionState`
335
+
336
+ ```tsx
337
+ "use client";
338
+ import { useActionState } from "react";
339
+ import { z } from "zod";
340
+
341
+ const Schema = z.object({
342
+ email: z.email(),
343
+ password: z.string().min(8),
344
+ });
345
+
346
+ async function loginAction(_prev: { error: string | null }, formData: FormData) {
347
+ const parsed = Schema.safeParse(Object.fromEntries(formData));
348
+ if (!parsed.success) return { error: parsed.error.issues[0].message };
349
+ return await loginRequest(parsed.data);
350
+ }
351
+
352
+ export function LoginForm() {
353
+ const [state, action, pending] = useActionState(loginAction, { error: null });
354
+ return (
355
+ <form action={action}>
356
+ <input name="email" type="email" />
357
+ <input name="password" type="password" />
358
+ {state.error && <p className="text-destructive">{state.error}</p>}
359
+ <button disabled={pending}>{pending ? "Signing in…" : "Sign in"}</button>
360
+ </form>
361
+ );
362
+ }
363
+ ```
364
+
256
365
  ## FORBIDDEN
257
366
 
258
367
  | Don't | Do |
259
368
  |---|---|
260
- | Trust API responses blindly | `Schema.parse(response)` |
261
- | Manual `if/else` validation | Zod schema |
369
+ | Trust API responses blindly | `Schema.parse(response)` (or `safeParse` for soft fail) |
370
+ | Manual `if/else` validation | Zod schema (single source of truth) |
262
371
  | Duplicate types + validation | `z.infer<typeof Schema>` |
263
372
  | `any` or `unknown` without validation | Parse with Zod first |
264
- | Validate only on server | Validate on BOTH client + server |
373
+ | Validate only on server | Validate on BOTH client and server |
374
+ | `z.string().email()` in v4 | `z.email()` (tree-shakable, top-level) |
375
+ | `required_error` / `invalid_type_error` / `errorMap` (v3) | Unified `error` parameter (v4) |
376
+ | Dump secrets into `NEXT_PUBLIC_*` | Server-only `ServerEnvSchema`; client gets only public keys |
377
+ | Re-implement common formats (URL, UUID, datetime, JWT) | `z.url()`, `z.uuid()`, `z.iso.datetime()`, `z.jwt()` |
265
378
 
266
379
  ## Rules
267
380
 
268
381
  1. **SINGLE SOURCE OF TRUTH** — schema defines type AND validation
269
- 2. **VALIDATE AT BOUNDARIES** — forms, API responses, env vars
270
- 3. **SAFE PARSE FOR UI** — `safeParse()` for user-facing, `parse()` for internal
271
- 4. **SHARED SCHEMAS** — same schema on frontend and backend
272
- 5. **TRANSFORM DATA** — clean/normalize during validation, not after
382
+ 2. **VALIDATE AT BOUNDARIES** — forms, API responses, env vars, queue messages
383
+ 3. **SAFE PARSE FOR UI** — `safeParse()` for user-facing forms, `parse()` for internal/trusted
384
+ 4. **SHARED SCHEMAS** — same schema on frontend and backend (workspace package)
385
+ 5. **TRANSFORM DURING VALIDATION** — `.toLowerCase()`, `.trim()`, `.transform(...)` happen as part of `parse`, not afterwards
386
+ 6. **PREFER TOP-LEVEL FORMAT VALIDATORS** in v4 — better tree-shaking + clearer intent
387
+
388
+ ## See Also
389
+
390
+ - `react-patterns` v2 — `useActionState`, `useOptimistic`, `use()` hook
391
+ - `react-ui-patterns` v2 — RHF + Zod + TanStack Query patterns
392
+ - `shadcn-ui` v2 — `<Form>` component built on RHF + Zod
393
+ - `_shared/skills/security-baseline` v2 — input validation as defense layer
394
+ - `_shared/skills/openapi-design` v2 — schemas → OpenAPI 3.2
@@ -1,11 +1,7 @@
1
1
  ---
2
2
  name: axios-laravel-api
3
- version: 1.0.0
4
- description: Axios HTTP client for Laravel 12 + Sanctum SPA `withCredentials`,
5
- `withXSRFToken`, `/sanctum/csrf-cookie` flow, and `401/403/419/422/5xx`
6
- interceptors. Use when wiring a React (or any SPA) frontend to a Laravel
7
- cookie-authenticated JSON API. Pairs with `laravel-api-architecture` and
8
- `react-api-standards`.
3
+ version: 2.0.0
4
+ description: "Axios HTTP client for Laravel 12 + Sanctum SPA in React 19 (Tailwind v4) projects. `withCredentials: true` + `withXSRFToken: true` + `/sanctum/csrf-cookie` priming flow, plus a single response interceptor that handles 401 (logout), 403 (forbidden toast), 419 (CSRF stale → re-prime + retry once), 422 (validation errors surfaced to RHF), 5xx (toast + log). Pairs with `laravel-api-architecture` (backend) and `react-api-standards` (frontend conventions). 2026 polish: Zod 4 response validation at the boundary, TanStack Query v5 `signal` threading for cancellation, React 19 forms via `useActionState` for client-side mutations. Invoke when wiring or auditing the API client, login flow, or CSRF/cookie configuration."
9
5
  ---
10
6
 
11
7
  # Axios + Laravel Sanctum SPA Client
@@ -1,25 +1,23 @@
1
1
  ---
2
2
  name: react-api-standards
3
- version: 1.0.0
4
- description: React 19 + Vite + Axios standards for a Laravel-backed JSON API SPA
5
- — Page shell + skeleton, async data via `api.get`, form 422 binding, route
6
- catch-all, no Inertia. Use for any new React page in a Laravel + Sanctum SPA
7
- project. Pairs with `axios-laravel-api` and `laravel-api-architecture`.
3
+ version: 2.0.0
4
+ description: "React 19 + Vite 6 + Axios standards for a Laravel-backed JSON API SPA. Page shell + skeleton-first paint, async data via `api.get`, 422 validation binding to React Hook Form, protected-route + catch-all routing, NO Inertia. Pairs with `axios-laravel-api` (HTTP client) and `laravel-api-architecture` (backend). 2026 polish: TanStack Query v5 (`isPending` semantics + `signal` threading), Zod 4 (`z.email()`/`z.url()` top-level), `useOptimistic` for instant feedback, `useActionState` for form submissions, `useFormStatus` to wire Submit buttons without prop drilling. Tailwind v4 with `@theme` and OKLCH tokens. Invoke for any new page, layout, or component in a Laravel + Sanctum SPA."
8
5
  ---
9
6
 
10
- # React 19 API-First Standards (Laravel + Axios)
7
+ # React 19 API-First Standards (Laravel + Axios) — 2026
11
8
 
12
9
  **ALWAYS invoke when creating React pages, components, or layouts in a project
13
10
  that uses a Laravel JSON API as backend (NOT Inertia.js).**
14
11
 
15
12
  ## Version Requirements
16
13
 
17
- - **React >= 19** — MANDATORY (`useActionState`, `useOptimistic`, `use`)
18
- - **Vite >= 5** with `@vitejs/plugin-react`
19
- - **TailwindCSS >= 4**
20
- - **Axios >= 1.6** (for `withXSRFToken` support)
21
- - **TanStack Query >= 5** (recommended for cached lists/details)
22
- - **react-router-dom >= 6** (client-side routing)
14
+ - **React 19.0** — MANDATORY (`useActionState`, `useOptimistic`, `useFormStatus`, `use()`)
15
+ - **Vite 6** with `@vitejs/plugin-react`
16
+ - **TailwindCSS 4** (CSS-first `@theme`, Oxide engine)
17
+ - **Axios 1.7** (for `withXSRFToken` and modern interceptor types)
18
+ - **TanStack Query 5** — `isPending` (no data yet) vs `isFetching` (any in-flight); thread `signal` through `queryFn`
19
+ - **Zod 4** — top-level `z.email()`/`z.url()` for tree-shaking
20
+ - **react-router-dom ≥ 7** (`<RouterProvider>` + data routers)
23
21
 
24
22
  ## Folder Structure
25
23