create-m5kdev 0.40.2 → 0.41.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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal-app/.cursor/rules/frontend-component-guide.mdc +95 -95
  3. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +103 -103
  4. package/templates/minimal-app/.cursor/rules/frontend-form-guide.mdc +80 -80
  5. package/templates/minimal-app/.cursor/rules/frontend-hook-guide.mdc +79 -79
  6. package/templates/minimal-app/.cursor/rules/frontend-i18n-guide.mdc +45 -45
  7. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -60
  8. package/templates/minimal-app/.cursor/rules/module-prompt-guide.mdc +214 -214
  9. package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +167 -167
  10. package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -53
  11. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +367 -367
  12. package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -71
  13. package/templates/minimal-app/apps/.gitignore.tpl +13 -13
  14. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  15. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +11 -22
  16. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  17. package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -44
  18. package/templates/minimal-app/apps/expo/.prettierignore +13 -13
  19. package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -9
  20. package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -156
  21. package/templates/minimal-app/apps/expo/README.md.tpl +44 -44
  22. package/templates/minimal-app/apps/expo/package.json.tpl +63 -63
  23. package/templates/minimal-app/apps/landing/package.json.tpl +34 -34
  24. package/templates/minimal-app/apps/server/drizzle/0000_whole_echo.sql +366 -366
  25. package/templates/minimal-app/apps/server/drizzle/0002_crazy_lilith.sql +46 -46
  26. package/templates/minimal-app/apps/server/drizzle/0003_freezing_fantastic_four.sql +3 -3
  27. package/templates/minimal-app/apps/server/drizzle/0004_slow_ezekiel_stane.sql +98 -98
  28. package/templates/minimal-app/apps/server/drizzle/0005_wonderful_the_professor.sql +1 -1
  29. package/templates/minimal-app/apps/server/drizzle/0006_lazy_blue_marvel.sql +158 -158
  30. package/templates/minimal-app/apps/server/drizzle/0007_abnormal_ultimo.sql +1 -1
  31. package/templates/minimal-app/apps/server/drizzle/0008_steady_nekra.sql +86 -86
  32. package/templates/minimal-app/apps/server/drizzle/0009_empty_starjammers.sql +45 -0
  33. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +2624 -2624
  34. package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +2670 -2670
  35. package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +2715 -2715
  36. package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2687 -2687
  37. package/templates/minimal-app/apps/server/drizzle/meta/0004_snapshot.json.tpl +2497 -2497
  38. package/templates/minimal-app/apps/server/drizzle/meta/0005_snapshot.json.tpl +2483 -2483
  39. package/templates/minimal-app/apps/server/drizzle/meta/0006_snapshot.json.tpl +3602 -3602
  40. package/templates/minimal-app/apps/server/drizzle/meta/0007_snapshot.json.tpl +3619 -3619
  41. package/templates/minimal-app/apps/server/drizzle/meta/0008_snapshot.json.tpl +3619 -3619
  42. package/templates/minimal-app/apps/server/drizzle/meta/0009_snapshot.json.tpl +3687 -0
  43. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +8 -1
  44. package/templates/minimal-app/apps/server/package.json.tpl +57 -57
  45. package/templates/minimal-app/apps/shared/package.json.tpl +27 -27
  46. package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +24 -24
  47. package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
  48. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  49. package/templates/minimal-app/apps/webapp/public/logo.svg.tpl +6 -6
  50. package/templates/minimal-app/pnpm-workspace.yaml.tpl +15 -15
  51. package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +0 -3
  52. package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +0 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m5kdev",
3
- "version": "0.40.2",
3
+ "version": "0.41.0",
4
4
  "license": "GPL-3.0-only",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -1,95 +1,95 @@
