create-aron-app 0.1.6 → 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 (234) hide show
  1. package/README.md +24 -31
  2. package/dist/index.js +39 -115
  3. package/package.json +7 -4
  4. package/templates/.cursor/rules/backend.mdc +112 -0
  5. package/templates/.cursor/rules/coding_standards.mdc +145 -0
  6. package/templates/.cursor/rules/frontend_architecture.mdc +334 -0
  7. package/templates/.github/workflows/ci.yml +40 -0
  8. package/templates/apps/api/_generated/api.d.ts +57 -0
  9. package/templates/apps/api/_generated/api.js +23 -0
  10. package/templates/apps/api/_generated/dataModel.d.ts +60 -0
  11. package/templates/apps/api/_generated/server.d.ts +143 -0
  12. package/templates/apps/api/_generated/server.js +93 -0
  13. package/templates/apps/api/http.ts +16 -0
  14. package/templates/apps/web/.env.example +10 -0
  15. package/templates/apps/web/.react-router/types/+future.ts +9 -0
  16. package/templates/apps/web/.react-router/types/+routes.ts +76 -0
  17. package/templates/apps/web/.react-router/types/+server-build.d.ts +18 -0
  18. package/templates/apps/web/.react-router/types/src/+types/root.ts +59 -0
  19. package/templates/apps/web/.react-router/types/src/routes/(auth)/+types/layout.ts +62 -0
  20. package/templates/apps/web/.react-router/types/src/routes/(auth)/sign-in/+types/index.ts +65 -0
  21. package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/[id].ts +68 -0
  22. package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/index.ts +68 -0
  23. package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/layout.ts +65 -0
  24. package/templates/apps/web/.react-router/types/src/routes/(dashboard)/+types/index.ts +65 -0
  25. package/templates/apps/web/.react-router/types/src/routes/(dashboard)/+types/layout.ts +62 -0
  26. package/templates/{react-router → apps/web}/project.json +9 -2
  27. package/templates/{react-router → apps/web}/react-router.config.ts +1 -1
  28. package/templates/apps/web/src/app.css +3 -0
  29. package/templates/{react-router → apps/web}/src/components/error_boundary.tsx +1 -1
  30. package/templates/apps/web/src/libs/convex_query_client.ts +11 -0
  31. package/templates/apps/web/src/libs/react_query_client.ts +17 -0
  32. package/templates/apps/web/src/libs/server/auth.ts +32 -0
  33. package/templates/apps/web/src/libs/server/protected.ts +17 -0
  34. package/templates/apps/web/src/providers/api_auth_provider.tsx +26 -0
  35. package/templates/apps/web/src/providers/global_provider.tsx +28 -0
  36. package/templates/apps/web/src/providers/navigation_loading_bar_provider.tsx +72 -0
  37. package/templates/apps/web/src/root.tsx +68 -0
  38. package/templates/{react-router/src/routes/auth → apps/web/src/routes/(auth)}/layout.tsx +1 -1
  39. package/templates/apps/web/src/routes/(dashboard)/(todos)/[id].tsx +33 -0
  40. package/templates/apps/web/src/routes/(dashboard)/(todos)/index.tsx +26 -0
  41. package/templates/apps/web/src/routes/(dashboard)/(todos)/layout.tsx +9 -0
  42. package/templates/apps/web/src/routes/(dashboard)/index.tsx +20 -0
  43. package/templates/apps/web/src/routes/(dashboard)/layout.tsx +20 -0
  44. package/templates/apps/web/src/routes.ts +14 -0
  45. package/templates/apps/web/src/surfaces/home/bootstrap.ts +9 -0
  46. package/templates/apps/web/src/surfaces/home/home.tsx +25 -0
  47. package/templates/apps/web/src/surfaces/home/install.tsx +17 -0
  48. package/templates/apps/web/src/surfaces/home/layout.tsx +35 -0
  49. package/templates/apps/web/src/surfaces/home/main/create.tsx +32 -0
  50. package/templates/apps/web/src/surfaces/sidebar/install.tsx +19 -0
  51. package/templates/apps/web/src/surfaces/sidebar/layout.tsx +119 -0
  52. package/templates/apps/web/src/surfaces/sidebar/nav_main/create.tsx +31 -0
  53. package/templates/apps/web/src/surfaces/sidebar/nav_main/nav_main.tsx +42 -0
  54. package/templates/apps/web/src/surfaces/sidebar/sidebar.tsx +18 -0
  55. package/templates/apps/web/src/surfaces/sidebar/user_menu/create.tsx +26 -0
  56. package/templates/{react-router/src/layouts/sidebar/sidebar_aside → apps/web/src/surfaces/sidebar/user_menu}/user_menu.tsx +13 -9
  57. package/templates/apps/web/src/surfaces/todos/all_todos/all_todos.tsx +25 -0
  58. package/templates/apps/web/src/surfaces/todos/all_todos/all_todos_controller.ts +47 -0
  59. package/templates/apps/web/src/surfaces/todos/all_todos/bootstrap.ts +21 -0
  60. package/templates/apps/web/src/surfaces/todos/all_todos/header/create.tsx +21 -0
  61. package/templates/apps/web/src/surfaces/todos/all_todos/install.tsx +20 -0
  62. package/templates/apps/web/src/surfaces/todos/all_todos/layout.tsx +35 -0
  63. package/templates/apps/web/src/surfaces/todos/all_todos/main/create.tsx +47 -0
  64. package/templates/apps/web/src/surfaces/todos/all_todos/main/main.tsx +68 -0
  65. package/templates/apps/web/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +54 -0
  66. package/templates/apps/web/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +97 -0
  67. package/templates/apps/web/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +11 -0
  68. package/templates/apps/web/src/surfaces/todos/single_todo/bootstrap.ts +35 -0
  69. package/templates/apps/web/src/surfaces/todos/single_todo/header/create.tsx +24 -0
  70. package/templates/apps/web/src/surfaces/todos/single_todo/header/header.tsx +25 -0
  71. package/templates/apps/web/src/surfaces/todos/single_todo/install.tsx +27 -0
  72. package/templates/apps/web/src/surfaces/todos/single_todo/layout.tsx +45 -0
  73. package/templates/apps/web/src/surfaces/todos/single_todo/main/create.tsx +35 -0
  74. package/templates/apps/web/src/surfaces/todos/single_todo/main/main.tsx +47 -0
  75. package/templates/apps/web/src/surfaces/todos/single_todo/single_todo.tsx +27 -0
  76. package/templates/apps/web/src/surfaces/todos/single_todo/single_todo_controller.ts +16 -0
  77. package/templates/{react-router → apps/web}/vite.config.ts +27 -3
  78. package/templates/{_base/biome.json → biome.json} +12 -0
  79. package/templates/bun.lock +1917 -0
  80. package/templates/{_base/emails → emails}/project.json +1 -1
  81. package/templates/package.json +91 -0
  82. package/templates/{_base/shared → shared}/assets/src/styles/global.css +14 -8
  83. package/templates/shared/ui/src/base/collapsible.tsx +31 -0
  84. package/templates/shared/ui/src/base/hover-card.tsx +42 -0
  85. package/templates/shared/ui/src/base/input-group.tsx +168 -0
  86. package/templates/shared/ui/src/base/panel.tsx +93 -0
  87. package/templates/{_base/shared → shared}/ui/src/hooks/use_mobile.tsx +1 -1
  88. package/templates/{_base/shared → shared}/ui/src/hooks/use_query_params.tsx +6 -7
  89. package/templates/{_base/shared → shared}/ui/tsconfig.json +1 -1
  90. package/templates/shared/utils/src/convex.ts +4 -0
  91. package/templates/{_base/tsconfig.base.json → tsconfig.base.json} +2 -1
  92. package/templates/_base/.cursor/commands/builder.md +0 -0
  93. package/templates/_base/.cursor/rules/api_architecture.mdc +0 -268
  94. package/templates/_base/.cursor/rules/coding_standards.mdc +0 -64
  95. package/templates/_base/.cursor/rules/convex_rules.mdc +0 -675
  96. package/templates/_base/.cursor/rules/frontend_rules.mdc +0 -268
  97. package/templates/_base/.env.convex.example +0 -3
  98. package/templates/_base/.github/workflows/ci.yml +0 -29
  99. package/templates/_base/_gitignore +0 -58
  100. package/templates/_base/package.json +0 -73
  101. package/templates/_base/shared/utils/src/convex.ts +0 -3
  102. package/templates/nextjs/.env.example +0 -8
  103. package/templates/nextjs/index.d.ts +0 -6
  104. package/templates/nextjs/next-env.d.ts +0 -5
  105. package/templates/nextjs/next.config.js +0 -22
  106. package/templates/nextjs/postcss.config.js +0 -17
  107. package/templates/nextjs/project.json +0 -22
  108. package/templates/nextjs/src/app/(auth)/layout.tsx +0 -21
  109. package/templates/nextjs/src/app/(auth)/not-allowed/page.tsx +0 -22
  110. package/templates/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +0 -15
  111. package/templates/nextjs/src/app/(dashboard)/layout.tsx +0 -27
  112. package/templates/nextjs/src/app/(dashboard)/page.tsx +0 -5
  113. package/templates/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +0 -23
  114. package/templates/nextjs/src/app/(dashboard)/todos/page.tsx +0 -16
  115. package/templates/nextjs/src/app/app.css +0 -3
  116. package/templates/nextjs/src/app/layout.tsx +0 -26
  117. package/templates/nextjs/src/convex.ts +0 -11
  118. package/templates/nextjs/src/middleware.ts +0 -18
  119. package/templates/nextjs/src/providers/convex_provider.tsx +0 -44
  120. package/templates/nextjs/src/surfaces/home_surface.tsx +0 -22
  121. package/templates/nextjs/src/surfaces/todos/all_todos_surface.tsx +0 -97
  122. package/templates/nextjs/src/surfaces/todos/create_todo_sheet.tsx +0 -107
  123. package/templates/nextjs/src/surfaces/todos/single_todo_surface.tsx +0 -90
  124. package/templates/nextjs/src/ui/sidebar/nav_link.tsx +0 -36
  125. package/templates/nextjs/src/ui/sidebar/sidebar.tsx +0 -125
  126. package/templates/nextjs/src/utils/font.ts +0 -9
  127. package/templates/nextjs/tsconfig.json +0 -42
  128. package/templates/react-router/.env.example +0 -8
  129. package/templates/react-router/src/app.css +0 -3
  130. package/templates/react-router/src/layouts/sidebar/sidebar_aside/sidebar_aside.tsx +0 -76
  131. package/templates/react-router/src/layouts/sidebar/sidebar_layout.tsx +0 -22
  132. package/templates/react-router/src/providers/api_auth_provider.tsx +0 -38
  133. package/templates/react-router/src/root.tsx +0 -37
  134. package/templates/react-router/src/routes/index.tsx +0 -9
  135. package/templates/react-router/src/routes/layout.tsx +0 -26
  136. package/templates/react-router/src/routes/todos/[id].tsx +0 -22
  137. package/templates/react-router/src/routes/todos/index.tsx +0 -13
  138. package/templates/react-router/src/routes.ts +0 -12
  139. package/templates/react-router/src/surfaces/home_surface.tsx +0 -20
  140. package/templates/react-router/src/surfaces/todos/all_todos_surface.tsx +0 -87
  141. package/templates/react-router/src/surfaces/todos/create_todo_sheet.tsx +0 -102
  142. package/templates/react-router/src/surfaces/todos/single_todo_surface.tsx +0 -81
  143. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/SKILL.md +0 -0
  144. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/SKILL.md +0 -0
  145. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/api-specs-context.sh +0 -0
  146. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/execute-request.sh +0 -0
  147. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-endpoint-detail.sh +0 -0
  148. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-tag-endpoints.sh +0 -0
  149. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-tags.js +0 -0
  150. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/SKILL.md +0 -0
  151. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-in.md +0 -0
  152. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-up.md +0 -0
  153. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-in.md +0 -0
  154. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-up.md +0 -0
  155. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/show-component.md +0 -0
  156. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/SKILL.md +0 -0
  157. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/api-routes.md +0 -0
  158. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/caching-auth.md +0 -0
  159. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/middleware-strategies.md +0 -0
  160. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/server-actions.md +0 -0
  161. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/server-vs-client.md +0 -0
  162. /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-webhooks/SKILL.md +0 -0
  163. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/SKILL.md +0 -0
  164. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/agents/openai.yml +0 -0
  165. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/assets/shadcn-small.png +0 -0
  166. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/assets/shadcn.png +0 -0
  167. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/cli.md +0 -0
  168. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/customization.md +0 -0
  169. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/evals/evals.json +0 -0
  170. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/mcp.md +0 -0
  171. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/base-vs-radix.md +0 -0
  172. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/composition.md +0 -0
  173. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/forms.md +0 -0
  174. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/icons.md +0 -0
  175. /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/styling.md +0 -0
  176. /package/templates/{_base/.cursor → .cursor}/commands/pr.md +0 -0
  177. /package/templates/{_base/.nvmrc → .nvmrc} +0 -0
  178. /package/templates/{_base/.vscode → .vscode}/settings.json +0 -0
  179. /package/templates/{_base/apps → apps}/api/auth.config.ts +0 -0
  180. /package/templates/{_base/apps → apps}/api/functions.ts +0 -0
  181. /package/templates/{_base/apps → apps}/api/project.json +0 -0
  182. /package/templates/{_base/apps → apps}/api/schema.ts +0 -0
  183. /package/templates/{_base/apps → apps}/api/todos/crud.ts +0 -0
  184. /package/templates/{_base/apps → apps}/api/todos/schema.ts +0 -0
  185. /package/templates/{_base/apps → apps}/api/todos/types.ts +0 -0
  186. /package/templates/{_base/apps → apps}/api/tsconfig.json +0 -0
  187. /package/templates/{_base/apps → apps}/api/types.ts +0 -0
  188. /package/templates/{react-router → apps/web}/postcss.config.js +0 -0
  189. /package/templates/{react-router → apps/web}/public/favicon.ico +0 -0
  190. /package/templates/{react-router/src/routes/auth/sign-in.tsx → apps/web/src/routes/(auth)/sign-in/index.tsx} +0 -0
  191. /package/templates/{react-router → apps/web}/tsconfig.json +0 -0
  192. /package/templates/{_base/convex.json → convex.json} +0 -0
  193. /package/templates/{_base/emails → emails}/tsconfig.json +0 -0
  194. /package/templates/{_base/emails → emails}/welcome_email.tsx +0 -0
  195. /package/templates/{_base/nx.json → nx.json} +0 -0
  196. /package/templates/{_base/scripts → scripts}/sync_convex_env.ts +0 -0
  197. /package/templates/{_base/shared → shared}/assets/image.d.ts +0 -0
  198. /package/templates/{_base/shared → shared}/assets/tsconfig.json +0 -0
  199. /package/templates/{_base/shared → shared}/ui/src/base/alert_dialog.tsx +0 -0
  200. /package/templates/{_base/shared → shared}/ui/src/base/badge.tsx +0 -0
  201. /package/templates/{_base/shared → shared}/ui/src/base/basic_data_table.tsx +0 -0
  202. /package/templates/{_base/shared → shared}/ui/src/base/button.tsx +0 -0
  203. /package/templates/{_base/shared → shared}/ui/src/base/button_group.tsx +0 -0
  204. /package/templates/{_base/shared → shared}/ui/src/base/card.tsx +0 -0
  205. /package/templates/{_base/shared → shared}/ui/src/base/checkbox.tsx +0 -0
  206. /package/templates/{_base/shared → shared}/ui/src/base/command.tsx +0 -0
  207. /package/templates/{_base/shared → shared}/ui/src/base/dialog.tsx +0 -0
  208. /package/templates/{_base/shared → shared}/ui/src/base/dropdown_menu.tsx +0 -0
  209. /package/templates/{_base/shared → shared}/ui/src/base/form.tsx +0 -0
  210. /package/templates/{_base/shared → shared}/ui/src/base/input.tsx +0 -0
  211. /package/templates/{_base/shared → shared}/ui/src/base/label.tsx +0 -0
  212. /package/templates/{_base/shared → shared}/ui/src/base/popover.tsx +0 -0
  213. /package/templates/{_base/shared → shared}/ui/src/base/radio_group.tsx +0 -0
  214. /package/templates/{_base/shared → shared}/ui/src/base/resizable.tsx +0 -0
  215. /package/templates/{_base/shared → shared}/ui/src/base/scroll_area.tsx +0 -0
  216. /package/templates/{_base/shared → shared}/ui/src/base/select.tsx +0 -0
  217. /package/templates/{_base/shared → shared}/ui/src/base/separator.tsx +0 -0
  218. /package/templates/{_base/shared → shared}/ui/src/base/sheet.tsx +0 -0
  219. /package/templates/{_base/shared → shared}/ui/src/base/side_bar.tsx +0 -0
  220. /package/templates/{_base/shared → shared}/ui/src/base/skeleton.tsx +0 -0
  221. /package/templates/{_base/shared → shared}/ui/src/base/spinner.tsx +0 -0
  222. /package/templates/{_base/shared → shared}/ui/src/base/switch.tsx +0 -0
  223. /package/templates/{_base/shared → shared}/ui/src/base/table.tsx +0 -0
  224. /package/templates/{_base/shared → shared}/ui/src/base/text_area.tsx +0 -0
  225. /package/templates/{_base/shared → shared}/ui/src/base/tooltip.tsx +0 -0
  226. /package/templates/{_base/shared → shared}/ui/src/base/utils.ts +0 -0
  227. /package/templates/{_base/shared → shared}/ui/src/hooks/use_keyboard_press.tsx +0 -0
  228. /package/templates/{_base/shared → shared}/ui/src/hooks/use_keyboard_release.tsx +0 -0
  229. /package/templates/{_base/shared → shared}/ui/src/hooks/use_mouse_click.tsx +0 -0
  230. /package/templates/{_base/shared → shared}/ui/src/hooks/use_mouse_location.tsx +0 -0
  231. /package/templates/{_base/shared → shared}/ui/src/hooks/use_outside_click.tsx +0 -0
  232. /package/templates/{_base/shared → shared}/utils/src/time.ts +0 -0
  233. /package/templates/{_base/shared → shared}/utils/tsconfig.json +0 -0
  234. /package/templates/{_base/skills-lock.json → skills-lock.json} +0 -0
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import "./app.css";
6
+
7
+ import { clerkMiddleware, rootAuthLoader } from "@clerk/react-router/server";
8
+ import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
9
+ import { Toaster } from "sonner";
10
+
11
+ import { protectedRoute } from "@/web/libs/server/protected";
12
+ import { ApiAuthProvider } from "@/web/providers/api_auth_provider";
13
+ import { GlobalProvider } from "@/web/providers/global_provider";
14
+ import { NavigationLoadingBarProvider } from "@/web/providers/navigation_loading_bar_provider";
15
+
16
+ import type { Route } from "./+types/root";
17
+
18
+ export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware(), protectedRoute];
19
+ export const loader = (args: Route.LoaderArgs) => rootAuthLoader(args);
20
+
21
+ export const links: Route.LinksFunction = () => {
22
+ return [
23
+ {
24
+ rel: "preconnect",
25
+ href: "https://fonts.googleapis.com",
26
+ },
27
+ {
28
+ rel: "preconnect",
29
+ href: "https://fonts.gstatic.com",
30
+ crossOrigin: "anonymous",
31
+ },
32
+ {
33
+ rel: "stylesheet",
34
+ href: "https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap",
35
+ },
36
+ ];
37
+ };
38
+
39
+ export function Layout({ children }: { children: React.ReactNode }) {
40
+ return (
41
+ <html lang="en">
42
+ <head>
43
+ <meta charSet="utf-8" />
44
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
45
+ <Meta />
46
+ <Links />
47
+ </head>
48
+ <body>
49
+ {children}
50
+ <ScrollRestoration />
51
+ <Scripts />
52
+ </body>
53
+ </html>
54
+ );
55
+ }
56
+
57
+ export default function App({ loaderData }: Route.ComponentProps) {
58
+ return (
59
+ <NavigationLoadingBarProvider>
60
+ <ApiAuthProvider loaderData={loaderData}>
61
+ <GlobalProvider>
62
+ <Outlet />
63
+ <Toaster richColors position="top-center" />
64
+ </GlobalProvider>
65
+ </ApiAuthProvider>
66
+ </NavigationLoadingBarProvider>
67
+ );
68
+ }
@@ -6,7 +6,7 @@ import { Outlet } from "react-router";
6
6
 
