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,342 @@
1
+ "use client";
2
+
3
+ import { useEffect, useEffectEvent, useState } from "react";
4
+ import { ArrowRight, Loader2, Orbit, Sparkles, Terminal } from "lucide-react";
5
+
6
+ import { PanelFrame } from "@/design-system/patterns/panel-frame";
7
+ import { StatusBadge } from "@/design-system/patterns/status-badge";
8
+ import { Button } from "@/design-system/primitives/button";
9
+ import type {
10
+ ActiveTenant,
11
+ CommandRun,
12
+ CommandTemplateKey,
13
+ } from "@/lib/platform/contracts";
14
+ import { commandTemplateKeys } from "@/lib/platform/contracts";
15
+ import { cn } from "@/lib/utils";
16
+
17
+ const commandTemplates: Array<{
18
+ key: CommandTemplateKey;
19
+ label: string;
20
+ helper: string;
21
+ }> = [
22
+ {
23
+ key: commandTemplateKeys[0],
24
+ label: "Python Version",
25
+ helper: "Check runtime interpreter availability.",
26
+ },
27
+ {
28
+ key: commandTemplateKeys[1],
29
+ label: "Echo Hello",
30
+ helper: "Validate command dispatch and echo output.",
31
+ },
32
+ {
33
+ key: commandTemplateKeys[2],
34
+ label: "System Info",
35
+ helper: "Inspect the runtime host environment.",
36
+ },
37
+ {
38
+ key: commandTemplateKeys[3],
39
+ label: "List Apps",
40
+ helper: "Enumerate app workloads visible to the runtime.",
41
+ },
42
+ ];
43
+
44
+ function commandTone(status: string) {
45
+ switch (status) {
46
+ case "succeeded":
47
+ case "completed":
48
+ return "success" as const;
49
+ case "queued":
50
+ case "running":
51
+ return "info" as const;
52
+ case "failed":
53
+ case "canceled":
54
+ return "danger" as const;
55
+ default:
56
+ return "default" as const;
57
+ }
58
+ }
59
+
60
+ async function readErrorDetail(response: Response, fallback: string) {
61
+ const payload = (await response.json().catch(() => null)) as
62
+ | { detail?: string }
63
+ | null;
64
+
65
+ return payload?.detail || fallback;
66
+ }
67
+
68
+ function Metric({
69
+ label,
70
+ value,
71
+ }: {
72
+ label: string;
73
+ value: string;
74
+ }) {
75
+ return (
76
+ <PanelFrame tone="raised" padding="md" className="min-w-36 space-y-2">
77
+ <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
78
+ {label}
79
+ </p>
80
+ <p className="text-sm font-semibold text-foreground">{value}</p>
81
+ </PanelFrame>
82
+ );
83
+ }
84
+
85
+ function OutputCard({
86
+ title,
87
+ body,
88
+ }: {
89
+ title: string;
90
+ body: string;
91
+ }) {
92
+ return (
93
+ <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-3">
94
+ <div className="flex items-center justify-between gap-3">
95
+ <p className="text-sm font-semibold text-foreground">{title}</p>
96
+ <StatusBadge tone="default">Stream</StatusBadge>
97
+ </div>
98
+ <div className="overflow-hidden rounded-lg border border-border bg-background">
99
+ <pre className="max-h-80 overflow-auto p-4 font-mono text-xs leading-6 whitespace-pre-wrap text-foreground">
100
+ {body}
101
+ </pre>
102
+ </div>
103
+ </PanelFrame>
104
+ );
105
+ }
106
+
107
+ export function RuntimeCommandDemo({
108
+ activeTenant,
109
+ onUnauthorized,
110
+ viewerName,
111
+ }: {
112
+ activeTenant: ActiveTenant;
113
+ onUnauthorized: () => void;
114
+ viewerName: string;
115
+ }) {
116
+ const [selectedTemplate, setSelectedTemplate] = useState<CommandTemplateKey>(
117
+ commandTemplates[0].key,
118
+ );
119
+ const [run, setRun] = useState<CommandRun | null>(null);
120
+ const [pending, setPending] = useState(false);
121
+ const [error, setError] = useState("");
122
+
123
+ const selectedTemplateMeta =
124
+ commandTemplates.find((template) => template.key === selectedTemplate) ??
125
+ commandTemplates[0];
126
+
127
+ const refreshRun = useEffectEvent(async (runId: string, tenantId: string) => {
128
+ const response = await fetch(`/api/gateway/commands/${runId}?tenant_id=${tenantId}`);
129
+
130
+ if (response.status === 401) {
131
+ onUnauthorized();
132
+ return;
133
+ }
134
+
135
+ if (!response.ok) {
136
+ return;
137
+ }
138
+
139
+ const payload = (await response.json()) as CommandRun;
140
+ setRun(payload);
141
+ });
142
+
143
+ useEffect(() => {
144
+ if (!run || !["queued", "running"].includes(run.status)) {
145
+ return;
146
+ }
147
+
148
+ const timeout = window.setTimeout(() => {
149
+ void refreshRun(run.run_id, activeTenant.tenant_id);
150
+ }, 1500);
151
+
152
+ return () => window.clearTimeout(timeout);
153
+ }, [activeTenant.tenant_id, run]);
154
+
155
+ async function dispatchCommand() {
156
+ setPending(true);
157
+ setError("");
158
+ setRun(null);
159
+
160
+ try {
161
+ const response = await fetch("/api/gateway/commands", {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({
165
+ tenant_id: activeTenant.tenant_id,
166
+ template_key: selectedTemplate,
167
+ }),
168
+ });
169
+
170
+ if (response.status === 401) {
171
+ onUnauthorized();
172
+ return;
173
+ }
174
+
175
+ if (!response.ok) {
176
+ setError(
177
+ await readErrorDetail(response, "Command dispatch failed."),
178
+ );
179
+ return;
180
+ }
181
+
182
+ const payload = (await response.json()) as CommandRun;
183
+ setRun(payload);
184
+ } catch {
185
+ setError("Unable to dispatch the command right now.");
186
+ } finally {
187
+ setPending(false);
188
+ }
189
+ }
190
+
191
+ return (
192
+ <div className="grid gap-6">
193
+ <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-6">
194
+ <div className="space-y-2">
195
+ <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
196
+ Example gateway flow
197
+ </p>
198
+ <h2 className="text-3xl font-semibold tracking-tight">
199
+ Optional runtime command demo
200
+ </h2>
201
+ <p className="max-w-3xl text-sm leading-7 text-muted-foreground">
202
+ This is example code bundled with the template, not the template&apos;s
203
+ default product identity. It demonstrates an authenticated gateway
204
+ proxy flow that stays tenant-scoped and CSRF-safe through the BFF.
205
+ </p>
206
+ </div>
207
+
208
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
209
+ {commandTemplates.map((template) => {
210
+ const isActive = template.key === selectedTemplate;
211
+
212
+ return (
213
+ <button
214
+ key={template.key}
215
+ type="button"
216
+ onClick={() => setSelectedTemplate(template.key)}
217
+ className={cn(
218
+ "rounded-xl border p-4 text-left transition-colors",
219
+ isActive
220
+ ? "border-primary/30 bg-primary/10 text-foreground shadow-sm"
221
+ : "border-border bg-surface-raised text-foreground hover:bg-muted",
222
+ )}
223
+ >
224
+ <div className="flex items-center justify-between gap-3">
225
+ <p className="text-sm font-semibold">{template.label}</p>
226
+ {isActive ? <StatusBadge tone="primary">Selected</StatusBadge> : null}
227
+ </div>
228
+ <p className="mt-2 text-sm leading-6 text-muted-foreground">
229
+ {template.helper}
230
+ </p>
231
+ </button>
232
+ );
233
+ })}
234
+ </div>
235
+
236
+ <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
237
+ <div className="space-y-1">
238
+ <p className="text-sm font-semibold text-foreground">
239
+ Ready to run {selectedTemplateMeta.label}
240
+ </p>
241
+ <p className="text-sm text-muted-foreground">
242
+ {selectedTemplateMeta.helper}
243
+ </p>
244
+ </div>
245
+ <div className="flex flex-wrap items-center gap-3">
246
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
247
+ <Button onClick={dispatchCommand} disabled={pending} className="gap-2">
248
+ {pending ? (
249
+ <Loader2 className="size-4 animate-spin" />
250
+ ) : (
251
+ <ArrowRight className="size-4" />
252
+ )}
253
+ {pending ? "Dispatching" : `Run ${selectedTemplateMeta.label}`}
254
+ </Button>
255
+ </div>
256
+ </div>
257
+ </PanelFrame>
258
+
259
+ <section className="grid gap-6 xl:grid-cols-[0.95fr,1.05fr]">
260
+ <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-4">
261
+ <div className="flex items-center justify-between gap-3">
262
+ <div className="space-y-1">
263
+ <p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
264
+ Projection
265
+ </p>
266
+ <h2 className="text-2xl font-semibold tracking-tight">
267
+ {run ? "Latest projected run" : "No projected run yet"}
268
+ </h2>
269
+ </div>
270
+ <Orbit className="size-5 text-primary" />
271
+ </div>
272
+
273
+ <div className="grid gap-3 sm:grid-cols-3">
274
+ <Metric label="Viewer" value={viewerName} />
275
+ <Metric label="Tenant" value={activeTenant.tenant_slug} />
276
+ <Metric label="Template" value={selectedTemplateMeta.label} />
277
+ </div>
278
+
279
+ {run ? (
280
+ <>
281
+ <div className="flex flex-wrap gap-3">
282
+ <StatusBadge tone={commandTone(run.status)}>{run.status}</StatusBadge>
283
+ <StatusBadge tone="primary">{run.template_key}</StatusBadge>
284
+ <StatusBadge
285
+ tone={
286
+ run.exit_code === null
287
+ ? "info"
288
+ : run.exit_code === 0
289
+ ? "success"
290
+ : "danger"
291
+ }
292
+ >
293
+ Exit code {run.exit_code === null ? "pending" : run.exit_code}
294
+ </StatusBadge>
295
+ </div>
296
+
297
+ <div className="grid gap-3 sm:grid-cols-3">
298
+ <Metric label="Run ID" value={run.run_id} />
299
+ <Metric label="Runtime" value={run.runtime_id} />
300
+ <Metric label="Source" value={run.source_ref} />
301
+ </div>
302
+
303
+ <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
304
+ <div className="flex items-center gap-2">
305
+ <Sparkles className="size-4 text-primary" />
306
+ <p className="text-sm font-semibold text-foreground">Safe summary</p>
307
+ </div>
308
+ <p className="text-sm leading-6 text-muted-foreground">
309
+ {run.safe_summary}
310
+ </p>
311
+ {run.dispatch_error ? (
312
+ <p className="text-sm leading-6 text-destructive">
313
+ {run.dispatch_error}
314
+ </p>
315
+ ) : null}
316
+ </PanelFrame>
317
+ </>
318
+ ) : (
319
+ <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
320
+ <div className="flex items-center gap-2">
321
+ <Terminal className="size-4 text-primary" />
322
+ <p className="text-sm font-semibold text-foreground">
323
+ Dispatch to begin
324
+ </p>
325
+ </div>
326
+ <p className="text-sm leading-6 text-muted-foreground">
327
+ Choose a template above and run it to demonstrate a browser →
328
+ platform → runtime → platform round trip through the example
329
+ gateway surface.
330
+ </p>
331
+ </PanelFrame>
332
+ )}
333
+ </PanelFrame>
334
+
335
+ <div className="grid gap-6">
336
+ <OutputCard title="Stdout tail" body={run?.stdout_tail || "(empty)"} />
337
+ <OutputCard title="Stderr tail" body={run?.stderr_tail || "(empty)"} />
338
+ </div>
339
+ </section>
340
+ </div>
341
+ );
342
+ }
@@ -0,0 +1,66 @@
1
+ import { PanelFrame } from "@/design-system/patterns/panel-frame";
2
+ import type { PublicEntry } from "@/lib/content/contracts";
3
+
4
+ function formatPublishedAt(value?: string | null) {
5
+ if (!value) {
6
+ return null;
7
+ }
8
+
9
+ return new Intl.DateTimeFormat("en-US", {
10
+ dateStyle: "medium",
11
+ }).format(new Date(value));
12
+ }
13
+
14
+ export function ContentArticle({
15
+ entry,
16
+ }: {
17
+ entry: PublicEntry;
18
+ }) {
19
+ const publishedAt = formatPublishedAt(entry.publishedAt);
20
+
21
+ return (
22
+ <article className="grid gap-6">
23
+ <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-4 border-border/70">
24
+ <div className="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground">
25
+ {entry.eyebrow ? <span>{entry.eyebrow}</span> : null}
26
+ {publishedAt ? <span>{publishedAt}</span> : null}
27
+ {entry.readingTime ? <span>{entry.readingTime}</span> : null}
28
+ </div>
29
+ <div className="space-y-3">
30
+ <h1 className="max-w-4xl text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
31
+ {entry.title}
32
+ </h1>
33
+ <p className="max-w-3xl text-base leading-8 text-muted-foreground sm:text-lg">
34
+ {entry.description}
35
+ </p>
36
+ </div>
37
+ </PanelFrame>
38
+
39
+ <PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-8 border-border/70">
40
+ <p className="max-w-3xl text-base leading-8 text-foreground">
41
+ {entry.body.intro}
42
+ </p>
43
+
44
+ <div className="grid gap-8">
45
+ {entry.body.sections.map((section) => (
46
+ <section key={section.heading} className="space-y-4">
47
+ <h2 className="text-2xl font-semibold tracking-tight">
48
+ {section.heading}
49
+ </h2>
50
+ <div className="space-y-4">
51
+ {section.paragraphs.map((paragraph) => (
52
+ <p
53
+ key={paragraph}
54
+ className="max-w-3xl text-sm leading-7 text-muted-foreground sm:text-base"
55
+ >
56
+ {paragraph}
57
+ </p>
58
+ ))}
59
+ </div>
60
+ </section>
61
+ ))}
62
+ </div>
63
+ </PanelFrame>
64
+ </article>
65
+ );
66
+ }
@@ -0,0 +1,108 @@
1
+ import type { Route } from "next";
2
+ import Link from "next/link";
3
+
4
+ import { ArrowRight } from "lucide-react";
5
+
6
+ import { PanelFrame } from "@/design-system/patterns/panel-frame";
7
+ import type { PublicContentKind, PublicEntrySummary } from "@/lib/content/contracts";
8
+ import { appRoutes } from "@/lib/app-routes";
9
+
10
+ function entryHref(entry: PublicEntrySummary) {
11
+ if (entry.kind === "docs") {
12
+ return appRoutes.docsPage(entry.slug);
13
+ }
14
+
15
+ return appRoutes.blogPost(entry.slug);
16
+ }
17
+
18
+ function formatPublishedAt(value?: string | null) {
19
+ if (!value) {
20
+ return null;
21
+ }
22
+
23
+ return new Intl.DateTimeFormat("en-US", {
24
+ dateStyle: "medium",
25
+ }).format(new Date(value));
26
+ }
27
+
28
+ export function ContentCollection({
29
+ description,
30
+ entries,
31
+ eyebrow,
32
+ kind,
33
+ title,
34
+ }: {
35
+ description: string;
36
+ entries: PublicEntrySummary[];
37
+ eyebrow: string;
38
+ kind: PublicContentKind;
39
+ title: string;
40
+ }) {
41
+ return (
42
+ <div className="grid gap-6">
43
+ <PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-4 border-border/70">
44
+ <p className="text-sm font-semibold uppercase tracking-[0.28em] text-muted-foreground">
45
+ {eyebrow}
46
+ </p>
47
+ <div className="space-y-3">
48
+ <h1 className="text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
49
+ {title}
50
+ </h1>
51
+ <p className="max-w-3xl text-base leading-8 text-muted-foreground sm:text-lg">
52
+ {description}
53
+ </p>
54
+ </div>
55
+ </PanelFrame>
56
+
57
+ <div className="grid gap-4">
58
+ {entries.map((entry) => {
59
+ const publishedAt = formatPublishedAt(entry.publishedAt);
60
+
61
+ return (
62
+ <Link
63
+ key={`${kind}-${entry.slug.join("/")}`}
64
+ href={entryHref(entry) as Route}
65
+ className="group"
66
+ >
67
+ <PanelFrame
68
+ tone="raised"
69
+ radius="xl"
70
+ padding="lg"
71
+ className="grid gap-4 border-border/70 transition-transform duration-200 group-hover:-translate-y-0.5 md:grid-cols-[1fr,auto]"
72
+ >
73
+ <div className="space-y-3">
74
+ <div className="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground">
75
+ {entry.eyebrow ? <span>{entry.eyebrow}</span> : null}
76
+ {publishedAt ? <span>{publishedAt}</span> : null}
77
+ {entry.readingTime ? <span>{entry.readingTime}</span> : null}
78
+ </div>
79
+ <div className="space-y-2">
80
+ <h2 className="text-2xl font-semibold tracking-tight">
81
+ {entry.title}
82
+ </h2>
83
+ <p className="text-sm leading-7 text-muted-foreground">
84
+ {entry.description}
85
+ </p>
86
+ </div>
87
+ </div>
88
+
89
+ <div className="flex items-center text-sm font-semibold text-foreground">
90
+ Read more
91
+ <ArrowRight className="ml-2 size-4 transition-transform group-hover:translate-x-0.5" />
92
+ </div>
93
+ </PanelFrame>
94
+ </Link>
95
+ );
96
+ })}
97
+
98
+ {entries.length === 0 ? (
99
+ <PanelFrame tone="raised" radius="xl" padding="lg" className="border-border/70">
100
+ <p className="text-sm leading-7 text-muted-foreground">
101
+ No {kind} entries are available for the current content adapter.
102
+ </p>
103
+ </PanelFrame>
104
+ ) : null}
105
+ </div>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,111 @@
1
+ import type { Route } from "next";
2
+ import Link from "next/link";
3
+
4
+ import { ArrowRight } from "lucide-react";
5
+
6
+ import { PanelFrame } from "@/design-system/patterns/panel-frame";
7
+ import { Button } from "@/design-system/primitives/button";
8
+ import type { MarketingPage } from "@/lib/content/contracts";
9
+
10
+ export function MarketingPageCanvas({
11
+ page,
12
+ }: {
13
+ page: MarketingPage;
14
+ }) {
15
+ return (
16
+ <div className="grid gap-8">
17
+ <section className="grid gap-6 lg:grid-cols-[1.15fr,0.85fr]">
18
+ <PanelFrame
19
+ tone="floating"
20
+ radius="xl"
21
+ padding="lg"
22
+ className="marketing-hero-surface relative overflow-hidden border-border/70"
23
+ >
24
+ <div className="marketing-hero-orb absolute inset-y-0 right-0 w-1/2 opacity-80" />
25
+ <div className="relative space-y-6">
26
+ <div className="space-y-3">
27
+ <p className="text-sm font-semibold uppercase tracking-[0.28em] text-muted-foreground">
28
+ {page.heroEyebrow}
29
+ </p>
30
+ <h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
31
+ {page.heroTitle}
32
+ </h1>
33
+ <p className="max-w-2xl text-base leading-8 text-muted-foreground sm:text-lg">
34
+ {page.heroBody}
35
+ </p>
36
+ </div>
37
+
38
+ <div className="flex flex-wrap gap-3">
39
+ <Button asChild type="button">
40
+ <Link href={page.primaryCta.href as Route}>
41
+ {page.primaryCta.label}
42
+ <ArrowRight className="size-4" />
43
+ </Link>
44
+ </Button>
45
+ {page.secondaryCta ? (
46
+ <Button asChild type="button" variant="outline">
47
+ <Link href={page.secondaryCta.href as Route}>
48
+ {page.secondaryCta.label}
49
+ </Link>
50
+ </Button>
51
+ ) : null}
52
+ </div>
53
+ </div>
54
+ </PanelFrame>
55
+
56
+ <div className="grid gap-4">
57
+ {page.sections.slice(0, 3).map((section) => (
58
+ <PanelFrame
59
+ key={section.title}
60
+ tone="raised"
61
+ radius="xl"
62
+ padding="lg"
63
+ className="space-y-3 border-border/70"
64
+ >
65
+ {section.eyebrow ? (
66
+ <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
67
+ {section.eyebrow}
68
+ </p>
69
+ ) : null}
70
+ <h2 className="text-xl font-semibold tracking-tight">{section.title}</h2>
71
+ <p className="text-sm leading-7 text-muted-foreground">{section.body}</p>
72
+ </PanelFrame>
73
+ ))}
74
+ </div>
75
+ </section>
76
+
77
+ <section className="grid gap-4 lg:grid-cols-3">
78
+ {page.sections.map((section) => (
79
+ <PanelFrame
80
+ key={`${page.pageKey}-${section.title}`}
81
+ tone="raised"
82
+ radius="xl"
83
+ padding="lg"
84
+ className="space-y-4 border-border/70"
85
+ >
86
+ {section.eyebrow ? (
87
+ <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
88
+ {section.eyebrow}
89
+ </p>
90
+ ) : null}
91
+ <div className="space-y-2">
92
+ <h3 className="text-2xl font-semibold tracking-tight">{section.title}</h3>
93
+ <p className="text-sm leading-7 text-muted-foreground">{section.body}</p>
94
+ </div>
95
+
96
+ {section.points?.length ? (
97
+ <ul className="space-y-2 text-sm leading-6 text-muted-foreground">
98
+ {section.points.map((point) => (
99
+ <li key={point} className="flex gap-3">
100
+ <span className="mt-2 size-2 rounded-full bg-primary" />
101
+ <span>{point}</span>
102
+ </li>
103
+ ))}
104
+ </ul>
105
+ ) : null}
106
+ </PanelFrame>
107
+ ))}
108
+ </section>
109
+ </div>
110
+ );
111
+ }