create-m5kdev 0.33.0 → 0.35.0

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 (147) hide show
  1. package/dist/src/catalog.cjs +21 -1
  2. package/dist/src/catalog.cjs.map +1 -1
  3. package/dist/src/catalog.d.cts +3 -1
  4. package/dist/src/create.cjs +6 -2
  5. package/dist/src/create.cjs.map +1 -1
  6. package/dist/src/doctor.cjs.map +1 -1
  7. package/dist/src/fs.cjs +10 -5
  8. package/dist/src/fs.cjs.map +1 -1
  9. package/dist/src/prompts.cjs +32 -0
  10. package/dist/src/prompts.cjs.map +1 -1
  11. package/dist/src/prompts.d.cts +7 -1
  12. package/dist/src/template.cjs +23 -4
  13. package/dist/src/template.cjs.map +1 -1
  14. package/dist/src/template.d.cts +17 -3
  15. package/dist/src/types.d.cts +11 -4
  16. package/package.json +4 -1
  17. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +1 -0
  18. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +1 -1
  19. package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +3 -3
  20. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +3 -2
  21. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
  22. package/templates/minimal-app/.cursor/rules/server-event-emit.mdc +41 -0
  23. package/templates/minimal-app/.dockerignore.tpl +31 -0
  24. package/templates/minimal-app/.gitignore.tpl +1 -0
  25. package/templates/minimal-app/AGENTS.md.tpl +14 -10
  26. package/templates/minimal-app/CLAUDE.md.tpl +133 -131
  27. package/templates/minimal-app/README.md.tpl +51 -19
  28. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  29. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +51 -39
  30. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -38
  31. package/templates/minimal-app/apps/e2e/servers.ts.tpl +136 -112
  32. package/templates/minimal-app/apps/e2e/tests/conversation.spec.ts.tpl +22 -0
  33. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -47
  34. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -73
  35. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -517
  36. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -94
  37. package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +35 -0
  38. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -289
  39. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -516
  40. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -107
  41. package/templates/minimal-app/apps/e2e/tests/workflow.spec.ts.tpl +26 -0
  42. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -10
  43. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  44. package/templates/minimal-app/apps/email/src/emails/trialEndingEmail.tsx.tpl +40 -0
  45. package/templates/minimal-app/apps/email/src/index.ts.tpl +7 -0
  46. package/templates/minimal-app/apps/email/src/resources.ts.tpl +72 -65
  47. package/templates/minimal-app/apps/expo/app.json.tpl +36 -36
  48. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -8
  49. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -16
  50. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -31
  51. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -151
  52. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -53
  53. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -8
  54. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -71
  55. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -19
  56. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -85
  57. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -40
  58. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -75
  59. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -193
  60. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -34
  61. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -37
  62. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -10
  63. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -22
  64. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -9
  65. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -11
  66. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -20
  67. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -11
  68. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -1
  69. package/templates/minimal-app/apps/landing/.env.production.example.tpl +7 -0
  70. package/templates/minimal-app/apps/landing/Dockerfile.tpl +39 -0
  71. package/templates/minimal-app/apps/landing/fly.toml.tpl +20 -0
  72. package/templates/minimal-app/apps/landing/index.html.tpl +35 -0
  73. package/templates/minimal-app/apps/landing/package.json.tpl +34 -0
  74. package/templates/minimal-app/apps/landing/src/App.tsx.tpl +12 -0
  75. package/templates/minimal-app/apps/landing/src/LandingPage.tsx.tpl +37 -0
  76. package/templates/minimal-app/apps/landing/src/index.css.tpl +24 -0
  77. package/templates/minimal-app/apps/landing/src/main.tsx.tpl +16 -0
  78. package/templates/minimal-app/apps/landing/src/vite-env.d.ts.tpl +11 -0
  79. package/templates/minimal-app/apps/landing/tsconfig.json.tpl +12 -0
  80. package/templates/minimal-app/apps/landing/vite.config.ts.tpl +25 -0
  81. package/templates/minimal-app/apps/server/AGENTS.md.tpl +55 -46
  82. package/templates/minimal-app/apps/server/{drizzle/seed.e2e.ts.tpl → db.e2e.ts.tpl} +57 -40
  83. package/templates/minimal-app/apps/server/{drizzle/seed.ts.tpl → db.ts.tpl} +27 -20
  84. package/templates/minimal-app/apps/server/drizzle/0003_silly_the_liberteens.sql +27 -0
  85. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +56 -166
  86. package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +1796 -1910
  87. package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +1818 -1932
  88. package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2137 -0
  89. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +8 -1
  90. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +3 -33
  91. package/templates/minimal-app/apps/server/package.json.tpl +5 -3
  92. package/templates/minimal-app/apps/server/src/app.ts.tpl +36 -0
  93. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +26 -0
  94. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +51 -0
  95. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +51 -0
  96. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.trpc.ts.tpl +16 -0
  97. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +28 -28
  98. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +28 -28
  99. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +57 -54
  100. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -67
  101. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +116 -116
  102. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -37
  103. package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -279
  104. package/templates/minimal-app/apps/server/src/schema.ts.tpl +31 -20
  105. package/templates/minimal-app/apps/server/src/types.ts.tpl +12 -12
  106. package/templates/minimal-app/apps/server/tsconfig.json.tpl +1 -1
  107. package/templates/minimal-app/apps/server/tsup.config.ts.tpl +23 -9
  108. package/templates/minimal-app/apps/shared/.env.example.tpl +5 -0
  109. package/templates/minimal-app/apps/shared/.env.production.example.tpl +23 -0
  110. package/templates/minimal-app/apps/shared/.env.tpl +5 -0
  111. package/templates/minimal-app/apps/shared/Dockerfile.tpl +48 -0
  112. package/templates/minimal-app/apps/shared/fly.toml.tpl +24 -0
  113. package/templates/minimal-app/apps/shared/package.json.tpl +26 -24
  114. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -28
  115. package/templates/minimal-app/apps/shared/src/modules/conversation/conversation.constants.ts.tpl +1 -0
  116. package/templates/minimal-app/apps/shared/src/modules/demo-workflow/demo-workflow.constants.ts.tpl +1 -0
  117. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  118. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  119. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -25
  120. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  121. package/templates/minimal-app/apps/webapp/public/push-sw.js.tpl +34 -34
  122. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +49 -44
  123. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +96 -65
  124. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +114 -93
  125. package/templates/minimal-app/apps/webapp/src/StarterServerEventProvider.tsx.tpl +38 -0
  126. package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -15
  127. package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
  128. package/templates/minimal-app/apps/webapp/src/modules/conversation/ConversationRoute.tsx.tpl +36 -0
  129. package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +88 -0
  130. package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +50 -0
  131. package/templates/minimal-app/apps/webapp/src/modules/workflows/WorkflowsRoute.tsx.tpl +97 -0
  132. package/templates/minimal-app/apps/webapp/src/modules/workflows/hooks/useWorkflowsRoute.ts.tpl +64 -0
  133. package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
  134. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -10
  135. package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
  136. package/templates/minimal-app/apps/webapp/translations/en/app.json.tpl +7 -7
  137. package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +193 -135
  138. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
  139. package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
  140. package/templates/minimal-app/biome.json.tpl +17 -12
  141. package/templates/minimal-app/package.json.tpl +7 -2
  142. package/templates/minimal-app/pnpm-workspace.yaml.tpl +14 -7
  143. package/templates/minimal-app/template.manifest.json +93 -2
  144. package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +0 -26
  145. package/templates/minimal-app/apps/server/drizzle/guard.ts.tpl +0 -36
  146. package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +0 -40
  147. package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +0 -16
