minutework 0.1.31 → 0.1.33

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 (168) hide show
  1. package/EXTERNAL_ALPHA.md +33 -33
  2. package/README.md +34 -34
  3. package/assets/claude-local/CLAUDE.md.template +12 -12
  4. package/assets/claude-local/skills/README.md +3 -1
  5. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +17 -4
  6. package/assets/claude-local/skills/capability-gap-reporting/SKILL.md +3 -3
  7. package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
  8. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +12 -5
  9. package/assets/claude-local/skills/layering-and-import-modes/SKILL.md +2 -2
  10. package/assets/claude-local/skills/openclaw-skill-importer/SKILL.md +2 -2
  11. package/assets/claude-local/skills/project-overview-and-strategy/SKILL.md +8 -8
  12. package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +11 -8
  13. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +6 -5
  14. package/assets/claude-local/skills/vuilder-discovery-output-contract/SKILL.md +6 -6
  15. package/assets/claude-local/skills/workspace-guidance-refresh/SKILL.md +4 -4
  16. package/assets/templates/fastapi-sidecar/pyproject.toml +1 -1
  17. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +1 -1
  18. package/assets/templates/mobile-app/.env.example +4 -4
  19. package/assets/templates/mobile-app/AGENTS.md +3 -3
  20. package/assets/templates/mobile-app/README.md +10 -10
  21. package/assets/templates/mobile-app/app/(app)/_layout.tsx +2 -2
  22. package/assets/templates/mobile-app/app/(app)/index.tsx +2 -2
  23. package/assets/templates/mobile-app/app/(auth)/login.tsx +3 -3
  24. package/assets/templates/mobile-app/app/_layout.tsx +1 -1
  25. package/assets/templates/mobile-app/babel.config.js +1 -1
  26. package/assets/templates/mobile-app/eas.json +1 -1
  27. package/assets/templates/mobile-app/expo-env.d.ts +1 -1
  28. package/assets/templates/mobile-app/metro.config.js +2 -2
  29. package/assets/templates/mobile-app/package.json +1 -1
  30. package/assets/templates/mobile-app/src/mw/client.ts +3 -3
  31. package/assets/templates/mobile-app/src/mw/contracts.ts +2 -2
  32. package/assets/templates/mobile-app/src/mw/endpoints.ts +2 -2
  33. package/assets/templates/mobile-app/src/mw/env.ts +4 -4
  34. package/assets/templates/mobile-app/src/mw/session.ts +1 -1
  35. package/assets/templates/mobile-app/template.json +1 -1
  36. package/assets/templates/mobile-app/tools/template/validate-template.mjs +2 -2
  37. package/assets/templates/mobile-app/tsconfig.json +1 -1
  38. package/assets/templates/next-tenant-app/.env.example +1 -1
  39. package/assets/templates/next-tenant-app/README.md +26 -138
  40. package/assets/templates/next-tenant-app/package.json +1 -0
  41. package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
  42. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
  43. package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
  44. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
  45. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
  46. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
  47. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
  48. package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
  49. package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
  50. package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
  51. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
  52. package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
  53. package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
  54. package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
  55. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
  56. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
  57. package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
  58. package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
  59. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
  60. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
  61. package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +1 -1
  62. package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
  63. package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
  64. package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
  65. package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
  66. package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
  67. package/assets/templates/next-tenant-app/template.json +3 -3
  68. package/assets/templates/next-tenant-app/template.schema.json +1 -0
  69. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
  70. package/assets/templates/next-tenant-app/tools/template/with-public-site-fixture.mjs +2 -2
  71. package/bin/minutework.js +1 -1
  72. package/dist/agent.js +7 -7
  73. package/dist/agent.js.map +1 -1
  74. package/dist/auth.js +7 -7
  75. package/dist/auth.js.map +1 -1
  76. package/dist/compile.js +5 -5
  77. package/dist/config.js +6 -6
  78. package/dist/config.js.map +1 -1
  79. package/dist/deploy.js +7 -7
  80. package/dist/deploy.js.map +1 -1
  81. package/dist/developer-client.js +2 -2
  82. package/dist/developer-client.js.map +1 -1
  83. package/dist/index.js +30 -30
  84. package/dist/index.js.map +1 -1
  85. package/dist/init.js +10 -10
  86. package/dist/init.js.map +1 -1
  87. package/dist/launcher.js +1 -1
  88. package/dist/launcher.js.map +1 -1
  89. package/dist/managed-engine.js +6 -6
  90. package/dist/managed-engine.js.map +1 -1
  91. package/dist/orchestrator-context.js +1 -1
  92. package/dist/orchestrator-context.js.map +1 -1
  93. package/dist/orchestrator.js +15 -15
  94. package/dist/orchestrator.js.map +1 -1
  95. package/dist/paths.js +1 -1
  96. package/dist/paths.js.map +1 -1
  97. package/dist/publish.js +3 -3
  98. package/dist/publish.js.map +1 -1
  99. package/dist/reporting.js +8 -8
  100. package/dist/reporting.js.map +1 -1
  101. package/dist/sandbox.js +5 -5
  102. package/dist/sandbox.js.map +1 -1
  103. package/dist/state.js +1 -1
  104. package/dist/state.js.map +1 -1
  105. package/dist/tokens.js +9 -9
  106. package/dist/tokens.js.map +1 -1
  107. package/dist/workspace-assets.js +6 -6
  108. package/dist/workspace-assets.js.map +1 -1
  109. package/dist/workspace.js +3 -3
  110. package/dist/workspace.js.map +1 -1
  111. package/package.json +3 -3
  112. package/vendor/workspace-mcp/context.d.ts +6 -6
  113. package/vendor/workspace-mcp/context.js +56 -56
  114. package/vendor/workspace-mcp/context.js.map +1 -1
  115. package/vendor/workspace-mcp/types.d.ts +4 -0
  116. package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
  117. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
  118. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
  119. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
  120. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
  121. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
  122. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
  123. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
  124. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
  125. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
  126. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
  127. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
  128. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
  129. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
  130. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
  131. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
  132. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
  133. package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
  134. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
  135. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
  136. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
  137. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
  138. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
  139. package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
  140. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
  141. package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
  142. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
  143. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
  144. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
  145. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
  146. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
  147. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
  148. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
  149. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
  150. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
  151. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
  152. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
  153. package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
  154. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
  155. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
  156. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
  157. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
  158. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
  159. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
  160. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
  161. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
  162. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
  163. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
  164. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
  165. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
  166. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
  167. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
  168. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
