@famgia/omnify-typescript 0.0.66 → 0.0.68

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.
Files changed (43) hide show
  1. package/ai-guides/react-form-guide.md +259 -0
  2. package/ai-guides/typescript-guide.md +53 -0
  3. package/dist/{chunk-4L77AHAC.js → chunk-6I4O23X6.js} +521 -66
  4. package/dist/chunk-6I4O23X6.js.map +1 -0
  5. package/dist/index.cjs +761 -65
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +138 -2
  8. package/dist/index.d.ts +138 -2
  9. package/dist/index.js +227 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/plugin.cjs +624 -75
  12. package/dist/plugin.cjs.map +1 -1
  13. package/dist/plugin.d.cts +6 -0
  14. package/dist/plugin.d.ts +6 -0
  15. package/dist/plugin.js +96 -11
  16. package/dist/plugin.js.map +1 -1
  17. package/package.json +4 -3
  18. package/scripts/postinstall.js +29 -40
  19. package/stubs/JapaneseAddressField.tsx.stub +289 -0
  20. package/stubs/JapaneseBankField.tsx.stub +212 -0
  21. package/stubs/JapaneseNameField.tsx.stub +194 -0
  22. package/stubs/ai-guides/checklists/react.md.stub +108 -0
  23. package/stubs/ai-guides/cursor/react-design.mdc.stub +289 -0
  24. package/stubs/ai-guides/cursor/react-form.mdc.stub +277 -0
  25. package/stubs/ai-guides/cursor/react-services.mdc.stub +304 -0
  26. package/stubs/ai-guides/cursor/react.mdc.stub +305 -0
  27. package/stubs/ai-guides/react/README.md.stub +221 -0
  28. package/stubs/ai-guides/react/antd-guide.md.stub +294 -0
  29. package/stubs/ai-guides/react/checklist.md.stub +108 -0
  30. package/stubs/ai-guides/react/datetime-guide.md.stub +137 -0
  31. package/stubs/ai-guides/react/design-philosophy.md.stub +363 -0
  32. package/stubs/ai-guides/react/i18n-guide.md.stub +211 -0
  33. package/stubs/ai-guides/react/laravel-integration.md.stub +181 -0
  34. package/stubs/ai-guides/react/service-pattern.md.stub +180 -0
  35. package/stubs/ai-guides/react/tanstack-query.md.stub +339 -0
  36. package/stubs/ai-guides/react/types-guide.md.stub +524 -0
  37. package/stubs/components-index.ts.stub +13 -0
  38. package/stubs/form-validation.ts.stub +106 -0
  39. package/stubs/rules/index.ts.stub +48 -0
  40. package/stubs/rules/kana.ts.stub +291 -0
  41. package/stubs/use-form-mutation.ts.stub +117 -0
  42. package/stubs/zod-i18n.ts.stub +32 -0
  43. package/dist/chunk-4L77AHAC.js.map +0 -1