@@ -1,65 +1,96 @@
1
- import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
2
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
- import { AppShell } from "@m5kdev/web-ui/modules/app/components/AppShell";
4
- import { AppSidebarContent } from "@m5kdev/web-ui/modules/app/components/AppSidebarContent";
5
- import { AppSidebarHeader } from "@m5kdev/web-ui/modules/app/components/AppSidebarHeader";
6
- import { AppSidebarUser } from "@m5kdev/web-ui/modules/app/components/AppSidebarUser";
7
- import { AuthOrganizationSelect } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationSelect";
8
- import { AuthUtilityImpersonationBanner } from "@m5kdev/web-ui/modules/auth/components/AuthUtilityImpersonationBanner";
9
- import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
10
- import { FileIcon, UsersIcon } from "lucide-react";
11
- import { useState } from "react";
12
- import { useTranslation } from "react-i18next";
13
- import { useNavigate } from "react-router";
14
-
15
- export function Layout() {
16
- const { data: session } = useSession();
17
- const { t } = useTranslation("{{APP_SLUG}}");
18
- const navigate = useNavigate();
19
-
20
- // ephemeral UI state: which collapsible sidebar groups are open
21
- const [navigationState, setNavigationState] = useState<Record<string, boolean>>({});
22
-
23
- const onSignOut = () => {
24
- authClient.signOut();
25
- navigate("/login");
26
- };
27
-
28
- return (
29
- <AppShell
30
- header={<AuthUtilityImpersonationBanner />}
31
- sidebar={{
32
- header: <AppSidebarHeader logo={{ src: "/logo.svg", alt: APP_NAME }} title={APP_NAME} />,
33
- content: (
34
- <>
35
- <div className="px-2 group-data-[collapsible=icon]:hidden">
36
- <AuthOrganizationSelect />
37
- </div>
38
- <AppSidebarContent
39
- navigationItems={[
40
- {
41
- label: t("layout.navigation.posts"),
42
- icon: <FileIcon />,
43
- link: "/",
44
- },
45
- {
46
- label: t("layout.navigation.members"),
47
- icon: <UsersIcon />,
48
- link: "/organization/members",
49
- },
50
- ]}
51
- navigationState={navigationState}
52
- onNavigationStateChange={setNavigationState}
53
- />
54
- </>
55
- ),
56
- footer: (
57
- // data-testid so e2e (and app tests) can assert on the signed-in user
58
- <div data-testid="session-email" className="contents">
59
- <AppSidebarUser user={session?.user} onSignOut={onSignOut} />
60
- </div>
61
- ),
62
- }}
63
- />
64
- );
65
- }
1
+ // biome-ignore-all assist/source/organizeImports: feature-gated lucide icons stay in marker blocks
2
+ import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
3
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
4
+ import { AppShell } from "@m5kdev/web-ui/modules/app/components/AppShell";
5
+ import { AppSidebarContent } from "@m5kdev/web-ui/modules/app/components/AppSidebarContent";
6
+ import { AppSidebarHeader } from "@m5kdev/web-ui/modules/app/components/AppSidebarHeader";
7
+ import { AppSidebarUser } from "@m5kdev/web-ui/modules/app/components/AppSidebarUser";
8
+ import { AuthOrganizationSelect } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationSelect";
9
+ import { AuthUtilityImpersonationBanner } from "@m5kdev/web-ui/modules/auth/components/AuthUtilityImpersonationBanner";
10
+ import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
11
+ import { FileIcon, UsersIcon } from "lucide-react";
12
+ // m5k:ai:start
13
+ import { SparklesIcon } from "lucide-react";
14
+ // m5k:ai:end
15
+ // m5k:files:start
16
+ import { ImageIcon } from "lucide-react";
17
+ // m5k:files:end
18
+ // m5k:workflows:start
19
+ import { WorkflowIcon } from "lucide-react";
20
+ // m5k:workflows:end
21
+ import { useState } from "react";
22
+ import { useTranslation } from "react-i18next";
23
+ import { useNavigate } from "react-router";
24
+
25
+ export function Layout() {
26
+ const { data: session } = useSession();
27
+ const { t } = useTranslation("{{APP_SLUG}}");
28
+ const navigate = useNavigate();
29
+
30
+ // ephemeral UI state: which collapsible sidebar groups are open
31
+ const [navigationState, setNavigationState] = useState<Record<string, boolean>>({});
32
+
33
+ const onSignOut = () => {
34
+ authClient.signOut();
35
+ navigate("/login");
36
+ };
37
+
38
+ return (
39
+ <AppShell
40
+ header={<AuthUtilityImpersonationBanner />}
41
+ sidebar={{
42
+ header: <AppSidebarHeader logo={{ src: "/logo.svg", alt: APP_NAME }} title={APP_NAME} />,
43
+ content: (
44
+ <>
45
+ <div className="px-2 group-data-[collapsible=icon]:hidden">
46
+ <AuthOrganizationSelect />
47
+ </div>
48
+ <AppSidebarContent
49
+ navigationItems={[
50
+ {
51
+ label: t("layout.navigation.posts"),
52
+ icon: <FileIcon />,
53
+ link: "/",
54
+ },
55
+ // m5k:ai:start
56
+ {
57
+ label: t("layout.navigation.conversation"),
58
+ icon: <SparklesIcon />,
59
+ link: "/conversation",
60
+ },
61
+ // m5k:ai:end
62
+ // m5k:files:start
63
+ {
64
+ label: t("layout.navigation.files"),
65
+ icon: <ImageIcon />,
66
+ link: "/files",
67
+ },
68
+ // m5k:files:end
69
+ // m5k:workflows:start
70
+ {
71
+ label: t("layout.navigation.workflows"),
72
+ icon: <WorkflowIcon />,
73
+ link: "/workflows",
74
+ },
75
+ // m5k:workflows:end
76
+ {
77
+ label: t("layout.navigation.members"),
78
+ icon: <UsersIcon />,
79
+ link: "/organization/members",
80
+ },
81
+ ]}
82
+ navigationState={navigationState}
83
+ onNavigationStateChange={setNavigationState}
84
+ />
85
+ </>
86
+ ),
87
+ footer: (
88
+ // data-testid so e2e (and app tests) can assert on the signed-in user
89
+ <div data-testid="session-email" className="contents">
90
+ <AppSidebarUser user={session?.user} onSignOut={onSignOut} />
91
+ </div>
92
+ ),
93
+ }}
94
+ />
95
+ );
96
+ }
@@ -1,93 +1,114 @@
1
- import { syncI18nLocale } from "@m5kdev/frontend/modules/app/utils/locale";
2
- import { AuthAdminRouter } from "@m5kdev/web-ui/modules/auth/components/AuthAdminRouter";
3
- import { AuthOrganizationAcceptInvitationRoute } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationAcceptInvitationRoute";
4
- import { AuthOrganizationRouter } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationRouter";
5
- import { AuthPublicRouter } from "@m5kdev/web-ui/modules/auth/components/AuthPublicRouter";
6
- import { AuthUserRouter } from "@m5kdev/web-ui/modules/auth/components/AuthUserRouter";
7
- import { AuthUtilityProtectedRoutes as ProtectedRoutes } from "@m5kdev/web-ui/modules/auth/components/AuthUtilityProtectedRoutes";
8
- import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
9
- import { useTranslation } from "react-i18next";
10
- import { Route, Routes } from "react-router";
11
- import { z } from "zod";
12
- import { PostsRoute } from "@/modules/posts/PostsRoute";
13
- import { Layout } from "./Layout";
14
-
15
- const preferenceSchema = z.object({
16
- compactMode: z.boolean().optional(),
17
- postsPerPage: z.number().min(1).max(20).optional(),
18
- });
19
-
20
- const preferenceControls = {
21
- compactMode: {
22
- label: "Compact mode",
23
- element: "switch",
24
- },
25
- postsPerPage: {
26
- label: "Posts per page",
27
- element: "number",
28
- min: 1,
29
- max: 20,
30
- step: 1,
31
- },
32
- } as const;
33
-
34
- function AuthHeader() {
35
- const { t } = useTranslation("{{APP_SLUG}}");
36
-
37
- return (
38
- <div className="text-center">
39
- <p className="text-[0.68rem] font-semibold uppercase tracking-[0.3em] text-default-500">
40
- {t("auth.header.eyebrow")}
41
- </p>
42
- <h1 className="mt-3 font-editorial text-4xl text-ink">{APP_NAME}</h1>
43
- <p className="mt-3 text-sm text-default-600">{t("auth.header.tagline")}</p>
44
- </div>
45
- );
46
- }
47
-
48
- export function Router() {
49
- const isWaitlist = import.meta.env.VITE_ENABLE_WAITLIST === "true";
50
-
51
- return (
52
- <Routes>
53
- {/* Built-in routers register as function calls so their <Route> elements
54
- land directly in this tree. */}
55
- {AuthPublicRouter({
56
- header: <AuthHeader />,
57
- waitlist: isWaitlist,
58
- onLocaleChange: syncI18nLocale,
59
- })}
60
-
61
- {/* public: a logged-out invitee must reach this route so it can send
62
- them to signup — inside ProtectedRoutes they'd bounce to /login */}
63
- <Route
64
- path="/organization/accept-invitation"
65
- element={<AuthOrganizationAcceptInvitationRoute />}
66
- />
67
-
68
- <Route
69
- element={
70
- <ProtectedRoutes>
71
- <Layout />
72
- </ProtectedRoutes>
73
- }
74
- >
75
- <Route index element={<PostsRoute />} />
76
-
77
- {AuthUserRouter({
78
- schema: preferenceSchema,
79
- controls: preferenceControls,
80
- onLocaleChange: syncI18nLocale,
81
- })}
82
- {AuthOrganizationRouter({
83
- schema: preferenceSchema,
84
- controls: preferenceControls,
85
- })}
86
- {AuthAdminRouter({
87
- enableWaitlist: true,
88
- enableAccountClaimActions: true,
89
- })}
90
- </Route>
91
- </Routes>
92
- );
93
- }
1
+ // biome-ignore-all assist/source/organizeImports: feature-gated route imports stay in marker blocks
2
+ import { syncI18nLocale } from "@m5kdev/frontend/modules/app/utils/locale";
3
+ import { AuthAdminRouter } from "@m5kdev/web-ui/modules/auth/components/AuthAdminRouter";
4
+ import { AuthOrganizationAcceptInvitationRoute } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationAcceptInvitationRoute";
5
+ import { AuthOrganizationRouter } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationRouter";
6
+ import { AuthPublicRouter } from "@m5kdev/web-ui/modules/auth/components/AuthPublicRouter";
7
+ import { AuthUserRouter } from "@m5kdev/web-ui/modules/auth/components/AuthUserRouter";
8
+ import { AuthUtilityProtectedRoutes as ProtectedRoutes } from "@m5kdev/web-ui/modules/auth/components/AuthUtilityProtectedRoutes";
9
+ import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
10
+ import { useTranslation } from "react-i18next";
11
+ import { Route, Routes } from "react-router";
12
+ import { z } from "zod";
13
+ import { PostsRoute } from "@/modules/posts/PostsRoute";
14
+ // m5k:ai:start
15
+ import { ConversationRoute } from "@/modules/conversation/ConversationRoute";
16
+ // m5k:ai:end
17
+ // m5k:files:start
18
+ import { FilesRoute } from "@/modules/files/FilesRoute";
19
+ // m5k:files:end
20
+ // m5k:workflows:start
21
+ import { WorkflowsRoute } from "@/modules/workflows/WorkflowsRoute";
22
+ // m5k:workflows:end
23
+ import { Layout } from "./Layout";
24
+
25
+ const preferenceSchema = z.object({
26
+ compactMode: z.boolean().optional(),
27
+ postsPerPage: z.number().min(1).max(20).optional(),
28
+ });
29
+
30
+ const preferenceControls = {
31
+ compactMode: {
32
+ label: "Compact mode",
33
+ element: "switch",
34
+ },
35
+ postsPerPage: {
36
+ label: "Posts per page",
37
+ element: "number",
38
+ min: 1,
39
+ max: 20,
40
+ step: 1,
41
+ },
42
+ } as const;
43
+
44
+ function AuthHeader() {
45
+ const { t } = useTranslation("{{APP_SLUG}}");
46
+
47
+ return (
48
+ <div className="text-center">
49
+ <p className="text-[0.68rem] font-semibold uppercase tracking-[0.3em] text-default-500">
50
+ {t("auth.header.eyebrow")}
51
+ </p>
52
+ <h1 className="mt-3 font-editorial text-4xl text-ink">{APP_NAME}</h1>
53
+ <p className="mt-3 text-sm text-default-600">{t("auth.header.tagline")}</p>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ export function Router() {
59
+ const isWaitlist = import.meta.env.VITE_ENABLE_WAITLIST === "true";
60
+
61
+ return (
62
+ <Routes>
63
+ {/* Built-in routers register as function calls so their <Route> elements
64
+ land directly in this tree. */}
65
+ {AuthPublicRouter({
66
+ header: <AuthHeader />,
67
+ waitlist: isWaitlist,
68
+ onLocaleChange: syncI18nLocale,
69
+ })}
70
+
71
+ {/* public: a logged-out invitee must reach this route so it can send
72
+ them to signup — inside ProtectedRoutes they'd bounce to /login */}
73
+ <Route
74
+ path="/organization/accept-invitation"
75
+ element={<AuthOrganizationAcceptInvitationRoute />}
76
+ />
77
+
78
+ <Route
79
+ element={
80
+ <ProtectedRoutes>
81
+ <Layout />
82
+ </ProtectedRoutes>
83
+ }
84
+ >
85
+ <Route index element={<PostsRoute />} />
86
+ {[
87
+ // m5k:ai:start
88
+ <Route key="conversation" path="conversation" element={<ConversationRoute />} />,
89
+ // m5k:ai:end
90
+ // m5k:files:start
91
+ <Route key="files" path="files" element={<FilesRoute />} />,
92
+ // m5k:files:end
93
+ // m5k:workflows:start
94
+ <Route key="workflows" path="workflows" element={<WorkflowsRoute />} />,
95
+ // m5k:workflows:end
96
+ ]}
97
+
98
+ {AuthUserRouter({
99
+ schema: preferenceSchema,
100
+ controls: preferenceControls,
101
+ onLocaleChange: syncI18nLocale,
102
+ })}
103
+ {AuthOrganizationRouter({
104
+ schema: preferenceSchema,
105
+ controls: preferenceControls,
106
+ })}
107
+ {AuthAdminRouter({
108
+ enableWaitlist: true,
109
+ enableAccountClaimActions: true,
110
+ })}
111
+ </Route>
112
+ </Routes>
113
+ );
114
+ }
@@ -0,0 +1,38 @@
1
+ // biome-ignore-all assist/source/organizeImports: feature-gated Server event imports stay in marker blocks
2
+ import { ServerEventProvider } from "@m5kdev/frontend/modules/app/components/ServerEventProvider";
3
+ // m5k:workflows:start
4
+ import type { QueryClient } from "@tanstack/react-query";
5
+ import { DEMO_WORKFLOW_SERVER_EVENT_RESOURCE } from "{{PACKAGE_SCOPE}}/shared/modules/demo-workflow/demo-workflow.constants";
6
+ // m5k:workflows:end
7
+ import type { ReactNode } from "react";
8
+ // m5k:workflows:start
9
+ import { useTRPC } from "./utils/trpc";
10
+ // m5k:workflows:end
11
+
12
+ type Props = {
13
+ children: ReactNode;
14
+ };
15
+
16
+ /** Kernel SSE for the authenticated session. Workflow handlers are feature-gated. */
17
+ export function StarterServerEventProvider({ children }: Props): ReactNode {
18
+ // m5k:workflows:start
19
+ const trpc = useTRPC();
20
+ const invalidateWorkflowList = (queryClient: QueryClient): void => {
21
+ void queryClient.invalidateQueries(trpc.workflow.list.queryFilter());
22
+ };
23
+ // m5k:workflows:end
24
+ return (
25
+ <ServerEventProvider
26
+ // m5k:workflows:start
27
+ handlers={{
28
+ [DEMO_WORKFLOW_SERVER_EVENT_RESOURCE]: (_event, queryClient) => {
29
+ invalidateWorkflowList(queryClient);
30
+ },
31
+ }}
32
+ onReconnect={invalidateWorkflowList}
33
+ // m5k:workflows:end
34
+ >
35
+ {children}
36
+ </ServerEventProvider>
37
+ );
38
+ }
@@ -1,15 +1,15 @@
1
- /* biome-ignore-all lint/suspicious/noUnknownAtRules: Tailwind v4 */
2
-
3
- @import "tailwindcss";
4
- @import "@heroui/styles";
5
- @import "tw-animate-css";
6
-
7
- @source "./node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
8
- @source "../node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
9
- @source "../../node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
10
- @source "../../../node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
11
- @source "../../../../node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
12
-
13
- :root {
14
- --accent: oklch(0.6204 0.195 253.83);
15
- }
1
+ /* biome-ignore-all lint/suspicious/noUnknownAtRules: Tailwind v4 */
2
+
3
+ @import "tailwindcss";
4
+ @import "@heroui/styles";
5
+ @import "tw-animate-css";
6
+
7
+ @source "./node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
8
+ @source "../node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
9
+ @source "../../node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
10
+ @source "../../../node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
11
+ @source "../../../../node_modules/@m5kdev/web-ui/dist/src/**/*.{mjs}";
12
+
13
+ :root {
14
+ --accent: oklch(0.6204 0.195 253.83);
15
+ }
@@ -1,26 +1,26 @@
1
- import type { PostHogConfig } from "posthog-js";
2
- import { PostHogProvider } from "posthog-js/react";
3
- import { StrictMode } from "react";
4
- import { createRoot } from "react-dom/client";
5
- import { App } from "./App";
6
- import "./index.css";
7
- import "./utils/i18n";
8
-
9
- const options = {
10
- api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
11
- defaults: "2025-05-24",
12
- } satisfies Partial<PostHogConfig>;
13
-
14
- const rootElement = document.getElementById("root");
15
-
16
- if (!rootElement) {
17
- throw new Error("Root element #root not found.");
18
- }
19
-
20
- createRoot(rootElement).render(
21
- <StrictMode>
22
- <PostHogProvider apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY ?? "demo"} options={options}>
23
- <App />
24
- </PostHogProvider>
25
- </StrictMode>
26
- );
1
+ import type { PostHogConfig } from "posthog-js";
2
+ import { PostHogProvider } from "posthog-js/react";
3
+ import { StrictMode } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { App } from "./App";
6
+ import "./index.css";
7
+ import "./utils/i18n";
8
+
9
+ const options = {
10
+ api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
11
+ defaults: "2025-05-24",
12
+ } satisfies Partial<PostHogConfig>;
13
+
14
+ const rootElement = document.getElementById("root");
15
+
16
+ if (!rootElement) {
17
+ throw new Error("Root element #root not found.");
18
+ }
19
+
20
+ createRoot(rootElement).render(
21
+ <StrictMode>
22
+ <PostHogProvider apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY ?? "demo"} options={options}>
23
+ <App />
24
+ </PostHogProvider>
25
+ </StrictMode>
26
+ );
@@ -0,0 +1,36 @@
1
+ import { Card, Skeleton } from "@heroui/react";
2
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
+ import { AiConversation } from "@m5kdev/web-ui/modules/ai/components/AiConversation";
4
+ import { STARTER_ASSISTANT_AGENT_ID } from "{{PACKAGE_SCOPE}}/shared/modules/conversation/conversation.constants";
5
+ import { useTranslation } from "react-i18next";
6
+
7
+ export function ConversationRoute() {
8
+ const { t } = useTranslation("{{APP_SLUG}}");
9
+ const { data: session } = useSession();
10
+ const threadId = session?.user?.id;
11
+
12
+ return (
13
+ <div
14
+ className="grid h-svh grid-rows-[auto_minmax(0,1fr)] gap-4 p-4"
15
+ data-testid="conversation-route"
16
+ >
17
+ <Card>
18
+ <Card.Header className="flex flex-col gap-1">
19
+ <p className="text-xs font-medium uppercase tracking-wide text-muted">
20
+ {t("conversation.hero.eyebrow")}
21
+ </p>
22
+ <Card.Title>{t("conversation.hero.title")}</Card.Title>
23
+ <Card.Description>{t("conversation.hero.body")}</Card.Description>
24
+ </Card.Header>
25
+ </Card>
26
+
27
+ {threadId ? (
28
+ <div className="min-h-0 overflow-hidden rounded-xl border">
29
+ <AiConversation agentId={STARTER_ASSISTANT_AGENT_ID} threadId={threadId} />
30
+ </div>
31
+ ) : (
32
+ <Skeleton className="min-h-0 rounded-xl" />
33
+ )}
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,88 @@
1
+ import { Button, Card, Chip, EmptyState, Skeleton } from "@heroui/react";
2
+ import { ImageIcon, UploadIcon } from "lucide-react";
3
+ import { useRef } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { useFilesRoute } from "./hooks/useFilesRoute";
6
+
7
+ export function FilesRoute() {
8
+ const { t } = useTranslation("{{APP_SLUG}}");
9
+ const files = useFilesRoute();
10
+ const inputRef = useRef<HTMLInputElement>(null);
11
+
12
+ return (
13
+ <div className="p-4 grid gap-6" data-testid="files-route">
14
+ <Card>
15
+ <Card.Header className="flex flex-col gap-1">
16
+ <p className="text-xs font-medium uppercase tracking-wide text-muted">
17
+ {t("files.hero.eyebrow")}
18
+ </p>
19
+ <Card.Title>{t("files.hero.title")}</Card.Title>
20
+ <Card.Description>{t("files.hero.body")}</Card.Description>
21
+ </Card.Header>
22
+ <Card.Content className="grid gap-4">
23
+ <div className="flex flex-wrap items-center gap-3">
24
+ <input
25
+ ref={inputRef}
26
+ type="file"
27
+ accept="image/jpeg,image/png,image/webp"
28
+ className="sr-only"
29
+ data-testid="file-upload-input"
30
+ onChange={(event) => {
31
+ const file = event.target.files?.[0];
32
+ if (file) files.upload(file);
33
+ event.target.value = "";
34
+ }}
35
+ />
36
+ <Button
37
+ variant="primary"
38
+ isDisabled={files.isUploading}
39
+ data-testid="file-upload"
40
+ onPress={() => inputRef.current?.click()}
41
+ >
42
+ <span className="inline-flex items-center gap-2">
43
+ <UploadIcon className="h-4 w-4" />
44
+ {t("files.hero.upload")}
45
+ </span>
46
+ </Button>
47
+ <Chip variant="soft" color="default">
48
+ {files.isFetching ? t("files.hero.syncing") : t("files.hero.synced")}
49
+ </Chip>
50
+ </div>
51
+ </Card.Content>
52
+ </Card>
53
+
54
+ {files.isLoading ? (
55
+ <div className="grid gap-3">
56
+ <Skeleton className="h-24 rounded-xl" />
57
+ <Skeleton className="h-24 rounded-xl" />
58
+ </div>
59
+ ) : files.rows.length === 0 ? (
60
+ <EmptyState className="flex flex-col items-center gap-4 py-10 text-center">
61
+ <ImageIcon className="size-6 text-muted" />
62
+ <div className="grid gap-2">
63
+ <p className="text-lg font-semibold">{t("files.empty.title")}</p>
64
+ <p className="max-w-xl text-sm text-muted">{t("files.empty.body")}</p>
65
+ </div>
66
+ </EmptyState>
67
+ ) : (
68
+ <div className="grid gap-3">
69
+ {files.rows.map((row) => (
70
+ <Card key={row.id} data-testid="file-row">
71
+ <Card.Header className="flex flex-wrap items-start justify-between gap-3">
72
+ <div className="grid gap-1">
73
+ <Card.Title>{row.originalName}</Card.Title>
74
+ <Card.Description>{row.contentType}</Card.Description>
75
+ </div>
76
+ {row.memberId ? (
77
+ <Chip size="sm" variant="soft" color="default">
78
+ {t("files.row.organization")}
79
+ </Chip>
80
+ ) : null}
81
+ </Card.Header>
82
+ </Card>
83
+ ))}
84
+ </div>
85
+ )}
86
+ </div>
87
+ );
88
+ }