minutework 0.1.32 → 0.1.34

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 (91) hide show
  1. package/assets/claude-local/skills/README.md +2 -0
  2. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +24 -1
  3. package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
  4. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +19 -3
  5. package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +9 -6
  6. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +5 -4
  7. package/assets/templates/next-tenant-app/README.md +37 -135
  8. package/assets/templates/next-tenant-app/package.json +1 -0
  9. package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
  10. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
  11. package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
  12. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
  13. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
  14. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
  15. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
  16. package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
  17. package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
  18. package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
  19. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
  20. package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
  21. package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
  22. package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
  23. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
  24. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
  25. package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
  26. package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
  27. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
  28. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
  29. package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
  30. package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
  31. package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
  32. package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
  33. package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
  34. package/assets/templates/next-tenant-app/template.json +3 -3
  35. package/assets/templates/next-tenant-app/template.schema.json +1 -0
  36. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
  37. package/package.json +2 -2
  38. package/vendor/workspace-mcp/types.d.ts +4 -0
  39. package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
  40. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
  41. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
  42. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
  43. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
  44. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
  45. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
  46. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
  47. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
  48. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
  49. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
  50. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
  51. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
  52. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
  53. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
  54. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
  55. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
  56. package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
  57. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
  58. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
  59. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
  60. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
  61. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
  62. package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
  63. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
  64. package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
  65. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
  66. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
  67. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
  68. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
  69. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
  70. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
  71. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
  72. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
  73. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
  74. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
  75. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
  76. package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
  77. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
  78. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
  79. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
  80. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
  81. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
  82. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
  83. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
  84. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
  85. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
  86. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
  87. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
  88. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
  89. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
  90. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
  91. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
@@ -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>
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
- import { Blocks, Building2, FolderKanban, ShieldCheck } from "lucide-react";
4
+ import { ArrowRight, Database, ShieldCheck, UserRound } from "lucide-react";
5
+ import type { TenantCustomerSession } from "@minutework/web-auth";
5
6
 
6
7
  import { PanelFrame } from "@/design-system/patterns/panel-frame";
7
8
  import { StatusBadge } from "@/design-system/patterns/status-badge";
8
9
  import { Button } from "@/design-system/primitives/button";
9
10
  import { appRoutes } from "@/lib/app-routes";
10
- import type { AuthenticatedPlatformSession } from "@/lib/platform/contracts";
11
11
 