7
7
  export default function AuthLayout() {
8
8
  return (
9
- <div className="flex flex-1 items-center justify-center h-svh">
9
+ <div className="flex min-h-svh flex-1 items-center justify-center p-6">
10
10
  <Outlet />
11
11
  </div>
12
12
  );
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { createConvexClient } from "@/web/libs/server/auth";
6
+ import { bootstrapSingleTodo } from "@/web/surfaces/todos/single_todo/bootstrap";
7
+ import { SingleTodo } from "@/web/surfaces/todos/single_todo/single_todo";
8
+
9
+ import type { Route } from "./+types/[id]";
10
+
11
+ export async function loader(args: Route.LoaderArgs) {
12
+ const client = await createConvexClient(args);
13
+ return bootstrapSingleTodo({ params: args.params, client });
14
+ }
15
+
16
+ export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
17
+ return serverLoader();
18
+ }
19
+
20
+ export function meta({ loaderData: bootstrap }: Route.MetaArgs) {
21
+ if (!bootstrap) {
22
+ return [{ title: "Todo" }];
23
+ }
24
+
25
+ return [
26
+ { title: `${bootstrap.todo.title} - Todos` },
27
+ { name: "description", content: bootstrap.todo.description ?? "Todo details" },
28
+ ];
29
+ }
30
+
31
+ export default function TodoPage({ loaderData: bootstrap }: Route.ComponentProps) {
32
+ return <SingleTodo bootstrap={bootstrap} />;
33
+ }
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { createConvexClient } from "@/web/libs/server/auth";
6
+ import { AllTodos } from "@/web/surfaces/todos/all_todos/all_todos";
7
+ import { bootstrapAllTodos } from "@/web/surfaces/todos/all_todos/bootstrap";
8
+
9
+ import type { Route } from "./+types/index";
10
+
11
+ export async function loader(args: Route.LoaderArgs) {
12
+ const client = await createConvexClient(args);
13
+ return bootstrapAllTodos({ client });
14
+ }
15
+
16
+ export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
17
+ return serverLoader();
18
+ }
19
+
20
+ export function meta(_args: Route.MetaArgs) {
21
+ return [{ title: "Todos" }, { name: "description", content: "Manage your todos" }];
22
+ }
23
+
24
+ export default function TodosPage({ loaderData: bootstrap }: Route.ComponentProps) {
25
+ return <AllTodos bootstrap={bootstrap} />;
26
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { Outlet } from "react-router";
6
+
7
+ export default function TodosLayout() {
8
+ return <Outlet />;
9
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { bootstrapHome } from "@/web/surfaces/home/bootstrap";
6
+ import { Home } from "@/web/surfaces/home/home";
7
+
8
+ import type { Route } from "./+types/index";
9
+
10
+ export async function clientLoader(_args: Route.ClientLoaderArgs) {
11
+ return bootstrapHome();
12
+ }
13
+
14
+ export function meta({ loaderData: bootstrap }: Route.MetaArgs) {
15
+ return [{ title: "Home" }, { name: "description", content: "Home" }];
16
+ }
17
+
18
+ export default function HomePage({ loaderData: bootstrap }: Route.ComponentProps) {
19
+ return <Home bootstrap={bootstrap} />;
20
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { CSSProperties } from "react";
6
+ import { Outlet } from "react-router";
7
+
8
+ import { SidebarInset, SidebarProvider } from "@/ui/base/side_bar";
9
+ import { Sidebar } from "@/web/surfaces/sidebar/sidebar";
10
+
11
+ export default function DashboardLayout() {
12
+ return (
13
+ <SidebarProvider style={{ "--sidebar-width": "265px" } as CSSProperties} defaultOpen={true}>
14
+ <Sidebar />
15
+ <SidebarInset className="bg-white">
16
+ <Outlet />
17
+ </SidebarInset>
18
+ </SidebarProvider>
19
+ );
20
+ }
@@ -0,0 +1,14 @@
1
+ import { index, layout, prefix, type RouteConfig, route } from "@react-router/dev/routes";
2
+
3
+ export default [
4
+ layout("./routes/(auth)/layout.tsx", [route("/sign-in/*", "./routes/(auth)/sign-in/index.tsx")]),
5
+ layout("./routes/(dashboard)/layout.tsx", [
6
+ index("./routes/(dashboard)/index.tsx"),
7
+ ...prefix("todos", [
8
+ layout("./routes/(dashboard)/(todos)/layout.tsx", [
9
+ index("./routes/(dashboard)/(todos)/index.tsx"),
10
+ route(":id", "./routes/(dashboard)/(todos)/[id].tsx"),
11
+ ]),
12
+ ]),
13
+ ]),
14
+ ] satisfies RouteConfig;
@@ -0,0 +1,9 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ export type HomeBootstrap = Record<string, never>;
6
+
7
+ export const bootstrapHome = async (): Promise<HomeBootstrap> => {
8
+ return {};
9
+ };
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { useMemo, useRef } from "react";
6
+
7
+ import type { HomeBootstrap } from "@/web/surfaces/home/bootstrap";
8
+ import { installHome } from "@/web/surfaces/home/install";
9
+ import { createLayout } from "@/web/surfaces/home/layout";
10
+
11
+ export type HomeProps = {
12
+ bootstrap: HomeBootstrap;
13
+ };
14
+
15
+ export const Home = ({ bootstrap }: HomeProps) => {
16
+ const { layout, Layout } = useMemo(() => createLayout(), []);
17
+ const installed = useRef(false);
18
+
19
+ if (!installed.current) {
20
+ installed.current = true;
21
+ installHome({ layout, bootstrap });
22
+ }
23
+
24
+ return <Layout />;
25
+ };
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { HomeBootstrap } from "@/web/surfaces/home/bootstrap";
6
+ import type { HomeLayoutController } from "@/web/surfaces/home/layout";
7
+
8
+ export type InstallHomeOpts = {
9
+ layout: HomeLayoutController;
10
+ bootstrap: HomeBootstrap;
11
+ };
12
+
13
+ export const installHome = ({ layout, bootstrap }: InstallHomeOpts) => {
14
+ import("@/web/surfaces/home/main/create").then(({ createMain }) => {
15
+ layout.setMain(createMain({ bootstrap }));
16
+ });
17
+ };
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { action, observable } from "mobx";
6
+ import { observer } from "mobx-react-lite";
7
+ import type { ComponentType } from "react";
8
+
9
+ import { Skeleton } from "@/ui/base/skeleton";
10
+
11
+ export class HomeLayoutController {
12
+ @observable.ref
13
+ accessor Main: ComponentType | undefined = undefined;
14
+
15
+ @action
16
+ setMain(Main: ComponentType) {
17
+ this.Main = Main;
18
+ }
19
+ }
20
+
21
+ export const createLayout = () => {
22
+ const layout = new HomeLayoutController();
23
+ return {
24
+ layout,
25
+ Layout: observer(() => {
26
+ const Main = layout.Main;
27
+
28
+ return (
29
+ <div className="flex w-full flex-1 flex-col">
30
+ {Main ? <Main /> : <Skeleton className="h-64 w-full rounded-md" />}
31
+ </div>
32
+ );
33
+ }),
34
+ };
35
+ };
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { useUser } from "@clerk/react-router";
6
+ import { observer } from "mobx-react-lite";
7
+ import { Link } from "react-router";
8
+
9
+ import { Button } from "@/ui/base/button";
10
+ import type { HomeBootstrap } from "@/web/surfaces/home/bootstrap";
11
+
12
+ export type CreateHomeMainOpts = {
13
+ bootstrap: HomeBootstrap;
14
+ };
15
+
16
+ export const createMain = ({ bootstrap: _bootstrap }: CreateHomeMainOpts) => {
17
+ return observer(() => {
18
+ const { user } = useUser();
19
+
20
+ return (
21
+ <main className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
22
+ <h1 className="text-3xl font-bold">
23
+ Welcome{user?.firstName ? `, ${user.firstName}` : ""}
24
+ </h1>
25
+ <p className="text-muted-foreground">Get started by managing your todos.</p>
26
+ <Button asChild>
27
+ <Link to="/todos">View Todos</Link>
28
+ </Button>
29
+ </main>
30
+ );
31
+ });
32
+ };
@@ -0,0 +1,19 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { SidebarLayoutController } from "@/web/surfaces/sidebar/layout";
6
+
7
+ type InstallSidebarOpts = {
8
+ layout: SidebarLayoutController;
9
+ };
10
+
11
+ export const installSidebar = ({ layout }: InstallSidebarOpts) => {
12
+ import("@/web/surfaces/sidebar/nav_main/create").then(({ createNavMain }) => {
13
+ layout.setNavLinks(createNavMain());
14
+ });
15
+
16
+ import("@/web/surfaces/sidebar/user_menu/create").then(({ createUserMenu }) => {
17
+ layout.setUserMenu(createUserMenu());
18
+ });
19
+ };
@@ -0,0 +1,119 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { Plus } from "lucide-react";
6
+ import { action, observable } from "mobx";
7
+ import { observer } from "mobx-react-lite";
8
+ import type { ComponentType } from "react";
9
+ import { Link } from "react-router";
10
+
11
+ import {
12
+ Sidebar as BaseSidebar,
13
+ SidebarContent,
14
+ SidebarFooter,
15
+ SidebarHeader,
16
+ SidebarMenu,
17
+ SidebarMenuButton,
18
+ SidebarMenuItem,
19
+ } from "@/ui/base/side_bar";
20
+ import { Skeleton } from "@/ui/base/skeleton";
21
+
22
+ export class SidebarLayoutController {
23
+ @observable.ref
24
+ accessor NavLinks: ComponentType | undefined = undefined;
25
+
26
+ @observable.ref
27
+ accessor UserMenu: ComponentType | undefined = undefined;
28
+
29
+ @action
30
+ setNavLinks(NavLinks: ComponentType) {
31
+ this.NavLinks = NavLinks;
32
+ }
33
+
34
+ @action
35
+ setUserMenu(UserMenu: ComponentType) {
36
+ this.UserMenu = UserMenu;
37
+ }
38
+ }
39
+
40
+ const BrandLink = () => {
41
+ return (
42
+ <SidebarMenu>
43
+ <SidebarMenuItem>
44
+ <SidebarMenuButton
45
+ size="lg"
46
+ className="h-auto flex-col items-start gap-0.5 p-0"
47
+ asChild
48
+ >
49
+ <Link
50
+ to="/"
51
+ className="flex flex-row w-full items-center gap-3 px-2 py-2 transition-opacity hover:opacity-80 active:opacity-70 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:[&>div:last-child]:hidden"
52
+ >
53
+ <div className="flex size-10 shrink-0 items-center justify-center rounded-md bg-sidebar-brand text-sm text-white font-semibold group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:text-xs">
54
+ A
55
+ </div>
56
+ <div className="flex min-w-0 flex-col">
57
+ <span className="truncate text-base font-bold text-sidebar-foreground">
58
+ App
59
+ </span>
60
+ <span className="truncate text-xs text-sidebar-muted-foreground">
61
+ Template
62
+ </span>
63
+ </div>
64
+ </Link>
65
+ </SidebarMenuButton>
66
+ </SidebarMenuItem>
67
+ </SidebarMenu>
68
+ );
69
+ };
70
+
71
+ export const createSidebarLayout = () => {
72
+ const layout = new SidebarLayoutController();
73
+
74
+ return {
75
+ layout,
76
+ Layout: observer(() => {
77
+ const NavLinks = layout.NavLinks;
78
+ const UserMenu = layout.UserMenu;
79
+
80
+ return (
81
+ <BaseSidebar
82
+ data-slot="app-sidebar"
83
+ className="shadow-sm"
84
+ collapsible="icon"
85
+ >
86
+ <SidebarHeader>
87
+ <BrandLink />
88
+ </SidebarHeader>
89
+ <SidebarContent className="flex flex-1 flex-col gap-4 overflow-hidden px-2">
90
+ <SidebarMenu>
91
+ <SidebarMenuItem>
92
+ <SidebarMenuButton
93
+ size="sm"
94
+ tooltip="Create a new todo"
95
+ className="bg-sidebar-primary text-sidebar-primary-foreground hover:bg-sidebar-primary/90 active:bg-sidebar-primary/80 justify-center font-semibold shadow-sm"
96
+ asChild
97
+ >
98
+ <Link
99
+ to="/todos"
100
+ className="flex items-center gap-2"
101
+ >
102
+ <Plus className="size-4" />
103
+ <span>New Todo</span>
104
+ </Link>
105
+ </SidebarMenuButton>
106
+ </SidebarMenuItem>
107
+ </SidebarMenu>
108
+ </SidebarContent>
109
+ <SidebarFooter className="gap-2">
110
+ {NavLinks ? <NavLinks /> : <Skeleton className="h-10 bg-sidebar-accent" />}
111
+ {UserMenu ? <UserMenu /> : <Skeleton className="h-12 bg-sidebar-accent" />}
112
+ </SidebarFooter>
113
+ </BaseSidebar>
114
+ );
115
+ }),
116
+ };
117
+ };
118
+
119
+ export type SidebarLayoutControllerType = SidebarLayoutController;
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { CheckSquare, Home } from "lucide-react";
6
+ import { observer } from "mobx-react-lite";
7
+ import { useLocation } from "react-router";
8
+
9
+ import { useSidebar } from "@/ui/base/side_bar";
10
+
11
+ import { NavMain } from "@/web/surfaces/sidebar/nav_main/nav_main";
12
+
13
+ const NAV_ITEMS = [
14
+ { to: "/", label: "Home", icon: Home, exact: true },
15
+ { to: "/todos", label: "Todos", icon: CheckSquare, exact: false },
16
+ ] as const;
17
+
18
+ export const createNavMain = () => {
19
+ return observer(() => {
20
+ const location = useLocation();
21
+ const { setOpenMobile } = useSidebar();
22
+
23
+ return (
24
+ <NavMain
25
+ items={[...NAV_ITEMS]}
26
+ pathname={location.pathname}
27
+ onNavigate={() => setOpenMobile(false)}
28
+ />
29
+ );
30
+ });
31
+ };
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { LucideIcon } from "lucide-react";
6
+ import { Link } from "react-router";
7
+
8
+ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/ui/base/side_bar";
9
+
10
+ export type NavMainItem = {
11
+ to: string;
12
+ label: string;
13
+ icon: LucideIcon;
14
+ exact: boolean;
15
+ };
16
+
17
+ type NavMainProps = {
18
+ items: NavMainItem[];
19
+ pathname: string;
20
+ onNavigate: () => void;
21
+ };
22
+
23
+ export const NavMain = ({ items, pathname, onNavigate }: NavMainProps) => {
24
+ return (
25
+ <SidebarMenu>
26
+ {items.map(({ to, label, icon: Icon, exact }) => {
27
+ const isActive = exact ? pathname === to : pathname.startsWith(to);
28
+
29
+ return (
30
+ <SidebarMenuItem key={to}>
31
+ <SidebarMenuButton isActive={isActive} asChild onClick={onNavigate}>
32
+ <Link to={to}>
33
+ <Icon className="size-4" />
34
+ <span>{label}</span>
35
+ </Link>
36
+ </SidebarMenuButton>
37
+ </SidebarMenuItem>
38
+ );
39
+ })}
40
+ </SidebarMenu>
41
+ );
42
+ };
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { useEffect, useMemo } from "react";
6
+
7
+ import { installSidebar } from "@/web/surfaces/sidebar/install";
8
+ import { createSidebarLayout } from "@/web/surfaces/sidebar/layout";
9
+
10
+ export const Sidebar = () => {
11
+ const { layout, Layout } = useMemo(() => createSidebarLayout(), []);
12
+
13
+ useEffect(() => {
14
+ installSidebar({ layout });
15
+ }, [layout]);
16
+
17
+ return <Layout />;
18
+ };
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { useUser } from "@clerk/react-router";
6
+ import { observer } from "mobx-react-lite";
7
+
8
+ import { UserMenu } from "@/web/surfaces/sidebar/user_menu/user_menu";
9
+
10
+ export const createUserMenu = () => {
11
+ return observer(() => {
12
+ const { user, isLoaded } = useUser();
13
+
14
+ if (isLoaded && !user) {
15
+ return null;
16
+ }
17
+
18
+ return (
19
+ <UserMenu
20
+ isLoaded={isLoaded}
21
+ fullName={user?.fullName}
22
+ email={user?.primaryEmailAddress?.emailAddress}
23
+ />
24
+ );
25
+ });
26
+ };
@@ -1,11 +1,19 @@
1
- import { UserButton, useUser } from "@clerk/react-router";
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { UserButton } from "@clerk/react-router";
2
6
 
3
7
  import { SidebarMenu, SidebarMenuItem } from "@/ui/base/side_bar";
4
8
  import { Skeleton } from "@/ui/base/skeleton";
5
9
 
6
- export const UserMenu = () => {
7
- const { user, isLoaded } = useUser();
10
+ type UserMenuProps = {
11
+ isLoaded: boolean;
12
+ fullName: string | null | undefined;
13
+ email: string | null | undefined;
14
+ };
8
15
 
16
+ export const UserMenu = ({ isLoaded, fullName, email }: UserMenuProps) => {
9
17
  if (!isLoaded) {
10
18
  return (
11
19
  <SidebarMenu>
@@ -16,18 +24,14 @@ export const UserMenu = () => {
16
24
  );
17
25
  }
18
26
 
19
- if (!user) return null;
20
-
21
27
  return (
22
28
  <SidebarMenu>
23
29
  <SidebarMenuItem className="w-full">
24
30
  <div className="flex w-full items-center gap-2 rounded-md border px-2 py-2">
25
31
  <UserButton />
26
32
  <div className="flex min-w-0 flex-col">
27
- <span className="truncate text-sm font-medium">{user.fullName}</span>
28
- <span className="truncate text-xs text-muted-foreground">
29
- {user.primaryEmailAddress?.emailAddress}
30
- </span>
33
+ <span className="truncate text-sm font-medium">{fullName}</span>
34
+ <span className="truncate text-xs text-muted-foreground">{email}</span>
31
35
  </div>
32
36
  </div>
33
37
  </SidebarMenuItem>
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { useMemo, useRef } from "react";
6
+
7
+ import type { AllTodosBootstrap } from "@/web/surfaces/todos/all_todos/bootstrap";
8
+ import { installAllTodos } from "@/web/surfaces/todos/all_todos/install";
9
+ import { createLayout } from "@/web/surfaces/todos/all_todos/layout";
10
+
11
+ export type AllTodosProps = {
12
+ bootstrap: AllTodosBootstrap;
13
+ };
14
+
15
+ export const AllTodos = ({ bootstrap }: AllTodosProps) => {
16
+ const { layout, Layout } = useMemo(createLayout, []);
17
+ const installed = useRef(false);
18
+
19
+ if (!installed.current) {
20
+ installed.current = true;
21
+ installAllTodos({ layout, bootstrap });
22
+ }
23
+
24
+ return <Layout />;
25
+ };