minutework 0.1.32 → 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 (91) hide show
  1. package/assets/claude-local/skills/README.md +2 -0
  2. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -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 +10 -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 +26 -138
  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,115 +1,22 @@
1
1
  "use client";
2
2
 
3
- import type { FormEvent } from "react";
4
- import { startTransition, useEffect, useEffectEvent, useState } from "react";
5
- import dynamic from "next/dynamic";
3
+ import { startTransition } from "react";
6
4
  import Link from "next/link";
7
5
  import { useRouter } from "next/navigation";
8
- import {
9
- Building2,
10
- KeyRound,
11
- LayoutDashboard,
12
- Loader2,
13
- LogOut,
14
- RefreshCcw,
15
- ShieldCheck,
16
- Workflow,
17
- } from "lucide-react";
6
+ import { LayoutDashboard, LogOut, PlaySquare } from "lucide-react";
18
7
 
19
- import { TenantDashboard } from "@/features/dashboard/components/tenant-dashboard";
20
8
  import { PanelFrame } from "@/design-system/patterns/panel-frame";
21
- import { StatusBadge } from "@/design-system/patterns/status-badge";
22
9
  import { ThemeModeToggle } from "@/design-system/patterns/theme-mode-toggle";
23
10
  import { Button } from "@/design-system/primitives/button";
24
- import { Input } from "@/design-system/primitives/input";
11
+ import { TenantDashboard } from "@/features/dashboard/components/tenant-dashboard";
25
12
  import { appRoutes } from "@/lib/app-routes";
