minutework 0.1.40 → 0.1.42

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 (180) hide show
  1. package/EXTERNAL_ALPHA.md +17 -1
  2. package/README.md +21 -1
  3. package/assets/claude-local/bundle.json +2 -1
  4. package/assets/claude-local/preambles/base.md +13 -0
  5. package/assets/claude-local/preambles/mobile.md +17 -0
  6. package/assets/claude-local/preambles/tenant.md +17 -0
  7. package/assets/claude-local/preambles/vuilder.md +29 -0
  8. package/assets/claude-local/skills/README.md +5 -0
  9. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +15 -0
  10. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +25 -9
  11. package/assets/claude-local/skills/shell-architecture/SKILL.md +15 -0
  12. package/assets/claude-local/skills/vuilder-public-site-authoring/SKILL.md +10 -4
  13. package/assets/claude-local/skills/vuilder-workspace-architecture/SKILL.md +78 -0
  14. package/assets/templates/vuilder-public-site/.env.example +13 -0
  15. package/assets/templates/vuilder-public-site/README.md +15 -0
  16. package/assets/templates/vuilder-public-site/eslint.config.mjs +6 -0
  17. package/assets/templates/vuilder-public-site/next-env.d.ts +4 -0
  18. package/assets/templates/vuilder-public-site/next.config.mjs +28 -0
  19. package/assets/templates/vuilder-public-site/package.json +40 -0
  20. package/assets/templates/vuilder-public-site/postcss.config.mjs +5 -0
  21. package/assets/templates/vuilder-public-site/src/app/api/onboarding/start/route.ts +19 -0
  22. package/assets/templates/vuilder-public-site/src/app/blog/[slug]/page.tsx +25 -0
  23. package/assets/templates/vuilder-public-site/src/app/blog/page.tsx +26 -0
  24. package/assets/templates/vuilder-public-site/src/app/globals.css +103 -0
  25. package/assets/templates/vuilder-public-site/src/app/layout.tsx +18 -0
  26. package/assets/templates/vuilder-public-site/src/app/onboarding/[[...step]]/page.tsx +39 -0
  27. package/assets/templates/vuilder-public-site/src/app/page.tsx +43 -0
  28. package/assets/templates/vuilder-public-site/src/lib/env.server.test.ts +47 -0
  29. package/assets/templates/vuilder-public-site/src/lib/env.server.ts +92 -0
  30. package/assets/templates/vuilder-public-site/src/lib/platform.server.ts +47 -0
  31. package/assets/templates/vuilder-public-site/src/lib/public-dj.server.ts +86 -0
  32. package/assets/templates/vuilder-public-site/src/lib/public-dj.test.ts +8 -0
  33. package/assets/templates/vuilder-public-site/src/lib/routes.test.ts +13 -0
  34. package/assets/templates/vuilder-public-site/src/lib/routes.ts +12 -0
  35. package/assets/templates/vuilder-public-site/template.json +21 -0
  36. package/assets/templates/vuilder-public-site/test/server-only-stub.ts +1 -0
  37. package/assets/templates/vuilder-public-site/tools/env/check-dev-env.mjs +109 -0
  38. package/assets/templates/vuilder-public-site/tools/env/check-dev-env.test.ts +49 -0
  39. package/assets/templates/vuilder-public-site/tools/template/validate-template.mjs +44 -0
  40. package/assets/templates/vuilder-public-site/tsconfig.json +23 -0
  41. package/assets/templates/vuilder-public-site/vitest.config.ts +15 -0
  42. package/assets/templates/vuilder-shell/.env.example +8 -0
  43. package/assets/templates/vuilder-shell/.storybook/main.ts +19 -0
  44. package/assets/templates/vuilder-shell/.storybook/preview.tsx +38 -0
  45. package/assets/templates/vuilder-shell/README.md +49 -0
  46. package/assets/templates/vuilder-shell/components.json +21 -0
  47. package/assets/templates/vuilder-shell/eslint.config.mjs +41 -0
  48. package/assets/templates/vuilder-shell/next-env.d.ts +6 -0
  49. package/assets/templates/vuilder-shell/next.config.mjs +33 -0
  50. package/assets/templates/vuilder-shell/package.json +61 -0
  51. package/assets/templates/vuilder-shell/postcss.config.mjs +8 -0
  52. package/assets/templates/vuilder-shell/public/.gitkeep +1 -0
  53. package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.test.ts +105 -0
  54. package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.ts +63 -0
  55. package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.test.ts +63 -0
  56. package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.ts +24 -0
  57. package/assets/templates/vuilder-shell/src/app/api/auth/session/route.test.ts +70 -0
  58. package/assets/templates/vuilder-shell/src/app/api/auth/session/route.ts +27 -0
  59. package/assets/templates/vuilder-shell/src/app/app/layout.tsx +17 -0
  60. package/assets/templates/vuilder-shell/src/app/app/page.tsx +30 -0
  61. package/assets/templates/vuilder-shell/src/app/blog/[slug]/page.tsx +15 -0
  62. package/assets/templates/vuilder-shell/src/app/blog/page.tsx +15 -0
  63. package/assets/templates/vuilder-shell/src/app/docs/[...slug]/page.tsx +15 -0
  64. package/assets/templates/vuilder-shell/src/app/docs/page.tsx +15 -0
  65. package/assets/templates/vuilder-shell/src/app/globals.css +70 -0
  66. package/assets/templates/vuilder-shell/src/app/layout.tsx +69 -0
  67. package/assets/templates/vuilder-shell/src/app/login/route.test.ts +33 -0
  68. package/assets/templates/vuilder-shell/src/app/login/route.ts +21 -0
  69. package/assets/templates/vuilder-shell/src/app/page.tsx +16 -0
  70. package/assets/templates/vuilder-shell/src/app/pricing/page.tsx +15 -0
  71. package/assets/templates/vuilder-shell/src/app/providers.tsx +25 -0
  72. package/assets/templates/vuilder-shell/src/app/robots.ts +21 -0
  73. package/assets/templates/vuilder-shell/src/app/sitemap.ts +33 -0
  74. package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/connect/page.tsx +31 -0
  75. package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/page.tsx +54 -0
  76. package/assets/templates/vuilder-shell/src/components/ui/button.tsx +59 -0
  77. package/assets/templates/vuilder-shell/src/components/ui/input.tsx +21 -0
  78. package/assets/templates/vuilder-shell/src/design-system/docs/governance.mdx +26 -0
  79. package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.stories.tsx +48 -0
  80. package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.tsx +26 -0
  81. package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.stories.tsx +26 -0
  82. package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.tsx +35 -0
  83. package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
  84. package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
  85. package/assets/templates/vuilder-shell/src/design-system/primitives/button.stories.tsx +37 -0
  86. package/assets/templates/vuilder-shell/src/design-system/primitives/button.ts +1 -0
  87. package/assets/templates/vuilder-shell/src/design-system/primitives/input.stories.tsx +26 -0
  88. package/assets/templates/vuilder-shell/src/design-system/primitives/input.ts +1 -0
  89. package/assets/templates/vuilder-shell/src/design-system/recipes/chrome.ts +28 -0
  90. package/assets/templates/vuilder-shell/src/design-system/tokens/foundation.css +31 -0
  91. package/assets/templates/vuilder-shell/src/design-system/tokens/index.css +3 -0
  92. package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.json +85 -0
  93. package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.ts +87 -0
  94. package/assets/templates/vuilder-shell/src/design-system/tokens/semantic.css +105 -0
  95. package/assets/templates/vuilder-shell/src/design-system/tokens/theme.css +59 -0
  96. package/assets/templates/vuilder-shell/src/design-system/tokens/tokens.stories.tsx +71 -0
  97. package/assets/templates/vuilder-shell/src/features/dashboard/components/tenant-dashboard.tsx +134 -0
  98. package/assets/templates/vuilder-shell/src/features/public-shell/components/static-public-page.tsx +58 -0
  99. package/assets/templates/vuilder-shell/src/features/shell/components/authenticated-app-layout-shell.tsx +84 -0
  100. package/assets/templates/vuilder-shell/src/features/shell/components/private-app-shell.tsx +22 -0
  101. package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-connect-screen.tsx +89 -0
  102. package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-workspace-screen.tsx +49 -0
  103. package/assets/templates/vuilder-shell/src/lib/app-routes.test.ts +37 -0
  104. package/assets/templates/vuilder-shell/src/lib/app-routes.ts +86 -0
  105. package/assets/templates/vuilder-shell/src/lib/auth-routes.server.test.ts +26 -0
  106. package/assets/templates/vuilder-shell/src/lib/auth-routes.server.ts +53 -0
  107. package/assets/templates/vuilder-shell/src/lib/http/same-origin.test.ts +23 -0
  108. package/assets/templates/vuilder-shell/src/lib/http/same-origin.ts +18 -0
  109. package/assets/templates/vuilder-shell/src/lib/platform/client.server.test.ts +201 -0
  110. package/assets/templates/vuilder-shell/src/lib/platform/client.server.ts +540 -0
  111. package/assets/templates/vuilder-shell/src/lib/platform/contracts.ts +190 -0
  112. package/assets/templates/vuilder-shell/src/lib/platform/endpoints.server.ts +29 -0
  113. package/assets/templates/vuilder-shell/src/lib/platform/env.server.ts +82 -0
  114. package/assets/templates/vuilder-shell/src/lib/platform/route-response.ts +33 -0
  115. package/assets/templates/vuilder-shell/src/lib/platform/session.server.ts +145 -0
  116. package/assets/templates/vuilder-shell/src/lib/public-site.test.ts +20 -0
  117. package/assets/templates/vuilder-shell/src/lib/public-site.ts +48 -0
  118. package/assets/templates/vuilder-shell/src/lib/theme-config.ts +10 -0
  119. package/assets/templates/vuilder-shell/src/lib/theme.tsx +159 -0
  120. package/assets/templates/vuilder-shell/src/lib/utils.ts +6 -0
  121. package/assets/templates/vuilder-shell/template.json +28 -0
  122. package/assets/templates/vuilder-shell/template.schema.json +171 -0
  123. package/assets/templates/vuilder-shell/test/server-only-stub.ts +1 -0
  124. package/assets/templates/vuilder-shell/tools/design-system/build-token-manifest.mjs +3 -0
  125. package/assets/templates/vuilder-shell/tools/design-system/check-imports.mjs +9 -0
  126. package/assets/templates/vuilder-shell/tools/design-system/check-stories.mjs +9 -0
  127. package/assets/templates/vuilder-shell/tools/design-system/check-values.mjs +9 -0
  128. package/assets/templates/vuilder-shell/tools/design-system/checks.mjs +238 -0
  129. package/assets/templates/vuilder-shell/tools/design-system/eslint-plugin-design-system.mjs +184 -0
  130. package/assets/templates/vuilder-shell/tools/design-system/playwright.config.mjs +34 -0
  131. package/assets/templates/vuilder-shell/tools/design-system/run-checks.mjs +22 -0
  132. package/assets/templates/vuilder-shell/tools/design-system/shared.mjs +166 -0
  133. package/assets/templates/vuilder-shell/tools/design-system/visual.spec.ts +41 -0
  134. package/assets/templates/vuilder-shell/tools/template/validate-route-contract.mjs +373 -0
  135. package/assets/templates/vuilder-shell/tools/template/validate-template.mjs +45 -0
  136. package/assets/templates/vuilder-shell/tools/template/with-public-site-fixture.mjs +45 -0
  137. package/assets/templates/vuilder-shell/tsconfig.json +42 -0
  138. package/assets/templates/vuilder-shell/vitest.config.ts +23 -0
  139. package/dist/auth.js +97 -31
  140. package/dist/auth.js.map +1 -1
  141. package/dist/deploy-state.d.ts +1 -0
  142. package/dist/deploy-state.js.map +1 -1
  143. package/dist/deploy.js +18 -4
  144. package/dist/deploy.js.map +1 -1
  145. package/dist/developer-client.d.ts +2 -1
  146. package/dist/developer-client.js.map +1 -1
  147. package/dist/index.js +12 -2
  148. package/dist/index.js.map +1 -1
  149. package/dist/init-prompt.js +21 -13
  150. package/dist/init-prompt.js.map +1 -1
  151. package/dist/init.d.ts +3 -1
  152. package/dist/init.js +105 -13
  153. package/dist/init.js.map +1 -1
  154. package/dist/orchestrator-context.js +17 -5
  155. package/dist/orchestrator-context.js.map +1 -1
  156. package/dist/orchestrator-state.d.ts +2 -2
  157. package/dist/orchestrator-state.js.map +1 -1
  158. package/dist/publish.js +12 -2
  159. package/dist/publish.js.map +1 -1
  160. package/dist/sandbox.js +11 -1
  161. package/dist/sandbox.js.map +1 -1
  162. package/dist/state.d.ts +2 -0
  163. package/dist/state.js +9 -0
  164. package/dist/state.js.map +1 -1
  165. package/dist/workspace-assets.d.ts +13 -0
  166. package/dist/workspace-assets.js +86 -5
  167. package/dist/workspace-assets.js.map +1 -1
  168. package/dist/workspace-bootstrap.d.ts +47 -2
  169. package/dist/workspace-bootstrap.js +45 -1
  170. package/dist/workspace-bootstrap.js.map +1 -1
  171. package/package.json +3 -3
  172. package/vendor/workspace-mcp/context.d.ts +3 -1
  173. package/vendor/workspace-mcp/context.js +134 -21
  174. package/vendor/workspace-mcp/context.js.map +1 -1
  175. package/vendor/workspace-mcp/types.d.ts +72 -7
  176. package/vendor/workspace-mcp/types.js +8 -4
  177. package/vendor/workspace-mcp/types.js.map +1 -1
  178. package/assets/templates/fastapi-sidecar/poetry.lock +0 -757
  179. package/assets/templates/next-tenant-app/package-lock.json +0 -9682
  180. package/assets/templates/next-tenant-app/pnpm-lock.yaml +0 -6062