@@ -0,0 +1,304 @@
1
+ ---
2
+ description: "Frontend Services layer - API communication with backend"
3
+ globs: ["{{TYPESCRIPT_BASE}}/services/**"]
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Frontend Services Rules
8
+
9
+ > **API Reference:** `storage/api-docs/api-docs.json` (OpenAPI 3.0)
10
+ > **Base Config:** `lib/api.ts`
11
+
12
+ ## ⛔ MUST: Check api-docs.json FIRST!
13
+
14
+ **BEFORE writing ANY service code, MUST read `storage/api-docs/api-docs.json`:**
15
+
16
+ ```bash
17
+ # 1. ALWAYS read api-docs.json first
18
+ cat storage/api-docs/api-docs.json | jq '.paths'
19
+
20
+ # 2. Check specific endpoint
21
+ cat storage/api-docs/api-docs.json | jq '.paths["/api/users"]'
22
+
23
+ # 3. Check request/response schemas
24
+ cat storage/api-docs/api-docs.json | jq '.components.schemas'
25
+ ```
26
+
27
+ ### Checklist (MANDATORY)
28
+
29
+ - [ ] **Read api-docs.json** - NO guessing!
30
+ - [ ] **Verify endpoint exists** - URL, method must match
31
+ - [ ] **Copy exact parameter names** - `filter[search]` not `search`
32
+ - [ ] **Match request body schema** - required fields, types
33
+ - [ ] **Match response schema** - data structure
34
+
35
+ ### ❌ DON'T:
36
+ ```typescript
37
+ // ❌ Guessing parameter names
38
+ search?: string; // WRONG - API uses filter[search]
39
+ sort_by?: string; // WRONG - API uses sort
40
+ sort_order?: "asc"|"desc"; // WRONG - API uses "-field" format
41
+ ```
42
+
43
+ ### ✅ DO:
44
+ ```typescript
45
+ // ✅ Copy exactly from api-docs.json
46
+
47
+ // Sort has enum? → Create union type!
48
+ export type UserSortField =
49
+ | "id" | "-id"
50
+ | "name_lastname" | "-name_lastname"
51
+ | "email" | "-email"
52
+ | "created_at" | "-created_at";
53
+
54
+ export interface UserListParams {
55
+ sort?: UserSortField; // Correct: type-safe from enum
56
+ filter?: {
57
+ search?: string; // Correct: maps to filter[search]
58
+ };
59
+ }
60
+ ```
61
+
62
+ ```bash
63
+ # Regenerate OpenAPI docs if needed
64
+ php artisan l5-swagger:generate
65
+ ```
66
+
67
+ ## Service File Structure
68
+
69
+ ```typescript
70
+ /**
71
+ * {Resource} Service
72
+ *
73
+ * CRUD operations for {Resource} resource.
74
+ * Maps to Laravel {Resource}Controller.
75
+ */
76
+
77
+ import api, { PaginatedResponse } from "@/lib/api";
78
+ import type { Resource, ResourceCreate, ResourceUpdate } from "@/types/model";
79
+
80
+ // =============================================================================
81
+ // Types - Only query params (Create/Update come from Omnify)
82
+ // =============================================================================
83
+
84
+ /** Sort fields - MUST match api-docs.json enum! */
85
+ export type ResourceSortField =
86
+ | "id" | "-id"
87
+ | "created_at" | "-created_at"
88
+ | "updated_at" | "-updated_at";
89
+ // Add other fields from api-docs.json enum
90
+
91
+ /** Query params for listing resources */
92
+ export interface ResourceListParams {
93
+ page?: number;
94
+ per_page?: number;
95
+ sort?: ResourceSortField; // Type-safe from enum
96
+ filter?: {
97
+ search?: string;
98
+ // Add other filters from api-docs.json
99
+ };
100
+ }
101
+
102
+ // =============================================================================
103
+ // Service
104
+ // =============================================================================
105
+
106
+ const BASE_URL = "/api/resources";
107
+
108
+ export const resourceService = {
109
+ /**
110
+ * Axios auto-serializes nested objects:
111
+ * { filter: { search: "x" } } → ?filter[search]=x
112
+ */
113
+ list: async (params?: ResourceListParams): Promise<PaginatedResponse<Resource>> => {
114
+ const { data } = await api.get(BASE_URL, { params });
115
+ return data;
116
+ },
117
+
118
+ get: async (id: number): Promise<Resource> => {
119
+ const { data } = await api.get(`${BASE_URL}/${id}`);
120
+ return data.data ?? data;
121
+ },
122
+
123
+ create: async (input: ResourceCreate): Promise<Resource> => {
124
+ const { data } = await api.post(BASE_URL, input);
125
+ return data.data ?? data;
126
+ },
127
+
128
+ update: async (id: number, input: ResourceUpdate): Promise<Resource> => {
129
+ const { data } = await api.put(`${BASE_URL}/${id}`, input);
130
+ return data.data ?? data;
131
+ },
132
+
133
+ delete: async (id: number): Promise<void> => {
134
+ await api.delete(`${BASE_URL}/${id}`);
135
+ },
136
+ };
137
+ ```
138
+
139
+ ## Types Rule
140
+
141
+ ```typescript
142
+ // ✅ Import model types from Omnify
143
+ import type { User, UserCreate, UserUpdate } from "@/types/model";
144
+
145
+ // ✅ Define only query/filter params locally
146
+ export interface UserListParams {
147
+ search?: string;
148
+ page?: number;
149
+ per_page?: number;
150
+ }
151
+
152
+ // ❌ DON'T redefine model types
153
+ export interface User { ... } // WRONG - use Omnify
154
+ export interface UserCreateInput { ... } // WRONG - use UserCreate
155
+ ```
156
+
157
+ ## Response Handling
158
+
159
+ ### Paginated List (GET /api/resources)
160
+ ```typescript
161
+ // OpenAPI: Returns { data: T[], links: {...}, meta: {...} }
162
+ list: async (params): Promise<PaginatedResponse<Resource>> => {
163
+ const { data } = await api.get(BASE_URL, { params });
164
+ return data; // Full response with data, links, meta
165
+ },
166
+ ```
167
+
168
+ ### Single Resource (GET /api/resources/:id)
169
+ ```typescript
170
+ // OpenAPI: Returns { data: T }
171
+ get: async (id: number): Promise<Resource> => {
172
+ const { data } = await api.get(`${BASE_URL}/${id}`);
173
+ return data.data ?? data; // Unwrap { data: T } wrapper
174
+ },
175
+ ```
176
+
177
+ ### Create/Update (POST/PUT)
178
+ ```typescript
179
+ // OpenAPI: Returns { data: T }
180
+ create: async (input: ResourceCreate): Promise<Resource> => {
181
+ const { data } = await api.post(BASE_URL, input);
182
+ return data.data ?? data; // Unwrap { data: T } wrapper
183
+ },
184
+ ```
185
+
186
+ ### Delete (DELETE /api/resources/:id)
187
+ ```typescript
188
+ // OpenAPI: Returns 204 No Content
189
+ delete: async (id: number): Promise<void> => {
190
+ await api.delete(`${BASE_URL}/${id}`);
191
+ },
192
+ ```
193
+
194
+ ## Auth Service Pattern (Sanctum)
195
+
196
+ For auth endpoints that need CSRF:
197
+
198
+ ```typescript
199
+ import api, { csrf } from "@/lib/api";
200
+
201
+ export const authService = {
202
+ login: async (input: LoginInput): Promise<void> => {
203
+ await csrf(); // Required for Sanctum
204
+ await api.post("/login", input);
205
+ },
206
+
207
+ logout: async (): Promise<void> => {
208
+ await api.post("/logout");
209
+ },
210
+
211
+ me: async (): Promise<User> => {
212
+ const { data } = await api.get<User>("/api/user");
213
+ return data;
214
+ },
215
+ };
216
+ ```
217
+
218
+ ## OpenAPI Parameter Mapping
219
+
220
+ Axios auto-serializes nested objects → **NO transform needed!**
221
+
222
+ ```typescript
223
+ // Frontend interface
224
+ { filter: { search: "John" }, sort: "-created_at", page: 1 }
225
+
226
+ // Axios auto-serializes to:
227
+ // ?filter[search]=John&sort=-created_at&page=1
228
+ ```
229
+
230
+ | OpenAPI Parameter | Frontend Interface | Notes |
231
+ | ----------------- | ------------------ | --------------------- |
232
+ | `filter[search]` | `filter.search` | Axios auto-serializes |
233
+ | `page` | `page` | Direct |
234
+ | `per_page` | `per_page` | Direct |
235
+ | `sort` | `sort` | Use enum type! |
236
+
237
+ ### Sort Field - MUST use enum!
238
+ ```typescript
239
+ // ❌ WRONG - not type-safe
240
+ sort?: string;
241
+
242
+ // ✅ CORRECT - copy enum from api-docs.json
243
+ export type ResourceSortField =
244
+ | "id" | "-id"
245
+ | "created_at" | "-created_at";
246
+
247
+ sort?: ResourceSortField; // Type-safe!
248
+ ```
249
+
250
+ ## Error Handling
251
+
252
+ Errors are handled globally in `lib/api.ts`. Services should:
253
+ - **NOT** catch errors (let them propagate to mutations/queries)
254
+ - **NOT** show messages (handled by TanStack Query callbacks)
255
+
256
+ ```typescript
257
+ // ❌ Wrong
258
+ create: async (input) => {
259
+ try {
260
+ const { data } = await api.post(BASE_URL, input);
261
+ return data.data;
262
+ } catch (e) {
263
+ console.error(e); // DON'T catch here
264
+ }
265
+ },
266
+
267
+ // ✅ Correct - let errors propagate
268
+ create: async (input) => {
269
+ const { data } = await api.post(BASE_URL, input);
270
+ return data.data ?? data;
271
+ },
272
+ ```
273
+
274
+ ## Naming Conventions
275
+
276
+ | Item | Convention | Example |
277
+ | -------------- | --------------------------- | ---------------------------- |
278
+ | File | `{resource}.ts` (singular) | `users.ts`, `auth.ts` |
279
+ | Service object | `{resource}Service` | `userService`, `authService` |
280
+ | List params | `{Resource}ListParams` | `UserListParams` |
281
+ | Base URL | `/api/{resources}` (plural) | `/api/users` |
282
+
283
+ ## Checklist Before Creating Service
284
+
285
+ - [ ] API endpoint exists in `storage/api-docs/api-docs.json`
286
+ - [ ] Omnify types exist in `types/model/`
287
+ - [ ] Request body matches OpenAPI `requestBody.content.application/json.schema`
288
+ - [ ] Response matches OpenAPI `responses.200.content.application/json.schema`
289
+ - [ ] Parameters match OpenAPI `parameters[]`
290
+
291
+ ## Quick Reference: Current API Endpoints
292
+
293
+ > From `storage/api-docs/api-docs.json`:
294
+
295
+ ### Users API
296
+ | Method | Endpoint | Description |
297
+ | ------ | ----------------- | -------------------------------- |
298
+ | GET | `/api/users` | Paginated list with search, sort |
299
+ | POST | `/api/users` | Create user |
300
+ | GET | `/api/users/{id}` | Get single user |
301
+ | PUT | `/api/users/{id}` | Update user |
302
+ | DELETE | `/api/users/{id}` | Delete user |
303
+
304
+ **Sort fields:** `id`, `name_lastname`, `name_firstname`, `email`, `created_at`, `updated_at`
@@ -0,0 +1,305 @@
1
+ ---
2
+ description: "React + Ant Design frontend development rules"
3
+ globs: ["{{TYPESCRIPT_BASE}}/**"]
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # React + Ant Design Rules
8
+
9
+ > **Related Rules:**
10
+ > - **Form Development:** `.cursor/rules/omnify/react-form.mdc`
11
+ > - **Design System:** `.cursor/rules/omnify/react-design.mdc`
12
+ > - **Services:** `.cursor/rules/omnify/react-services.mdc`
13
+
14
+ ## Guide Documentation
15
+
16
+ - Read `.claude/guides/react/` for patterns
17
+
18
+ ## Critical Rules
19
+
20
+ 1. **Use Ant Design** - Don't recreate existing components
21
+ 2. **Use Omnify types** - Import from `@/types/model`, don't duplicate
22
+ 3. **Use i18n** - Use `useTranslations()` for UI text
23
+ 4. **Service Layer** - API calls in `services/`, not components
24
+ 5. **TanStack Query** - For all server state, no `useState` + `useEffect`
25
+
26
+ ## Quick Patterns
27
+
28
+ ### Service
29
+
30
+ ```typescript
31
+ export const userService = {
32
+ list: (params?: UserListParams) => api.get("/api/users", { params }).then(r => r.data),
33
+ create: (input: UserCreate) => api.post("/api/users", input).then(r => r.data.data),
34
+ };
35
+ ```
36
+
37
+ ### Query
38
+
39
+ ```typescript
40
+ const { data, isLoading } = useQuery({
41
+ queryKey: queryKeys.users.list(filters),
42
+ queryFn: () => userService.list(filters),
43
+ });
44
+ ```
45
+
46
+ ### Mutation
47
+
48
+ ```typescript
49
+ const mutation = useMutation({
50
+ mutationFn: userService.create,
51
+ onSuccess: () => {
52
+ queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
53
+ message.success(t("messages.created"));
54
+ },
55
+ onError: (error) => form.setFields(getFormErrors(error)),
56
+ });
57
+ ```
58
+
59
+ ## Types Rule
60
+
61
+ ```typescript
62
+ // ✅ Use Omnify-generated types
63
+ import type { User, UserCreate } from "@/types/model";
64
+
65
+ // ✅ Only define query params locally
66
+ export interface UserListParams { ... }
67
+
68
+ // ❌ Don't redefine Omnify types
69
+ export interface UserCreateInput { ... } // WRONG
70
+ ```
71
+
72
+ ## Form Pattern (Omnify)
73
+
74
+ ### Imports
75
+
76
+ ```typescript
77
+ import { Form, Input, Button, Space, Card, Divider } from 'antd';
78
+ import type { FormInstance } from 'antd';
79
+ import { zodRule, setZodLocale } from '@/omnify/lib';
80
+ import { OmnifyForm } from '@/omnify/components';
81
+ import {
82
+ type Customer,
83
+ type CustomerCreate,
84
+ customerSchemas,
85
+ customerI18n,
86
+ getCustomerFieldLabel,
87
+ getCustomerFieldPlaceholder,
88
+ } from '@/omnify/schemas';
89
+ ```
90
+
91
+ ### Helper Functions (MUST define)
92
+
93
+ ```typescript
94
+ const LOCALE = 'ja';
95
+ setZodLocale(LOCALE);
96
+
97
+ const label = (key: string) => getCustomerFieldLabel(key, LOCALE);
98
+ const placeholder = (key: string) => getCustomerFieldPlaceholder(key, LOCALE);
99
+ const rule = (key: keyof typeof customerSchemas) =>
100
+ zodRule(customerSchemas[key], label(key));
101
+ ```
102
+
103
+ ### Form.Item Pattern
104
+
105
+ ```typescript
106
+ <Form.Item
107
+ name="email"
108
+ label={label('email')}
109
+ rules={[rule('email')]}
110
+ >
111
+ <Input type="email" placeholder={placeholder('email')} />
112
+ </Form.Item>
113
+ ```
114
+
115
+ ### Compound Fields (Japan Plugin)
116
+
117
+ ```typescript
118
+ // Japanese Name (4 fields)
119
+ <OmnifyForm.JapaneseName
120
+ schemas={customerSchemas}
121
+ i18n={customerI18n}
122
+ prefix="name"
123
+ required
124
+ />
125
+
126
+ // Japanese Address (5 fields)
127
+ <OmnifyForm.JapaneseAddress
128
+ form={form}
129
+ schemas={customerSchemas}
130
+ i18n={customerI18n}
131
+ prefix="address"
132
+ />
133
+ ```
134
+
135
+ ### Key Files
136
+
137
+ | File | Purpose |
138
+ | ------------------------ | ----------------------------- |
139
+ | `@/omnify/schemas` | Types, schemas, i18n (generated) |
140
+ | `@/omnify/lib` | `zodRule()`, `setZodLocale()` |
141
+ | `@/omnify/components` | `OmnifyForm.*` components |
142
+
143
+ ### Rules
144
+
145
+ 1. **Import from `@/omnify/*`** - All Omnify utilities
146
+ 2. **Define helpers** - `label()`, `placeholder()`, `rule()`
147
+ 3. **Use `Divider`** - For form sections
148
+ 4. **Use `label()` helper** - Get from `getUserPropertyDisplayName()`
149
+
150
+ ## File Location
151
+
152
+ | What | Where |
153
+ | ----------------------- | ------------------------------------------- |
154
+ | Component (1 feature) | `features/{feature}/` |
155
+ | Component (2+ features) | `components/common/` |
156
+ | Service (API) | `services/` (ALWAYS) |
157
+ | Hook (1 feature) | `features/{feature}/` |
158
+ | Hook (2+ features) | `hooks/` |
159
+ | Zod Schema | `schemas/{model}.ts` |
160
+ | Validation utils | `lib/form-validation.ts`, `lib/zod-i18n.ts` |
161
+
162
+ ## Ant Design v6 Deprecated Props
163
+
164
+ | Deprecated | Use Instead |
165
+ | -------------------------- | ----------------------- |
166
+ | `visible` | `open` |
167
+ | `direction` (Space) | `orientation` |
168
+ | `dropdownMatchSelectWidth` | `popupMatchSelectWidth` |
169
+
170
+ ## Ant Design Static Method Warning
171
+
172
+ ⚠️ **Warning:** `Static function can not consume context like dynamic theme`
173
+
174
+ ```typescript
175
+ // ❌ Wrong - Static import has no context
176
+ import { message } from "antd";
177
+ message.success("Done"); // Warning!
178
+
179
+ // ✅ Correct - Use App.useApp() hook
180
+ import { App } from "antd";
181
+
182
+ function MyComponent() {
183
+ const { message, notification, modal } = App.useApp();
184
+ message.success("Done"); // ✅ No warning
185
+ }
186
+ ```
187
+
188
+ **Note:**
189
+ - `App` component is already wrapped in `AntdThemeProvider`
190
+ - Just use `App.useApp()` in your component/hook
191
+ - Applies to: `message`, `notification`, `modal`
192
+
193
+ ## Common Mistakes
194
+
195
+ ```typescript
196
+ // ❌ Wrong
197
+ useEffect(() => { fetchData() }, []); // Use useQuery
198
+ const [users, setUsers] = useState([]); // Use TanStack for server state
199
+ <Button>Save</Button> // Use i18n
200
+
201
+ // ✅ Correct
202
+ const { data } = useQuery({ queryKey, queryFn });
203
+ <Button>{t("common.save")}</Button>
204
+ ```
205
+
206
+ ### Form Validation Mistakes
207
+
208
+ ```typescript
209
+ // ❌ Wrong - Hardcoded message in schema
210
+ z.string().min(1, "Name is required")
211
+
212
+ // ❌ Wrong - Define schema in component
213
+ const schema = z.object({ name: z.string() });
214
+
215
+ // ❌ Wrong - No field label comment
216
+ <Form.Item name="name" label={label("name")}>
217
+
218
+ // ✅ Correct - Schema in schemas/, messages in i18n/
219
+ import { userSchemas } from "@/schemas/user";
220
+ {/* Name */}
221
+ <Form.Item name="name" label={label("name")} rules={[zodRule(userSchemas.name, label("name"))]}>
222
+ ```
223
+
224
+ ### Form.useForm() Warning
225
+
226
+ ⚠️ **Warning:** `Instance created by useForm is not connected to any Form element`
227
+
228
+ ```typescript
229
+ // ❌ Wrong - Export hook creates unused form instance
230
+ export function useUserForm() {
231
+ return Form.useForm(); // Instance not connected to any Form!
232
+ }
233
+
234
+ // ❌ Wrong - Create form instance but don't pass to Form
235
+ const [form] = Form.useForm();
236
+ return <Form>...</Form> // Missing form={form}
237
+
238
+ // ✅ Correct - Form instance must connect to Form
239
+ const [form] = Form.useForm();
240
+ return <Form form={form}>...</Form>
241
+ ```
242
+
243
+ **Rule:** Each `Form.useForm()` must have exactly one corresponding `<Form form={form}>`.
244
+
245
+ ### Backend Validation Errors (422)
246
+
247
+ ⚠️ **IMPORTANT:** Form must display errors from backend!
248
+
249
+ **Form component MUST receive `form` prop from parent:**
250
+
251
+ ```typescript
252
+ // Form component
253
+ interface UserFormProps {
254
+ form: FormInstance; // ← REQUIRED
255
+ onSubmit: (values: UserCreate) => void;
256
+ // ...
257
+ }
258
+
259
+ export function UserForm({ form, onSubmit, ... }: UserFormProps) {
260
+ return (
261
+ <Form form={form} onFinish={onSubmit}>
262
+ ...
263
+ </Form>
264
+ );
265
+ }
266
+ ```
267
+
268
+ **Page creates form and handles backend errors:**
269
+
270
+ ```typescript
271
+ // Page component
272
+ export default function NewUserPage() {
273
+ const [form] = Form.useForm(); // ← Create in parent
274
+
275
+ const mutation = useMutation({
276
+ mutationFn: userService.create,
277
+ onSuccess: () => { ... },
278
+ onError: (error) => {
279
+ form.setFields(getFormErrors(error)); // ← Set errors from backend
280
+ },
281
+ });
282
+
283
+ return (
284
+ <UserForm
285
+ form={form} // ← Pass to form component
286
+ onSubmit={(values) => mutation.mutate(values)}
287
+ />
288
+ );
289
+ }
290
+ ```
291
+
292
+ **Helper `getFormErrors`** (available in `lib/api.ts`):
293
+
294
+ ```typescript
295
+ import { getFormErrors } from "@/lib/api";
296
+
297
+ // Converts Laravel 422 response to Ant Design format:
298
+ // { errors: { email: ["Email already exists"] } }
299
+ // → [{ name: "email", errors: ["Email already exists"] }]
300
+ ```
301
+
302
+ **Rules:**
303
+ 1. Form component does NOT create `Form.useForm()` - receives from parent
304
+ 2. Page creates form instance and passes down
305
+ 3. `onError` calls `form.setFields(getFormErrors(error))`