chore: improve instructions

This commit is contained in:
melvinchia3636
2026-06-02 17:11:33 +08:00
parent f9862648c2
commit 8907ff6bee
3 changed files with 34 additions and 25 deletions

View File

@@ -261,7 +261,7 @@ Here is the **correct Zod type to use for each form field type**, utilizing Zod'
| Email | `z.email('Invalid email')` (top-level factory) |
| URL | `z.url('Invalid URL')` |
| UUID | `z.uuid()` — any UUID version |
| Color hex | `z.string().regex(/^#[0-9A-Fa-f]{6}$/, 'Color must be a valid hex color (e.g. #FF0000)')` |
| Color hex | `z.string().regex(/^#[0-9A-Fa-f]{6}$/, 'Color must be a valid hex color (e.g. #FF0000)')` |
| Icon identifier | `z.string().regex(/^[a-z]+:[a-z-]+$/)` — or just `z.string()` if you trust the icon picker |
| Lowercase / Uppercase | `z.string().lowercase()` / `z.string().uppercase()` |
| Trim whitespace | `z.string().trim()` (overwrites value) |
@@ -332,9 +332,9 @@ Here is the **correct Zod type to use for each form field type**, utilizing Zod'
##### Location Fields (`<LocationField>`)
| Validation | Zod Code |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Required location object | `z.object({ name: z.string(), location: z.object({ latitude: z.number(), longitude: z.number() }), formattedAddress: z.string() })` |
| Validation | Zod Code |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Required location object | `z.object({ name: z.string(), location: z.object({ latitude: z.number(), longitude: z.number() }), formattedAddress: z.string() })` |
| Optional location | `z.object({...}).optional()` (use `.optional()` — NOT `.nullable()`, because react-hook-form uses `undefined` for unset fields, not `null`) |
> **Important:** Location fields must use `.optional()`, not `.nullable()`. React-hook-form represents unset/empty fields as `undefined`, and using `.nullable()` (`| null`) creates a type mismatch with the form's `FieldValues` generic. Always use `.optional()` for optional locations. The default value should be `undefined` (or omit it from `defaultValues`) rather than `null`.
@@ -661,9 +661,11 @@ return (
```
> ### 🚨 Direct handler rule
>
> If no preprocessing or transformation is needed before the API call, **always** pass the mutation function directly — `handler: mutation.mutateAsync`. This works because `mutation.mutateAsync` already accepts the form data as its argument and returns a promise. Only inline the handler if you need to transform the data first (e.g., `dayjs(date).format('YYYY-MM-DD')`, stripping tracking fields like `_type`, converting `FileValue` with `convertFormFileFieldData`, etc.).
>
> **✅ Correct (no transform needed):**
>
> ```tsx
> submissionConfig={{
> template: 'create',
@@ -672,6 +674,7 @@ return (
> ```
>
> **❌ Unnecessary wrapping (no transform needed):**
>
> ```tsx
> submissionConfig={{
> template: 'create',
@@ -680,6 +683,7 @@ return (
> ```
>
> **✅ Correct (transform needed):**
>
> ```tsx
> submissionConfig={{
> handler: async formData => {
@@ -849,13 +853,13 @@ formStateStore.getState().type // access type from any context
In the new system, this is replaced by `react-hook-form`'s API:
| Old Zustand pattern | New `react-hook-form` equivalent |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `formStateStore.getState()` | `form.getValues()` |
| `formStateStore.getState().fieldName` | `form.getValues('fieldName')` |
| `formStateStore.subscribe(callback)` | `useWatch({ control })` in render, or `form.watch((data) => {...})` for side-effects |
| `setData(old => ({ ...old, key: val }))` | `form.setValue('key', val)` |
| `formStateStore.getState().fieldName` in field's `actionButtonOption.onClick` | Just use JS closure — `form` is available in the component scope |
| Old Zustand pattern | New `react-hook-form` equivalent |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `formStateStore.getState()` | `form.getValues()` |
| `formStateStore.getState().fieldName` | `form.getValues('fieldName')` |
| `formStateStore.subscribe(callback)` | `useWatch({ control })` in render, or `form.watch((data) => {...})` for side-effects |
| `setData(old => ({ ...old, key: val }))` | `form.setValue('key', val)` |
| `formStateStore.getState().fieldName` in field's `actionButtonOption.onClick` | Just use JS closure — `form` is available in the component scope |
> **Important:** When using `form.setValue()` programmatically (not triggered by user input), pass `{ shouldValidate: true }` as the third argument to ensure the new value is validated immediately. Without this, the error state won't update until the next user interaction. Example: `form.setValue('family', metadata.family, { shouldValidate: true })`.
@@ -1273,6 +1277,7 @@ const overrideKey = useWatch({ control: form.control, name: 'overrideKey' })
```
This applies everywhere you need to reactively read form values in the render path:
- Conditional field visibility (Step 6)
- Derived/computed listbox options (Step 7)
- Any other render-time value reading from the form

View File

@@ -284,17 +284,17 @@ Each `response.<status>()` corresponds to an output key declared in the config.
Each `response.<method>()` call must correspond to a key declared in the `output` object. The method name determines which HTTP status code is returned:
| Output key | Response method | HTTP code |
| --------------- | ------------------------ | --------- |
| `OK` | `response.ok(payload)` | 200 |
| `CREATED` | `response.created(payload)` | 201 |
| `ACCEPTED` | `response.accepted()` | 202 |
| `NO_CONTENT` | `response.noContent()` | 204 |
| `BAD_REQUEST` | `response.badRequest(msg)` | 400 |
| `UNAUTHORIZED` | `response.unauthorized()` | 401 |
| `FORBIDDEN` | `response.forbidden()` | 403 |
| `NOT_FOUND` | `response.notFound()` | 404 |
| `CONFLICT` | `response.conflict()` | 409 |
| Output key | Response method | HTTP code |
| -------------- | --------------------------- | --------- |
| `OK` | `response.ok(payload)` | 200 |
| `CREATED` | `response.created(payload)` | 201 |
| `ACCEPTED` | `response.accepted()` | 202 |
| `NO_CONTENT` | `response.noContent()` | 204 |
| `BAD_REQUEST` | `response.badRequest(msg)` | 400 |
| `UNAUTHORIZED` | `response.unauthorized()` | 401 |
| `FORBIDDEN` | `response.forbidden()` | 403 |
| `NOT_FOUND` | `response.notFound()` | 404 |
| `CONFLICT` | `response.conflict()` | 409 |
```typescript
// ✅ Correct — NO_CONTENT in output, response.noContent() in callback

View File

@@ -291,7 +291,7 @@ rbl?: ResponsiveProp<RadiusToken> // Bottom Left
rbr?: ResponsiveProp<RadiusToken> // Bottom Right
}
````
```
#### Example:
```tsx
@@ -305,7 +305,7 @@ rbr?: ResponsiveProp<RadiusToken> // Bottom Right
>
Box Content
</Box>
````
```
---
@@ -576,7 +576,11 @@ interface WithDivideProps {
```tsx
<Stack gap="none">
{items.map(item => (
<WithDivide key={item.id} axis="y" color={{ base: 'bg-300', dark: 'bg-800' }}>
<WithDivide
key={item.id}
axis="y"
color={{ base: 'bg-300', dark: 'bg-800' }}
>
<Box p="md">{item.name}</Box>
</WithDivide>
))}