create-aron-app 0.1.6 → 0.1.7
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/dist/index.js +50 -115
- package/package.json +5 -2
- package/templates/{_base/.cursor → .cursor}/rules/api_architecture.mdc +27 -33
- package/templates/{_base/.cursor → .cursor}/rules/coding_standards.mdc +1 -1
- package/templates/{_base/.cursor → .cursor}/rules/convex_rules.mdc +78 -422
- package/templates/.cursor/rules/frontend_architecture_core.mdc +495 -0
- package/templates/.cursor/rules/frontend_architecture_nextjs.mdc +458 -0
- package/templates/.cursor/rules/frontend_architecture_reactrouter.mdc +473 -0
- package/templates/apps/api/_generated/api.d.ts +57 -0
- package/templates/apps/api/_generated/api.js +23 -0
- package/templates/apps/api/_generated/dataModel.d.ts +60 -0
- package/templates/apps/api/_generated/server.d.ts +143 -0
- package/templates/apps/api/_generated/server.js +93 -0
- package/templates/apps/api/http.ts +16 -0
- package/templates/apps/nextjs/.env.example +10 -0
- package/templates/{nextjs → apps/nextjs}/project.json +5 -5
- package/templates/{nextjs → apps/nextjs}/src/app/(auth)/not-allowed/page.tsx +1 -0
- package/templates/apps/nextjs/src/app/(dashboard)/layout.tsx +22 -0
- package/templates/apps/nextjs/src/app/(dashboard)/page.tsx +12 -0
- package/templates/{nextjs → apps/nextjs}/src/app/(dashboard)/todos/[id]/page.tsx +5 -2
- package/templates/apps/nextjs/src/app/(dashboard)/todos/page.tsx +19 -0
- package/templates/apps/nextjs/src/middleware.ts +18 -0
- package/templates/apps/nextjs/src/surfaces/home/bootstrap.ts +9 -0
- package/templates/apps/nextjs/src/surfaces/home/home.tsx +27 -0
- package/templates/apps/nextjs/src/surfaces/home/install.tsx +17 -0
- package/templates/apps/nextjs/src/surfaces/home/layout.tsx +44 -0
- package/templates/apps/nextjs/src/surfaces/home/main/create.tsx +34 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/install.tsx +23 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/layout.tsx +118 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/create.tsx +19 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_config.ts +22 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_main.tsx +25 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/create.tsx +21 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +33 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/sidebar.tsx +23 -0
- package/templates/{nextjs/src/ui/sidebar/nav_link.tsx → apps/nextjs/src/surfaces/sidebar/ui/sidebar_nav_link.tsx} +13 -10
- package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/create.tsx +28 -0
- package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/user_menu.tsx +42 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos.tsx +29 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos_controller.ts +61 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/bootstrap.ts +21 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/header/create.tsx +23 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/install.tsx +23 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/layout.tsx +44 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/create.tsx +49 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/main.tsx +70 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +56 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +99 -0
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +11 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/bootstrap.ts +32 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/header/create.tsx +26 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/header/header.tsx +22 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/install.tsx +27 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/layout.tsx +55 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/create.tsx +38 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/main.tsx +49 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo.tsx +29 -0
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo_controller.ts +13 -0
- package/templates/apps/nextjs/src/utils/auth.ts +18 -0
- package/templates/apps/react-router/.env.example +10 -0
- package/templates/apps/react-router/.react-router/types/+future.ts +9 -0
- package/templates/apps/react-router/.react-router/types/+routes.ts +71 -0
- package/templates/apps/react-router/.react-router/types/+server-build.d.ts +18 -0
- package/templates/apps/react-router/.react-router/types/src/+types/root.ts +59 -0
- package/templates/apps/react-router/.react-router/types/src/routes/(auth)/+types/layout.ts +62 -0
- package/templates/apps/react-router/.react-router/types/src/routes/(auth)/sign-in/+types/index.ts +65 -0
- package/templates/apps/react-router/.react-router/types/src/routes/(dashboard)/+types/index.ts +65 -0
- package/templates/apps/react-router/.react-router/types/src/routes/(dashboard)/+types/layout.ts +62 -0
- package/templates/apps/react-router/.react-router/types/src/routes/(dashboard)/todos/+types/[id].ts +65 -0
- package/templates/apps/react-router/.react-router/types/src/routes/(dashboard)/todos/+types/index.ts +65 -0
- package/templates/{react-router → apps/react-router}/project.json +4 -4
- package/templates/apps/react-router/src/app.css +3 -0
- package/templates/{react-router → apps/react-router}/src/components/error_boundary.tsx +1 -1
- package/templates/{react-router → apps/react-router}/src/providers/api_auth_provider.tsx +2 -0
- package/templates/{react-router/src/routes/auth → apps/react-router/src/routes/(auth)}/layout.tsx +1 -1
- package/templates/apps/react-router/src/routes/(dashboard)/index.tsx +19 -0
- package/templates/apps/react-router/src/routes/(dashboard)/layout.tsx +37 -0
- package/templates/apps/react-router/src/routes/(dashboard)/todos/[id].tsx +19 -0
- package/templates/apps/react-router/src/routes/(dashboard)/todos/index.tsx +19 -0
- package/templates/apps/react-router/src/routes.ts +12 -0
- package/templates/apps/react-router/src/surfaces/home/bootstrap.ts +9 -0
- package/templates/apps/react-router/src/surfaces/home/home.tsx +25 -0
- package/templates/apps/react-router/src/surfaces/home/install.tsx +17 -0
- package/templates/apps/react-router/src/surfaces/home/layout.tsx +35 -0
- package/templates/apps/react-router/src/surfaces/home/main/create.tsx +32 -0
- package/templates/apps/react-router/src/surfaces/sidebar/install.tsx +23 -0
- package/templates/apps/react-router/src/surfaces/sidebar/layout.tsx +110 -0
- package/templates/apps/react-router/src/surfaces/sidebar/nav_main/create.tsx +31 -0
- package/templates/apps/react-router/src/surfaces/sidebar/nav_main/nav_main.tsx +42 -0
- package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/create.tsx +21 -0
- package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +31 -0
- package/templates/apps/react-router/src/surfaces/sidebar/sidebar.tsx +18 -0
- package/templates/apps/react-router/src/surfaces/sidebar/user_menu/create.tsx +26 -0
- package/templates/{react-router/src/layouts/sidebar/sidebar_aside → apps/react-router/src/surfaces/sidebar/user_menu}/user_menu.tsx +13 -9
- package/templates/apps/react-router/src/surfaces/todos/all_todos/all_todos.tsx +25 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/all_todos_controller.ts +47 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/bootstrap.ts +18 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/header/create.tsx +21 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/install.tsx +20 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/layout.tsx +35 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/main/create.tsx +47 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/main/main.tsx +68 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +54 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +97 -0
- package/templates/apps/react-router/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +11 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/bootstrap.ts +36 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/header/create.tsx +32 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/header/header.tsx +25 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/install.tsx +27 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/layout.tsx +45 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/main/create.tsx +35 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/main/main.tsx +47 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/single_todo.tsx +27 -0
- package/templates/apps/react-router/src/surfaces/todos/single_todo/single_todo_controller.ts +16 -0
- package/templates/{react-router → apps/react-router}/vite.config.ts +27 -3
- package/templates/{_base/biome.json → biome.json} +7 -0
- package/templates/bun.lock +3187 -0
- package/templates/{_base/emails → emails}/project.json +1 -1
- package/templates/{_base/nx.json → nx.json} +11 -0
- package/templates/package.json +92 -0
- package/templates/{_base/tsconfig.base.json → tsconfig.base.json} +4 -1
- package/templates/_base/.cursor/rules/frontend_rules.mdc +0 -268
- package/templates/_base/.env.convex.example +0 -3
- package/templates/_base/_gitignore +0 -58
- package/templates/_base/package.json +0 -73
- package/templates/nextjs/.env.example +0 -8
- package/templates/nextjs/src/app/(dashboard)/layout.tsx +0 -27
- package/templates/nextjs/src/app/(dashboard)/page.tsx +0 -5
- package/templates/nextjs/src/app/(dashboard)/todos/page.tsx +0 -16
- package/templates/nextjs/src/middleware.ts +0 -18
- package/templates/nextjs/src/surfaces/home_surface.tsx +0 -22
- package/templates/nextjs/src/surfaces/todos/all_todos_surface.tsx +0 -97
- package/templates/nextjs/src/surfaces/todos/create_todo_sheet.tsx +0 -107
- package/templates/nextjs/src/surfaces/todos/single_todo_surface.tsx +0 -90
- package/templates/nextjs/src/ui/sidebar/sidebar.tsx +0 -125
- package/templates/react-router/.env.example +0 -8
- package/templates/react-router/src/app.css +0 -3
- package/templates/react-router/src/layouts/sidebar/sidebar_aside/sidebar_aside.tsx +0 -76
- package/templates/react-router/src/layouts/sidebar/sidebar_layout.tsx +0 -22
- package/templates/react-router/src/routes/index.tsx +0 -9
- package/templates/react-router/src/routes/layout.tsx +0 -26
- package/templates/react-router/src/routes/todos/[id].tsx +0 -22
- package/templates/react-router/src/routes/todos/index.tsx +0 -13
- package/templates/react-router/src/routes.ts +0 -12
- package/templates/react-router/src/surfaces/home_surface.tsx +0 -20
- package/templates/react-router/src/surfaces/todos/all_todos_surface.tsx +0 -87
- package/templates/react-router/src/surfaces/todos/create_todo_sheet.tsx +0 -102
- package/templates/react-router/src/surfaces/todos/single_todo_surface.tsx +0 -81
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/api-specs-context.sh +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/execute-request.sh +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-endpoint-detail.sh +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-tag-endpoints.sh +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-tags.js +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-in.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-up.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-in.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-up.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/show-component.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/api-routes.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/caching-auth.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/middleware-strategies.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/server-actions.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/server-vs-client.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-webhooks/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/agents/openai.yml +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/assets/shadcn-small.png +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/assets/shadcn.png +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/cli.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/customization.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/evals/evals.json +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/mcp.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/base-vs-radix.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/composition.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/forms.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/icons.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/styling.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/commands/builder.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/commands/pr.md +0 -0
- /package/templates/{_base/.github → .github}/workflows/ci.yml +0 -0
- /package/templates/{_base/.nvmrc → .nvmrc} +0 -0
- /package/templates/{_base/.vscode → .vscode}/settings.json +0 -0
- /package/templates/{_base/apps → apps}/api/auth.config.ts +0 -0
- /package/templates/{_base/apps → apps}/api/functions.ts +0 -0
- /package/templates/{_base/apps → apps}/api/project.json +0 -0
- /package/templates/{_base/apps → apps}/api/schema.ts +0 -0
- /package/templates/{_base/apps → apps}/api/todos/crud.ts +0 -0
- /package/templates/{_base/apps → apps}/api/todos/schema.ts +0 -0
- /package/templates/{_base/apps → apps}/api/todos/types.ts +0 -0
- /package/templates/{_base/apps → apps}/api/tsconfig.json +0 -0
- /package/templates/{_base/apps → apps}/api/types.ts +0 -0
- /package/templates/{nextjs → apps/nextjs}/index.d.ts +0 -0
- /package/templates/{nextjs → apps/nextjs}/next-env.d.ts +0 -0
- /package/templates/{nextjs → apps/nextjs}/next.config.js +0 -0
- /package/templates/{nextjs → apps/nextjs}/postcss.config.js +0 -0
- /package/templates/{nextjs → apps/nextjs}/src/app/(auth)/layout.tsx +0 -0
- /package/templates/{nextjs → apps/nextjs}/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +0 -0
- /package/templates/{nextjs → apps/nextjs}/src/app/app.css +0 -0
- /package/templates/{nextjs → apps/nextjs}/src/app/layout.tsx +0 -0
- /package/templates/{nextjs → apps/nextjs}/src/providers/convex_provider.tsx +0 -0
- /package/templates/{nextjs/src → apps/nextjs/src/utils}/convex.ts +0 -0
- /package/templates/{nextjs → apps/nextjs}/src/utils/font.ts +0 -0
- /package/templates/{nextjs → apps/nextjs}/tsconfig.json +0 -0
- /package/templates/{react-router → apps/react-router}/postcss.config.js +0 -0
- /package/templates/{react-router → apps/react-router}/public/favicon.ico +0 -0
- /package/templates/{react-router → apps/react-router}/react-router.config.ts +0 -0
- /package/templates/{react-router → apps/react-router}/src/root.tsx +0 -0
- /package/templates/{react-router/src/routes/auth/sign-in.tsx → apps/react-router/src/routes/(auth)/sign-in/index.tsx} +0 -0
- /package/templates/{react-router → apps/react-router}/tsconfig.json +0 -0
- /package/templates/{_base/convex.json → convex.json} +0 -0
- /package/templates/{_base/emails → emails}/tsconfig.json +0 -0
- /package/templates/{_base/emails → emails}/welcome_email.tsx +0 -0
- /package/templates/{_base/scripts → scripts}/sync_convex_env.ts +0 -0
- /package/templates/{_base/shared → shared}/assets/image.d.ts +0 -0
- /package/templates/{_base/shared → shared}/assets/src/styles/global.css +0 -0
- /package/templates/{_base/shared → shared}/assets/tsconfig.json +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/alert_dialog.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/badge.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/basic_data_table.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/button.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/button_group.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/card.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/checkbox.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/command.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/dialog.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/dropdown_menu.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/form.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/input.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/label.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/popover.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/radio_group.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/resizable.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/scroll_area.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/select.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/separator.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/sheet.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/side_bar.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/skeleton.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/spinner.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/switch.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/table.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/text_area.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/tooltip.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/utils.ts +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_keyboard_press.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_keyboard_release.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_mobile.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_mouse_click.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_mouse_location.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_outside_click.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_query_params.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/tsconfig.json +0 -0
- /package/templates/{_base/shared → shared}/utils/src/convex.ts +0 -0
- /package/templates/{_base/shared → shared}/utils/src/time.ts +0 -0
- /package/templates/{_base/shared → shared}/utils/tsconfig.json +0 -0
- /package/templates/{_base/skills-lock.json → skills-lock.json} +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Frontend surface architecture — Next.js App Router + Convex variant. Covers MobX setup with makeObservable, server-side bootstrap with fetchQuery, and "use client" boundaries. Extends frontend_architecture_core.
|
|
3
|
+
globs: apps/nextjs/src/**
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Frontend Surface Architecture — Next.js + Convex
|
|
8
|
+
|
|
9
|
+
This document covers the Next.js-specific implementation of the surface architecture defined in `frontend_architecture_core`. Read that document first for the layer model, MobX philosophy, and component rules.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
### MobX
|
|
16
|
+
|
|
17
|
+
Next.js uses SWC for compilation, not Babel. TC39 Stage 3 decorator support in SWC requires pinned WASM plugins that break on Next.js version upgrades. Use `makeObservable` instead — identical semantics, zero build tooling risk, works with SWC out of the box.
|
|
18
|
+
|
|
19
|
+
Do **not** set `experimentalDecorators: true`. Do **not** add a `babel.config.js` — this disables SWC entirely.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { makeObservable, observable, action, computed } from "mobx";
|
|
23
|
+
|
|
24
|
+
export class ExampleController {
|
|
25
|
+
value = "";
|
|
26
|
+
isPending = false;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
makeObservable(this, {
|
|
30
|
+
value: observable,
|
|
31
|
+
isPending: observable,
|
|
32
|
+
isReady: computed,
|
|
33
|
+
setValue: action,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get isReady() {
|
|
38
|
+
return this.value.trim().length > 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setValue(v: string) {
|
|
42
|
+
this.value = v;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Rules:
|
|
48
|
+
- Use `makeObservable` with an explicit annotation map in the constructor
|
|
49
|
+
- Use `observable.ref` for fields that hold object/component references (only reference equality needed)
|
|
50
|
+
- Use `runInAction(() => { ... })` when mutating observables inside an async callback or Promise `.then`
|
|
51
|
+
|
|
52
|
+
### Convex Client
|
|
53
|
+
|
|
54
|
+
The `ConvexReactClient` singleton is exported from the provider so presenters can call mutations directly outside of React:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// src/providers/convex_provider.tsx
|
|
58
|
+
export const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Rules:
|
|
62
|
+
- Presenters import `convex` from `@/web/providers/convex_provider` to call `convex.mutation()` directly
|
|
63
|
+
|
|
64
|
+
### Auth Token Helper
|
|
65
|
+
|
|
66
|
+
Required for passing authenticated requests to `fetchQuery` during server rendering:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// src/utils/auth.ts
|
|
70
|
+
import { auth } from "@clerk/nextjs/server";
|
|
71
|
+
|
|
72
|
+
export const getAuthToken = async () => {
|
|
73
|
+
return (await (await auth()).getToken()) ?? undefined;
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## App routing: `(auth)` vs `(dashboard)`
|
|
80
|
+
|
|
81
|
+
Next.js [route groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups) use parentheses: `app/(auth)/` and `app/(dashboard)/`. **Parent folder names do not appear in the URL.**
|
|
82
|
+
|
|
83
|
+
| Group | Role |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `(auth)` | Sign-in and other unauthenticated pages. **No** app sidebar — minimal or centered layout (see `(auth)/layout.tsx`). |
|
|
86
|
+
| `(dashboard)` | Authenticated app **shell**: `SidebarProvider` + **Sidebar** surface + `SidebarInset` wrapping `{children}`. All main app routes nest here. |
|
|
87
|
+
|
|
88
|
+
Canonical URLs (unchanged by group folders):
|
|
89
|
+
|
|
90
|
+
- `/` — home
|
|
91
|
+
- `/todos` — all todos
|
|
92
|
+
- `/todos/[id]` — single todo
|
|
93
|
+
- `/sign-in/[[...sign-in]]` (or your Clerk catch-all) — auth
|
|
94
|
+
|
|
95
|
+
The `(dashboard)/layout.tsx` file should remain a **Server Component** when possible: compose providers and the client **Sidebar** entry only; avoid pulling server-only auth into client trees unnecessarily. The Sidebar surface module is a **`"use client"`** boundary (MobX + optional hooks).
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Shell surface vs page surfaces
|
|
100
|
+
|
|
101
|
+
- **Shell (layout-mounted):** `surfaces/sidebar/` — mounted **only** from `(dashboard)/layout.tsx`. Persists across navigations inside the dashboard. Uses **`SidebarLayout`** + **`installSidebar`** with lazy section `create*.tsx` files.
|
|
102
|
+
- **Page surfaces:** `surfaces/home/`, `surfaces/todos/all_todos/`, `surfaces/todos/single_todo/` — mounted from **`page.tsx`** under `(dashboard)/`. Each follows `frontend_architecture_core`: `bootstrap.ts` → `[feature].tsx` → `install.tsx` → layout slots → section `create.tsx` / display / controllers.
|
|
103
|
+
|
|
104
|
+
See `frontend_architecture_core` for folder naming: **`all_todos`**, **`single_todo`**, and exported components **`AllTodos`**, **`SingleTodo`** (or `AllTodoSurface` / `SingleTodoSurface` if you keep the `Surface` suffix consistently).
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Sidebar surface (layout chrome)
|
|
109
|
+
|
|
110
|
+
Required structure (slots are **product-dependent** — add/remove sections as needed):
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
surfaces/sidebar/
|
|
114
|
+
sidebar.tsx # client entry: useMemo(createSidebarLayout), useEffect(() => installSidebar({ layout }))
|
|
115
|
+
layout.tsx # SidebarLayout + createSidebarLayout() — MobX observable slots + shell JSX
|
|
116
|
+
install.tsx # installSidebar — dynamic import() each section's create
|
|
117
|
+
nav_main/ # example section
|
|
118
|
+
create.tsx
|
|
119
|
+
nav_main.tsx # dumb display
|
|
120
|
+
nav_secondary/
|
|
121
|
+
create.tsx
|
|
122
|
+
nav_secondary.tsx
|
|
123
|
+
user_menu/
|
|
124
|
+
create.tsx
|
|
125
|
+
user_menu.tsx
|
|
126
|
+
ui/ # optional: shared dumb pieces for this surface only
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Install timing:** Layout-mounted chrome uses **`useEffect`** to run `installSidebar` once (avoids some Strict Mode / hydration edge cases for persistent shell). **Route-mounted** feature surfaces use the **synchronous `useRef` install guard**.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Route → surface map
|
|
134
|
+
|
|
135
|
+
| URL | Mount |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| `/` | `Home` (`surfaces/home/`) |
|
|
138
|
+
| `/todos` | `AllTodos` (`surfaces/todos/all_todos/`) |
|
|
139
|
+
| `/todos/[id]` | `SingleTodo` (`surfaces/todos/single_todo/`) |
|
|
140
|
+
| `(dashboard)` layout | `Sidebar` (`surfaces/sidebar/`) |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Route + Bootstrap Pattern
|
|
145
|
+
|
|
146
|
+
Each Next.js page is a **Server Component** that calls the surface's bootstrap function, then renders the client surface with the bootstrap as a prop. The bootstrap runs `fetchQuery` to load the initial Convex data server-side, so the client surface renders with data immediately — no loading spinner on first paint.
|
|
147
|
+
|
|
148
|
+
### Page File (`app/(group)/[path]/[id]/page.tsx`)
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import type { Metadata } from "next";
|
|
152
|
+
import type { TodoId } from "@/api/todos/types";
|
|
153
|
+
import { bootstrapTodoDetail } from "@/web/surfaces/todo_detail/bootstrap";
|
|
154
|
+
import { TodoDetail } from "@/web/surfaces/todo_detail/todo_detail";
|
|
155
|
+
import { getAuthToken } from "@/web/utils/auth";
|
|
156
|
+
|
|
157
|
+
type TodoDetailPageProps = {
|
|
158
|
+
params: Promise<{ id: TodoId }>;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const metadata: Metadata = {
|
|
162
|
+
title: "Todo",
|
|
163
|
+
description: "Todo detail",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export default async function TodoDetailPage({ params }: TodoDetailPageProps) {
|
|
167
|
+
const { id } = await params;
|
|
168
|
+
const token = await getAuthToken();
|
|
169
|
+
const bootstrap = await bootstrapTodoDetail({ todoId: id, token });
|
|
170
|
+
return <TodoDetail bootstrap={bootstrap} />;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Rules:
|
|
175
|
+
- Page is an `async` Server Component — no `"use client"` directive
|
|
176
|
+
- `params` is a `Promise` in Next.js App Router — always `await` it
|
|
177
|
+
- `getAuthToken()` passes the Clerk JWT to `fetchQuery` for authenticated queries
|
|
178
|
+
- Page passes `bootstrap` directly to the surface — no intermediate data reshaping
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Layer Reference (Next.js-specific)
|
|
183
|
+
|
|
184
|
+
### `bootstrap.ts`
|
|
185
|
+
|
|
186
|
+
Async function called in the server page. Uses `fetchQuery` from `convex/nextjs` to load the surface's primary entity server-side. The result is passed as `initialData` to queries in the `create` layer.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { fetchQuery } from "convex/nextjs";
|
|
190
|
+
|
|
191
|
+
import { api } from "@/api/_generated/api";
|
|
192
|
+
import type { Todo, TodoId } from "@/api/todos/types";
|
|
193
|
+
|
|
194
|
+
export type TodoDetailBootstrapArgs = {
|
|
195
|
+
todoId: TodoId;
|
|
196
|
+
token?: string;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export type TodoDetailBootstrap = Awaited<ReturnType<typeof bootstrapTodoDetail>>;
|
|
200
|
+
|
|
201
|
+
export const bootstrapTodoDetail = async ({ todoId, token }: TodoDetailBootstrapArgs) => {
|
|
202
|
+
const todo = await fetchQuery(
|
|
203
|
+
api.todos.crud.getTodo,
|
|
204
|
+
{ todoId },
|
|
205
|
+
{ token },
|
|
206
|
+
);
|
|
207
|
+
return { todoId, todo };
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Rules:
|
|
212
|
+
- Use `fetchQuery` from `convex/nextjs` — returns the actual data value, passes `token` for authenticated queries
|
|
213
|
+
- Auth failures/redirects belong here, not in components
|
|
214
|
+
|
|
215
|
+
### `[feature].tsx` — Surface Entry Component
|
|
216
|
+
|
|
217
|
+
`"use client"` boundary. Receives `bootstrap` from the server page. **Does not instantiate controllers** — that happens in `install.tsx`.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
"use client";
|
|
221
|
+
|
|
222
|
+
import { useRouter } from "next/navigation";
|
|
223
|
+
import { useMemo, useRef } from "react";
|
|
224
|
+
|
|
225
|
+
import type { TodoDetailBootstrap } from "@/web/surfaces/todo_detail/bootstrap";
|
|
226
|
+
import { installTodoDetail } from "@/web/surfaces/todo_detail/install";
|
|
227
|
+
import { createLayout } from "@/web/surfaces/todo_detail/layout";
|
|
228
|
+
|
|
229
|
+
export type TodoDetailProps = {
|
|
230
|
+
bootstrap: TodoDetailBootstrap;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const TodoDetail = ({ bootstrap }: TodoDetailProps) => {
|
|
234
|
+
const router = useRouter();
|
|
235
|
+
const { layout, Layout } = useMemo(() => createLayout(), []);
|
|
236
|
+
const installed = useRef(false);
|
|
237
|
+
|
|
238
|
+
if (!installed.current) {
|
|
239
|
+
installed.current = true;
|
|
240
|
+
installTodoDetail({ layout, bootstrap, router });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return <Layout />;
|
|
244
|
+
};
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Rules:
|
|
248
|
+
- Always add `"use client"` — this is the boundary between server and client
|
|
249
|
+
- Use `useRouter` from `next/navigation`
|
|
250
|
+
- **Never instantiate controllers here** — pass deps to install, which creates the controller
|
|
251
|
+
- All imports use `@/` aliases — no relative imports
|
|
252
|
+
|
|
253
|
+
### `install.tsx`
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
|
257
|
+
|
|
258
|
+
import type { TodoDetailBootstrap } from "@/web/surfaces/todo_detail/bootstrap";
|
|
259
|
+
import type { TodoDetailLayoutController } from "@/web/surfaces/todo_detail/layout";
|
|
260
|
+
import { TodoDetailController } from "@/web/surfaces/todo_detail/todo_detail_controller";
|
|
261
|
+
|
|
262
|
+
export type InstallTodoDetailOpts = {
|
|
263
|
+
layout: TodoDetailLayoutController;
|
|
264
|
+
bootstrap: TodoDetailBootstrap;
|
|
265
|
+
router: AppRouterInstance;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export const installTodoDetail = ({ layout, bootstrap, router }: InstallTodoDetailOpts) => {
|
|
269
|
+
const controller = new TodoDetailController();
|
|
270
|
+
|
|
271
|
+
import("@/web/surfaces/todo_detail/header/create").then(({ createHeader }) => {
|
|
272
|
+
layout.setHeader(createHeader({ controller, bootstrap }));
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
import("@/web/surfaces/todo_detail/main/create").then(({ createMain }) => {
|
|
276
|
+
layout.setMain(createMain({ controller, bootstrap, router }));
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Rules:
|
|
282
|
+
- Use `AppRouterInstance` from `next/dist/shared/lib/app-router-context.shared-runtime` for the router type
|
|
283
|
+
- **Controller instantiated here** — exactly once
|
|
284
|
+
- Dynamic `import()` paths use `@/` aliases
|
|
285
|
+
|
|
286
|
+
### `layout.tsx`
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
"use client";
|
|
290
|
+
|
|
291
|
+
import { makeObservable, observable, action, runInAction } from "mobx";
|
|
292
|
+
import { observer } from "mobx-react-lite";
|
|
293
|
+
import type { ComponentType } from "react";
|
|
294
|
+
|
|
295
|
+
import { Skeleton } from "@/ui/base/skeleton";
|
|
296
|
+
|
|
297
|
+
export class LayoutController {
|
|
298
|
+
Header: ComponentType | undefined = undefined;
|
|
299
|
+
Main: ComponentType | undefined = undefined;
|
|
300
|
+
|
|
301
|
+
constructor() {
|
|
302
|
+
makeObservable(this, {
|
|
303
|
+
Header: observable.ref,
|
|
304
|
+
Main: observable.ref,
|
|
305
|
+
setHeader: action,
|
|
306
|
+
setMain: action,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
setHeader(Header: ComponentType) {
|
|
311
|
+
runInAction(() => {
|
|
312
|
+
this.Header = Header;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
setMain(Main: ComponentType) {
|
|
317
|
+
runInAction(() => {
|
|
318
|
+
this.Main = Main;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const createLayout = () => {
|
|
324
|
+
const layout = new LayoutController();
|
|
325
|
+
return {
|
|
326
|
+
layout,
|
|
327
|
+
Layout: observer(() => {
|
|
328
|
+
const Header = layout.Header;
|
|
329
|
+
const Main = layout.Main;
|
|
330
|
+
return (
|
|
331
|
+
<div className="flex flex-col gap-8 w-full">
|
|
332
|
+
{Header ? <Header /> : <Skeleton className="h-12" />}
|
|
333
|
+
{Main ? <Main /> : <Skeleton className="h-64" />}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}),
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Rules:
|
|
342
|
+
- `runInAction` required when mutating observables inside a Promise `.then`
|
|
343
|
+
|
|
344
|
+
### `[section]/create.tsx`
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
"use client";
|
|
348
|
+
|
|
349
|
+
import { convexQuery } from "@convex-dev/react-query";
|
|
350
|
+
import { useQuery } from "@tanstack/react-query";
|
|
351
|
+
import { observer } from "mobx-react-lite";
|
|
352
|
+
|
|
353
|
+
import { api } from "@/api/_generated/api";
|
|
354
|
+
import type { TodoDetailBootstrap } from "@/web/surfaces/todo_detail/bootstrap";
|
|
355
|
+
import type { TodoDetailController } from "@/web/surfaces/todo_detail/todo_detail_controller";
|
|
356
|
+
import { Main } from "@/web/surfaces/todo_detail/main/main";
|
|
357
|
+
|
|
358
|
+
export type CreateMainOpts = {
|
|
359
|
+
controller: TodoDetailController;
|
|
360
|
+
bootstrap: TodoDetailBootstrap;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export const createMain = ({ controller, bootstrap }: CreateMainOpts) => {
|
|
364
|
+
return observer(() => {
|
|
365
|
+
const { data: todo } = useQuery({
|
|
366
|
+
initialData: bootstrap.todo,
|
|
367
|
+
...convexQuery(api.todos.crud.getTodo, { todoId: bootstrap.todoId }),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<Main
|
|
372
|
+
todo={todo}
|
|
373
|
+
onToggle={(isCompleted) =>
|
|
374
|
+
controller.updateTodo({
|
|
375
|
+
todoId: bootstrap.todoId,
|
|
376
|
+
isCompleted,
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
onEdit={() => controller.openEdit(bootstrap.todoId)}
|
|
380
|
+
/>
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Rules:
|
|
387
|
+
- Add `"use client"` — create files are client components
|
|
388
|
+
- Spread `convexQuery` with `initialData: bootstrap.[entity]` — eliminates loading flash, query stays reactive after hydration
|
|
389
|
+
- All imports use `@/` aliases — no relative imports
|
|
390
|
+
|
|
391
|
+
### Display Components
|
|
392
|
+
|
|
393
|
+
Rules:
|
|
394
|
+
- No `"use client"` directive needed — display components have no hooks (exception: `useFormContext` for form displays, which does need `"use client"`)
|
|
395
|
+
|
|
396
|
+
### `[section]_presenter.ts`
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
import { makeObservable, observable, action, computed, runInAction } from "mobx";
|
|
400
|
+
import { convex } from "@/web/providers/convex_provider";
|
|
401
|
+
import { api } from "@/api/_generated/api";
|
|
402
|
+
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
|
403
|
+
|
|
404
|
+
export type NewTodoPresenterOpts = {
|
|
405
|
+
router: AppRouterInstance;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
export class NewTodoPresenter {
|
|
409
|
+
private readonly router: AppRouterInstance;
|
|
410
|
+
|
|
411
|
+
input = "";
|
|
412
|
+
isPending = false;
|
|
413
|
+
|
|
414
|
+
constructor({ router }: NewTodoPresenterOpts) {
|
|
415
|
+
this.router = router;
|
|
416
|
+
|
|
417
|
+
makeObservable(this, {
|
|
418
|
+
input: observable,
|
|
419
|
+
isPending: observable,
|
|
420
|
+
isSubmitDisabled: computed,
|
|
421
|
+
setInput: action,
|
|
422
|
+
submit: action,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
get isSubmitDisabled() {
|
|
427
|
+
return !this.input.trim() || this.isPending;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
setInput(value: string) {
|
|
431
|
+
this.input = value;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async submit() {
|
|
435
|
+
runInAction(() => {
|
|
436
|
+
this.isPending = true;
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
await convex.mutation(api.todos.crud.createTodo, {
|
|
441
|
+
title: this.input.trim()
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
runInAction(() => {
|
|
445
|
+
this.input = "";
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
} finally {
|
|
449
|
+
runInAction(() => {
|
|
450
|
+
this.isPending = false;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Rules:
|
|
458
|
+
- Use `runInAction` when mutating observables after an `await`
|