create-aron-app 0.1.7 → 0.1.8

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 (160) hide show
  1. package/README.md +24 -31
  2. package/dist/index.js +38 -49
  3. package/package.json +3 -3
  4. package/templates/.cursor/rules/backend.mdc +112 -0
  5. package/templates/.cursor/rules/coding_standards.mdc +85 -4
  6. package/templates/.cursor/rules/frontend_architecture.mdc +334 -0
  7. package/templates/.github/workflows/ci.yml +17 -6
  8. package/templates/apps/{react-router → web}/.react-router/types/+routes.ts +11 -6
  9. package/templates/apps/{react-router/.react-router/types/src/routes/(dashboard)/todos → web/.react-router/types/src/routes/(dashboard)/(todos)}/+types/[id].ts +5 -2
  10. package/templates/apps/{react-router/.react-router/types/src/routes/(dashboard)/todos → web/.react-router/types/src/routes/(dashboard)/(todos)}/+types/index.ts +5 -2
  11. package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/layout.ts +65 -0
  12. package/templates/apps/{react-router → web}/project.json +11 -4
  13. package/templates/apps/{react-router → web}/react-router.config.ts +1 -1
  14. package/templates/apps/web/src/libs/convex_query_client.ts +11 -0
  15. package/templates/apps/web/src/libs/react_query_client.ts +17 -0
  16. package/templates/apps/web/src/libs/server/auth.ts +32 -0
  17. package/templates/apps/web/src/libs/server/protected.ts +17 -0
  18. package/templates/apps/web/src/providers/api_auth_provider.tsx +26 -0
  19. package/templates/apps/web/src/providers/global_provider.tsx +28 -0
  20. package/templates/apps/web/src/providers/navigation_loading_bar_provider.tsx +72 -0
  21. package/templates/apps/web/src/root.tsx +68 -0
  22. package/templates/apps/web/src/routes/(dashboard)/(todos)/[id].tsx +33 -0
  23. package/templates/apps/web/src/routes/(dashboard)/(todos)/index.tsx +26 -0
  24. package/templates/apps/web/src/routes/(dashboard)/(todos)/layout.tsx +9 -0
  25. package/templates/apps/{react-router → web}/src/routes/(dashboard)/index.tsx +3 -2
  26. package/templates/apps/web/src/routes/(dashboard)/layout.tsx +20 -0
  27. package/templates/apps/{react-router → web}/src/routes.ts +4 -2
  28. package/templates/apps/{nextjs → web}/src/surfaces/sidebar/install.tsx +1 -5
  29. package/templates/apps/{react-router → web}/src/surfaces/sidebar/layout.tsx +24 -15
  30. package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos.tsx +1 -1
  31. package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos_controller.ts +1 -1
  32. package/templates/apps/web/src/surfaces/todos/all_todos/bootstrap.ts +21 -0
  33. package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/bootstrap.ts +4 -5
  34. package/templates/apps/{nextjs → web}/src/surfaces/todos/single_todo/header/create.tsx +4 -6
  35. package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo_controller.ts +1 -1
  36. package/templates/biome.json +5 -0
  37. package/templates/bun.lock +11 -1281
  38. package/templates/nx.json +0 -11
  39. package/templates/package.json +2 -3
  40. package/templates/shared/assets/src/styles/global.css +14 -8
  41. package/templates/shared/ui/src/base/collapsible.tsx +31 -0
  42. package/templates/shared/ui/src/base/hover-card.tsx +42 -0
  43. package/templates/shared/ui/src/base/input-group.tsx +168 -0
  44. package/templates/shared/ui/src/base/panel.tsx +93 -0
  45. package/templates/shared/ui/src/hooks/use_mobile.tsx +1 -1
  46. package/templates/shared/ui/src/hooks/use_query_params.tsx +6 -7
  47. package/templates/shared/ui/tsconfig.json +1 -1
  48. package/templates/shared/utils/src/convex.ts +2 -1
  49. package/templates/tsconfig.base.json +2 -4
  50. package/templates/.cursor/commands/builder.md +0 -0
  51. package/templates/.cursor/rules/api_architecture.mdc +0 -262
  52. package/templates/.cursor/rules/convex_rules.mdc +0 -331
  53. package/templates/.cursor/rules/frontend_architecture_core.mdc +0 -495
  54. package/templates/.cursor/rules/frontend_architecture_nextjs.mdc +0 -458
  55. package/templates/.cursor/rules/frontend_architecture_reactrouter.mdc +0 -473
  56. package/templates/apps/nextjs/.env.example +0 -10
  57. package/templates/apps/nextjs/index.d.ts +0 -6
  58. package/templates/apps/nextjs/next-env.d.ts +0 -5
  59. package/templates/apps/nextjs/next.config.js +0 -22
  60. package/templates/apps/nextjs/postcss.config.js +0 -17
  61. package/templates/apps/nextjs/project.json +0 -22
  62. package/templates/apps/nextjs/src/app/(auth)/layout.tsx +0 -21
  63. package/templates/apps/nextjs/src/app/(auth)/not-allowed/page.tsx +0 -23
  64. package/templates/apps/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +0 -15
  65. package/templates/apps/nextjs/src/app/(dashboard)/layout.tsx +0 -22
  66. package/templates/apps/nextjs/src/app/(dashboard)/page.tsx +0 -12
  67. package/templates/apps/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +0 -26
  68. package/templates/apps/nextjs/src/app/(dashboard)/todos/page.tsx +0 -19
  69. package/templates/apps/nextjs/src/app/app.css +0 -3
  70. package/templates/apps/nextjs/src/app/layout.tsx +0 -26
  71. package/templates/apps/nextjs/src/middleware.ts +0 -18
  72. package/templates/apps/nextjs/src/providers/convex_provider.tsx +0 -44
  73. package/templates/apps/nextjs/src/surfaces/home/home.tsx +0 -27
  74. package/templates/apps/nextjs/src/surfaces/home/layout.tsx +0 -44
  75. package/templates/apps/nextjs/src/surfaces/home/main/create.tsx +0 -34
  76. package/templates/apps/nextjs/src/surfaces/sidebar/layout.tsx +0 -118
  77. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/create.tsx +0 -19
  78. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_config.ts +0 -22
  79. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_main.tsx +0 -25
  80. package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/create.tsx +0 -21
  81. package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +0 -33
  82. package/templates/apps/nextjs/src/surfaces/sidebar/sidebar.tsx +0 -23
  83. package/templates/apps/nextjs/src/surfaces/sidebar/ui/sidebar_nav_link.tsx +0 -39
  84. package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/create.tsx +0 -28
  85. package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/user_menu.tsx +0 -42
  86. package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos.tsx +0 -29
  87. package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos_controller.ts +0 -61
  88. package/templates/apps/nextjs/src/surfaces/todos/all_todos/bootstrap.ts +0 -21
  89. package/templates/apps/nextjs/src/surfaces/todos/all_todos/header/create.tsx +0 -23
  90. package/templates/apps/nextjs/src/surfaces/todos/all_todos/install.tsx +0 -23
  91. package/templates/apps/nextjs/src/surfaces/todos/all_todos/layout.tsx +0 -44
  92. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/create.tsx +0 -49
  93. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/main.tsx +0 -70
  94. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +0 -56
  95. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +0 -99
  96. package/templates/apps/nextjs/src/surfaces/todos/single_todo/bootstrap.ts +0 -32
  97. package/templates/apps/nextjs/src/surfaces/todos/single_todo/header/header.tsx +0 -22
  98. package/templates/apps/nextjs/src/surfaces/todos/single_todo/install.tsx +0 -27
  99. package/templates/apps/nextjs/src/surfaces/todos/single_todo/layout.tsx +0 -55
  100. package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/create.tsx +0 -38
  101. package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/main.tsx +0 -49
  102. package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo.tsx +0 -29
  103. package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo_controller.ts +0 -13
  104. package/templates/apps/nextjs/src/utils/auth.ts +0 -18
  105. package/templates/apps/nextjs/src/utils/convex.ts +0 -11
  106. package/templates/apps/nextjs/src/utils/font.ts +0 -9
  107. package/templates/apps/nextjs/tsconfig.json +0 -42
  108. package/templates/apps/react-router/src/providers/api_auth_provider.tsx +0 -40
  109. package/templates/apps/react-router/src/root.tsx +0 -37
  110. package/templates/apps/react-router/src/routes/(dashboard)/layout.tsx +0 -37
  111. package/templates/apps/react-router/src/routes/(dashboard)/todos/[id].tsx +0 -19
  112. package/templates/apps/react-router/src/routes/(dashboard)/todos/index.tsx +0 -19
  113. package/templates/apps/react-router/src/surfaces/home/bootstrap.ts +0 -9
  114. package/templates/apps/react-router/src/surfaces/home/install.tsx +0 -17
  115. package/templates/apps/react-router/src/surfaces/sidebar/install.tsx +0 -23
  116. package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/create.tsx +0 -21
  117. package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +0 -31
  118. package/templates/apps/react-router/src/surfaces/todos/all_todos/bootstrap.ts +0 -18
  119. package/templates/apps/react-router/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +0 -11
  120. package/templates/apps/react-router/src/surfaces/todos/single_todo/header/create.tsx +0 -32
  121. /package/templates/apps/{react-router → web}/.env.example +0 -0
  122. /package/templates/apps/{react-router → web}/.react-router/types/+future.ts +0 -0
  123. /package/templates/apps/{react-router → web}/.react-router/types/+server-build.d.ts +0 -0
  124. /package/templates/apps/{react-router → web}/.react-router/types/src/+types/root.ts +0 -0
  125. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(auth)/+types/layout.ts +0 -0
  126. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(auth)/sign-in/+types/index.ts +0 -0
  127. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(dashboard)/+types/index.ts +0 -0
  128. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(dashboard)/+types/layout.ts +0 -0
  129. /package/templates/apps/{react-router → web}/postcss.config.js +0 -0
  130. /package/templates/apps/{react-router → web}/public/favicon.ico +0 -0
  131. /package/templates/apps/{react-router → web}/src/app.css +0 -0
  132. /package/templates/apps/{react-router → web}/src/components/error_boundary.tsx +0 -0
  133. /package/templates/apps/{react-router → web}/src/routes/(auth)/layout.tsx +0 -0
  134. /package/templates/apps/{react-router → web}/src/routes/(auth)/sign-in/index.tsx +0 -0
  135. /package/templates/apps/{nextjs → web}/src/surfaces/home/bootstrap.ts +0 -0
  136. /package/templates/apps/{react-router → web}/src/surfaces/home/home.tsx +0 -0
  137. /package/templates/apps/{nextjs → web}/src/surfaces/home/install.tsx +0 -0
  138. /package/templates/apps/{react-router → web}/src/surfaces/home/layout.tsx +0 -0
  139. /package/templates/apps/{react-router → web}/src/surfaces/home/main/create.tsx +0 -0
  140. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/nav_main/create.tsx +0 -0
  141. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/nav_main/nav_main.tsx +0 -0
  142. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/sidebar.tsx +0 -0
  143. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/user_menu/create.tsx +0 -0
  144. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/user_menu/user_menu.tsx +0 -0
  145. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/header/create.tsx +0 -0
  146. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/install.tsx +0 -0
  147. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/layout.tsx +0 -0
  148. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/create.tsx +0 -0
  149. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/main.tsx +0 -0
  150. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +0 -0
  151. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +0 -0
  152. /package/templates/apps/{nextjs → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +0 -0
  153. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/header/header.tsx +0 -0
  154. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/install.tsx +0 -0
  155. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/layout.tsx +0 -0
  156. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/main/create.tsx +0 -0
  157. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/main/main.tsx +0 -0
  158. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo.tsx +0 -0
  159. /package/templates/apps/{react-router → web}/tsconfig.json +0 -0
  160. /package/templates/apps/{react-router → web}/vite.config.ts +0 -0
@@ -1,495 +0,0 @@
1
- ---
2
- description: Core frontend surface architecture — framework-agnostic layer model, MobX patterns, dependency injection, data flow, and component rules. Apply across all frontend apps.
3
- globs: apps/*/src/**
4
- alwaysApply: false
5
- ---
6
-
7
- # Frontend Surface Architecture — Core
8
-
9
- Each surface is an isolated vertical slice. Data flows strictly downward. State is held in observable MobX classes, not component hooks. Components are assembled lazily via dynamic `import()`.
10
-
11
- **Key principle: display components are dumb, creates are smart closures, controllers are the brain.**
12
-
13
- **When to use a Presenter vs Controller:** Use a **presenter** when a section has its own business logic (mutations, input state, submit handling) that doesn't need to coordinate with other sections. Use a **controller** when state must be shared across multiple sections (e.g. dialog open/close, selected item). Presenters are scoped to a single component; controllers span the feature.
14
-
15
- ---
16
-
17
- ## Layer Stack
18
-
19
- ```
20
- [route entry] → mounts surface, passes bootstrap
21
- bootstrap.ts → param validation + data loading (no UI)
22
- [feature].tsx → surface entry (useRef guard, triggers install, receives bootstrap)
23
- install.tsx → wiring (lazy-imports creates, seeds layout slots, instantiates controller)
24
- layout.tsx → LayoutController + reactive Layout shell
25
- [feature]_controller.ts → MobX class managing cross-component state
26
- [section]_presenter.ts → MobX class scoped to one section (business logic, mutations, input state)
27
- [section]/create.tsx → factory returning an observer() component closed over deps
28
- [section]/[section].tsx → pure display component (props only, no hooks — see useForm exception)
29
- ui/ → shared display components scoped to this surface
30
- ```
31
-
32
- ---
33
-
34
- ## Folder Structure
35
-
36
- ```
37
- surfaces/
38
- todo_detail/
39
- todo_detail.tsx # surface entry component (receives bootstrap)
40
- bootstrap.ts # data loading, exports TodoDetailBootstrap
41
- install.tsx # wiring — lazy imports, seeds layout, instantiates controller
42
- layout.tsx # LayoutController + createLayout()
43
- todo_detail_controller.ts # MobX state class
44
- header/
45
- create.tsx # createHeader({ navigate }) — thin component, no display file
46
- main/
47
- create.tsx # createMain({ controller, bootstrap })
48
- main.tsx # pure display: MainProps
49
- new_todo_sheet/ # child surface inside parent section (DI tree integrity)
50
- create.tsx # createNewTodoSheet({ controller })
51
- new_todo_sheet.tsx # display with useFormContext (form exception)
52
- schema.ts # Zod schema + inferred type
53
- ui/
54
- status_badge.tsx # shared display scoped to this surface
55
- ```
56
-
57
- Naming conventions:
58
- - `Opts` suffix for factory/install arg types: `CreateHeaderOpts`, `InstallTodoDetailOpts`
59
- - `Props` suffix for component prop types: `HeaderProps`, `MainProps`
60
- - Controller class scoped to feature: `TodoDetailController`, `DashboardController`
61
- - `bootstrap` stays as `bootstrap` — one per surface
62
- - All file names must be **snake_case** (e.g. `todo_detail.tsx`, `new_todo_sheet.tsx`)
63
- - Use **`All`** for list surfaces and **`Single`** for id-specific surfaces
64
- - **CRUD naming:** Use `new`, `edit`, `delete` for frontend CRUD component names — never `create` (clashes with the `create.tsx` factory pattern). E.g., `new_todo_sheet/`, `edit_todo_dialog/`, `delete_todo_dialog/`.
65
- - **Component name de-scoping:** Display components must NOT repeat parent surface/section names. The directory path provides scope. E.g., inside `single_todo/header/`, the display is `header.tsx` exporting `Header` — not `single_todo_header.tsx` exporting `SingleTodoHeader`. Inside `all_todos/main/`, the display is `main.tsx` exporting `Main`.
66
-
67
- ---
68
-
69
- ## Bootstrap Pattern
70
-
71
- Bootstrap validates params, handles auth redirects, and loads the primary entity for the surface. Bootstrap data is immutable after mount — mutable state lives in the controller.
72
-
73
- Rules:
74
- - One bootstrap per surface; lives in `surfaces/[feature]/bootstrap.ts`
75
- - Auth failures/redirects belong here, not in components
76
- - Export `[Feature]BootstrapArgs` and `[Feature]Bootstrap` (inferred from return type)
77
-
78
- ---
79
-
80
- ## `[feature].tsx` — Surface Entry Component
81
-
82
- Receives `bootstrap` from the route. Creates the layout shell, triggers install once on mount via a `useRef` guard. The `useRef` guard runs install synchronously during the first render — StrictMode safe, no double-fire.
83
-
84
- **Dependency injection:** Libraries and framework hooks (e.g. router, toast) must be declared at the surface level and injected downwards into install → creates → controllers/presenters. Never call router hooks inside install or create — obtain them in the surface entry and pass them into install.
85
-
86
- Rules:
87
- - Bootstrap is passed in as a prop from the route — never fetched inside the surface
88
- - `createLayout()` inside `useMemo` runs once synchronously
89
- - `useRef` guard runs install synchronously on first render, never again
90
- - No business logic here — mounting point only
91
- - **Never instantiate controllers here** — controller creation belongs exclusively in `install.tsx`
92
- - External deps (router, toast, etc.) are obtained at surface level and injected into install
93
-
94
- ---
95
-
96
- ## `install.tsx` — Wiring Function
97
-
98
- Single place where all deps are assembled. **Instantiates the controller**, lazy-loads creates. This is the **explicit dependency manifest** for the surface.
99
-
100
- Do not use React Context to share the controller. Every component's access to the controller is explicit and traceable through the install chain.
101
-
102
- Rules:
103
- - **`controller` instantiated here** — exactly once, shared across all creates
104
- - Dynamic `import()` makes each section a separate bundle chunk
105
- - `install` is a plain function, not a component or hook
106
- - Child creates that don't need the controller should not receive it — access is explicit
107
-
108
- ---
109
-
110
- ## `layout.tsx` — Layout Controller and Shell
111
-
112
- Rules:
113
- - Use `observable.ref` for component slots — MobX only needs reference equality
114
- - Template structure (flex, grid, spacing) defined here; sections don't know about neighbours
115
-
116
- ---
117
-
118
- ## `[feature]_controller.ts` — Controller
119
-
120
- Rules:
121
- - No React imports — pure class
122
- - Use `computed get` for derived values — components re-render only when the value changes
123
- - Controllers manage coordination state (what is open, what is selected)
124
- - **UI state** (dialog open/close, sheet visibility) belongs in the controller, not in `useState` calls inside `create.tsx`
125
- - Server/async data belongs in the `create` layer, not the controller
126
-
127
- ---
128
-
129
- ## `[section]_presenter.ts` — Presenter (component-scoped)
130
-
131
- Use when a section has its own business logic (mutations, input state, submit handling) that doesn't need to coordinate with other sections. Calls Convex mutations directly via the exported `convex` singleton — no hooks required.
132
-
133
- Rules:
134
- - No React imports — pure class
135
- - Call mutations via `convex.mutation(api.xxx, args)` — convex singleton imported from the provider
136
- - Options injected via constructor — use `Opts` suffix, destructure and assign to private readonly fields
137
- - `create` keeps only hooks that cannot be moved (e.g. `useFormContext` which requires React context)
138
-
139
- ---
140
-
141
- ## `[section]/create.tsx` — Create Function
142
-
143
- Factory closing over `controller` and `bootstrap`, returning an `observer()` component.
144
-
145
- Rules:
146
- - Child `create` calls go at the top of the factory body, **outside** the returned observer — run once, not per render
147
- - Returned component is always wrapped in `observer()`
148
- - Hook calls (`useQuery`) go **inside** the returned observer
149
- - Pass `bootstrap` data as `initialData` to seed the query on first render — eliminates loading flash, the Convex subscription takes over reactivity after mount
150
- - No `useMutation` in creates — mutations belong in controllers (`convex.mutation()`) or presenters; pass action callbacks as props to display components
151
- - **No React state** (`useState`, `useReducer`) in create.tsx — UI state (open/close, selections) belongs in a controller or presenter. Only third-party hooks (`useQuery`, `useForm`) are acceptable inside the observer.
152
- - Inline JSX callbacks must be multi-line, never one-liners
153
-
154
- ---
155
-
156
- ## Display Components
157
-
158
- Pure presentational. Everything via props. No hooks, no business logic, no controller/bootstrap imports.
159
-
160
- **Exception — form display components:** Display components that own a form may call `useFormContext()` to access the form provided by the parent `create.tsx` via `<FormProvider>`. Never pass `UseFormReturn` as a prop — always use `FormProvider` + `useFormContext()`.
161
-
162
- **Thin component rule:** If the display component is thin (few props, minimal JSX, no complex layout), skip the separate display file. The `create.tsx` alone is sufficient — it directly renders the JSX inside the observer. This avoids unnecessary files for trivial components like a back-button header.
163
-
164
- Rules:
165
- - Callbacks are plain `() => void` props — the `create.tsx` sibling maps controller actions to these props
166
- - Always use domain type aliases (`Todo`, `TodoId`) — never raw `Doc<"todos">` or `Id<"todos">`
167
- - Never import `Ent<T>` — it is a backend-only type from `convex-ents` carrying write methods
168
- - Explicit `return (...)` always — no implicit arrow returns
169
-
170
- ---
171
-
172
- ## Schema Pattern
173
-
174
- Zod schemas live in `schema.ts` inside the section directory. Type infers are declared **above** the schema object and the type name matches the schema variable name.
175
-
176
- ```typescript
177
- export type NewTodoSchema = z.infer<typeof newTodoSchema>;
178
- export const newTodoSchema = z.object({
179
- title: z.string().min(1, "Title is required"),
180
- description: z.string().optional(),
181
- });
182
- ```
183
-
184
- Rules:
185
- - `z.infer<typeof schema>` type alias goes **above** the const — hoisting makes this safe
186
- - Type name matches schema name: `newTodoSchema` → `NewTodoSchema`
187
- - One schema per file — no duplicate definitions
188
-
189
- ---
190
-
191
- ## Form Management
192
-
193
- - Schema defined in a `schema.ts` file inside the form's section directory (e.g. `new_todo_sheet/schema.ts`)
194
- - `useForm` called inside the **create.tsx** factory (inside the observer)
195
- - `<FormProvider>` wraps the display component so children use `useFormContext()` — **never pass `UseFormReturn` as a prop**
196
- - Display component calls `useFormContext()` to access the form (this is the one hook exception for display components)
197
- - Form open/close state belongs in a **controller** (not `useState` inside create.tsx)
198
- - `controller.closeDialog()` / `controller.closeNewTodoSheet()` called in `onSuccess` — UI transitions in one place
199
-
200
- Sub-fields with `useFormContext`:
201
- ```typescript
202
- export const createTitleField = () => {
203
- return () => {
204
- const { register, formState: { errors } } = useFormContext<NewTodoSchema>();
205
- return <input {...register("title")} />;
206
- };
207
- };
208
- ```
209
-
210
- ---
211
-
212
- ## DI Tree Integrity
213
-
214
- Child surfaces (dialogs, sheets, drawers) must live **inside the parent section directory** that creates them. This keeps the dependency injection tree traceable — the parent's `create.tsx` lazy-imports the child surface, and the child's deps are a subset of the parent's.
215
-
216
- ```
217
- main/
218
- create.tsx # imports new_todo_sheet/create
219
- main.tsx # display
220
- new_todo_sheet/ # child surface — created from main/create.tsx
221
- create.tsx
222
- new_todo_sheet.tsx
223
- schema.ts
224
- ```
225
-
226
- Do not place child surfaces as siblings of the parent section. If `main/create.tsx` creates the sheet, the sheet directory belongs under `main/`.
227
-
228
- ---
229
-
230
- ## Component Rules
231
-
232
- ### Arrow Functions
233
-
234
- Always export components as **const arrow functions**:
235
-
236
- ```typescript
237
- // Good
238
- export const ComponentName = ({ prop1, prop2 }: ComponentNameProps) => {
239
- return <div />;
240
- };
241
-
242
- // Bad
243
- export function ComponentName({ prop1, prop2 }: ComponentNameProps) {
244
- return <div />;
245
- }
246
- ```
247
-
248
- ### Explicit Returns
249
-
250
- Always use explicit `return` statements in component functions. No implicit arrow returns for JSX.
251
-
252
- ```typescript
253
- // Good
254
- export const Header = ({ title }: HeaderProps) => {
255
- return <h1>{title}</h1>;
256
- };
257
-
258
- // Bad — implicit arrow return
259
- export const Header = ({ title }: HeaderProps) => <h1>{title}</h1>;
260
- ```
261
-
262
- ### Named Prop Types
263
-
264
- Every component must have a named prop type declared immediately above it. Never inline prop types on the component signature.
265
-
266
- ```typescript
267
- // Good
268
- type HeaderProps = {
269
- title: string;
270
- onBack: () => void;
271
- };
272
-
273
- export const Header = ({ title, onBack }: HeaderProps) => {
274
- return <h1>{title}</h1>;
275
- };
276
-
277
- // Bad — inline type
278
- export const Header = ({ title }: { title: string }) => {
279
- return <h1>{title}</h1>;
280
- };
281
- ```
282
-
283
- ### No One-Liner Inline Handlers
284
-
285
- Multi-line format is required for all inline JSX callbacks, even simple ones.
286
-
287
- ```typescript
288
- // Good
289
- onToggle={(isCompleted) =>
290
- controller.updateTodo({
291
- todoId: bootstrap.todoId,
292
- isCompleted,
293
- })
294
- }
295
-
296
- // Also good — block body when there is more than one expression
297
- onToggle={(isCompleted) => {
298
- controller.updateTodo({
299
- todoId: bootstrap.todoId,
300
- isCompleted,
301
- });
302
- }}
303
-
304
- // Bad — single line
305
- onToggle={(isCompleted) => controller.updateTodo({ todoId: bootstrap.todoId, isCompleted })}
306
- ```
307
-
308
- ### Create Factory Props vs Display Props
309
-
310
- The `create` factory has two layers for receiving data: **factory opts** (known at creation time) and **observer props** (provided at render time). This applies to all prop types — component slots, IDs, data items, callbacks — not just components.
311
-
312
- **Factory opts (`Opts`)** — values known when the factory is called. Closed over in the observer, not re-provided per render:
313
-
314
- ```typescript
315
- // header/create.tsx — component slot + controller known at creation time
316
- type CreateHeaderOpts = {
317
- NewTodoSheet: ComponentType;
318
- };
319
-
320
- export const createHeader = ({ NewTodoSheet }: CreateHeaderOpts) => {
321
- return observer(() => {
322
- return (
323
- <div>
324
- <h1>Todos</h1>
325
- <NewTodoSheet />
326
- </div>
327
- );
328
- });
329
- };
330
-
331
- // parent main/create.tsx
332
- const NewTodoSheet = createNewTodoSheet({ controller });
333
- const Header = createHeader({ NewTodoSheet });
334
-
335
- return observer(() => {
336
- return (
337
- <main>
338
- <Header />
339
- <Main /* ... */ />
340
- </main>
341
- );
342
- });
343
- ```
344
-
345
- **Observer props (`Props`)** — values that vary per instance or are provided at render time by a parent. Declared as a named type on the observer returned by the factory:
346
-
347
- ```typescript
348
- // todo_card/create.tsx — todoId varies per list item
349
- type CreateTodoCardOpts = {
350
- controller: AllTodosController;
351
- };
352
-
353
- type TodoCardProps = {
354
- todoId: TodoId;
355
- };
356
-
357
- export const createTodoCard = ({ controller }: CreateTodoCardOpts) => {
358
- return observer(({ todoId }: TodoCardProps) => {
359
- const { data: todo } = useQuery(/* ... */);
360
- return (
361
- <TodoCard
362
- todo={todo}
363
- onDelete={() => {
364
- void controller.deleteTodo({ todoId });
365
- }}
366
- />
367
- );
368
- });
369
- };
370
-
371
- // parent list/create.tsx — passes per-item data at render time
372
- const TodoCard = createTodoCard({ controller });
373
-
374
- return observer(() => {
375
- return (
376
- <ul>
377
- {todos.map((todo) => (
378
- <TodoCard key={todo._id} todoId={todo._id} />
379
- ))}
380
- </ul>
381
- );
382
- });
383
- ```
384
-
385
- Component slots follow the same split — factory opt when known at creation time, observer prop when provided at render time:
386
-
387
- ```typescript
388
- // observer prop for a component slot
389
- type HeaderProps = {
390
- SomeComponent: ComponentType;
391
- };
392
-
393
- export const createHeader = ({ navigate }: CreateHeaderOpts) => {
394
- return observer(({ SomeComponent }: HeaderProps) => {
395
- return (
396
- <div>
397
- <SomeComponent />
398
- <Header
399
- onBack={() => {
400
- navigate("/todos");
401
- }}
402
- />
403
- </div>
404
- );
405
- });
406
- };
407
- ```
408
-
409
- **Bad** — component slots or dynamic data on a pure display component:
410
-
411
- ```typescript
412
- // Bad — display component receiving pre-rendered ReactNode
413
- type HeaderProps = {
414
- toolSlot: React.ReactNode;
415
- };
416
-
417
- export const Header = ({ toolSlot }: HeaderProps) => {
418
- return (
419
- <div>
420
- <h1>Todos</h1>
421
- {toolSlot}
422
- </div>
423
- );
424
- };
425
-
426
- <Header toolSlot={<NewTodoSheet />} />
427
- ```
428
-
429
- Rules:
430
- - **Factory opts** for values known at creation time (controller, bootstrap, component slots created by the parent)
431
- - **Observer props** for values that vary per instance or are provided at render time (IDs, per-item data, component slots passed down dynamically)
432
- - Component slots are always `ComponentType`, never `ReactNode` — named after the component they receive
433
- - Pure display components receive only plain data props and `() => void` callbacks — never `ComponentType` slots, IDs that drive data fetching, or any logic that belongs in `create.tsx`
434
-
435
- ### Domain Type Imports
436
-
437
- Always import the aliased domain type from the entity's `types.ts`. Never use raw Convex generics in frontend code.
438
-
439
- ```typescript
440
- // Good
441
- import type { Todo, TodoId } from "@/api/todos/types";
442
-
443
- // Bad — raw Convex generics
444
- import type { Doc, Id } from "@/api/_generated/dataModel";
445
- const todoId: Id<"todos"> = ...;
446
- const todo: Doc<"todos"> = ...;
447
- ```
448
-
449
- Each entity's `types.ts` exports `EntityId = Id<"entity">` and `Entity = Doc<"entity">`. Import from there.
450
-
451
- `Ent<T>` is a backend-only type from `convex-ents`. Never import it in frontend files — use the `Entity` alias (e.g. `Todo`) from the domain `types.ts` instead.
452
-
453
- ### Path Alias Imports
454
-
455
- Always use path alias imports (`@/...`). **No relative imports** — not even within the same feature folder. Every `import` statement must use the `@/` alias.
456
-
457
- ```typescript
458
- // Good — always alias
459
- import { Button } from "@/ui/base/button";
460
- import type { TodoId } from "@/api/todos/types";
461
- import { createHeader } from "@/web/surfaces/todos/all_todos/header/create";
462
- import { createNewTodoSheet } from "@/web/surfaces/todos/all_todos/main/new_todo_sheet/create";
463
-
464
- // Bad — relative paths (even within same feature)
465
- import { createLayout } from "./layout";
466
- import type { AllTodosBootstrap } from "../bootstrap";
467
- import { Main } from "./main";
468
- ```
469
-
470
- ---
471
-
472
- ## Install Timing Consistency
473
-
474
- **Layout-mounted surfaces** (sidebar, persistent chrome): Use `useEffect` to call install once. The layout persists across navigations, so `useEffect` with a stable `layout` dependency is appropriate.
475
-
476
- **Route-mounted surfaces** (pages): Use the synchronous `useRef` install guard. The surface mounts fresh per navigation, so synchronous install during first render avoids flicker.
477
-
478
- Pick the correct pattern based on mount context and apply it consistently across all surfaces of the same type.
479
-
480
- ---
481
-
482
- ## Quick Reference
483
-
484
- | File | Can use hooks? | Imports controller? |
485
- |---|---|---|
486
- | Route entry | Framework-dependent | No |
487
- | `bootstrap.ts` | No | No |
488
- | `[feature].tsx` | `useRef`, `useMemo`, router hooks | No |
489
- | `install.tsx` | No | Instantiates it |
490
- | `layout.tsx` | No | No |
491
- | `[name]_controller.ts` | No (pure class) | — |
492
- | `[section]_presenter.ts` | No (pure class) | — |
493
- | `[section]/create.tsx` | Yes (inside observer) | Yes (via closure) |
494
- | `[section]/[name].tsx` | `useFormContext` only | No |
495
- | `ui/[name].tsx` | No | No |