create-m5kdev 0.25.1 → 0.25.2
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 +1 -1
- package/templates/minimal-app/.cursor/rules/frontend-component-guide.mdc +95 -0
- package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +59 -0
- package/templates/minimal-app/.cursor/rules/frontend-form-guide.mdc +80 -0
- package/templates/minimal-app/.cursor/rules/frontend-hook-guide.mdc +79 -0
- package/templates/minimal-app/.cursor/rules/frontend-i18n-guide.mdc +45 -0
- package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +1 -1
- package/templates/minimal-app/AGENTS.md.tpl +7 -0
- package/templates/minimal-app/apps/email/package.json.tpl +3 -3
- package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +24 -18
- package/templates/minimal-app/apps/webapp/public/logo.svg +6 -0
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +59 -153
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +100 -111
- package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +307 -751
- package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +138 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +124 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +116 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +92 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -0
- package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +85 -0
- package/templates/minimal-app/apps/webapp/translations/en/blog-app.json.tpl +2 -0
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +6 -5
package/package.json
CHANGED
|
@@ -0,0 +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.
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
## Queries
|
|
12
|
+
|
|
13
|
+
Use `useTRPC()` and the generated `queryOptions`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const trpc = useTRPC();
|
|
17
|
+
const { data, isLoading, isFetching } = useQuery(
|
|
18
|
+
trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<PostsListOutputSchema>
|
|
19
|
+
);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Input and output types come from the shared Zod schemas in `apps/shared` —
|
|
23
|
+
never redeclare them locally.
|
|
24
|
+
- Search inputs that feed queries go through `useDeferredValue`, and URL-state
|
|
25
|
+
updates are wrapped in `startTransition`.
|
|
26
|
+
|
|
27
|
+
## Mutations
|
|
28
|
+
|
|
29
|
+
Use `mutationOptions` with success/error handling declared where the mutation
|
|
30
|
+
is created (inside a custom hook):
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const createMutation = useMutation(
|
|
34
|
+
trpc.posts.create.mutationOptions({
|
|
35
|
+
onSuccess: async () => {
|
|
36
|
+
toast.success(t("posts.toast.created"));
|
|
37
|
+
await queryClient.invalidateQueries(trpc.posts.list.queryFilter());
|
|
38
|
+
},
|
|
39
|
+
onError: (error) => toast.error(error.message),
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Invalidation discipline
|
|
45
|
+
|
|
46
|
+
- Invalidate intentionally with the matching `queryFilter()` after mutations.
|
|
47
|
+
- Never call `queryClient.clear()` or invalidate everything.
|
|
48
|
+
|
|
49
|
+
## Pending state
|
|
50
|
+
|
|
51
|
+
- Screen-level: `mutation.isPending`.
|
|
52
|
+
- Row-level (a list where one item is being deleted):
|
|
53
|
+
`mutation.isPending && mutation.variables?.id === row.id`, exposed from the
|
|
54
|
+
hook as `deletingId` / `publishingId`.
|
|
55
|
+
|
|
56
|
+
## Placement
|
|
57
|
+
|
|
58
|
+
Queries and mutations live in custom hooks under
|
|
59
|
+
`modules/<feature>/hooks/`, not in components — see the frontend hook guide.
|
|
@@ -0,0 +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.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: apps/**/webapp/src/**/hooks/**
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend Custom Hook Style Guide
|
|
7
|
+
|
|
8
|
+
Feature logic lives in custom hooks so components stay display-focused.
|
|
9
|
+
|
|
10
|
+
## When to create a hook
|
|
11
|
+
|
|
12
|
+
Create a custom hook when a component accumulates real logic: multiple
|
|
13
|
+
`useQuery`/`useMutation` calls, `queryClient` invalidation, nuqs URL state,
|
|
14
|
+
or `useEffect` orchestration. A component that only needs one small hook and
|
|
15
|
+
some ephemeral UI state does not need a wrapper — this rule is about keeping
|
|
16
|
+
the JSX editable, not about ceremony.
|
|
17
|
+
|
|
18
|
+
## Structure
|
|
19
|
+
|
|
20
|
+
- Location: `src/modules/<feature>/hooks/use<Name>.ts`
|
|
21
|
+
- Compose small, focused hooks into one screen-level hook so the route
|
|
22
|
+
component makes a single call:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
modules/posts/hooks/
|
|
26
|
+
usePostsList.ts // list query + URL state (search, status, page)
|
|
27
|
+
usePostActions.ts // reusable mutations: save, publish, delete
|
|
28
|
+
usePostsRoute.ts // composes both for the PostsRoute screen
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
export function usePostsRoute() {
|
|
33
|
+
const list = usePostsList();
|
|
34
|
+
const actions = usePostActions();
|
|
35
|
+
return { ...list, ...actions };
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Reusable actions get their own hook
|
|
40
|
+
|
|
41
|
+
Mutations that more than one component may trigger (delete, publish, archive)
|
|
42
|
+
are always extracted into their own hook — never inlined into a screen hook —
|
|
43
|
+
so other components can reuse them with the same toasts, confirmation dialogs,
|
|
44
|
+
and invalidation.
|
|
45
|
+
|
|
46
|
+
## What a hook owns
|
|
47
|
+
|
|
48
|
+
- tRPC queries/mutations and their `queryFilter()` invalidation.
|
|
49
|
+
- nuqs URL state (filters, pagination, tabs) and its update functions
|
|
50
|
+
(wrap updates in `startTransition`).
|
|
51
|
+
- Derived data (`useMemo`): rows, totals, page counts, stats.
|
|
52
|
+
- Side effects of actions: success/error toasts, confirmation dialogs
|
|
53
|
+
(`useDialog`), cache invalidation.
|
|
54
|
+
- Pending indicators: booleans (`isSaving`) or row-scoped ids
|
|
55
|
+
(`deletingId`) derived from `mutation.isPending` + `mutation.variables`.
|
|
56
|
+
|
|
57
|
+
## Return shape
|
|
58
|
+
|
|
59
|
+
Return a flat object of data, derived values, pending flags, and action
|
|
60
|
+
functions. Action functions used by forms should resolve to a success boolean
|
|
61
|
+
instead of throwing, so components can close dialogs without try/catch:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const savePost = async (payload: PostCreateInputSchema, id?: string): Promise<boolean> => {
|
|
65
|
+
try {
|
|
66
|
+
if (id) await updateMutation.mutateAsync({ id, ...payload });
|
|
67
|
+
else await createMutation.mutateAsync(payload);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false; // the mutation's onError already showed a toast
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## What a hook must not do
|
|
76
|
+
|
|
77
|
+
- Return JSX or take React nodes as arguments.
|
|
78
|
+
- Read DOM elements directly (the submit handler that parses `FormData`
|
|
79
|
+
stays in the form component — see the frontend form guide).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: apps/**/webapp/src/**/*.tsx
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend i18n Style Guide
|
|
7
|
+
|
|
8
|
+
All user-facing copy goes through i18next. No hardcoded strings in JSX,
|
|
9
|
+
toasts, aria-labels, or placeholders.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
const { t } = useTranslation("<app-namespace>");
|
|
15
|
+
<h2>{t("posts.hero.title")}</h2>
|
|
16
|
+
<Input placeholder={t("posts.filters.searchPlaceholder")} aria-label={t("posts.filters.searchLabel")} />
|
|
17
|
+
toast.success(t("posts.toast.created"));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `useTranslation(...)` is one of the few hooks always allowed directly inside
|
|
21
|
+
display components.
|
|
22
|
+
- Interpolate variables through i18next, never string concatenation:
|
|
23
|
+
`t("posts.pagination.summary", { page, pageCount })`.
|
|
24
|
+
|
|
25
|
+
## Where keys live
|
|
26
|
+
|
|
27
|
+
- App copy: `apps/webapp/translations/en/<namespace>.json` — add the key in
|
|
28
|
+
the same change that uses it.
|
|
29
|
+
- Role labels and app-level chrome: `apps/webapp/translations/app.json`
|
|
30
|
+
(resolved as `app:...`, e.g. `app:organization.role.editor`).
|
|
31
|
+
- Auth/billing route copy ships with `@m5kdev/web-ui` (`web-ui:` namespace) —
|
|
32
|
+
do not duplicate it.
|
|
33
|
+
|
|
34
|
+
## Key conventions
|
|
35
|
+
|
|
36
|
+
- Structure keys as `<feature>.<section>.<name>`:
|
|
37
|
+
`posts.editor.fields.title`, `posts.toast.created`, `layout.push.cta`.
|
|
38
|
+
- Reuse existing keys before adding near-duplicates.
|
|
39
|
+
|
|
40
|
+
## Server errors
|
|
41
|
+
|
|
42
|
+
Backend `ServerError` responses carry i18n-style client messages
|
|
43
|
+
(`server.error.<layer>.<code>`). When surfacing mutation errors, prefer a
|
|
44
|
+
translated, human message; fall back to `error.message` only when no
|
|
45
|
+
translation exists.
|
|
@@ -32,3 +32,10 @@
|
|
|
32
32
|
- For upgrading existing apps, see the docs site guide `apps/docs/docs/guides/custom-app-roles-migration.md`.
|
|
33
33
|
- Use `nuqs` for URL state, React Router for routing, HeroUI for UI primitives, and Tailwind v4 for styling.
|
|
34
34
|
- Prefer shared framework utilities from `@m5kdev/frontend` and `@m5kdev/web-ui` before adding local duplicates.
|
|
35
|
+
- React 19 idioms: no `forwardRef` (pass `ref` as a prop), no `defaultProps`, use `startTransition`/`useDeferredValue` for low-priority updates.
|
|
36
|
+
- Prefer HeroUI v3 components over hand-rolled HTML + Tailwind whenever an equivalent exists; Tailwind is for layout and styling on top of them.
|
|
37
|
+
- Keep components display-focused: data fetching, mutations, URL state, and effect chains live in custom hooks under `modules/<feature>/hooks/` (small focused hooks composed into one screen-level hook, e.g. `usePostsRoute`). Ephemeral UI state (modal open, selected row) may stay in the component. Extract reusable tRPC actions (delete, publish) into their own hooks.
|
|
38
|
+
- Fetch all server data with TanStack Query over tRPC (`useTRPC()` + `queryOptions`/`mutationOptions`); invalidate with the matching `queryFilter()` after mutations. No `fetch`/axios.
|
|
39
|
+
- Forms are uncontrolled HeroUI `Form` + `TextField`/`TextArea` with native HTML validation (`isRequired`, `type`, `minLength`) and `FieldError`; read values from `FormData` on submit. Never add form libraries (react-hook-form, formik). Validation that HTML cannot express (e.g. confirm-password) happens in the submit handler.
|
|
40
|
+
- All user-facing copy goes through i18next (`useTranslation`); keys live in `apps/webapp/translations/`.
|
|
41
|
+
- The `posts` feature (`apps/webapp/src/modules/posts/`) is the reference implementation for these conventions; see `.cursor/rules/frontend-*.mdc` for the detailed guides.
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
"email:dev": "react-email dev --dir src/emails"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@m5kdev/commons": "
|
|
14
|
-
"@m5kdev/email": "
|
|
13
|
+
"@m5kdev/commons": "catalog:",
|
|
14
|
+
"@m5kdev/email": "catalog:",
|
|
15
15
|
"@react-email/components": "catalog:",
|
|
16
16
|
"react": "catalog:",
|
|
17
17
|
"react-dom": "catalog:"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@m5kdev/backend": "
|
|
20
|
+
"@m5kdev/backend": "catalog:",
|
|
21
21
|
"@m5kdev/config": "catalog:",
|
|
22
22
|
"@types/node": "catalog:",
|
|
23
23
|
"@types/react": "catalog:",
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
# AGENTS.md
|
|
2
|
-
|
|
3
|
-
## Frontend Stack
|
|
4
|
-
|
|
5
|
-
- React + TypeScript
|
|
6
|
-
- React Router v7
|
|
7
|
-
- HeroUI
|
|
8
|
-
- Tailwind CSS v4
|
|
9
|
-
- `nuqs` for URL state
|
|
10
|
-
- TanStack Query + tRPC
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Keep
|
|
16
|
-
- Keep
|
|
17
|
-
-
|
|
18
|
-
-
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Frontend Stack
|
|
4
|
+
|
|
5
|
+
- React 19 + TypeScript
|
|
6
|
+
- React Router v7
|
|
7
|
+
- HeroUI v3
|
|
8
|
+
- Tailwind CSS v4
|
|
9
|
+
- `nuqs` for URL state
|
|
10
|
+
- TanStack Query + tRPC
|
|
11
|
+
- i18next for all user-facing copy
|
|
12
|
+
|
|
13
|
+
## Conventions
|
|
14
|
+
|
|
15
|
+
- Keep providers centralized in `src/Providers.tsx`.
|
|
16
|
+
- Keep routing in `src/Router.tsx`.
|
|
17
|
+
- Keep feature code grouped under `src/modules/<feature>/` with `components/` and `hooks/` subfolders.
|
|
18
|
+
- Prefer HeroUI primitives and existing framework utilities before adding custom abstractions.
|
|
19
|
+
- Components stay display-focused: queries, mutations, URL state, and effects live in custom hooks (`hooks/use<Feature>Route.ts` composed from smaller hooks); ephemeral UI state (modal open, selected row) may stay in the component. Reusable tRPC actions (delete, publish) get their own hook.
|
|
20
|
+
- Derive values during render instead of syncing state with `useEffect`.
|
|
21
|
+
- Forms are uncontrolled: HeroUI `Form` + `TextField` with native HTML validation and `FieldError`, values read from `FormData` on submit. No form libraries. Extra checks that HTML validation cannot express happen in the submit handler.
|
|
22
|
+
- Fetch through `useTRPC()` + `queryOptions`/`mutationOptions` only; invalidate with `queryFilter()` after mutations.
|
|
23
|
+
- Use `useTranslation` for local app copy and rely on the bundled `web-ui` translations for auth routes.
|
|
24
|
+
- The `posts` module is the reference implementation; detailed guides live in `.cursor/rules/frontend-*.mdc`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="App logo">
|
|
2
|
+
<rect x="4" y="4" width="56" height="56" rx="14" fill="#b45309"/>
|
|
3
|
+
<path d="M20 18h18a6 6 0 0 1 6 6v22a2 2 0 0 1-3.2 1.6L36 44H20a4 4 0 0 1-4-4V22a4 4 0 0 1 4-4z" fill="#fffbeb"/>
|
|
4
|
+
<line x1="23" y1="27" x2="39" y2="27" stroke="#b45309" stroke-width="3" stroke-linecap="round"/>
|
|
5
|
+
<line x1="23" y1="34" x2="39" y2="34" stroke="#d97706" stroke-width="3" stroke-linecap="round"/>
|
|
6
|
+
</svg>
|