26
- import type {
27
- AuthenticatedPlatformSession,
28
- PasswordStatus,
29
- SessionMembership,
30
- } from "@/lib/platform/contracts";
31
-
32
- type ShellView = "dashboard" | "runtime-command-demo";
33
-
34
- function RuntimeCommandDemoFallback() {
35
- return (
36
- <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-3">
37
- <div className="flex items-center gap-2">
38
- <Loader2 className="size-4 animate-spin text-primary" />
39
- <p className="text-sm font-semibold text-foreground">
40
- Loading runtime example
41
- </p>
42
- </div>
43
- <p className="text-sm leading-6 text-muted-foreground">
44
- The optional example module is loaded only when the runtime example route
45
- is active.
46
- </p>
47
- </PanelFrame>
48
- );
49
- }
50
-
51
- const RuntimeCommandDemo = dynamic(
52
- () =>
53
- import(
54
- "@/features/examples/runtime-command-demo/components/runtime-command-demo"
55
- ).then((module) => module.RuntimeCommandDemo),
56
- {
57
- loading: () => <RuntimeCommandDemoFallback />,
58
- },
59
- );
60
-
61
- function membershipById(
62
- memberships: SessionMembership[],
63
- tenantId: string,
64
- ) {
65
- return memberships.find((membership) => membership.tenant_id === tenantId) ?? null;
66
- }
67
-
68
- async function readErrorDetail(response: Response, fallback: string) {
69
- const payload = (await response.json().catch(() => null)) as
70
- | { detail?: string }
71
- | null;
13
+ import { useMinuteWorkAuth, useMinuteWorkSession } from "@minutework/web-auth/react";
72
14
 
73
- return payload?.detail || fallback;
74
- }
75
-
76
- export function PrivateAppShell({
77
- appName,
78
- initialSession,
79
- operatorConsoleHref,
80
- runtimeCommandExampleEnabled,
81
- view,
82
- }: {
83
- appName: string;
84
- initialSession: AuthenticatedPlatformSession;
85
- operatorConsoleHref?: string;
86
- runtimeCommandExampleEnabled: boolean;
87
- view: ShellView;
88
- }) {
15
+ export function PrivateAppShell({ appName }: { appName: string }) {
89
16
  const router = useRouter();
90
- const [session, setSession] = useState(initialSession);
91
- const [selectedTenantId, setSelectedTenantId] = useState(
92
- initialSession.active_tenant_id,
93
- );
94
- const [signingOut, setSigningOut] = useState(false);
95
- const [tenantPending, setTenantPending] = useState(false);
96
- const [tenantError, setTenantError] = useState("");
97
- const [tenantNotice, setTenantNotice] = useState("");
98
- const [passwordStatus, setPasswordStatus] = useState<PasswordStatus | null>(null);
99
- const [passwordLoading, setPasswordLoading] = useState(true);
100
- const [passwordPending, setPasswordPending] = useState(false);
101
- const [passwordError, setPasswordError] = useState("");
102
- const [passwordNotice, setPasswordNotice] = useState("");
103
- const [currentPassword, setCurrentPassword] = useState("");
104
- const [newPassword, setNewPassword] = useState("");
105
- const [confirmPassword, setConfirmPassword] = useState("");
106
-
107
- const activeTenant = session.active_tenant;
108
- const activeMembership = membershipById(
109
- session.memberships,
110
- session.active_tenant_id,
111
- );
112
- const pendingMembership = membershipById(session.memberships, selectedTenantId);
17
+ const { logout } = useMinuteWorkAuth();
18
+ const { session, loading, authenticated, emailVerificationRequired } =
19
+ useMinuteWorkSession();
113
20
 
114
21
  function redirectToLogin() {
115
22
  startTransition(() => {
@@ -118,188 +25,42 @@ export function PrivateAppShell({
118
25
  });
119
26
  }
120
27
 
121
- const loadPasswordStatus = useEffectEvent(async () => {
122
- setPasswordLoading(true);
123
-
124
- try {
125
- const response = await fetch("/api/auth/password-status", {
126
- method: "GET",
127
- cache: "no-store",
128
- });
129
-
130
- if (response.status === 401) {
131
- redirectToLogin();
132
- return;
133
- }
134
-
135
- if (!response.ok) {
136
- setPasswordError(
137
- await readErrorDetail(response, "Unable to load password settings."),
138
- );
139
- return;
140
- }
141
-
142
- const payload = (await response.json()) as PasswordStatus;
143
- setPasswordStatus(payload);
144
- setPasswordError("");
145
- } catch {
146
- setPasswordError("Unable to load password settings.");
147
- } finally {
148
- setPasswordLoading(false);
149
- }
150
- });
151
-
152
- useEffect(() => {
153
- setSelectedTenantId(session.active_tenant_id);
154
- }, [session.active_tenant_id]);
155
-
156
- useEffect(() => {
157
- void loadPasswordStatus();
158
- }, []);
159
-
160
28
  async function handleLogout() {
161
- setSigningOut(true);
162
- await fetch("/api/auth/logout", { method: "POST" }).catch(() => null);
29
+ await logout().catch(() => undefined);
163
30
  redirectToLogin();
164
31
  }
165
32
 
166
- async function handleTenantSwitch() {
167
- if (!selectedTenantId || selectedTenantId === session.active_tenant_id) {
168
- return;
169
- }
170
-
171
- setTenantPending(true);
172
- setTenantError("");
173
- setTenantNotice("");
174
-
175
- try {
176
- const response = await fetch("/api/auth/context", {
177
- method: "PUT",
178
- headers: { "Content-Type": "application/json" },
179
- body: JSON.stringify({ tenant_id: selectedTenantId }),
180
- });
181
-
182
- if (response.status === 401) {
183
- redirectToLogin();
184
- return;
185
- }
186
-
187
- if (!response.ok) {
188
- setTenantError(
189
- await readErrorDetail(response, "Unable to update tenant context."),
190
- );
191
- return;
192
- }
193
-
194
- const payload = (await response.json()) as AuthenticatedPlatformSession;
195
- setSession(payload);
196
- setTenantNotice("Tenant context updated.");
197
- } catch {
198
- setTenantError("Unable to update tenant context.");
199
- } finally {
200
- setTenantPending(false);
201
- }
202
- }
203
-
204
- async function handleTenantReset() {
205
- setTenantPending(true);
206
- setTenantError("");
207
- setTenantNotice("");
208
-
209
- try {
210
- const response = await fetch("/api/auth/context", {
211
- method: "DELETE",
212
- });
213
-
214
- if (response.status === 401) {
215
- redirectToLogin();
216
- return;
217
- }
218
-
219
- if (!response.ok) {
220
- setTenantError(
221
- await readErrorDetail(response, "Unable to reset tenant context."),
222
- );
223
- return;
224
- }
225
-
226
- const payload = (await response.json()) as AuthenticatedPlatformSession;
227
- setSession(payload);
228
- setTenantNotice("Tenant context reset to the platform default.");
229
- } catch {
230
- setTenantError("Unable to reset tenant context.");
231
- } finally {
232
- setTenantPending(false);
233
- }
234
- }
235
-
236
- async function handlePasswordSubmit(event: FormEvent<HTMLFormElement>) {
237
- event.preventDefault();
238
-
239
- if (newPassword !== confirmPassword) {
240
- setPasswordError("Passwords must match.");
241
- return;
242
- }
243
-
244
- setPasswordPending(true);
245
- setPasswordError("");
246
- setPasswordNotice("");
247
-
248
- try {
249
- const response = await fetch("/api/auth/password-change", {
250
- method: "POST",
251
- headers: { "Content-Type": "application/json" },
252
- body: JSON.stringify({
253
- current_password: currentPassword,
254
- new_password: newPassword,
255
- confirm_password: confirmPassword,
256
- }),
257
- });
258
-
259
- if (response.status === 401) {
260
- redirectToLogin();
261
- return;
262
- }
263
-
264
- if (!response.ok) {
265
- setPasswordError(
266
- await readErrorDetail(response, "Unable to change password."),
267
- );
268
- return;
269
- }
270
-
271
- const payload = (await response.json()) as PasswordStatus;
272
- setPasswordStatus(payload);
273
- setCurrentPassword("");
274
- setNewPassword("");
275
- setConfirmPassword("");
276
- setPasswordNotice("Password saved.");
277
- } catch {
278
- setPasswordError("Unable to change password.");
279
- } finally {
280
- setPasswordPending(false);
281
- }
33
+ if (loading) {
34
+ return (
35
+ <main className="min-h-screen bg-background text-foreground">
36
+ <div className="mx-auto flex min-h-screen max-w-5xl items-center px-6 py-10">
37
+ <PanelFrame tone="floating" radius="xl" padding="lg" className="w-full">
38
+ <p className="text-sm text-muted-foreground">Loading session</p>
39
+ </PanelFrame>
40
+ </div>
41
+ </main>
42
+ );
282
43
  }
283
44
 
284
- if (!activeTenant) {
45
+ if (!authenticated || !session?.customer_membership) {
285
46
  return (
286
47
  <main className="min-h-screen bg-background text-foreground">
287
48
  <div className="mx-auto flex min-h-screen max-w-5xl items-center px-6 py-10">
288
49
  <PanelFrame tone="floating" radius="xl" padding="lg" className="w-full space-y-4">
289
50
  <div className="space-y-2">
290
- <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
291
- Missing tenant context
292
- </p>
293
51
  <h1 className="text-3xl font-semibold tracking-tight">
294
- This account is authenticated but has no active tenant membership.
52
+ {emailVerificationRequired
53
+ ? "Email verification required"
54
+ : "Log in to continue"}
295
55
  </h1>
296
56
  <p className="text-sm leading-7 text-muted-foreground">
297
- The template expects an authenticated membership-backed tenant
298
- session before entering the private app shell.
57
+ {emailVerificationRequired
58
+ ? "Finish verification from your email before opening the workspace."
59
+ : "This area is available to verified customers."}
299
60
  </p>
300
61
  </div>
301
- <Button onClick={handleLogout} variant="outline" className="w-fit">
302
- Return to login
62
+ <Button onClick={redirectToLogin} className="w-fit">
63
+ Open login
303
64
  </Button>
304
65
  </PanelFrame>
305
66
  </div>
@@ -307,317 +68,52 @@ export function PrivateAppShell({
307
68
  );
308
69
  }
309
70
 
310
- const content =
311
- view === "runtime-command-demo" ? (
312
- <RuntimeCommandDemo
313
- activeTenant={activeTenant}
314
- onUnauthorized={redirectToLogin}
315
- viewerName={session.user.username}
316
- />
317
- ) : (
318
- <TenantDashboard
319
- appName={appName}
320
- runtimeCommandExampleEnabled={runtimeCommandExampleEnabled}
321
- session={session}
322
- />
323
- );
324
-
325
71
  return (
326
72
  <main className="min-h-screen bg-background text-foreground">
327
73
  <div className="mx-auto flex min-h-screen max-w-7xl flex-col gap-6 px-6 py-8">
328
74
  <header className="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between">
329
- <div className="space-y-4">
330
- <div className="space-y-2">
331
- <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
332
- Private Next.js Tenant Template
333
- </p>
334
- <h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
335
- {appName}
336
- </h1>
337
- <p className="max-w-3xl text-base leading-7 text-muted-foreground sm:text-lg">
338
- A canonical `platform_session_bff` scaffold for tenant-facing
339
- Builder apps. The browser talks only to this Next.js app, and the
340
- app keeps platform auth and CSRF credentials server-owned.
341
- </p>
342
- </div>
343
-
344
- <div className="flex flex-wrap gap-3">
345
- <StatusBadge tone="primary">platform_session_bff</StatusBadge>
346
- <StatusBadge tone="info">Active tenant aware</StatusBadge>
347
- <StatusBadge tone="success">Builder-ready scaffold</StatusBadge>
348
- </div>
75
+ <div className="space-y-2">
76
+ <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
77
+ {session.tenant.name}
78
+ </p>
79
+ <h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
80
+ {appName}
81
+ </h1>
82
+ <p className="max-w-3xl text-base leading-7 text-muted-foreground sm:text-lg">
83
+ Signed in as {session.user?.email || session.user?.username}.
84
+ </p>
349
85
  </div>
350
86
 
351
87
  <div className="flex flex-col gap-3 sm:flex-row sm:items-start">
352
88
  <ThemeModeToggle className="w-full sm:w-72" />
353
- {operatorConsoleHref ? (
354
- <Button asChild type="button" variant="outline">
355
- <a href={operatorConsoleHref} target="_blank" rel="noreferrer">
356
- Open /ops
357
- </a>
358
- </Button>
359
- ) : null}
360
89
  <Button
361
90
  type="button"
362
91
  variant="outline"
363
92
  className="gap-2"
364
93
  onClick={handleLogout}
365
- disabled={signingOut}
366
94
  >
367
- {signingOut ? (
368
- <Loader2 className="size-4 animate-spin" />
369
- ) : (
370
- <LogOut className="size-4" />
371
- )}
372
- {signingOut ? "Signing out" : "Log out"}
95
+ <LogOut className="size-4" />
96
+ Log out
373
97
  </Button>
374
98
  </div>
375
99
  </header>
376
100
 
377
101
  <nav className="flex flex-wrap gap-2">
378
- <Button asChild variant={view === "dashboard" ? "default" : "outline"}>
102
+ <Button asChild variant="default">
379
103
  <Link href={appRoutes.appHome}>
380
104
  <LayoutDashboard className="size-4" />
381
105
  Dashboard
382
106
  </Link>
383
107
  </Button>
384
- {runtimeCommandExampleEnabled ? (
385
- <Button
386
- asChild
387
- variant={view === "runtime-command-demo" ? "default" : "outline"}
388
- >
389
- <Link href={appRoutes.runtimeCommandExample}>
390
- <Workflow className="size-4" />
391
- Runtime example
392
- </Link>
393
- </Button>
394
- ) : null}
108
+ <Button asChild variant="outline">
109
+ <Link href={appRoutes.demo}>
110
+ <PlaySquare className="size-4" />
111
+ Demo
112
+ </Link>
113
+ </Button>
395
114
  </nav>
396
115
 
397
- <section className="grid gap-6 xl:grid-cols-[1.2fr,0.8fr]">
398
- <div>{content}</div>
399
-
400
- <div className="grid gap-6">
401
- <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-5">
402
- <div className="space-y-2">
403
- <div className="flex items-center gap-2">
404
- <Building2 className="size-4 text-primary" />
405
- <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
406
- Tenant context
407
- </p>
408
- </div>
409
- <h2 className="text-2xl font-semibold tracking-tight">
410
- {activeTenant.tenant_name}
411
- </h2>
412
- <p className="text-sm leading-6 text-muted-foreground">
413
- Signed in as{" "}
414
- <span className="font-medium text-foreground">
415
- {session.user.username}
416
- </span>{" "}
417
- with role{" "}
418
- <span className="font-medium text-foreground">
419
- {activeMembership?.role ?? activeTenant.role}
420
- </span>
421
- .
422
- </p>
423
- </div>
424
-
425
- <div className="space-y-2">
426
- <label
427
- className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
428
- htmlFor="tenant-context"
429
- >
430
- Active tenant
431
- </label>
432
- <select
433
- id="tenant-context"
434
- className="flex h-12 w-full rounded-xl border border-border bg-background px-4 text-sm text-foreground outline-none transition-colors focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40"
435
- value={selectedTenantId}
436
- onChange={(event) => {
437
- setSelectedTenantId(event.target.value);
438
- setTenantError("");
439
- setTenantNotice("");
440
- }}
441
- disabled={tenantPending}
442
- >
443
- {session.memberships.map((membership) => (
444
- <option key={membership.tenant_id} value={membership.tenant_id}>
445
- {membership.tenant_name} ({membership.tenant_slug})
446
- </option>
447
- ))}
448
- </select>
449
- </div>
450
-
451
- {pendingMembership ? (
452
- <PanelFrame tone="raised" radius="xl" padding="md" className="space-y-2">
453
- <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
454
- Selected membership
455
- </p>
456
- <p className="text-sm font-semibold text-foreground">
457
- {pendingMembership.tenant_name}
458
- </p>
459
- <p className="text-sm text-muted-foreground">
460
- {pendingMembership.tenant_slug} · {pendingMembership.role}
461
- </p>
462
- </PanelFrame>
463
- ) : null}
464
-
465
- <div className="flex flex-wrap gap-3">
466
- <Button
467
- type="button"
468
- className="gap-2"
469
- onClick={handleTenantSwitch}
470
- disabled={tenantPending || selectedTenantId === session.active_tenant_id}
471
- >
472
- {tenantPending ? (
473
- <Loader2 className="size-4 animate-spin" />
474
- ) : (
475
- <Building2 className="size-4" />
476
- )}
477
- Apply tenant
478
- </Button>
479
- <Button
480
- type="button"
481
- variant="outline"
482
- className="gap-2"
483
- onClick={handleTenantReset}
484
- disabled={tenantPending}
485
- >
486
- <RefreshCcw className="size-4" />
487
- Reset context
488
- </Button>
489
- </div>
490
-
491
- {tenantError ? <p className="text-sm text-destructive">{tenantError}</p> : null}
492
- {tenantNotice ? <p className="text-sm text-primary">{tenantNotice}</p> : null}
493
- </PanelFrame>
494
-
495
- <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-5">
496
- <div className="space-y-2">
497
- <div className="flex items-center gap-2">
498
- <ShieldCheck className="size-4 text-primary" />
499
- <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
500
- Password settings
501
- </p>
502
- </div>
503
- <h2 className="text-2xl font-semibold tracking-tight">
504
- Account security
505
- </h2>
506
- <p className="text-sm leading-6 text-muted-foreground">
507
- Password status and updates are proxied through the same
508
- server-owned BFF as login, logout, and session restore.
509
- </p>
510
- </div>
511
-
512
- {passwordLoading ? (
513
- <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
514
- <div className="flex items-center gap-2">
515
- <Loader2 className="size-4 animate-spin text-primary" />
516
- <p className="text-sm font-semibold text-foreground">
517
- Loading password status
518
- </p>
519
- </div>
520
- </PanelFrame>
521
- ) : (
522
- <form className="space-y-4" onSubmit={handlePasswordSubmit}>
523
- <div className="flex flex-wrap gap-3">
524
- <StatusBadge tone={passwordStatus?.has_password ? "success" : "info"}>
525
- {passwordStatus?.has_password
526
- ? "Password enabled"
527
- : "Password missing"}
528
- </StatusBadge>
529
- <StatusBadge tone="default">Platform session</StatusBadge>
530
- </div>
531
-
532
- {passwordStatus?.has_password ? (
533
- <div className="space-y-2">
534
- <label
535
- className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
536
- htmlFor="current-password"
537
- >
538
- Current password
539
- </label>
540
- <Input
541
- id="current-password"
542
- type="password"
543
- autoComplete="current-password"
544
- className="h-12"
545
- value={currentPassword}
546
- onChange={(event) => {
547
- setCurrentPassword(event.target.value);
548
- setPasswordError("");
549
- setPasswordNotice("");
550
- }}
551
- />
552
- </div>
553
- ) : null}
554
-
555
- <div className="space-y-2">
556
- <label
557
- className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
558
- htmlFor="new-password"
559
- >
560
- New password
561
- </label>
562
- <Input
563
- id="new-password"
564
- type="password"
565
- autoComplete="new-password"
566
- className="h-12"
567
- value={newPassword}
568
- onChange={(event) => {
569
- setNewPassword(event.target.value);
570
- setPasswordError("");
571
- setPasswordNotice("");
572
- }}
573
- />
574
- </div>
575
-
576
- <div className="space-y-2">
577
- <label
578
- className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
579
- htmlFor="confirm-password"
580
- >
581
- Confirm password
582
- </label>
583
- <Input
584
- id="confirm-password"
585
- type="password"
586
- autoComplete="new-password"
587
- className="h-12"
588
- value={confirmPassword}
589
- onChange={(event) => {
590
- setConfirmPassword(event.target.value);
591
- setPasswordError("");
592
- setPasswordNotice("");
593
- }}
594
- />
595
- </div>
596
-
597
- {passwordError ? (
598
- <p className="text-sm text-destructive">{passwordError}</p>
599
- ) : null}
600
- {passwordNotice ? (
601
- <p className="text-sm text-primary">{passwordNotice}</p>
602
- ) : null}
603
-
604
- <Button
605
- type="submit"
606
- className="gap-2"
607
- disabled={passwordPending || passwordLoading}
608
- >
609
- {passwordPending ? (
610
- <Loader2 className="size-4 animate-spin" />
611
- ) : (
612
- <KeyRound className="size-4" />
613
- )}
614
- {passwordStatus?.has_password ? "Update password" : "Create password"}
615
- </Button>
616
- </form>
617
- )}
618
- </PanelFrame>
619
- </div>
620
- </section>
116
+ <TenantDashboard appName={appName} session={session} />
621
117
  </div>
622
118
  </main>
623
119
  );
@@ -38,7 +38,7 @@ const docsIndexRoute: Route = "/docs";
38
38
  const blogIndexRoute: Route = "/blog";
39
39
  const loginRoute: Route = "/login";
40
40
  const appHomeRoute: Route = "/app";
41
- const runtimeCommandExampleRoute: Route = "/app/examples/runtime-commands";
41
+ const demoRoute = "/app/demo" as Route;
42
42
 
43
43
  export const appRoutes = {
44
44
  publicHome: publicHomeRoute,
@@ -55,5 +55,5 @@ export const appRoutes = {
55
55
  },
56
56
  login: loginRoute,
57
57
  appHome: appHomeRoute,
58
- runtimeCommandExample: runtimeCommandExampleRoute,
58
+ demo: demoRoute,
59
59
  };