create-m5kdev 0.25.1 → 0.25.3

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 (29) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal-app/.cursor/rules/frontend-component-guide.mdc +95 -0
  3. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +59 -0
  4. package/templates/minimal-app/.cursor/rules/frontend-form-guide.mdc +80 -0
  5. package/templates/minimal-app/.cursor/rules/frontend-hook-guide.mdc +79 -0
  6. package/templates/minimal-app/.cursor/rules/frontend-i18n-guide.mdc +45 -0
  7. package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +1 -1
  8. package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +1 -1
  9. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +1 -1
  10. package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +1 -1
  11. package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +1 -1
  12. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
  13. package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +1 -1
  14. package/templates/minimal-app/AGENTS.md.tpl +7 -0
  15. package/templates/minimal-app/apps/email/package.json.tpl +3 -3
  16. package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +24 -18
  17. package/templates/minimal-app/apps/webapp/public/logo.svg +6 -0
  18. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +59 -153
  19. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +100 -111
  20. package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +307 -751
  21. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +138 -0
  22. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +124 -0
  23. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +116 -0
  24. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +92 -0
  25. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -0
  26. package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -0
  27. package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +85 -0
  28. package/templates/minimal-app/apps/webapp/translations/en/blog-app.json.tpl +2 -0
  29. package/templates/minimal-app/pnpm-workspace.yaml.tpl +6 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m5kdev",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "license": "GPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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.
@@ -1,5 +1,5 @@
1
1
  ---
2
- globs: apps/*/server/src/modules/**/*.db.ts
2
+ globs: apps/**/server/src/modules/**/*.db.ts
3
3
  alwaysApply: false
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- globs: apps/*/server/src/modules/**/*.grants.ts
2
+ globs: apps/**/server/src/modules/**/*.grants.ts
3
3
  alwaysApply: false
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- globs: apps/*/server/src/modules/**/*.module.ts
2
+ globs: apps/**/server/src/modules/**/*.module.ts
3
3
  alwaysApply: false
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- globs: apps/*/server/src/modules/**/*.repository.ts
2
+ globs: apps/**/server/src/modules/**/*.repository.ts
3
3
  alwaysApply: false
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- globs: apps/*/shared/src/modules/**/*.schema.ts
2
+ globs: apps/**/shared/src/modules/**/*.schema.ts
3
3
  alwaysApply: false
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- globs: apps/*/server/src/modules/**/*.service.ts
2
+ globs: apps/**/server/src/modules/**/*.service.ts
3
3
  alwaysApply: false
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- globs: apps/*/server/src/modules/**/*.trpc.ts
2
+ globs: apps/**/server/src/modules/**/*.trpc.ts
3
3
  alwaysApply: false
4
4
  ---
5
5
 
@@ -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": "workspace:*",
14
- "@m5kdev/email": "workspace:*",
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": "workspace:*",
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
- ## Conventions
13
-
14
- - Keep providers centralized in `src/Providers.tsx`.
15
- - Keep routing in `src/Router.tsx`.
16
- - Keep feature code grouped under `src/modules/<feature>/`.
17
- - Prefer HeroUI primitives and existing framework utilities before adding custom abstractions.
18
- - Use `useTranslation` for local app copy and rely on the bundled `web-ui` translations for auth routes.
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>