@@ -0,0 +1,24 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import { hasTrustedBrowserOrigin } from "@/lib/http/same-origin";
4
+ import { revokeVuilderShellSession } from "@/lib/platform/client.server";
5
+ import {
6
+ clearPlatformSessionFromResponse,
7
+ readPlatformAuthState,
8
+ } from "@/lib/platform/session.server";
9
+
10
+ export async function POST(request: Request) {
11
+ if (!hasTrustedBrowserOrigin(request)) {
12
+ return NextResponse.json(
13
+ { detail: "Same-origin browser submission required." },
14
+ { status: 403 },
15
+ );
16
+ }
17
+
18
+ const authState = await readPlatformAuthState();
19
+ await revokeVuilderShellSession(authState).catch(() => null);
20
+ const response = new NextResponse(null, { status: 204 });
21
+ response.headers.set("Cache-Control", "no-store");
22
+ clearPlatformSessionFromResponse(response);
23
+ return response;
24
+ }
@@ -0,0 +1,70 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ loadCurrentSession: vi.fn(),
5
+ readPlatformAuthState: vi.fn(),
6
+ syncPlatformAuthStateToResponse: vi.fn(),
7
+ toPlatformErrorResponse: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("@/lib/platform/client.server", () => ({
11
+ loadCurrentSession: mocks.loadCurrentSession,
12
+ }));
13
+
14
+ vi.mock("@/lib/platform/route-response", () => ({
15
+ toPlatformErrorResponse: mocks.toPlatformErrorResponse,
16
+ }));
17
+
18
+ vi.mock("@/lib/platform/session.server", () => ({
19
+ readPlatformAuthState: mocks.readPlatformAuthState,
20
+ syncPlatformAuthStateToResponse: mocks.syncPlatformAuthStateToResponse,
21
+ }));
22
+
23
+ import { GET } from "./route";
24
+
25
+ describe("GET /api/auth/session", () => {
26
+ afterEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ it("returns the current session with no-store cache headers", async () => {
31
+ const authState = { shellSessionToken: "shell-token-1" };
32
+ const nextAuthState = { shellSessionToken: "shell-token-2" };
33
+ mocks.readPlatformAuthState.mockResolvedValue(authState);
34
+ mocks.loadCurrentSession.mockResolvedValue({
35
+ authState: nextAuthState,
36
+ data: { authenticated: true },
37
+ });
38
+
39
+ const response = await GET();
40
+
41
+ expect(response.status).toBe(200);
42
+ expect(response.headers.get("Cache-Control")).toBe("no-store");
43
+ await expect(response.json()).resolves.toEqual({ authenticated: true });
44
+ expect(mocks.syncPlatformAuthStateToResponse).toHaveBeenCalledWith(
45
+ expect.any(Response),
46
+ authState,
47
+ nextAuthState,
48
+ );
49
+ });
50
+
51
+ it("returns platform errors with no-store cache headers", async () => {
52
+ const authState = { shellSessionToken: "shell-token-1" };
53
+ const error = new Error("platform failed");
54
+ mocks.readPlatformAuthState.mockResolvedValue(authState);
55
+ mocks.loadCurrentSession.mockRejectedValue(error);
56
+ mocks.toPlatformErrorResponse.mockReturnValue(
57
+ Response.json({ detail: "Platform error" }, { status: 503 }),
58
+ );
59
+
60
+ const response = await GET();
61
+
62
+ expect(response.status).toBe(503);
63
+ expect(response.headers.get("Cache-Control")).toBe("no-store");
64
+ expect(mocks.toPlatformErrorResponse).toHaveBeenCalledWith(
65
+ error,
66
+ "Unable to load the current platform session.",
67
+ authState,
68
+ );
69
+ });
70
+ });
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import { loadCurrentSession } from "@/lib/platform/client.server";
4
+ import { toPlatformErrorResponse } from "@/lib/platform/route-response";
5
+ import {
6
+ readPlatformAuthState,
7
+ syncPlatformAuthStateToResponse,
8
+ } from "@/lib/platform/session.server";
9
+
10
+ export async function GET() {
11
+ const authState = await readPlatformAuthState();
12
+ try {
13
+ const result = await loadCurrentSession(authState);
14
+ const response = NextResponse.json(result.data, { status: 200 });
15
+ response.headers.set("Cache-Control", "no-store");
16
+ syncPlatformAuthStateToResponse(response, authState, result.authState);
17
+ return response;
18
+ } catch (error) {
19
+ const response = toPlatformErrorResponse(
20
+ error,
21
+ "Unable to load the current platform session.",
22
+ authState,
23
+ );
24
+ response.headers.set("Cache-Control", "no-store");
25
+ return response;
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ import type { Metadata } from "next";
2
+ import type { ReactNode } from "react";
3
+
4
+ export const metadata: Metadata = {
5
+ robots: {
6
+ index: false,
7
+ follow: false,
8
+ },
9
+ };
10
+
11
+ export default function AuthenticatedAppLayout({
12
+ children,
13
+ }: {
14
+ children: ReactNode;
15
+ }) {
16
+ return children;
17
+ }
@@ -0,0 +1,30 @@
1
+ import { notFound, redirect } from "next/navigation";
2
+
3
+ import { appRoutes } from "@/lib/app-routes";
4
+ import { loadCurrentWorkspaceShellSession } from "@/lib/platform/client.server";
5
+ import { readPlatformAuthState } from "@/lib/platform/session.server";
6
+
7
+ export const metadata = {
8
+ title: "Workspace",
9
+ };
10
+
11
+ export default async function AppHomePage() {
12
+ const authState = await readPlatformAuthState();
13
+ const shellSession = (await loadCurrentWorkspaceShellSession(authState)).data;
14
+ const { membership, session, workspaceCanOpen, workspaceCanView } =
15
+ shellSession;
16
+
17
+ if (!session.authenticated) {
18
+ redirect(appRoutes.login);
19
+ }
20
+
21
+ if (!membership || !workspaceCanView) {
22
+ notFound();
23
+ }
24
+
25
+ if (!workspaceCanOpen) {
26
+ redirect(appRoutes.workspaceConnect(membership.workspace_slug));
27
+ }
28
+
29
+ redirect(appRoutes.workspaceShell(membership.workspace_slug));
30
+ }
@@ -0,0 +1,15 @@
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
+
3
+ export const metadata = {
4
+ title: "Blog",
5
+ };
6
+
7
+ export default function BlogPostPage() {
8
+ return (
9
+ <StaticPublicPage
10
+ eyebrow="Blog"
11
+ title="Update"
12
+ body="A customer-facing update."
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,15 @@
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
+
3
+ export const metadata = {
4
+ title: "Blog",
5
+ };
6
+
7
+ export default function BlogIndexPage() {
8
+ return (
9
+ <StaticPublicPage
10
+ eyebrow="Blog"
11
+ title="Updates"
12
+ body="News, releases, and customer notes."
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,15 @@
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
+
3
+ export const metadata = {
4
+ title: "Docs",
5
+ };
6
+
7
+ export default function DocsPage() {
8
+ return (
9
+ <StaticPublicPage
10
+ eyebrow="Docs"
11
+ title="Documentation page"
12
+ body="Helpful context for customer workflows."
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,15 @@
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
+
3
+ export const metadata = {
4
+ title: "Docs",
5
+ };
6
+
7
+ export default function DocsIndexPage() {
8
+ return (
9
+ <StaticPublicPage
10
+ eyebrow="Docs"
11
+ title="Customer documentation"
12
+ body="Guides, onboarding notes, and product documentation."
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,70 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "../design-system/tokens/index.css";
4
+
5
+ @layer base {
6
+ html {
7
+ height: 100%;
8
+ }
9
+
10
+ body {
11
+ min-height: 100%;
12
+ margin: 0;
13
+ }
14
+
15
+ * {
16
+ @apply border-border outline-ring/50;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ body {
21
+ @apply bg-background text-foreground antialiased;
22
+ }
23
+
24
+ a {
25
+ color: inherit;
26
+ }
27
+ }
28
+
29
+ @layer components {
30
+ .public-site-background {
31
+ background:
32
+ linear-gradient(
33
+ 180deg,
34
+ var(--color-background) 0%,
35
+ color-mix(in oklab, var(--color-surface-raised) 68%, white) 100%
36
+ );
37
+ }
38
+
39
+ .public-site-orb {
40
+ background:
41
+ radial-gradient(
42
+ circle at top left,
43
+ color-mix(in oklab, var(--color-primary) 28%, white) 0%,
44
+ transparent 48%
45
+ ),
46
+ radial-gradient(
47
+ circle at top right,
48
+ color-mix(in oklab, var(--color-chart-2) 18%, white) 0%,
49
+ transparent 42%
50
+ );
51
+ }
52
+
53
+ .marketing-hero-surface {
54
+ background:
55
+ linear-gradient(
56
+ 135deg,
57
+ color-mix(in oklab, var(--color-surface) 78%, white) 0%,
58
+ color-mix(in oklab, var(--color-primary) 8%, white) 100%
59
+ );
60
+ }
61
+
62
+ .marketing-hero-orb {
63
+ background:
64
+ radial-gradient(
65
+ circle at center,
66
+ color-mix(in oklab, var(--color-chart-2) 14%, white) 0%,
67
+ transparent 70%
68
+ );
69
+ }
70
+ }
@@ -0,0 +1,69 @@
1
+ import "./globals.css";
2
+ import type { Metadata } from "next";
3
+ import { Geist, Geist_Mono } from "next/font/google";
4
+ import { cookies } from "next/headers";
5
+ import type { ReactNode } from "react";
6
+
7
+ import { AppProviders } from "./providers";
8
+ import { resolvePublicMetadataBase } from "@/lib/public-site";
9
+ import {
10
+ isThemeMode,
11
+ THEME_COOKIE_NAME,
12
+ type ThemeMode,
13
+ } from "@/lib/theme-config";
14
+
15
+ const geistSans = Geist({
16
+ subsets: ["latin"],
17
+ variable: "--font-sans",
18
+ });
19
+
20
+ const geistMono = Geist_Mono({
21
+ subsets: ["latin"],
22
+ variable: "--font-mono",
23
+ });
24
+
25
+ export function generateMetadata(): Metadata {
26
+ const metadataBase = resolvePublicMetadataBase();
27
+ const appName = process.env.MW_TEMPLATE_APP_NAME || "Vuilder Shell";
28
+
29
+ return {
30
+ ...(metadataBase ? { metadataBase } : {}),
31
+ title: {
32
+ default: appName,
33
+ template: `%s | ${appName}`,
34
+ },
35
+ description: "Customer-facing branded tenant shell.",
36
+ openGraph: {
37
+ title: appName,
38
+ description: "Customer-facing branded tenant shell.",
39
+ siteName: appName,
40
+ type: "website",
41
+ ...(process.env.MW_PUBLIC_BASE_URL ? { url: process.env.MW_PUBLIC_BASE_URL } : {}),
42
+ },
43
+ };
44
+ }
45
+
46
+ export default async function RootLayout({ children }: { children: ReactNode }) {
47
+ const cookieStore = await cookies();
48
+ const themeCookie = cookieStore.get(THEME_COOKIE_NAME)?.value;
49
+ const initialTheme: ThemeMode = isThemeMode(themeCookie) ? themeCookie : "system";
50
+ const resolvedThemeClass =
51
+ initialTheme === "light" || initialTheme === "dark" ? initialTheme : undefined;
52
+ const colorScheme =
53
+ initialTheme === "light" || initialTheme === "dark" ? initialTheme : undefined;
54
+
55
+ return (
56
+ <html
57
+ lang="en"
58
+ suppressHydrationWarning
59
+ className={resolvedThemeClass}
60
+ style={colorScheme ? { colorScheme } : undefined}
61
+ >
62
+ <body
63
+ className={`${geistSans.variable} ${geistMono.variable} bg-background font-sans text-foreground antialiased`}
64
+ >
65
+ <AppProviders initialTheme={initialTheme}>{children}</AppProviders>
66
+ </body>
67
+ </html>
68
+ );
69
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ applyVuilderShellHandoffStateToResponse: vi.fn(),
5
+ createVuilderShellHandoffState: vi.fn(() => "state-1"),
6
+ }));
7
+
8
+ vi.mock("@/lib/platform/session.server", () => ({
9
+ applyVuilderShellHandoffStateToResponse:
10
+ mocks.applyVuilderShellHandoffStateToResponse,
11
+ createVuilderShellHandoffState: mocks.createVuilderShellHandoffState,
12
+ }));
13
+
14
+ import { GET } from "./route";
15
+
16
+ describe("GET /login", () => {
17
+ it("sets shell handoff state and redirects to central SSO", () => {
18
+ const response = GET(
19
+ new Request("https://shell.example.com/login?return_to=%2Fw%2Ffleet-alpha"),
20
+ );
21
+
22
+ expect(response.status).toBe(307);
23
+ expect(response.headers.get("location")).toBe(
24
+ "http://127.0.0.1:3400/login?returnTo=http%3A%2F%2F127.0.0.1%3A3301%2Fw%2Ffleet-alpha%2Fconnect%3Fmw_shell_state%3Dstate-1",
25
+ );
26
+ expect(response.headers.get("cache-control")).toBe("no-store");
27
+ expect(response.headers.get("referrer-policy")).toBe("no-referrer");
28
+ expect(mocks.applyVuilderShellHandoffStateToResponse).toHaveBeenCalledWith(
29
+ expect.any(Response),
30
+ "state-1",
31
+ );
32
+ });
33
+ });
@@ -0,0 +1,21 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import { buildCentralSsoLoginUrl } from "@/lib/auth-routes.server";
4
+ import {
5
+ applyVuilderShellHandoffStateToResponse,
6
+ createVuilderShellHandoffState,
7
+ } from "@/lib/platform/session.server";
8
+
9
+ export function GET(request: Request) {
10
+ const url = new URL(request.url);
11
+ const returnPath =
12
+ url.searchParams.get("return_to") ?? url.searchParams.get("returnTo");
13
+ const handoffState = createVuilderShellHandoffState();
14
+ const response = NextResponse.redirect(
15
+ buildCentralSsoLoginUrl(returnPath, handoffState),
16
+ );
17
+ response.headers.set("Cache-Control", "no-store");
18
+ response.headers.set("Referrer-Policy", "no-referrer");
19
+ applyVuilderShellHandoffStateToResponse(response, handoffState);
20
+ return response;
21
+ }
@@ -0,0 +1,16 @@
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
+
3
+ export const metadata = {
4
+ title: "Vuilder Shell",
5
+ description: "Customer-facing branded tenant shell.",
6
+ };
7
+
8
+ export default function HomePage() {
9
+ return (
10
+ <StaticPublicPage
11
+ eyebrow="Vuilder Shell"
12
+ title="A branded customer workspace connected to MinuteWork"
13
+ body="Customer workspace routes stay branded while identity, provisioning, and app-pack install authority stay on the platform."
14
+ />
15
+ );
16
+ }
@@ -0,0 +1,15 @@
1
+ import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
2
+
3
+ export const metadata = {
4
+ title: "Pricing",
5
+ };
6
+
7
+ export default function PricingPage() {
8
+ return (
9
+ <StaticPublicPage
10
+ eyebrow="Pricing"
11
+ title="Plans for customer access"
12
+ body="Clear plan options for customer workspaces."
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { ThemeProvider } from "@/lib/theme";
6
+ import type { ThemeMode } from "@/lib/theme-config";
7
+
8
+ export function AppProviders({
9
+ children,
10
+ initialTheme,
11
+ }: {
12
+ children: ReactNode;
13
+ initialTheme: ThemeMode;
14
+ }) {
15
+ return (
16
+ <ThemeProvider
17
+ attribute="class"
18
+ defaultTheme={initialTheme}
19
+ enableSystem
20
+ disableTransitionOnChange
21
+ >
22
+ {children}
23
+ </ThemeProvider>
24
+ );
25
+ }
@@ -0,0 +1,21 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ import { resolvePublicMetadataBase } from "@/lib/public-site";
4
+
5
+ export default function robots(): MetadataRoute.Robots {
6
+ const metadataBase = resolvePublicMetadataBase();
7
+ return {
8
+ rules: [
9
+ {
10
+ userAgent: "*",
11
+ allow: "/",
12
+ },
13
+ ],
14
+ ...(metadataBase
15
+ ? {
16
+ sitemap: new URL("/sitemap.xml", metadataBase).toString(),
17
+ host: metadataBase.host,
18
+ }
19
+ : {}),
20
+ };
21
+ }
@@ -0,0 +1,33 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ import { appRoutes } from "@/lib/app-routes";
4
+ import { resolvePublicSiteUrl } from "@/lib/public-site";
5
+
6
+ function buildSitemapUrl(pathname: string) {
7
+ return resolvePublicSiteUrl(pathname)?.toString() ?? pathname;
8
+ }
9
+
10
+ export default function sitemap(): MetadataRoute.Sitemap {
11
+ return [
12
+ {
13
+ url: buildSitemapUrl(appRoutes.publicHome),
14
+ changeFrequency: "weekly",
15
+ priority: 1,
16
+ },
17
+ {
18
+ url: buildSitemapUrl(appRoutes.pricing),
19
+ changeFrequency: "monthly",
20
+ priority: 0.8,
21
+ },
22
+ {
23
+ url: buildSitemapUrl(appRoutes.docsIndex),
24
+ changeFrequency: "weekly",
25
+ priority: 0.8,
26
+ },
27
+ {
28
+ url: buildSitemapUrl(appRoutes.blogIndex),
29
+ changeFrequency: "weekly",
30
+ priority: 0.8,
31
+ },
32
+ ];
33
+ }
@@ -0,0 +1,31 @@
1
+ import { notFound } from "next/navigation";
2
+
3
+ import { VuilderConnectScreen } from "@/features/vuilder-shell/components/vuilder-connect-screen";
4
+ import { loadWorkspaceShellSession } from "@/lib/platform/client.server";
5
+ import { readPlatformAuthState } from "@/lib/platform/session.server";
6
+
7
+ type WorkspaceConnectPageProps = {
8
+ params: Promise<{
9
+ workspace_slug: string;
10
+ }>;
11
+ };
12
+
13
+ export const metadata = {
14
+ title: "Connect Workspace",
15
+ };
16
+
17
+ export default async function WorkspaceConnectPage({
18
+ params,
19
+ }: WorkspaceConnectPageProps) {
20
+ const { workspace_slug: workspaceSlug } = await params;
21
+ const authState = await readPlatformAuthState();
22
+ const shellSession = (await loadWorkspaceShellSession(authState, workspaceSlug))
23
+ .data;
24
+ const { membership, session, workspaceCanView } = shellSession;
25
+
26
+ if (session.authenticated && (!membership || !workspaceCanView)) {
27
+ notFound();
28
+ }
29
+
30
+ return <VuilderConnectScreen session={session} workspaceSlug={workspaceSlug} />;
31
+ }
@@ -0,0 +1,54 @@
1
+ import { notFound, redirect } from "next/navigation";
2
+
3
+ import { AuthenticatedAppLayoutShell } from "@/features/shell/components/authenticated-app-layout-shell";
4
+ import { VuilderWorkspaceScreen } from "@/features/vuilder-shell/components/vuilder-workspace-screen";
5
+ import { loadWorkspaceShellSession } from "@/lib/platform/client.server";
6
+ import { appRoutes } from "@/lib/app-routes";
7
+ import { readPlatformAuthState } from "@/lib/platform/session.server";
8
+
9
+ type WorkspacePageProps = {
10
+ params: Promise<{
11
+ workspace_slug: string;
12
+ }>;
13
+ };
14
+
15
+ export const metadata = {
16
+ title: "Workspace",
17
+ };
18
+
19
+ export default async function WorkspacePage({ params }: WorkspacePageProps) {
20
+ const { workspace_slug: workspaceSlug } = await params;
21
+ const appName = process.env.MW_TEMPLATE_APP_NAME || "Vuilder Shell";
22
+ const authState = await readPlatformAuthState();
23
+ const shellSession = (await loadWorkspaceShellSession(authState, workspaceSlug))
24
+ .data;
25
+ const { membership, session, workspaceCanOpen, workspaceCanView } =
26
+ shellSession;
27
+
28
+ if (!session.authenticated) {
29
+ redirect(appRoutes.loginForWorkspace(workspaceSlug));
30
+ }
31
+
32
+ if (!membership || !workspaceCanView) {
33
+ notFound();
34
+ }
35
+
36
+ if (!workspaceCanOpen) {
37
+ redirect(appRoutes.workspaceConnect(workspaceSlug));
38
+ }
39
+
40
+ return (
41
+ <AuthenticatedAppLayoutShell
42
+ appName={appName}
43
+ membership={membership}
44
+ session={session}
45
+ >
46
+ <VuilderWorkspaceScreen
47
+ appName={appName}
48
+ membership={membership}
49
+ session={session}
50
+ workspaceSlug={workspaceSlug}
51
+ />
52
+ </AuthenticatedAppLayoutShell>
53
+ );
54
+ }