12
12
  function DashboardMetric({
13
13
  label,
@@ -28,40 +28,32 @@ function DashboardMetric({
28
28
 
29
29
  export function TenantDashboard({
30
30
  appName,
31
- runtimeCommandExampleEnabled,
32
31
  session,
33
32
  }: {
34
33
  appName: string;
35
- runtimeCommandExampleEnabled: boolean;
36
- session: AuthenticatedPlatformSession;
34
+ session: TenantCustomerSession;
37
35
  }) {
38
36
  return (
39
37
  <div className="grid gap-6">
40
38
  <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-6">
41
39
  <div className="space-y-2">
42
- <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
43
- Authenticated Workspace
44
- </p>
45
40
  <h2 className="text-3xl font-semibold tracking-tight">
46
- {appName} ships with a reusable authenticated shell behind `/app`.
41
+ {appName} workspace
47
42
  </h2>
48
43
  <p className="max-w-3xl text-sm leading-7 text-muted-foreground">
49
- This template is designed for Builder materialization into a writable
50
- sandbox. Public storytelling lives at the root, while the
51
- authenticated shell, tenant context controls, password settings,
52
- route gating, and BFF auth boundary remain real under `/app`.
44
+ Customer session state is loaded through the MinuteWork web auth SDK.
53
45
  </p>
54
46
  </div>
55
47
 
56
48
  <div className="grid gap-3 md:grid-cols-3">
57
- <DashboardMetric label="Signed-in user" value={session.user.username} />
58
49
  <DashboardMetric
59
- label="Active tenant"
60
- value={session.active_tenant.tenant_name}
50
+ label="Customer"
51
+ value={session.user?.display_name || session.user?.email || "Customer"}
61
52
  />
53
+ <DashboardMetric label="Tenant" value={session.tenant.name} />
62
54
  <DashboardMetric
63
- label="Membership count"
64
- value={`${session.memberships.length}`}
55
+ label="Role"
56
+ value={session.customer_membership?.roles.join(", ") || "customer"}
65
57
  />
66
58
  </div>
67
59
  </PanelFrame>
@@ -71,85 +63,58 @@ export function TenantDashboard({
71
63
  <div className="flex items-center gap-2">
72
64
  <ShieldCheck className="size-5 text-primary" />
73
65
  <h3 className="text-xl font-semibold tracking-tight">
74
- Auth profile
66
+ Tenant web auth
75
67
  </h3>
76
68
  </div>
77
69
  <div className="flex flex-wrap gap-3">
78
- <StatusBadge tone="primary">platform_session_bff</StatusBadge>
79
- <StatusBadge tone="info">Server-owned cookies</StatusBadge>
80
- <StatusBadge tone="success">CSRF forwarding</StatusBadge>
70
+ <StatusBadge tone="primary">tenant_web_auth_sdk</StatusBadge>
71
+ <StatusBadge tone="info">HttpOnly cookies</StatusBadge>
72
+ <StatusBadge tone="success">CSRF protected</StatusBadge>
81
73
  </div>
82
74
  <p className="text-sm leading-7 text-muted-foreground">
83
- The browser talks only to this Next.js app. Upstream platform session
84
- cookies and CSRF tokens remain server-owned inside route handlers.
85
- Request-scoped tenant overrides stay scoped to feature actions unless
86
- the explicit session-context APIs are used.
75
+ Customer sessions stay same-origin through /_mw.
87
76
  </p>
88
77
  </PanelFrame>
89
78
 
90
79
  <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-4">
91
80
  <div className="flex items-center gap-2">
92
- <Building2 className="size-5 text-primary" />
81
+ <Database className="size-5 text-primary" />
93
82
  <h3 className="text-xl font-semibold tracking-tight">
94
- Builder materialization
83
+ Manifest API
95
84
  </h3>
96
85
  </div>
97
86
  <p className="text-sm leading-7 text-muted-foreground">
98
- The canonical template bundle is meant to be copied into
99
- a writable workspace copy before an agent edits it. In runtime
100
- Builder flows that writable root lives at
101
- `BuilderWorkspace.sandbox_root/app/`; in CLI scaffolds the same
102
- starter lands in `tenant-app/`. This source bundle stays governed
103
- and read-only in normal authoring flows.
87
+ Query and action calls go through the SDK and are authorized as
88
+ `tenant_customer`.
104
89
  </p>
90
+ <Button asChild className="w-fit">
91
+ <Link href={appRoutes.demo}>
92
+ Open demo
93
+ <ArrowRight className="size-4" />
94
+ </Link>
95
+ </Button>
105
96
  </PanelFrame>
106
97
  </section>
107
98
 
108
- <section className="grid gap-6 xl:grid-cols-[1fr,1fr]">
109
- <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-4">
110
- <div className="flex items-center gap-2">
111
- <Blocks className="size-5 text-primary" />
112
- <h3 className="text-xl font-semibold tracking-tight">
113
- Scaffold contents
114
- </h3>
115
- </div>
116
- <ul className="space-y-3 text-sm leading-6 text-muted-foreground">
117
- <li>Design-system primitives, patterns, docs, and governance checks.</li>
118
- <li>Public marketing, docs, and blog routes with a swappable content adapter.</li>
119
- <li>Typed platform session BFF routes for login, context, and password flows.</li>
120
- <li>Tenant-aware shell components ready for Builder-specific feature work.</li>
121
- </ul>
122
- </PanelFrame>
123
-
124
- <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-4">
125
- <div className="flex items-center gap-2">
126
- <FolderKanban className="size-5 text-primary" />
127
- <h3 className="text-xl font-semibold tracking-tight">
128
- Optional example surface
129
- </h3>
130
- </div>
131
- <p className="text-sm leading-7 text-muted-foreground">
132
- The runtime-command example is bundled as optional sample code rather
133
- than the template&apos;s default product identity.
134
- </p>
135
- {runtimeCommandExampleEnabled ? (
136
- <Button asChild className="w-fit">
137
- <Link href={appRoutes.runtimeCommandExample}>
138
- Open runtime command example
139
- </Link>
140
- </Button>
141
- ) : (
142
- <div className="space-y-2">
143
- <StatusBadge tone="default">Disabled by default</StatusBadge>
144
- <p className="text-sm leading-6 text-muted-foreground">
145
- Set `MW_ENABLE_RUNTIME_COMMAND_EXAMPLE=true` in the materialized
146
- workspace to enable the example route and its matching gateway
147
- proxy handlers.
148
- </p>
149
- </div>
99
+ <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-4">
100
+ <div className="flex items-center gap-2">
101
+ <UserRound className="size-5 text-primary" />
102
+ <h3 className="text-xl font-semibold tracking-tight">
103
+ Session payload
104
+ </h3>
105
+ </div>
106
+ <pre className="overflow-auto rounded-md bg-muted p-4 text-xs text-muted-foreground">
107
+ {JSON.stringify(
108
+ {
109
+ principal_kind: session.principal_kind,
110
+ tenant_id: session.tenant.id,
111
+ customer_membership_id: session.customer_membership?.id,
112
+ },
113
+ null,
114
+ 2,
150
115
  )}
151
- </PanelFrame>
152
- </section>
116
+ </pre>
117
+ </PanelFrame>
153
118
  </div>
154
119
  );
155
120
  }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Database, Play, RefreshCcw } from "lucide-react";
5
+
6
+ import { PanelFrame } from "@/design-system/patterns/panel-frame";
7
+ import { Button } from "@/design-system/primitives/button";
8
+ import { Input } from "@/design-system/primitives/input";
9
+ import { createIdempotencyKey, mw } from "@/mw/client";
10
+
11
+ type DemoResult = {
12
+ data?: unknown;
13
+ count?: number;
14
+ created?: boolean;
15
+ idempotency_key?: string;
16
+ };
17
+
18
+ export function ManifestDemo() {
19
+ const [title, setTitle] = useState("Demo record");
20
+ const [result, setResult] = useState<DemoResult | null>(null);
21
+ const [error, setError] = useState("");
22
+ const [pending, setPending] = useState(false);
23
+
24
+ async function runQuery() {
25
+ setPending(true);
26
+ setError("");
27
+ try {
28
+ setResult(await mw.query<DemoResult>("demo.list", { limit: 10 }));
29
+ } catch (caught) {
30
+ setError(caught instanceof Error ? caught.message : "Query failed.");
31
+ } finally {
32
+ setPending(false);
33
+ }
34
+ }
35
+
36
+ async function runAction() {
37
+ setPending(true);
38
+ setError("");
39
+ try {
40
+ setResult(
41
+ await mw.action<DemoResult>(
42
+ "demo.create",
43
+ { title, status: "new" },
44
+ { idempotencyKey: createIdempotencyKey("demo-create") },
45
+ ),
46
+ );
47
+ } catch (caught) {
48
+ setError(caught instanceof Error ? caught.message : "Action failed.");
49
+ } finally {
50
+ setPending(false);
51
+ }
52
+ }
53
+
54
+ return (
55
+ <div className="grid gap-6">
56
+ <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-5">
57
+ <div className="flex items-center gap-2">
58
+ <Database className="size-5 text-primary" />
59
+ <h2 className="text-2xl font-semibold tracking-tight">
60
+ Manifest query and action
61
+ </h2>
62
+ </div>
63
+ <div className="grid gap-3 md:grid-cols-[1fr,auto,auto]">
64
+ <Input
65
+ value={title}
66
+ onChange={(event) => setTitle(event.target.value)}
67
+ className="h-11"
68
+ aria-label="Demo title"
69
+ />
70
+ <Button type="button" variant="outline" onClick={runQuery} disabled={pending}>
71
+ <RefreshCcw className="size-4" />
72
+ Query
73
+ </Button>
74
+ <Button type="button" onClick={runAction} disabled={pending}>
75
+ <Play className="size-4" />
76
+ Action
77
+ </Button>
78
+ </div>
79
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
80
+ </PanelFrame>
81
+
82
+ <PanelFrame tone="raised" radius="xl" padding="lg">
83
+ <pre className="min-h-40 overflow-auto rounded-md bg-muted p-4 text-xs text-muted-foreground">
84
+ {result ? JSON.stringify(result, null, 2) : "No result yet"}
85
+ </pre>
86
+ </PanelFrame>
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,58 @@
1
+ import Link from "next/link";
2
+ import { ArrowRight } from "lucide-react";
3
+
4
+ import { Button } from "@/design-system/primitives/button";
5
+ import { appRoutes } from "@/lib/app-routes";
6
+
7
+ export function StaticPublicPage({
8
+ eyebrow,
9
+ title,
10
+ body,
11
+ }: {
12
+ eyebrow: string;
13
+ title: string;
14
+ body: string;
15
+ }) {
16
+ return (
17
+ <main className="min-h-screen bg-background text-foreground">
18
+ <div className="mx-auto flex min-h-screen max-w-6xl flex-col px-6 py-6">
19
+ <nav className="flex items-center justify-between">
20
+ <Link className="text-sm font-semibold text-foreground" href={appRoutes.publicHome}>
21
+ Tenant App
22
+ </Link>
23
+ <div className="flex items-center gap-2">
24
+ <Button asChild variant="ghost">
25
+ <Link href={appRoutes.pricing}>Pricing</Link>
26
+ </Button>
27
+ <Button asChild variant="ghost">
28
+ <Link href={appRoutes.docsIndex}>Docs</Link>
29
+ </Button>
30
+ <Button asChild>
31
+ <Link href={appRoutes.login}>Log in</Link>
32
+ </Button>
33
+ </div>
34
+ </nav>
35
+
36
+ <section className="grid flex-1 content-center gap-8 py-16">
37
+ <div className="max-w-3xl space-y-5">
38
+ <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
39
+ {eyebrow}
40
+ </p>
41
+ <h1 className="text-5xl font-semibold tracking-tight text-balance sm:text-6xl">
42
+ {title}
43
+ </h1>
44
+ <p className="max-w-2xl text-lg leading-8 text-muted-foreground">
45
+ {body}
46
+ </p>
47
+ <Button asChild className="w-fit">
48
+ <Link href={appRoutes.login}>
49
+ Continue
50
+ <ArrowRight className="size-4" />
51
+ </Link>
52
+ </Button>
53
+ </div>
54
+ </section>
55
+ </div>
56
+ </main>
57
+ );
58
+ }