@@ -1,49 +1,16 @@
1
- import { redirect } from "next/navigation";
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
2
 
3
- import { MarketingPageCanvas } from "@/features/public-shell/components/marketing-page-canvas";
4
- import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
5
- import { getMarketingPage, getSiteConfig } from "@/lib/content/adapter.server";
6
- import { buildEmptyMarketingPage } from "@/lib/content/empty-state";
7
- import { appRoutes } from "@/lib/app-routes";
8
- import {
9
- buildDisabledPublicMetadata,
10
- buildPublicMetadata,
11
- isPublicContentDisabled,
12
- } from "@/lib/public-site";
13
-
14
- export async function generateMetadata() {
15
- if (isPublicContentDisabled()) {
16
- return buildDisabledPublicMetadata();
17
- }
18
-
19
- const [siteConfig, page] = await Promise.all([
20
- getSiteConfig(),
21
- getMarketingPage("home"),
22
- ]);
23
- const resolvedPage = page ?? buildEmptyMarketingPage("home", siteConfig);
24
-
25
- return buildPublicMetadata({
26
- title: resolvedPage.seo.title,
27
- description: resolvedPage.seo.description,
28
- path: resolvedPage.path,
29
- siteName: siteConfig.siteName,
30
- });
31
- }
32
-
33
- export default async function HomePage() {
34
- if (isPublicContentDisabled()) {
35
- redirect(appRoutes.appHome);
36
- }
37
-
38
- const [siteConfig, page] = await Promise.all([
39
- getSiteConfig(),
40
- getMarketingPage("home"),
41
- ]);
42
- const resolvedPage = page ?? buildEmptyMarketingPage("home", siteConfig);
3
+ export const metadata = {
4
+ title: "Tenant App",
5
+ description: "Customer-facing tenant app.",
6
+ };
43
7
 
8
+ export default function HomePage() {
44
9
  return (
45
- <PublicSiteShell activeHref={appRoutes.publicHome} siteConfig={siteConfig}>
46
- <MarketingPageCanvas page={resolvedPage} />
47
- </PublicSiteShell>
10
+ <StaticPublicPage
11
+ eyebrow="Tenant App"
12
+ title="A customer app connected to MinuteWork"
13
+ body="Public pages stay generic, while customer auth and manifest APIs come from the MinuteWork web SDK."
14
+ />
48
15
  );
49
16
  }
@@ -1,49 +1,15 @@
1
- import { notFound } from "next/navigation";
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
2
 
3
- import { MarketingPageCanvas } from "@/features/public-shell/components/marketing-page-canvas";
4
- import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
5
- import { getMarketingPage, getSiteConfig } from "@/lib/content/adapter.server";
6
- import { buildEmptyMarketingPage } from "@/lib/content/empty-state";
7
- import { appRoutes } from "@/lib/app-routes";
8
- import {
9
- buildDisabledPublicMetadata,
10
- buildPublicMetadata,
11
- isPublicContentDisabled,
12
- } from "@/lib/public-site";
13
-
14
- export async function generateMetadata() {
15
- if (isPublicContentDisabled()) {
16
- return buildDisabledPublicMetadata();
17
- }
18
-
19
- const [siteConfig, page] = await Promise.all([
20
- getSiteConfig(),
21
- getMarketingPage("pricing"),
22
- ]);
23
- const resolvedPage = page ?? buildEmptyMarketingPage("pricing", siteConfig);
24
-
25
- return buildPublicMetadata({
26
- title: resolvedPage.seo.title,
27
- description: resolvedPage.seo.description,
28
- path: resolvedPage.path,
29
- siteName: siteConfig.siteName,
30
- });
31
- }
32
-
33
- export default async function PricingPage() {
34
- if (isPublicContentDisabled()) {
35
- notFound();
36
- }
37
-
38
- const [siteConfig, page] = await Promise.all([
39
- getSiteConfig(),
40
- getMarketingPage("pricing"),
41
- ]);
42
- const resolvedPage = page ?? buildEmptyMarketingPage("pricing", siteConfig);
3
+ export const metadata = {
4
+ title: "Pricing",
5
+ };
43
6
 
7
+ export default function PricingPage() {
44
8
  return (
45
- <PublicSiteShell activeHref={appRoutes.pricing} siteConfig={siteConfig}>
46
- <MarketingPageCanvas page={resolvedPage} />
47
- </PublicSiteShell>
9
+ <StaticPublicPage
10
+ eyebrow="Pricing"
11
+ title="Plans for customer access"
12
+ body="Clear plan options for customer workspaces."
13
+ />
48
14
  );
49
15
  }
@@ -4,6 +4,7 @@ import type { ReactNode } from "react";
4
4
 
5
5
  import { ThemeProvider } from "@/lib/theme";
6
6
  import type { ThemeMode } from "@/lib/theme-config";
7
+ import { MinuteWorkSdkProvider } from "@/mw/provider";
7
8
 
8
9
  export function AppProviders({
9
10
  children,
@@ -19,7 +20,7 @@ export function AppProviders({
19
20
  enableSystem
20
21
  disableTransitionOnChange
21
22
  >
22
- {children}
23
+ <MinuteWorkSdkProvider>{children}</MinuteWorkSdkProvider>
23
24
  </ThemeProvider>
24
25
  );
25
26
  }
@@ -1,24 +1,9 @@
1
1
  import type { MetadataRoute } from "next";
2
2
 
3
- import {
4
- isPublicContentDisabled,
5
- resolvePublicMetadataBase,
6
- } from "@/lib/public-site";
3
+ import { resolvePublicMetadataBase } from "@/lib/public-site";
7
4
 
8
5
  export default function robots(): MetadataRoute.Robots {
9
6
  const metadataBase = resolvePublicMetadataBase();
10
-
11
- if (!metadataBase || isPublicContentDisabled()) {
12
- return {
13
- rules: [
14
- {
15
- userAgent: "*",
16
- disallow: "/",
17
- },
18
- ],
19
- };
20
- }
21
-
22
7
  return {
23
8
  rules: [
24
9
  {
@@ -26,7 +11,11 @@ export default function robots(): MetadataRoute.Robots {
26
11
  allow: "/",
27
12
  },
28
13
  ],
29
- sitemap: new URL("/sitemap.xml", metadataBase).toString(),
30
- host: metadataBase.host,
14
+ ...(metadataBase
15
+ ? {
16
+ sitemap: new URL("/sitemap.xml", metadataBase).toString(),
17
+ host: metadataBase.host,
18
+ }
19
+ : {}),
31
20
  };
32
21
  }
@@ -1,30 +1,14 @@
1
1
  import type { MetadataRoute } from "next";
2
2
 
3
- import { listEntries } from "@/lib/content/adapter.server";
4
3
  import { appRoutes } from "@/lib/app-routes";
5
- import { isPublicContentDisabled, resolvePublicSiteUrl } from "@/lib/public-site";
4
+ import { resolvePublicSiteUrl } from "@/lib/public-site";
6
5
 
7
6
  function buildSitemapUrl(pathname: string) {
8
- const url = resolvePublicSiteUrl(pathname);
9
-
10
- if (!url) {
11
- throw new Error("MW_PUBLIC_BASE_URL is required to build a public sitemap.");
12
- }
13
-
14
- return url.toString();
7
+ return resolvePublicSiteUrl(pathname)?.toString() ?? pathname;
15
8
  }
16
9
 
17
- export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
18
- if (isPublicContentDisabled()) {
19
- return [];
20
- }
21
-
22
- const [docsEntries, blogEntries] = await Promise.all([
23
- listEntries("docs"),
24
- listEntries("blog"),
25
- ]);
26
-
27
- const entries: MetadataRoute.Sitemap = [
10
+ export default function sitemap(): MetadataRoute.Sitemap {
11
+ return [
28
12
  {
29
13
  url: buildSitemapUrl(appRoutes.publicHome),
30
14
  changeFrequency: "weekly",
@@ -46,23 +30,4 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
46
30
  priority: 0.8,
47
31
  },
48
32
  ];
49
-
50
- entries.push(
51
- ...docsEntries.map((entry) => ({
52
- url: buildSitemapUrl(appRoutes.docsPage(entry.slug)),
53
- changeFrequency: "weekly" as const,
54
- priority: 0.7,
55
- })),
56
- );
57
-
58
- entries.push(
59
- ...blogEntries.map((entry) => ({
60
- url: buildSitemapUrl(appRoutes.blogPost(entry.slug)),
61
- changeFrequency: "monthly" as const,
62
- priority: 0.7,
63
- ...(entry.publishedAt ? { lastModified: new Date(entry.publishedAt) } : {}),
64
- })),
65
- );
66
-
67
- return entries;
68
33
  }
@@ -1,108 +1,81 @@
1
1
  "use client";
2
2
 
3
- import { startTransition, useEffect, useState } from "react";
4
3
  import type { FormEvent } from "react";
4
+ import { startTransition, useState } from "react";
5
+ import { ArrowRight, MailCheck } from "lucide-react";
6
+ import { useRouter, useSearchParams } from "next/navigation";
5
7
 
6
- import { ArrowRight, ShieldCheck, TerminalSquare } from "lucide-react";
7
- import { useRouter } from "next/navigation";
8
-
9
- import { Button } from "@/design-system/primitives/button";
10
- import { Input } from "@/design-system/primitives/input";
11
8
  import { PanelFrame } from "@/design-system/patterns/panel-frame";
12
9
  import { ThemeModeToggle } from "@/design-system/patterns/theme-mode-toggle";
10
+ import { Button } from "@/design-system/primitives/button";
11
+ import { Input } from "@/design-system/primitives/input";
13
12
  import { appRoutes } from "@/lib/app-routes";
13
+ import { useMinuteWorkAuth } from "@minutework/web-auth/react";
14
+
15
+ type AuthMode = "login" | "signup";
14
16
 
15
- export function LoginScreen({
16
- appName,
17
- operatorConsoleHref,
18
- }: {
19
- appName: string;
20
- operatorConsoleHref: string;
21
- }) {
17
+ export function LoginScreen({ appName }: { appName: string }) {
22
18
  const router = useRouter();
23
- const [username, setUsername] = useState("demo-user");
24
- const [password, setPassword] = useState("demo-password");
19
+ const searchParams = useSearchParams();
20
+ const { login, signup } = useMinuteWorkAuth();
21
+ const [mode, setMode] = useState<AuthMode>("login");
22
+ const [email, setEmail] = useState("");
23
+ const [displayName, setDisplayName] = useState("");
24
+ const [password, setPassword] = useState("");
25
+ const [notice, setNotice] = useState("");
25
26
  const [error, setError] = useState("");
26
27
  const [submitting, setSubmitting] = useState(false);
27
28
 
28
- useEffect(() => {
29
- void fetch("/api/auth/session", {
30
- method: "GET",
31
- cache: "no-store",
32
- }).catch(() => null);
33
- }, []);
34
-
35
29
  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
36
30
  event.preventDefault();
37
31
  setSubmitting(true);
38
32
  setError("");
33
+ setNotice("");
39
34
 
40
35
  try {
41
- const response = await fetch("/api/auth/login", {
42
- method: "POST",
43
- headers: { "Content-Type": "application/json" },
44
- body: JSON.stringify({ username, password }),
45
- });
46
-
47
- if (!response.ok) {
48
- const payload = (await response.json().catch(() => ({}))) as {
49
- detail?: string;
50
- };
51
- setError(payload.detail || "Login failed.");
36
+ if (mode === "signup") {
37
+ await signup({
38
+ email,
39
+ password,
40
+ displayName,
41
+ claimRef: searchParams.get("claim_ref") || undefined,
42
+ });
43
+ setNotice("Check your email to finish verification.");
52
44
  setSubmitting(false);
53
45
  return;
54
46
  }
55
47
 
48
+ await login({ email, password });
56
49
  startTransition(() => {
57
50
  router.replace(appRoutes.appHome);
58
51
  router.refresh();
59
52
  });
60
- } catch {
61
- setError("Unable to reach the platform right now.");
53
+ } catch (caught) {
54
+ const message =
55
+ caught instanceof Error ? caught.message : "Unable to complete auth.";
56
+ setError(message);
62
57
  setSubmitting(false);
63
58
  }
64
59
  }
65
60
 
66
61
  return (
67
62
  <main className="min-h-screen bg-background text-foreground">
68
- <div className="mx-auto flex min-h-screen max-w-6xl flex-col gap-10 px-6 py-8 lg:flex-row lg:items-center lg:justify-between">
69
- <div className="flex justify-end lg:hidden">
70
- <ThemeModeToggle className="max-w-xs" />
71
- </div>
72
-
73
- <section className="max-w-2xl space-y-6">
63
+ <div className="mx-auto flex min-h-screen max-w-6xl flex-col gap-8 px-6 py-8 lg:flex-row lg:items-center lg:justify-between">
64
+ <section className="max-w-2xl space-y-5">
65
+ <div className="flex justify-end lg:hidden">
66
+ <ThemeModeToggle className="max-w-xs" />
67
+ </div>
74
68
  <div className="space-y-3">
75
69
  <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
76
- Combined Web Starter
70
+ {appName}
77
71
  </p>
78
72
  <h1 className="max-w-xl text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
79
- Cross the public-to-private boundary without splitting the app.
73
+ Customer workspace
80
74
  </h1>
81
75
  <p className="max-w-xl text-base leading-7 text-muted-foreground sm:text-lg">
82
- {appName} keeps marketing, docs, and blog routes public at the
83
- root while preserving a server-owned platform session BFF under
84
- `/app`.
76
+ Use your customer account to continue.
85
77
  </p>
86
78
  </div>
87
-
88
- <div className="grid gap-3 sm:grid-cols-2">
89
- <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
90
- <TerminalSquare className="size-5 text-primary" />
91
- <p className="text-sm font-medium">Generic tenant shell</p>
92
- <p className="text-sm leading-6 text-muted-foreground">
93
- Start with public storytelling at the root and authenticated
94
- workspace routing under `/app` instead of a private-only home page.
95
- </p>
96
- </PanelFrame>
97
- <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
98
- <ShieldCheck className="size-5 text-primary" />
99
- <p className="text-sm font-medium">Server-owned session</p>
100
- <p className="text-sm leading-6 text-muted-foreground">
101
- Session cookies stay on the server boundary while public pages
102
- remain free of tenant-context reads and private BFF assumptions.
103
- </p>
104
- </PanelFrame>
105
- </div>
106
79
  </section>
107
80
 
108
81
  <div className="flex w-full max-w-md flex-col gap-4 self-center lg:self-auto">
@@ -111,37 +84,69 @@ export function LoginScreen({
111
84
  </div>
112
85
 
113
86
  <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-6 shadow-2xl">
114
- <div className="space-y-2">
115
- <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
116
- Template sign-in
117
- </p>
118
- <h2 className="text-2xl font-semibold tracking-tight">
119
- Log into {appName}
120
- </h2>
121
- <p className="text-sm leading-6 text-muted-foreground">
122
- Use platform tenant credentials to establish a server-owned
123
- session before entering the authenticated shell under `/app`.
124
- </p>
87
+ <div className="grid grid-cols-2 gap-2 rounded-lg border border-border p-1">
88
+ <Button
89
+ type="button"
90
+ variant={mode === "login" ? "default" : "ghost"}
91
+ onClick={() => {
92
+ setMode("login");
93
+ setError("");
94
+ setNotice("");
95
+ }}
96
+ >
97
+ Log in
98
+ </Button>
99
+ <Button
100
+ type="button"
101
+ variant={mode === "signup" ? "default" : "ghost"}
102
+ onClick={() => {
103
+ setMode("signup");
104
+ setError("");
105
+ setNotice("");
106
+ }}
107
+ >
108
+ Sign up
109
+ </Button>
125
110
  </div>
126
111
 
127
112
  <form className="space-y-5" onSubmit={handleSubmit}>
113
+ {mode === "signup" ? (
114
+ <div className="space-y-2">
115
+ <label
116
+ className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
117
+ htmlFor="display-name"
118
+ >
119
+ Name
120
+ </label>
121
+ <Input
122
+ id="display-name"
123
+ autoComplete="name"
124
+ className="h-12 text-base"
125
+ value={displayName}
126
+ onChange={(event) => setDisplayName(event.target.value)}
127
+ />
128
+ </div>
129
+ ) : null}
130
+
128
131
  <div className="space-y-2">
129
132
  <label
130
133
  className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
131
- htmlFor="username"
134
+ htmlFor="email"
132
135
  >
133
- Username
136
+ Email
134
137
  </label>
135
138
  <Input
136
- id="username"
137
- name="username"
138
- autoComplete="username"
139
+ id="email"
140
+ name="email"
141
+ type="email"
142
+ autoComplete="email"
139
143
  className="h-12 text-base"
140
- value={username}
144
+ value={email}
141
145
  onChange={(event) => {
142
- setUsername(event.target.value);
146
+ setEmail(event.target.value);
143
147
  setError("");
144
148
  }}
149
+ required
145
150
  />
146
151
  </div>
147
152
 
@@ -156,40 +161,34 @@ export function LoginScreen({
156
161
  id="password"
157
162
  name="password"
158
163
  type="password"
159
- autoComplete="current-password"
164
+ autoComplete={mode === "signup" ? "new-password" : "current-password"}
160
165
  className="h-12 text-base"
161
166
  value={password}
162
167
  onChange={(event) => {
163
168
  setPassword(event.target.value);
164
169
  setError("");
165
170
  }}
171
+ required
166
172
  />
167
173
  </div>
168
174
 
169
- {error ? (
170
- <p className="text-sm leading-6 text-destructive">{error}</p>
175
+ {notice ? (
176
+ <div className="flex items-start gap-2 rounded-md border border-border bg-muted/40 p-3 text-sm text-foreground">
177
+ <MailCheck className="mt-0.5 size-4 text-primary" />
178
+ <p>{notice}</p>
179
+ </div>
171
180
  ) : null}
181
+ {error ? <p className="text-sm leading-6 text-destructive">{error}</p> : null}
172
182
 
173
183
  <Button
174
184
  type="submit"
175
185
  className="h-12 w-full text-sm font-semibold"
176
186
  disabled={submitting}
177
187
  >
178
- {submitting ? "Signing in" : "Sign In"}
188
+ {submitting ? "Working" : mode === "signup" ? "Create account" : "Log in"}
179
189
  <ArrowRight className="size-4" />
180
190
  </Button>
181
191
  </form>
182
-
183
- <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4">
184
- <p className="text-sm text-muted-foreground">
185
- Need the operator console instead?
186
- </p>
187
- <Button asChild type="button" variant="outline">
188
- <a href={operatorConsoleHref} target="_blank" rel="noreferrer">
189
- Open /ops
190
- </a>
191
- </Button>
192
- </div>
193
192
  </PanelFrame>
194
193
  </div>
195
194
  </div>