1
- ---
2
- globs: apps/**/webapp/src/**/*.tsx
3
- alwaysApply: false
4
- ---
5
-
6
- # Frontend Component Style Guide
7
-
8
- Conventions for React components in the webapp. The stack is fixed: React 19,
9
- HeroUI v3, Tailwind CSS v4, react-router v7, nuqs, TanStack Query over tRPC,
10
- i18next. Do not introduce alternatives.
11
-
12
- ## HeroUI before raw Tailwind
13
-
14
- When a HeroUI component exists for the element you are building, use it instead
15
- of hand-rolled HTML + Tailwind: `Button`, `Input`, `TextField`, `TextArea`,
16
- `Select`, `ListBox`, `Modal`, `Card`, `Chip`, `Skeleton`, `Form`, `Label`,
17
- `FieldError`, `toast`. Tailwind utilities are for layout, spacing, and visual
18
- styling on top of these primitives — not for rebuilding them.
19
-
20
- ```tsx
21
- // GOOD
22
- <Button variant="primary" isPending={isSaving} onPress={openCreate}>...</Button>
23
-
24
- // BAD — hand-rolled button
25
- <div className="cursor-pointer rounded bg-blue-600 px-4 py-2" onClick={...}>
26
- ```
27
-
28
- ## Components stay readable, logic lives in hooks
29
-
30
- The measure is readability, not dogma: someone editing the JSX should not have
31
- to scroll through business logic to find it. Data fetching, mutations, URL
32
- state, and effect chains belong in custom hooks (see the frontend hook guide).
33
-
34
- Allowed directly in a component:
35
-
36
- - `useTranslation` and small formatting helpers (dates, reading time).
37
- - `useId` for accessibility wiring.
38
- - Ephemeral UI state that never leaves the component: modal open/closed,
39
- selected row, hover/expanded toggles.
40
- - A few small hooks that comfortably fit before the returned JSX.
41
-
42
- Not allowed directly in feature/route components:
43
-
44
- - `useQuery` / `useMutation` clusters, `queryClient` calls, tRPC wiring.
45
- - `useEffect` chains that synchronize state.
46
- - nuqs URL-state definitions for filters/pagination/tabs.
47
-
48
- ```tsx
49
- // GOOD — one feature hook, ephemeral UI state local, display-focused JSX
50
- export function PostsRoute() {
51
- const { t } = useTranslation("blog");
52
- const posts = usePostsRoute();
53
- const [selectedPostId, setSelectedPostId] = useState<string>();
54
- const selectedPost = posts.rows.find((r) => r.id === selectedPostId) ?? posts.rows[0];
55
- return (/* JSX built from `posts` + local UI state */);
56
- }
57
- ```
58
-
59
- ## Derive, don't sync
60
-
61
- Compute values from data during render instead of mirroring them into state
62
- with `useEffect`.
63
-
64
- ```tsx
65
- // GOOD
66
- const selectedPost = rows.find((r) => r.id === selectedPostId) ?? rows[0];
67
-
68
- // BAD — effect that re-syncs state after every data change
69
- useEffect(() => {
70
- if (!rows.some((r) => r.id === selectedPostId)) setSelectedPostId(rows[0]?.id);
71
- }, [rows, selectedPostId]);
72
- ```
73
-
74
- ## React 19 idioms
75
-
76
- - No `forwardRef` — pass `ref` as a regular prop.
77
- - No `defaultProps` — use parameter defaults.
78
- - Use `startTransition` / `useDeferredValue` for low-priority updates such as
79
- search inputs that feed queries.
80
-
81
- ## Required states
82
-
83
- Every data-driven view renders explicit loading (`Skeleton`), empty, and error
84
- states. Never return `null` while data is loading.
85
-
86
- ## Subcomponents
87
-
88
- Presentational subcomponents receive everything via props — no data fetching,
89
- no feature hooks inside them. Co-locate them under
90
- `modules/<feature>/components/` (or in-file when tiny).
91
-
92
- ## Text
93
-
94
- All user-facing strings go through `useTranslation` — no hardcoded copy. See
95
- the frontend i18n guide.
1
+ ---
2
+ globs: apps/**/webapp/src/**/*.tsx
3
+ alwaysApply: false
4
+ ---
5
+
6
+ # Frontend Component Style Guide
7
+
8
+ Conventions for React components in the webapp. The stack is fixed: React 19,
9
+ HeroUI v3, Tailwind CSS v4, react-router v7, nuqs, TanStack Query over tRPC,
10
+ i18next. Do not introduce alternatives.
11
+
12
+ ## HeroUI before raw Tailwind
13
+
14
+ When a HeroUI component exists for the element you are building, use it instead
15
+ of hand-rolled HTML + Tailwind: `Button`, `Input`, `TextField`, `TextArea`,
16
+ `Select`, `ListBox`, `Modal`, `Card`, `Chip`, `Skeleton`, `Form`, `Label`,
17
+ `FieldError`, `toast`. Tailwind utilities are for layout, spacing, and visual
18
+ styling on top of these primitives — not for rebuilding them.
19
+
20
+ ```tsx
21
+ // GOOD
22
+ <Button variant="primary" isPending={isSaving} onPress={openCreate}>...</Button>
23
+
24
+ // BAD — hand-rolled button
25
+ <div className="cursor-pointer rounded bg-blue-600 px-4 py-2" onClick={...}>
26
+ ```
27
+
28
+ ## Components stay readable, logic lives in hooks
29
+
30
+ The measure is readability, not dogma: someone editing the JSX should not have
31
+ to scroll through business logic to find it. Data fetching, mutations, URL
32
+ state, and effect chains belong in custom hooks (see the frontend hook guide).
33
+
34
+ Allowed directly in a component:
35
+
36
+ - `useTranslation` and small formatting helpers (dates, reading time).
37
+ - `useId` for accessibility wiring.
38
+ - Ephemeral UI state that never leaves the component: modal open/closed,
39
+ selected row, hover/expanded toggles.
40
+ - A few small hooks that comfortably fit before the returned JSX.
41
+
42
+ Not allowed directly in feature/route components:
43
+
44
+ - `useQuery` / `useMutation` clusters, `queryClient` calls, tRPC wiring.
45
+ - `useEffect` chains that synchronize state.
46
+ - nuqs URL-state definitions for filters/pagination/tabs.
47
+
48
+ ```tsx
49
+ // GOOD — one feature hook, ephemeral UI state local, display-focused JSX
50
+ export function PostsRoute() {
51
+ const { t } = useTranslation("blog");
52
+ const posts = usePostsRoute();
53
+ const [selectedPostId, setSelectedPostId] = useState<string>();
54
+ const selectedPost = posts.rows.find((r) => r.id === selectedPostId) ?? posts.rows[0];
55
+ return (/* JSX built from `posts` + local UI state */);
56
+ }
57
+ ```
58
+
59
+ ## Derive, don't sync
60
+
61
+ Compute values from data during render instead of mirroring them into state
62
+ with `useEffect`.
63
+
64
+ ```tsx
65
+ // GOOD
66
+ const selectedPost = rows.find((r) => r.id === selectedPostId) ?? rows[0];
67
+
68
+ // BAD — effect that re-syncs state after every data change
69
+ useEffect(() => {
70
+ if (!rows.some((r) => r.id === selectedPostId)) setSelectedPostId(rows[0]?.id);
71
+ }, [rows, selectedPostId]);
72
+ ```
73
+
74
+ ## React 19 idioms
75
+
76
+ - No `forwardRef` — pass `ref` as a regular prop.
77
+ - No `defaultProps` — use parameter defaults.
78
+ - Use `startTransition` / `useDeferredValue` for low-priority updates such as
79
+ search inputs that feed queries.
80
+
81
+ ## Required states
82
+
83
+ Every data-driven view renders explicit loading (`Skeleton`), empty, and error
84
+ states. Never return `null` while data is loading.
85
+
86
+ ## Subcomponents
87
+
88
+ Presentational subcomponents receive everything via props — no data fetching,
89
+ no feature hooks inside them. Co-locate them under
90
+ `modules/<feature>/components/` (or in-file when tiny).
91
+
92
+ ## Text
93
+
94
+ All user-facing strings go through `useTranslation` — no hardcoded copy. See
95
+ the frontend i18n guide.
@@ -1,103 +1,103 @@
1
- ---
2
- globs: apps/**/webapp/src/**/*.ts*
3
- alwaysApply: false
4
- ---
5
-
6
- # Frontend Data Fetching Style Guide
7
-
8
- All server communication goes through tRPC with the TanStack React Query
9
- integration. No `fetch`, no axios, no hand-written API clients.
10
-
11
- ## Types come from the server — never hand-mirror them
12
-
13
- Row and payload types are **inferred from the router**, not redeclared:
14
-
15
- ```ts
16
- import type { inferRouterOutputs } from "@trpc/server";
17
- import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
18
-
19
- type PostsListOutput = inferRouterOutputs<AppRouter>["posts"]["list"];
20
- export type PostRow = PostsListOutput["rows"][number];
21
- ```
22
-
23
- Shared Zod schemas (`apps/shared`) are only for runtime needs — form
24
- validation, or schemas the server DTO also imports. Never write a shared
25
- schema that just mirrors a server response; it will drift (see the shared
26
- schema guide).
27
-
28
- ## Queries
29
-
30
- Use `useTRPC()` and the generated `queryOptions`:
31
-
32
- ```ts
33
- const trpc = useTRPC();
34
- const { data, isLoading, isFetching } = useQuery(
35
- trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<PostsListOutput>
36
- );
37
- ```
38
-
39
- - Search inputs that feed queries go through `useDeferredValue`, and URL-state
40
- updates are wrapped in `startTransition`.
41
-
42
- ## List endpoints speak the generic query contract
43
-
44
- Module list procedures accept `QueryInput` from
45
- `@m5kdev/commons/modules/schemas/query.schema` — `page`, `limit`, `sort`,
46
- `order`, `q` (global search), and `filters`. Do not invent per-module fields
47
- like `search` or `status` at the top level; they will be stripped by input
48
- validation and silently ignored.
49
-
50
- ```ts
51
- const listInput = useMemo<QueryInput>(
52
- () => ({
53
- page,
54
- limit: PAGE_SIZE,
55
- q: deferredSearch || undefined,
56
- filters:
57
- status === "all"
58
- ? undefined
59
- : [{ columnId: "status", type: "enum", method: "equals", value: status }],
60
- sort: "updatedAt",
61
- order: "desc",
62
- }),
63
- [deferredSearch, page, status]
64
- );
65
- ```
66
-
67
- ## Mutations
68
-
69
- Use `mutationOptions` with success/error handling declared where the mutation
70
- is created (inside a custom hook):
71
-
72
- ```ts
73
- const createMutation = useMutation(
74
- trpc.posts.create.mutationOptions({
75
- onSuccess: async () => {
76
- toast.success(t("posts.toast.created"));
77
- await queryClient.invalidateQueries(trpc.posts.list.queryFilter());
78
- },
79
- onError: (error) => toast.error(error.message),
80
- })
81
- );
82
- ```
83
-
84
- Mutation input shapes are enforced by the router's inferred types — no
85
- `satisfies SomeSchema` casts needed for simple `{ id }` payloads.
86
-
87
- ## Invalidation discipline
88
-
89
- - Invalidate intentionally with the matching `queryFilter()` after mutations.
90
- - Never call `queryClient.clear()` or invalidate everything.
91
- - Background Workflow completions: invalidate from a `ServerEventProvider` handler. The trigger mutation only returns `jobId`. See `.cursor/rules/server-event-emit.mdc`.
92
-
93
- ## Pending state
94
-
95
- - Screen-level: `mutation.isPending`.
96
- - Row-level (a list where one item is being deleted):
97
- `mutation.isPending && mutation.variables?.id === row.id`, exposed from the
98
- hook as `deletingId` / `publishingId`.
99
-
100
- ## Placement
101
-
102
- Queries and mutations live in custom hooks under
103
- `modules/<feature>/hooks/`, not in components — see the frontend hook guide.
1
+ ---
2
+ globs: apps/**/webapp/src/**/*.ts*
3
+ alwaysApply: false
4
+ ---
5
+
6
+ # Frontend Data Fetching Style Guide
7
+
8
+ All server communication goes through tRPC with the TanStack React Query
9
+ integration. No `fetch`, no axios, no hand-written API clients.
10
+
11
+ ## Types come from the server — never hand-mirror them
12
+
13
+ Row and payload types are **inferred from the router**, not redeclared:
14
+
15
+ ```ts
16
+ import type { inferRouterOutputs } from "@trpc/server";
17
+ import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
18
+
19
+ type PostsListOutput = inferRouterOutputs<AppRouter>["posts"]["list"];
20
+ export type PostRow = PostsListOutput["rows"][number];
21
+ ```
22
+
23
+ Shared Zod schemas (`apps/shared`) are only for runtime needs — form
24
+ validation, or schemas the server DTO also imports. Never write a shared
25
+ schema that just mirrors a server response; it will drift (see the shared
26
+ schema guide).
27
+
28
+ ## Queries
29
+
30
+ Use `useTRPC()` and the generated `queryOptions`:
31
+
32
+ ```ts
33
+ const trpc = useTRPC();
34
+ const { data, isLoading, isFetching } = useQuery(
35
+ trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<PostsListOutput>
36
+ );
37
+ ```
38
+
39
+ - Search inputs that feed queries go through `useDeferredValue`, and URL-state
40
+ updates are wrapped in `startTransition`.
41
+
42
+ ## List endpoints speak the generic query contract
43
+
44
+ Module list procedures accept `QueryInput` from
45
+ `@m5kdev/commons/modules/schemas/query.schema` — `page`, `limit`, `sort`,
46
+ `order`, `q` (global search), and `filters`. Do not invent per-module fields
47
+ like `search` or `status` at the top level; they will be stripped by input
48
+ validation and silently ignored.
49
+
50
+ ```ts
51
+ const listInput = useMemo<QueryInput>(
52
+ () => ({
53
+ page,
54
+ limit: PAGE_SIZE,
55
+ q: deferredSearch || undefined,
56
+ filters:
57
+ status === "all"
58
+ ? undefined
59
+ : [{ columnId: "status", type: "enum", method: "equals", value: status }],
60
+ sort: "updatedAt",
61
+ order: "desc",
62
+ }),
63
+ [deferredSearch, page, status]
64
+ );
65
+ ```
66
+
67
+ ## Mutations
68
+
69
+ Use `mutationOptions` with success/error handling declared where the mutation
70
+ is created (inside a custom hook):
71
+
72
+ ```ts
73
+ const createMutation = useMutation(
74
+ trpc.posts.create.mutationOptions({
75
+ onSuccess: async () => {
76
+ toast.success(t("posts.toast.created"));
77
+ await queryClient.invalidateQueries(trpc.posts.list.queryFilter());
78
+ },
79
+ onError: (error) => toast.error(error.message),
80
+ })
81
+ );
82
+ ```
83
+
84
+ Mutation input shapes are enforced by the router's inferred types — no
85
+ `satisfies SomeSchema` casts needed for simple `{ id }` payloads.
86
+
87
+ ## Invalidation discipline
88
+
89
+ - Invalidate intentionally with the matching `queryFilter()` after mutations.
90
+ - Never call `queryClient.clear()` or invalidate everything.
91
+ - Background Workflow completions: invalidate from a `ServerEventProvider` handler. The trigger mutation only returns `jobId`. See `.cursor/rules/server-event-emit.mdc`.
92
+
93
+ ## Pending state
94
+
95
+ - Screen-level: `mutation.isPending`.
96
+ - Row-level (a list where one item is being deleted):
97
+ `mutation.isPending && mutation.variables?.id === row.id`, exposed from the
98
+ hook as `deletingId` / `publishingId`.
99
+
100
+ ## Placement
101
+
102
+ Queries and mutations live in custom hooks under
103
+ `modules/<feature>/hooks/`, not in components — see the frontend hook guide.
@@ -1,80 +1,80 @@
1
- ---
2
- globs: apps/**/webapp/src/**/*.tsx
3
- alwaysApply: false
4
- ---
5
-
6
- # Frontend Form Style Guide
7
-
8
- Forms are uncontrolled HeroUI v3 forms with native HTML validation. The server
9
- already validates every input through tRPC + Zod — the client only needs the
10
- built-in validators, which HeroUI renders nicely.
11
-
12
- ## Never use form libraries
13
-
14
- Do not add react-hook-form, formik, react-final-form, or any controlled-form
15
- state manager. No `useState` per field, no `onChange` mirroring.
16
-
17
- ## The pattern
18
-
19
- `Form` + `TextField`/`TextArea` with `name`, `defaultValue`, and `isRequired` /
20
- `type` / `minLength` for validation; `FieldError` renders the native message.
21
- Read values from `FormData` on submit.
22
-
23
- ```tsx
24
- export function PostEditorForm({ post, isSaving, onSave, onClose }: Props) {
25
- const { t } = useTranslation("blog");
26
-
27
- const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
28
- event.preventDefault();
29
- const formData = new FormData(event.currentTarget);
30
- const payload = {
31
- title: String(formData.get("title") ?? "").trim(),
32
- content: String(formData.get("content") ?? "").trim(),
33
- };
34
- if (await onSave(payload, post?.id)) onClose();
35
- };
36
-
37
- return (
38
- // key remounts the form so defaultValue updates when editing another row
39
- <Form key={post?.id ?? "new"} onSubmit={onSubmit}>
40
- <TextField isRequired name="title" defaultValue={post?.title ?? ""}>
41
- <Label>{t("posts.editor.fields.title")}</Label>
42
- <Input />
43
- <FieldError />
44
- </TextField>
45
- <TextField isRequired name="content" defaultValue={post?.content ?? ""}>
46
- <Label>{t("posts.editor.fields.content")}</Label>
47
- <TextArea rows={10} />
48
- <FieldError />
49
- </TextField>
50
- <Button type="submit" variant="primary" isPending={isSaving}>
51
- {t("posts.editor.save")}
52
- </Button>
53
- </Form>
54
- );
55
- }
56
- ```
57
-
58
- The HeroUI `Form` blocks submission and shows `FieldError` messages until
59
- native constraints pass — no validation code needed for required fields,
60
- email formats, or lengths.
61
-
62
- ## Extra validation on submit only
63
-
64
- When a rule cannot be expressed with native HTML validation (for example,
65
- password and confirm-password matching), check it inside the submit handler
66
- after reading `FormData`, and surface the failure with a toast or field-level
67
- message. Do not reach for a validation library for this.
68
-
69
- ```tsx
70
- if (formData.get("password") !== formData.get("confirmPassword")) {
71
- toast.error(t("auth.errors.passwordMismatch"));
72
- return;
73
- }
74
- ```
75
-
76
- ## Submission state
77
-
78
- - The submit `Button` gets `isPending` from the mutation's pending flag.
79
- - The mutation itself (and its success/error toasts) lives in a custom hook;
80
- the form component only parses `FormData` and calls the action.
1
+ ---
2
+ globs: apps/**/webapp/src/**/*.tsx
3
+ alwaysApply: false
4
+ ---
5
+
6
+ # Frontend Form Style Guide
7
+
8
+ Forms are uncontrolled HeroUI v3 forms with native HTML validation. The server
9
+ already validates every input through tRPC + Zod — the client only needs the
10
+ built-in validators, which HeroUI renders nicely.
11
+
12
+ ## Never use form libraries
13
+
14
+ Do not add react-hook-form, formik, react-final-form, or any controlled-form
15
+ state manager. No `useState` per field, no `onChange` mirroring.
16
+
17
+ ## The pattern
18
+
19
+ `Form` + `TextField`/`TextArea` with `name`, `defaultValue`, and `isRequired` /
20
+ `type` / `minLength` for validation; `FieldError` renders the native message.
21
+ Read values from `FormData` on submit.
22
+
23
+ ```tsx
24
+ export function PostEditorForm({ post, isSaving, onSave, onClose }: Props) {
25
+ const { t } = useTranslation("blog");
26
+
27
+ const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
28
+ event.preventDefault();
29
+ const formData = new FormData(event.currentTarget);
30
+ const payload = {
31
+ title: String(formData.get("title") ?? "").trim(),
32
+ content: String(formData.get("content") ?? "").trim(),
33
+ };
34
+ if (await onSave(payload, post?.id)) onClose();
35
+ };
36
+
37
+ return (
38
+ // key remounts the form so defaultValue updates when editing another row
39
+ <Form key={post?.id ?? "new"} onSubmit={onSubmit}>
40
+ <TextField isRequired name="title" defaultValue={post?.title ?? ""}>
41
+ <Label>{t("posts.editor.fields.title")}</Label>
42
+ <Input />
43
+ <FieldError />
44
+ </TextField>
45
+ <TextField isRequired name="content" defaultValue={post?.content ?? ""}>
46
+ <Label>{t("posts.editor.fields.content")}</Label>
47
+ <TextArea rows={10} />
48
+ <FieldError />
49
+ </TextField>
50
+ <Button type="submit" variant="primary" isPending={isSaving}>
51
+ {t("posts.editor.save")}
52
+ </Button>
53
+ </Form>
54
+ );
55
+ }
56
+ ```
57
+
58
+ The HeroUI `Form` blocks submission and shows `FieldError` messages until
59
+ native constraints pass — no validation code needed for required fields,
60
+ email formats, or lengths.
61
+
62
+ ## Extra validation on submit only
63
+
64
+ When a rule cannot be expressed with native HTML validation (for example,
65
+ password and confirm-password matching), check it inside the submit handler
66
+ after reading `FormData`, and surface the failure with a toast or field-level
67
+ message. Do not reach for a validation library for this.
68
+
69
+ ```tsx
70
+ if (formData.get("password") !== formData.get("confirmPassword")) {
71
+ toast.error(t("auth.errors.passwordMismatch"));
72
+ return;
73
+ }
74
+ ```
75
+
76
+ ## Submission state
77
+
78
+ - The submit `Button` gets `isPending` from the mutation's pending flag.
79
+ - The mutation itself (and its success/error toasts) lives in a custom hook;
80
+ the form component only parses `FormData` and calls the action.