minutework 0.1.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 (203) hide show
  1. package/EXTERNAL_ALPHA.md +74 -0
  2. package/README.md +57 -0
  3. package/assets/claude-local/CLAUDE.md.template +45 -0
  4. package/assets/claude-local/bundle.json +22 -0
  5. package/assets/claude-local/skills/README.md +6 -0
  6. package/assets/claude-local/skills/app-pack-authoring.md +8 -0
  7. package/assets/claude-local/skills/event-bus.md +8 -0
  8. package/assets/claude-local/skills/ontology-mapping.md +8 -0
  9. package/assets/claude-local/skills/openclaw-skill-importer.md +7 -0
  10. package/assets/claude-local/skills/schema-engine.md +8 -0
  11. package/assets/claude-local/skills/secrets-runtime-bridge.md +9 -0
  12. package/assets/claude-local/skills/sidecar-generation.md +9 -0
  13. package/assets/templates/fastapi-sidecar/.env.example +8 -0
  14. package/assets/templates/fastapi-sidecar/README.md +77 -0
  15. package/assets/templates/fastapi-sidecar/poetry.lock +757 -0
  16. package/assets/templates/fastapi-sidecar/pyproject.toml +42 -0
  17. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/__init__.py +3 -0
  18. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/auth.py +70 -0
  19. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/__init__.py +3 -0
  20. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/client.py +71 -0
  21. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/logging_utils.py +25 -0
  22. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +85 -0
  23. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/receipts.py +24 -0
  24. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/settings.py +41 -0
  25. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/template_validation.py +26 -0
  26. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/worker.py +33 -0
  27. package/assets/templates/fastapi-sidecar/template.json +43 -0
  28. package/assets/templates/fastapi-sidecar/template.schema.json +160 -0
  29. package/assets/templates/fastapi-sidecar/tests/conftest.py +36 -0
  30. package/assets/templates/fastapi-sidecar/tests/test_app.py +39 -0
  31. package/assets/templates/fastapi-sidecar/tests/test_auth.py +32 -0
  32. package/assets/templates/fastapi-sidecar/tests/test_bridge_client.py +31 -0
  33. package/assets/templates/fastapi-sidecar/tests/test_materialization.py +55 -0
  34. package/assets/templates/fastapi-sidecar/tests/test_template_contract.py +49 -0
  35. package/assets/templates/fastapi-sidecar/tests/test_worker.py +7 -0
  36. package/assets/templates/fastapi-sidecar/tools/template/validate_template.py +20 -0
  37. package/assets/templates/next-tenant-app/.env.example +8 -0
  38. package/assets/templates/next-tenant-app/.storybook/main.ts +19 -0
  39. package/assets/templates/next-tenant-app/.storybook/preview.tsx +38 -0
  40. package/assets/templates/next-tenant-app/README.md +115 -0
  41. package/assets/templates/next-tenant-app/components.json +21 -0
  42. package/assets/templates/next-tenant-app/eslint.config.mjs +41 -0
  43. package/assets/templates/next-tenant-app/next-env.d.ts +6 -0
  44. package/assets/templates/next-tenant-app/next.config.ts +8 -0
  45. package/assets/templates/next-tenant-app/package-lock.json +9682 -0
  46. package/assets/templates/next-tenant-app/package.json +59 -0
  47. package/assets/templates/next-tenant-app/pnpm-lock.yaml +6062 -0
  48. package/assets/templates/next-tenant-app/postcss.config.mjs +8 -0
  49. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +90 -0
  50. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +78 -0
  51. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +31 -0
  52. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +16 -0
  53. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +79 -0
  54. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +40 -0
  55. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +42 -0
  56. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +29 -0
  57. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +26 -0
  58. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +40 -0
  59. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +47 -0
  60. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +43 -0
  61. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +45 -0
  62. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +83 -0
  63. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +30 -0
  64. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +20 -0
  65. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +62 -0
  66. package/assets/templates/next-tenant-app/src/app/app/page.tsx +24 -0
  67. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +70 -0
  68. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +57 -0
  69. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +42 -0
  70. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +37 -0
  71. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +70 -0
  72. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +55 -0
  73. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +42 -0
  74. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +37 -0
  75. package/assets/templates/next-tenant-app/src/app/globals.css +70 -0
  76. package/assets/templates/next-tenant-app/src/app/layout.tsx +69 -0
  77. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +55 -0
  78. package/assets/templates/next-tenant-app/src/app/login/page.tsx +33 -0
  79. package/assets/templates/next-tenant-app/src/app/page.test.ts +56 -0
  80. package/assets/templates/next-tenant-app/src/app/page.tsx +35 -0
  81. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +55 -0
  82. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +35 -0
  83. package/assets/templates/next-tenant-app/src/app/providers.tsx +25 -0
  84. package/assets/templates/next-tenant-app/src/app/robots.test.ts +20 -0
  85. package/assets/templates/next-tenant-app/src/app/robots.ts +18 -0
  86. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +49 -0
  87. package/assets/templates/next-tenant-app/src/app/sitemap.ts +54 -0
  88. package/assets/templates/next-tenant-app/src/components/ui/button.tsx +59 -0
  89. package/assets/templates/next-tenant-app/src/components/ui/input.tsx +21 -0
  90. package/assets/templates/next-tenant-app/src/design-system/docs/governance.mdx +26 -0
  91. package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.stories.tsx +48 -0
  92. package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.tsx +26 -0
  93. package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.stories.tsx +26 -0
  94. package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.tsx +35 -0
  95. package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
  96. package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
  97. package/assets/templates/next-tenant-app/src/design-system/primitives/button.stories.tsx +37 -0
  98. package/assets/templates/next-tenant-app/src/design-system/primitives/button.ts +1 -0
  99. package/assets/templates/next-tenant-app/src/design-system/primitives/input.stories.tsx +26 -0
  100. package/assets/templates/next-tenant-app/src/design-system/primitives/input.ts +1 -0
  101. package/assets/templates/next-tenant-app/src/design-system/recipes/chrome.ts +28 -0
  102. package/assets/templates/next-tenant-app/src/design-system/tokens/foundation.css +31 -0
  103. package/assets/templates/next-tenant-app/src/design-system/tokens/index.css +3 -0
  104. package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.json +85 -0
  105. package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.ts +87 -0
  106. package/assets/templates/next-tenant-app/src/design-system/tokens/semantic.css +105 -0
  107. package/assets/templates/next-tenant-app/src/design-system/tokens/theme.css +59 -0
  108. package/assets/templates/next-tenant-app/src/design-system/tokens/tokens.stories.tsx +71 -0
  109. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +198 -0
  110. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +153 -0
  111. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +342 -0
  112. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +66 -0
  113. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +108 -0
  114. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +111 -0
  115. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +111 -0
  116. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +624 -0
  117. package/assets/templates/next-tenant-app/src/lib/app-routes.test.ts +20 -0
  118. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +59 -0
  119. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +189 -0
  120. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +318 -0
  121. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +232 -0
  122. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +339 -0
  123. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +5 -0
  124. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +96 -0
  125. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +75 -0
  126. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +25 -0
  127. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +170 -0
  128. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +661 -0
  129. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +131 -0
  130. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +34 -0
  131. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +102 -0
  132. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +87 -0
  133. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +33 -0
  134. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +108 -0
  135. package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +20 -0
  136. package/assets/templates/next-tenant-app/src/lib/public-site.ts +49 -0
  137. package/assets/templates/next-tenant-app/src/lib/theme-config.ts +10 -0
  138. package/assets/templates/next-tenant-app/src/lib/theme.tsx +159 -0
  139. package/assets/templates/next-tenant-app/src/lib/utils.ts +6 -0
  140. package/assets/templates/next-tenant-app/template.json +27 -0
  141. package/assets/templates/next-tenant-app/template.schema.json +160 -0
  142. package/assets/templates/next-tenant-app/test/server-only-stub.ts +1 -0
  143. package/assets/templates/next-tenant-app/tools/design-system/build-token-manifest.mjs +3 -0
  144. package/assets/templates/next-tenant-app/tools/design-system/check-imports.mjs +9 -0
  145. package/assets/templates/next-tenant-app/tools/design-system/check-stories.mjs +9 -0
  146. package/assets/templates/next-tenant-app/tools/design-system/check-values.mjs +9 -0
  147. package/assets/templates/next-tenant-app/tools/design-system/checks.mjs +238 -0
  148. package/assets/templates/next-tenant-app/tools/design-system/eslint-plugin-design-system.mjs +184 -0
  149. package/assets/templates/next-tenant-app/tools/design-system/playwright.config.mjs +34 -0
  150. package/assets/templates/next-tenant-app/tools/design-system/run-checks.mjs +22 -0
  151. package/assets/templates/next-tenant-app/tools/design-system/shared.mjs +166 -0
  152. package/assets/templates/next-tenant-app/tools/design-system/visual.spec.ts +41 -0
  153. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +39 -0
  154. package/assets/templates/next-tenant-app/tools/template/validate-template.mjs +45 -0
  155. package/assets/templates/next-tenant-app/tsconfig.json +42 -0
  156. package/assets/templates/next-tenant-app/vitest.config.ts +25 -0
  157. package/bin/minutework.js +40 -0
  158. package/dist/auth.d.ts +59 -0
  159. package/dist/auth.js +338 -0
  160. package/dist/auth.js.map +1 -0
  161. package/dist/browser.d.ts +1 -0
  162. package/dist/browser.js +26 -0
  163. package/dist/browser.js.map +1 -0
  164. package/dist/cli.d.ts +2 -0
  165. package/dist/cli.js +5 -0
  166. package/dist/cli.js.map +1 -0
  167. package/dist/compile.d.ts +20 -0
  168. package/dist/compile.js +121 -0
  169. package/dist/compile.js.map +1 -0
  170. package/dist/config.d.ts +25 -0
  171. package/dist/config.js +102 -0
  172. package/dist/config.js.map +1 -0
  173. package/dist/deploy-state.d.ts +35 -0
  174. package/dist/deploy-state.js +30 -0
  175. package/dist/deploy-state.js.map +1 -0
  176. package/dist/deploy.d.ts +22 -0
  177. package/dist/deploy.js +308 -0
  178. package/dist/deploy.js.map +1 -0
  179. package/dist/developer-client.d.ts +88 -0
  180. package/dist/developer-client.js +78 -0
  181. package/dist/developer-client.js.map +1 -0
  182. package/dist/index.d.ts +27 -0
  183. package/dist/index.js +290 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/init.d.ts +22 -0
  186. package/dist/init.js +421 -0
  187. package/dist/init.js.map +1 -0
  188. package/dist/launcher.d.ts +1 -0
  189. package/dist/launcher.js +50 -0
  190. package/dist/launcher.js.map +1 -0
  191. package/dist/paths.d.ts +12 -0
  192. package/dist/paths.js +33 -0
  193. package/dist/paths.js.map +1 -0
  194. package/dist/sandbox.d.ts +30 -0
  195. package/dist/sandbox.js +852 -0
  196. package/dist/sandbox.js.map +1 -0
  197. package/dist/state.d.ts +46 -0
  198. package/dist/state.js +82 -0
  199. package/dist/state.js.map +1 -0
  200. package/dist/tokens.d.ts +14 -0
  201. package/dist/tokens.js +293 -0
  202. package/dist/tokens.js.map +1 -0
  203. package/package.json +43 -0
@@ -0,0 +1,70 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "../design-system/tokens/index.css";
4
+
5
+ @layer base {
6
+ html {
7
+ height: 100%;
8
+ }
9
+
10
+ body {
11
+ min-height: 100%;
12
+ margin: 0;
13
+ }
14
+
15
+ * {
16
+ @apply border-border outline-ring/50;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ body {
21
+ @apply bg-background text-foreground antialiased;
22
+ }
23
+
24
+ a {
25
+ color: inherit;
26
+ }
27
+ }
28
+
29
+ @layer components {
30
+ .public-site-background {
31
+ background:
32
+ linear-gradient(
33
+ 180deg,
34
+ var(--color-background) 0%,
35
+ color-mix(in oklab, var(--color-surface-raised) 68%, white) 100%
36
+ );
37
+ }
38
+
39
+ .public-site-orb {
40
+ background:
41
+ radial-gradient(
42
+ circle at top left,
43
+ color-mix(in oklab, var(--color-primary) 28%, white) 0%,
44
+ transparent 48%
45
+ ),
46
+ radial-gradient(
47
+ circle at top right,
48
+ color-mix(in oklab, var(--color-chart-2) 18%, white) 0%,
49
+ transparent 42%
50
+ );
51
+ }
52
+
53
+ .marketing-hero-surface {
54
+ background:
55
+ linear-gradient(
56
+ 135deg,
57
+ color-mix(in oklab, var(--color-surface) 78%, white) 0%,
58
+ color-mix(in oklab, var(--color-primary) 8%, white) 100%
59
+ );
60
+ }
61
+
62
+ .marketing-hero-orb {
63
+ background:
64
+ radial-gradient(
65
+ circle at center,
66
+ color-mix(in oklab, var(--color-chart-2) 14%, white) 0%,
67
+ transparent 70%
68
+ );
69
+ }
70
+ }
@@ -0,0 +1,69 @@
1
+ import "./globals.css";
2
+ import type { Metadata } from "next";
3
+ import { Geist, Geist_Mono } from "next/font/google";
4
+ import { cookies } from "next/headers";
5
+ import type { ReactNode } from "react";
6
+
7
+ import { AppProviders } from "./providers";
8
+ import { env } from "@/lib/platform/env.server";
9
+ import { resolvePublicMetadataBase } from "@/lib/public-site";
10
+ import {
11
+ isThemeMode,
12
+ THEME_COOKIE_NAME,
13
+ type ThemeMode,
14
+ } from "@/lib/theme-config";
15
+
16
+ const geistSans = Geist({
17
+ subsets: ["latin"],
18
+ variable: "--font-sans",
19
+ });
20
+
21
+ const geistMono = Geist_Mono({
22
+ subsets: ["latin"],
23
+ variable: "--font-mono",
24
+ });
25
+
26
+ export function generateMetadata(): Metadata {
27
+ return {
28
+ metadataBase: resolvePublicMetadataBase(),
29
+ title: {
30
+ default: env.MW_TEMPLATE_APP_NAME,
31
+ template: `%s | ${env.MW_TEMPLATE_APP_NAME}`,
32
+ },
33
+ description:
34
+ "SEO-first public and authenticated Next.js starter with a server-owned platform session BFF.",
35
+ openGraph: {
36
+ title: env.MW_TEMPLATE_APP_NAME,
37
+ description:
38
+ "SEO-first public and authenticated Next.js starter with a server-owned platform session BFF.",
39
+ url: env.MW_PUBLIC_BASE_URL,
40
+ siteName: env.MW_TEMPLATE_APP_NAME,
41
+ type: "website",
42
+ },
43
+ };
44
+ }
45
+
46
+ export default async function RootLayout({ children }: { children: ReactNode }) {
47
+ const cookieStore = await cookies();
48
+ const themeCookie = cookieStore.get(THEME_COOKIE_NAME)?.value;
49
+ const initialTheme: ThemeMode = isThemeMode(themeCookie) ? themeCookie : "system";
50
+ const resolvedThemeClass =
51
+ initialTheme === "light" || initialTheme === "dark" ? initialTheme : undefined;
52
+ const colorScheme =
53
+ initialTheme === "light" || initialTheme === "dark" ? initialTheme : undefined;
54
+
55
+ return (
56
+ <html
57
+ lang="en"
58
+ suppressHydrationWarning
59
+ className={resolvedThemeClass}
60
+ style={colorScheme ? { colorScheme } : undefined}
61
+ >
62
+ <body
63
+ className={`${geistSans.variable} ${geistMono.variable} bg-background font-sans text-foreground antialiased`}
64
+ >
65
+ <AppProviders initialTheme={initialTheme}>{children}</AppProviders>
66
+ </body>
67
+ </html>
68
+ );
69
+ }
@@ -0,0 +1,55 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const redirect = vi.fn((href: string) => {
4
+ throw new Error(`redirect:${href}`);
5
+ });
6
+ const resolveCurrentSession = vi.fn();
7
+ const readPlatformAuthState = vi.fn();
8
+
9
+ vi.mock("next/navigation", () => ({
10
+ redirect,
11
+ }));
12
+
13
+ vi.mock("@/features/auth/components/login-screen", () => ({
14
+ LoginScreen: () => null,
15
+ }));
16
+
17
+ vi.mock("@/lib/platform/client.server", () => ({
18
+ resolveCurrentSession,
19
+ }));
20
+
21
+ vi.mock("@/lib/platform/endpoints.server", () => ({
22
+ platformAuthEndpoints: {
23
+ operatorConsole: "http://127.0.0.1:8000/ops/login/",
24
+ },
25
+ }));
26
+
27
+ vi.mock("@/lib/platform/env.server", () => ({
28
+ env: {
29
+ MW_TEMPLATE_APP_NAME: "MinuteWork Combined Starter",
30
+ },
31
+ }));
32
+
33
+ vi.mock("@/lib/platform/session.server", () => ({
34
+ readPlatformAuthState,
35
+ }));
36
+
37
+ describe("login page", () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ vi.resetModules();
41
+ });
42
+
43
+ it("redirects authenticated users back to the home shell", async () => {
44
+ readPlatformAuthState.mockResolvedValue({
45
+ sessionId: "session-1",
46
+ csrfToken: "csrf-1",
47
+ });
48
+ resolveCurrentSession.mockResolvedValue({ authenticated: true });
49
+
50
+ const page = await import("./page");
51
+
52
+ await expect(page.default()).rejects.toThrow("redirect:/app");
53
+ expect(redirect).toHaveBeenCalledWith("/app");
54
+ });
55
+ });
@@ -0,0 +1,33 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { LoginScreen } from "@/features/auth/components/login-screen";
4
+ import { appRoutes } from "@/lib/app-routes";
5
+ import { resolveCurrentSession } from "@/lib/platform/client.server";
6
+ import { platformAuthEndpoints } from "@/lib/platform/endpoints.server";
7
+ import { env } from "@/lib/platform/env.server";
8
+ import { readPlatformAuthState } from "@/lib/platform/session.server";
9
+
10
+ export const metadata = {
11
+ title: "Sign In",
12
+ description:
13
+ "Authenticate into the combined MinuteWork starter to continue into the private workspace surface under /app.",
14
+ robots: {
15
+ index: false,
16
+ follow: false,
17
+ },
18
+ };
19
+
20
+ export default async function LoginPage() {
21
+ const session = await resolveCurrentSession(await readPlatformAuthState());
22
+
23
+ if (session.authenticated) {
24
+ redirect(appRoutes.appHome);
25
+ }
26
+
27
+ return (
28
+ <LoginScreen
29
+ appName={env.MW_TEMPLATE_APP_NAME}
30
+ operatorConsoleHref={platformAuthEndpoints.operatorConsole}
31
+ />
32
+ );
33
+ }
@@ -0,0 +1,56 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const getMarketingPage = vi.fn();
4
+ const getSiteConfig = vi.fn();
5
+
6
+ vi.mock("@/features/public-shell/components/marketing-page-canvas", () => ({
7
+ MarketingPageCanvas: () => null,
8
+ }));
9
+
10
+ vi.mock("@/features/public-shell/components/public-site-shell", () => ({
11
+ PublicSiteShell: ({ children }: { children: unknown }) => children,
12
+ }));
13
+
14
+ vi.mock("@/lib/content/adapter.server", () => ({
15
+ getMarketingPage,
16
+ getSiteConfig,
17
+ }));
18
+
19
+ describe("home page", () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ vi.resetModules();
23
+ });
24
+
25
+ it("renders public home content without auth redirects", async () => {
26
+ getSiteConfig.mockResolvedValue({
27
+ siteName: "MinuteWork Combined Starter",
28
+ siteDescription: "Combined starter",
29
+ primaryCta: { label: "Sign In", href: "/login" },
30
+ secondaryCta: { label: "Docs", href: "/docs" },
31
+ });
32
+ getMarketingPage.mockResolvedValue({
33
+ pageKey: "home",
34
+ });
35
+
36
+ const page = await import("./page");
37
+
38
+ await expect(page.default()).resolves.toBeDefined();
39
+ expect(getSiteConfig).toHaveBeenCalledTimes(1);
40
+ expect(getMarketingPage).toHaveBeenCalledWith("home");
41
+ });
42
+
43
+ it("renders the built-in empty home shell when no home page has been published", async () => {
44
+ getSiteConfig.mockResolvedValue({
45
+ siteName: "MinuteWork Combined Starter",
46
+ siteDescription: "Combined starter",
47
+ primaryCta: { label: "Sign In", href: "/login" },
48
+ secondaryCta: { label: "Docs", href: "/docs" },
49
+ });
50
+ getMarketingPage.mockResolvedValue(null);
51
+
52
+ const page = await import("./page");
53
+
54
+ await expect(page.default()).resolves.toBeDefined();
55
+ });
56
+ });
@@ -0,0 +1,35 @@
1
+ import { MarketingPageCanvas } from "@/features/public-shell/components/marketing-page-canvas";
2
+ import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
3
+ import { getMarketingPage, getSiteConfig } from "@/lib/content/adapter.server";
4
+ import { buildEmptyMarketingPage } from "@/lib/content/empty-state";
5
+ import { appRoutes } from "@/lib/app-routes";
6
+ import { buildPublicMetadata } from "@/lib/public-site";
7
+
8
+ export async function generateMetadata() {
9
+ const [siteConfig, page] = await Promise.all([
10
+ getSiteConfig(),
11
+ getMarketingPage("home"),
12
+ ]);
13
+ const resolvedPage = page ?? buildEmptyMarketingPage("home", siteConfig);
14
+
15
+ return buildPublicMetadata({
16
+ title: resolvedPage.seo.title,
17
+ description: resolvedPage.seo.description,
18
+ path: resolvedPage.path,
19
+ siteName: siteConfig.siteName,
20
+ });
21
+ }
22
+
23
+ export default async function HomePage() {
24
+ const [siteConfig, page] = await Promise.all([
25
+ getSiteConfig(),
26
+ getMarketingPage("home"),
27
+ ]);
28
+ const resolvedPage = page ?? buildEmptyMarketingPage("home", siteConfig);
29
+
30
+ return (
31
+ <PublicSiteShell activeHref={appRoutes.publicHome} siteConfig={siteConfig}>
32
+ <MarketingPageCanvas page={resolvedPage} />
33
+ </PublicSiteShell>
34
+ );
35
+ }
@@ -0,0 +1,55 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const getMarketingPage = vi.fn();
4
+ const getSiteConfig = vi.fn();
5
+
6
+ vi.mock("@/features/public-shell/components/marketing-page-canvas", () => ({
7
+ MarketingPageCanvas: () => null,
8
+ }));
9
+
10
+ vi.mock("@/features/public-shell/components/public-site-shell", () => ({
11
+ PublicSiteShell: ({ children }: { children: unknown }) => children,
12
+ }));
13
+
14
+ vi.mock("@/lib/content/adapter.server", () => ({
15
+ getMarketingPage,
16
+ getSiteConfig,
17
+ }));
18
+
19
+ describe("pricing page", () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ vi.resetModules();
23
+ });
24
+
25
+ it("renders seeded pricing content", async () => {
26
+ getSiteConfig.mockResolvedValue({
27
+ siteName: "MinuteWork Combined Starter",
28
+ siteDescription: "Combined starter",
29
+ primaryCta: { label: "Sign In", href: "/login" },
30
+ secondaryCta: { label: "Docs", href: "/docs" },
31
+ });
32
+ getMarketingPage.mockResolvedValue({
33
+ pageKey: "pricing",
34
+ });
35
+
36
+ const page = await import("./page");
37
+
38
+ await expect(page.default()).resolves.toBeDefined();
39
+ expect(getMarketingPage).toHaveBeenCalledWith("pricing");
40
+ });
41
+
42
+ it("renders the built-in empty pricing shell when no pricing page has been published", async () => {
43
+ getSiteConfig.mockResolvedValue({
44
+ siteName: "MinuteWork Combined Starter",
45
+ siteDescription: "Combined starter",
46
+ primaryCta: { label: "Sign In", href: "/login" },
47
+ secondaryCta: { label: "Docs", href: "/docs" },
48
+ });
49
+ getMarketingPage.mockResolvedValue(null);
50
+
51
+ const page = await import("./page");
52
+
53
+ await expect(page.default()).resolves.toBeDefined();
54
+ });
55
+ });
@@ -0,0 +1,35 @@
1
+ import { MarketingPageCanvas } from "@/features/public-shell/components/marketing-page-canvas";
2
+ import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
3
+ import { getMarketingPage, getSiteConfig } from "@/lib/content/adapter.server";
4
+ import { buildEmptyMarketingPage } from "@/lib/content/empty-state";
5
+ import { appRoutes } from "@/lib/app-routes";
6
+ import { buildPublicMetadata } from "@/lib/public-site";
7
+
8
+ export async function generateMetadata() {
9
+ const [siteConfig, page] = await Promise.all([
10
+ getSiteConfig(),
11
+ getMarketingPage("pricing"),
12
+ ]);
13
+ const resolvedPage = page ?? buildEmptyMarketingPage("pricing", siteConfig);
14
+
15
+ return buildPublicMetadata({
16
+ title: resolvedPage.seo.title,
17
+ description: resolvedPage.seo.description,
18
+ path: resolvedPage.path,
19
+ siteName: siteConfig.siteName,
20
+ });
21
+ }
22
+
23
+ export default async function PricingPage() {
24
+ const [siteConfig, page] = await Promise.all([
25
+ getSiteConfig(),
26
+ getMarketingPage("pricing"),
27
+ ]);
28
+ const resolvedPage = page ?? buildEmptyMarketingPage("pricing", siteConfig);
29
+
30
+ return (
31
+ <PublicSiteShell activeHref={appRoutes.pricing} siteConfig={siteConfig}>
32
+ <MarketingPageCanvas page={resolvedPage} />
33
+ </PublicSiteShell>
34
+ );
35
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { ThemeProvider } from "@/lib/theme";
6
+ import type { ThemeMode } from "@/lib/theme-config";
7
+
8
+ export function AppProviders({
9
+ children,
10
+ initialTheme,
11
+ }: {
12
+ children: ReactNode;
13
+ initialTheme: ThemeMode;
14
+ }) {
15
+ return (
16
+ <ThemeProvider
17
+ attribute="class"
18
+ defaultTheme={initialTheme}
19
+ enableSystem
20
+ disableTransitionOnChange
21
+ >
22
+ {children}
23
+ </ThemeProvider>
24
+ );
25
+ }
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import robots from "./robots";
4
+
5
+ describe("robots route", () => {
6
+ it("returns a public robots policy and sitemap url", () => {
7
+ expect(process.env.MW_PUBLIC_BASE_URL).toBe("http://127.0.0.1:3000");
8
+
9
+ const metadata = robots();
10
+
11
+ expect(metadata.host).toBe("127.0.0.1:3000");
12
+ expect(metadata.sitemap).toBe("http://127.0.0.1:3000/sitemap.xml");
13
+ expect(metadata.rules).toEqual([
14
+ {
15
+ userAgent: "*",
16
+ allow: "/",
17
+ },
18
+ ]);
19
+ });
20
+ });
@@ -0,0 +1,18 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ import { resolvePublicMetadataBase } from "@/lib/public-site";
4
+
5
+ export default function robots(): MetadataRoute.Robots {
6
+ const metadataBase = resolvePublicMetadataBase();
7
+
8
+ return {
9
+ rules: [
10
+ {
11
+ userAgent: "*",
12
+ allow: "/",
13
+ },
14
+ ],
15
+ sitemap: new URL("/sitemap.xml", metadataBase).toString(),
16
+ host: metadataBase.host,
17
+ };
18
+ }
@@ -0,0 +1,49 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const { listEntries } = vi.hoisted(() => ({
4
+ listEntries: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("@/lib/content/adapter.server", () => ({
8
+ listEntries,
9
+ }));
10
+
11
+ import sitemap from "./sitemap";
12
+
13
+ describe("sitemap route", () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ it("includes the public route surface and published content entries", async () => {
19
+ listEntries.mockImplementation(async (kind: string) =>
20
+ kind === "docs"
21
+ ? [{ slug: ["guides", "public-site-content"] }]
22
+ : [{ slug: ["public-site-api-default"], publishedAt: "2026-03-26T10:00:00.000Z" }],
23
+ );
24
+
25
+ expect(process.env.MW_PUBLIC_BASE_URL).toBe("http://127.0.0.1:3000");
26
+
27
+ const entries = await sitemap();
28
+ const urls = entries.map((entry) => entry.url);
29
+
30
+ expect(urls).toContain("http://127.0.0.1:3000/");
31
+ expect(urls).toContain("http://127.0.0.1:3000/pricing");
32
+ expect(urls).toContain("http://127.0.0.1:3000/docs");
33
+ expect(urls).toContain("http://127.0.0.1:3000/docs/guides/public-site-content");
34
+ expect(urls).toContain("http://127.0.0.1:3000/blog");
35
+ expect(urls).toContain("http://127.0.0.1:3000/blog/public-site-api-default");
36
+ });
37
+
38
+ it("keeps home and pricing in the sitemap even when the snapshot has no marketing pages", async () => {
39
+ listEntries.mockResolvedValue([]);
40
+
41
+ const entries = await sitemap();
42
+ const urls = entries.map((entry) => entry.url);
43
+
44
+ expect(urls).toContain("http://127.0.0.1:3000/");
45
+ expect(urls).toContain("http://127.0.0.1:3000/pricing");
46
+ expect(urls).toContain("http://127.0.0.1:3000/docs");
47
+ expect(urls).toContain("http://127.0.0.1:3000/blog");
48
+ });
49
+ });
@@ -0,0 +1,54 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ import { listEntries } from "@/lib/content/adapter.server";
4
+ import { appRoutes } from "@/lib/app-routes";
5
+ import { resolvePublicSiteUrl } from "@/lib/public-site";
6
+
7
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
8
+ const [docsEntries, blogEntries] = await Promise.all([
9
+ listEntries("docs"),
10
+ listEntries("blog"),
11
+ ]);
12
+
13
+ const entries: MetadataRoute.Sitemap = [
14
+ {
15
+ url: resolvePublicSiteUrl(appRoutes.publicHome).toString(),
16
+ changeFrequency: "weekly",
17
+ priority: 1,
18
+ },
19
+ {
20
+ url: resolvePublicSiteUrl(appRoutes.pricing).toString(),
21
+ changeFrequency: "monthly",
22
+ priority: 0.8,
23
+ },
24
+ {
25
+ url: resolvePublicSiteUrl(appRoutes.docsIndex).toString(),
26
+ changeFrequency: "weekly",
27
+ priority: 0.8,
28
+ },
29
+ {
30
+ url: resolvePublicSiteUrl(appRoutes.blogIndex).toString(),
31
+ changeFrequency: "weekly",
32
+ priority: 0.8,
33
+ },
34
+ ];
35
+
36
+ entries.push(
37
+ ...docsEntries.map((entry) => ({
38
+ url: resolvePublicSiteUrl(appRoutes.docsPage(entry.slug)).toString(),
39
+ changeFrequency: "weekly" as const,
40
+ priority: 0.7,
41
+ })),
42
+ );
43
+
44
+ entries.push(
45
+ ...blogEntries.map((entry) => ({
46
+ url: resolvePublicSiteUrl(appRoutes.blogPost(entry.slug)).toString(),
47
+ changeFrequency: "monthly" as const,
48
+ priority: 0.7,
49
+ ...(entry.publishedAt ? { lastModified: new Date(entry.publishedAt) } : {}),
50
+ })),
51
+ );
52
+
53
+ return entries;
54
+ }
@@ -0,0 +1,59 @@
1
+ import * as React from "react";
2
+
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const buttonVariants = cva(
9
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-white hover:bg-destructive/90 dark:bg-destructive/60",
16
+ outline:
17
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
18
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
24
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
25
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
26
+ icon: "size-9",
27
+ "icon-sm": "size-8",
28
+ "icon-lg": "size-10",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ },
36
+ );
37
+
38
+ function Button({
39
+ className,
40
+ variant,
41
+ size,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"button"> &
45
+ VariantProps<typeof buttonVariants> & {
46
+ asChild?: boolean;
47
+ }) {
48
+ const Comp = asChild ? Slot : "button";
49
+
50
+ return (
51
+ <Comp
52
+ data-slot="button"
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ export { Button, buttonVariants };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30 md:text-sm",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Input };