@pantheon-systems/create-p1-starter-kit 0.13.0 → 0.14.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.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The render pipeline itself is tested in the SDK (create-published-page.test.tsx).
3
+ * What matters here is the wiring: that both routes are shims over the factory,
4
+ * and that the segment config Next.js statically analyzes survives in the files.
5
+ */
6
+
7
+ import { readFileSync } from "fs";
8
+ import { resolve, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { describe, expect, it, vi } from "vitest";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const starterDir = resolve(__dirname, "..");
14
+
15
+ const factory = vi.hoisted(() => ({
16
+ Page: () => null,
17
+ HomePage: () => null,
18
+ generateMetadata: vi.fn(),
19
+ generateHomeMetadata: vi.fn(),
20
+ generateStaticParams: vi.fn(),
21
+ }));
22
+
23
+ vi.mock("@pantheon-systems/p1-next-sdk/server", () => ({
24
+ createPublishedPage: vi.fn(() => factory),
25
+ loadRouteTemplateKeys: vi.fn(),
26
+ }));
27
+
28
+ // The routes reach these through app/published-pages.tsx. Mocked so this stays
29
+ // a wiring test: importing the real modules drags in the whole datasource graph,
30
+ // which is not installed when the built template's copy of this test runs
31
+ // inside the create-p1-starter-kit package.
32
+ vi.mock("../lib/remote-datasource-fetchers", () => ({
33
+ REMOTE_DATASOURCE_FETCHERS: [],
34
+ }));
35
+ vi.mock("../lib/page-seo", () => ({ resolvePageMetadata: vi.fn() }));
36
+
37
+ vi.mock("../app/[...puckPath]/client", () => ({ Client: () => null }));
38
+ vi.mock("../components/puck/welcome-block-render", () => ({
39
+ WelcomeBlockRender: () => null,
40
+ }));
41
+
42
+ const routes = {
43
+ catchAll: "app/[...puckPath]/page.tsx",
44
+ home: "app/page.tsx",
45
+ } as const;
46
+
47
+ describe("published page routes are shims over the SDK factory", () => {
48
+ it("wires the catch-all route to the factory", async () => {
49
+ const route = await import("../app/[...puckPath]/page");
50
+ expect(route.default).toBe(factory.Page);
51
+ expect(route.generateMetadata).toBe(factory.generateMetadata);
52
+ expect(route.generateStaticParams).toBe(factory.generateStaticParams);
53
+ });
54
+
55
+ it("wires the home route to the factory", async () => {
56
+ const route = await import("../app/page");
57
+ expect(route.default).toBe(factory.HomePage);
58
+ expect(route.generateMetadata).toBe(factory.generateHomeMetadata);
59
+ });
60
+
61
+ // Next.js statically analyzes segment-config exports, so this value cannot be
62
+ // re-exported through the factory — a computed or forwarded `revalidate` goes
63
+ // undetected and the route silently loses its revalidation window.
64
+ it.each(Object.values(routes))(
65
+ "keeps revalidate a literal export in %s",
66
+ (file) => {
67
+ const source = readFileSync(resolve(starterDir, file), "utf-8");
68
+ expect(source).toMatch(/^export const revalidate = \d+;$/m);
69
+ },
70
+ );
71
+
72
+ // The pipeline is the SDK's now; a copy growing back here is the regression
73
+ // this guards against.
74
+ it.each(Object.values(routes))("leaves no pipeline logic in %s", (file) => {
75
+ const source = readFileSync(resolve(starterDir, file), "utf-8");
76
+ for (const symbol of [
77
+ "loadPublishedPage",
78
+ "loadRemoteDatasourceContext",
79
+ "resolveDataTemplates",
80
+ "isInternalPath",
81
+ "notFound",
82
+ ]) {
83
+ expect(source).not.toContain(symbol);
84
+ }
85
+ });
86
+ });
@@ -1,130 +1,23 @@
1
1
  /**
2
2
  * Catch-all route that renders user-facing pages generated by Puck.
3
+ *
4
+ * The pipeline lives in the SDK — see createPublishedPage in
5
+ * app/published-pages.tsx for what this route is made of.
3
6
  */
4
7
 
5
- import { cache } from "react";
6
- import type { Metadata } from "next";
7
- import { notFound } from "next/navigation";
8
- import {
9
- loadRemoteDatasourceContext,
10
- extractReferencedDatasourceIds,
11
- resolveDataTemplates,
12
- pagePathFromCatchAllSegments,
13
- } from "@pantheon-systems/puck-css/server";
14
- import {
15
- createCssQueryFetchers,
16
- loadPublishedPage,
17
- loadRouteTemplateKeys,
18
- } from "@pantheon-systems/p1-next-sdk/server";
19
- import { REMOTE_DATASOURCE_FETCHERS } from "../../lib/remote-datasource-fetchers";
20
- import { ContentUnavailable } from "../../components/content-unavailable";
21
- import { resolvePageMetadata } from "../../lib/page-seo";
22
- import { Client } from "./client";
23
-
24
- const getCcrQueryFetchers = cache(() => createCssQueryFetchers());
25
-
26
- // Document namespaces that live alongside pages but are never routable.
27
- const INTERNAL_PATH_PREFIXES = ["/_registry", "/_redirects"];
28
-
29
- // Lowercased to match the server, which normalizes document paths to lower case
30
- // before looking them up — so /_Redirects/x resolves the same record as
31
- // /_redirects/x and must be refused just the same.
32
- function isInternalPath(path: string): boolean {
33
- const normalized = path.toLowerCase();
34
- return INTERNAL_PATH_PREFIXES.some(
35
- (prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`),
36
- );
37
- }
8
+ import { published } from "../published-pages";
38
9
 
39
10
  /**
40
11
  * Backstop only: publishing calls revalidatePath for the affected routes, so
41
12
  * cached pages normally refresh the moment their content changes. This bounds
42
13
  * how long an edit made outside that path (a direct API write, a restored
43
14
  * branch) can stay stale.
15
+ *
16
+ * Must stay a literal here: Next.js statically analyzes segment-config exports,
17
+ * so a value re-exported from the factory would go undetected.
44
18
  */
45
19
  export const revalidate = 300;
46
20
 
47
- /**
48
- * Empty on purpose. Routes are authored in P1, so there is nothing to enumerate
49
- * at build time — but declaring this is what marks the segment statically
50
- * renderable at all; without it every path here renders fully dynamically and
51
- * no response is ever cacheable. Unlisted paths render on first request and are
52
- * cached from then on (dynamicParams defaults to true).
53
- */
54
- export function generateStaticParams(): { puckPath: string[] }[] {
55
- return [];
56
- }
57
-
58
- export async function generateMetadata({
59
- params,
60
- }: {
61
- params: Promise<{ puckPath: string[] }>;
62
- }): Promise<Metadata> {
63
- const { puckPath = [] } = await params;
64
- const path = pagePathFromCatchAllSegments(puckPath);
65
-
66
- if (isInternalPath(path)) {
67
- return { title: "Not Found" };
68
- }
69
-
70
- const result = await loadPublishedPage(path);
71
- if (result.status === "missing") return { title: "Not Found" };
72
- if (result.status === "unavailable") {
73
- return { title: "Temporarily unavailable" };
74
- }
75
- return resolvePageMetadata({ pageData: result.data, path });
76
- }
77
-
78
- export default async function Page({
79
- params,
80
- }: {
81
- params: Promise<{ puckPath: string[] }>;
82
- }) {
83
- const { puckPath = [] } = await params;
84
- const path = pagePathFromCatchAllSegments(puckPath);
85
-
86
- if (isInternalPath(path)) {
87
- notFound();
88
- }
89
-
90
- const result = await loadPublishedPage(path);
91
-
92
- // A real 404, so misses are not cached as successes now that this route is
93
- // statically renderable. An outage is deliberately not a 404 — that would
94
- // deindex published pages over a transient blip.
95
- if (result.status === "missing") {
96
- notFound();
97
- }
98
- if (result.status === "unavailable") {
99
- return <ContentUnavailable />;
100
- }
101
-
102
- const data = result.data;
103
-
104
- const [routeTemplateKeys, ccrQueryFetchers] = await Promise.all([
105
- loadRouteTemplateKeys(),
106
- getCcrQueryFetchers(),
107
- ]);
108
- const builtinFetchers = [...REMOTE_DATASOURCE_FETCHERS, ...ccrQueryFetchers];
109
-
110
- const referencedDatasourceIds = extractReferencedDatasourceIds(data);
111
- const context = await loadRemoteDatasourceContext({
112
- fetchImpl: fetch,
113
- pagePath: path,
114
- routeTemplateKeys,
115
- builtinFetchers,
116
- referencedDatasourceIds,
117
- });
118
- const resolvedData = await resolveDataTemplates(data, context);
119
-
120
- return (
121
- <Client
122
- data={resolvedData}
123
- pageMetadata={{
124
- route: path,
125
- documentName: data.root.props?.title as string | undefined,
126
- pageType: "page",
127
- }}
128
- />
129
- );
130
- }
21
+ export const generateStaticParams = published.generateStaticParams;
22
+ export const generateMetadata = published.generateMetadata;
23
+ export default published.Page;
@@ -31,42 +31,12 @@ import "@pantheon-systems/puck-css/pds/styles.css";
31
31
 
32
32
  import { ChatbotFlagProvider } from "../../../../components/ChatbotFlagProvider";
33
33
  import { P1Lockup } from "../../../../components/p1-lockup";
34
+ import styles from "../../../../components/welcome-block.module.css";
34
35
  import config from "../../../../puck.config";
35
36
  import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../../../../lib/chatbot-flag/feature-gate";
36
37
  import { createGenerateWithAIHandler } from "../../../../lib/chatbot-flag/ai-generate";
37
38
  import { getDraftRequestChannel } from "../../../../lib/chatbot-flag/draft-request-channel";
38
39
 
39
- const DEFAULT_PAGE_DATA = {
40
- root: { props: { title: "New page" } },
41
- content: [],
42
- zones: {},
43
- };
44
-
45
- const DEFAULT_ROOT_PAGE_DATA = {
46
- root: { props: { title: "Welcome | P1 site" } },
47
- content: [
48
- {
49
- type: "P1WelcomeBlock",
50
- props: {
51
- id: "seed-welcome",
52
- heading: "Welcome to your new Pantheon P1 Site.",
53
- description: "You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages.",
54
- ctaLabel: "Sign-in to P1",
55
- ctaHref: "/p1",
56
- footnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
57
- loggedInHeading: "Welcome to your new Pantheon P1 Site.",
58
- loggedInDescription: "You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site.",
59
- loggedInCtaLabel: "Edit this page with P1 Visual Editor",
60
- loggedInCtaHref: "/p1",
61
- loggedInSecondaryLabel: "Go to P1 Dashboard",
62
- loggedInFootnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
63
- showLogo: true,
64
- },
65
- },
66
- ],
67
- zones: {},
68
- };
69
-
70
40
  let p1Config: ReturnType<typeof createNextConfig> | null = null;
71
41
  let p1ConfigError: string | null = null;
72
42
 
@@ -84,33 +54,31 @@ function P1SignInPage() {
84
54
  const { login, isLoading, error } = useP1Auth();
85
55
 
86
56
  return (
87
- <div className="w-full max-w-[620px] mx-auto flex flex-col items-center text-center px-8 py-16 font-['Inter',system-ui,sans-serif] text-[#1a1a2e] min-h-screen justify-center">
88
- <P1Lockup />
57
+ <div className={styles.surface}>
58
+ <div className={styles.inner}>
59
+ <P1Lockup />
89
60
 
90
- <h1 className="text-[2.5rem] leading-[1.08] font-semibold m-0 mb-3" style={{ fontSize: '2.5rem' }}>
91
- Your Collaborative Website Management Workspace.
92
- </h1>
93
- <p className="text-base leading-6 text-[#5a5a6e] max-w-[54ch] m-0">
94
- Log in to your Pantheon P1 account to edit your P1 powered website.
95
- If you don&apos;t have yet a Pantheon P1 account, contact us{" "}
96
- <a href="https://pantheon.io/contact-us" className="text-blue-600 underline">here</a>.
97
- </p>
61
+ <h1 className={styles.heading}>
62
+ Your Collaborative Website Management Workspace.
63
+ </h1>
64
+ <p className={styles.description}>
65
+ Log in to your Pantheon P1 account to edit your P1 powered website.
66
+ If you don&apos;t have yet a Pantheon P1 account, contact us{" "}
67
+ <a href="https://pantheon.io/contact-us" className={styles.link}>here</a>.
68
+ </p>
98
69
 
99
- <div className="flex gap-3 mt-8 justify-center">
100
- <button
101
- className="inline-flex items-center justify-center h-12 px-6 gap-2 rounded-full border border-[#1a1a2e] bg-[#1a1a2e] text-white font-['Inter',system-ui,sans-serif] text-lg font-medium leading-none whitespace-nowrap cursor-pointer transition-colors duration-200 hover:bg-[#2d2d44] hover:border-[#2d2d44] focus-visible:outline focus-visible:outline-1 focus-visible:outline-blue-600 focus-visible:outline-offset-1 disabled:opacity-40 disabled:cursor-not-allowed"
102
- onClick={() => void login()}
103
- disabled={isLoading}
104
- >
105
- {isLoading ? "Signing in..." : "Continue"}
106
- </button>
107
- </div>
70
+ <div className={styles.actions}>
71
+ <button
72
+ className={styles.button}
73
+ onClick={() => void login()}
74
+ disabled={isLoading}
75
+ >
76
+ {isLoading ? "Signing in..." : "Continue"}
77
+ </button>
78
+ </div>
108
79
 
109
- {error && (
110
- <p className="mt-4 text-sm text-red-600 bg-red-50 px-3 py-2 rounded-md">
111
- {error}
112
- </p>
113
- )}
80
+ {error && <p className={styles.error}>{error}</p>}
81
+ </div>
114
82
  </div>
115
83
  );
116
84
  }
@@ -208,7 +176,6 @@ function RoleSwitcher({
208
176
 
209
177
  function EditorContent({ path }: { path: string }) {
210
178
  const router = useRouter();
211
- const { getToken } = useP1Auth();
212
179
  const { data: editorCtx } = useEditorContext(path);
213
180
  const {
214
181
  context: remoteDatasourceContext,
@@ -258,27 +225,10 @@ function EditorContent({ path }: { path: string }) {
258
225
  [router],
259
226
  );
260
227
 
261
- const handleDocumentNotFound = useCallback(
262
- async (docPath: string, _error: Error) => {
263
- const initialData = docPath === "/" ? DEFAULT_ROOT_PAGE_DATA : DEFAULT_PAGE_DATA;
264
- const token = await getToken();
265
- const headers: Record<string, string> = { "Content-Type": "application/json" };
266
- if (token) headers["Authorization"] = `Bearer ${token}`;
267
- const res = await fetch("/p1/api/structure/page", {
268
- method: "POST",
269
- headers,
270
- body: JSON.stringify({ path: docPath, initialData }),
271
- });
272
- return res.ok;
273
- },
274
- [getToken],
275
- );
276
-
277
228
  const { loading, reloading, hasContent, error, puckKey, puckProps } = useP1Editor({
278
229
  documentPath: path,
279
230
  puckConfig: editorConfig,
280
231
  additionalPlugins,
281
- onDocumentNotFound: handleDocumentNotFound,
282
232
  pluginOptions: {
283
233
  onDocumentSelect: handleDocumentSelect,
284
234
  onGenerateWithAI: createGenerateWithAIHandler(draftRequests, chatbotEnabled),
@@ -1,77 +1,19 @@
1
- import {
2
- resolveDataTemplates,
3
- extractReferencedDatasourceIds,
4
- loadRemoteDatasourceContext,
5
- } from "@pantheon-systems/puck-css/server";
6
- import {
7
- loadPublishedPage,
8
- loadRouteTemplateKeys,
9
- } from "@pantheon-systems/p1-next-sdk/server";
10
- import type { Metadata } from "next";
11
- import { WelcomeBlockRender } from "../components/puck/welcome-block-render";
12
- import { resolvePageMetadata } from "../lib/page-seo";
13
- import { REMOTE_DATASOURCE_FETCHERS } from "../lib/remote-datasource-fetchers";
14
- import { Client } from "./[...puckPath]/client";
1
+ /**
2
+ * Home route. The pipeline lives in the SDK — see createPublishedPage in
3
+ * app/published-pages.tsx for what this route is made of.
4
+ */
5
+
6
+ import { published } from "./published-pages";
15
7
 
16
8
  /**
17
9
  * Backstop only: publishing calls revalidatePath("/"), so the home page
18
10
  * normally refreshes the moment its content changes. This bounds how long an
19
11
  * edit made outside that path can stay stale.
12
+ *
13
+ * Must stay a literal here: Next.js statically analyzes segment-config exports,
14
+ * so a value re-exported from the factory would go undetected.
20
15
  */
21
16
  export const revalidate = 300;
22
17
 
23
- export async function generateMetadata(): Promise<Metadata> {
24
- const result = await loadPublishedPage("/");
25
- if (result.status !== "ok") {
26
- return { title: "P1 Starter Kit" };
27
- }
28
- return resolvePageMetadata({ pageData: result.data, path: "/" });
29
- }
30
-
31
- export default async function HomePage() {
32
- const result = await loadPublishedPage("/");
33
-
34
- // Unlike the catch-all, "/" never 404s: it is a single fixed URL rather than
35
- // an unbounded crawler surface, and the welcome block is the correct state for
36
- // a freshly scaffolded site — including one with no backend configured yet.
37
- if (result.status === "ok") {
38
- const data = result.data;
39
- const routeTemplateKeys = await loadRouteTemplateKeys();
40
- const referencedDatasourceIds = extractReferencedDatasourceIds(data);
41
- const context = await loadRemoteDatasourceContext({
42
- fetchImpl: fetch,
43
- pagePath: "/",
44
- routeTemplateKeys,
45
- builtinFetchers: REMOTE_DATASOURCE_FETCHERS,
46
- referencedDatasourceIds,
47
- });
48
- const resolvedData = await resolveDataTemplates(data, context);
49
- return (
50
- <Client
51
- data={resolvedData}
52
- pageMetadata={{
53
- route: "/",
54
- documentName: data.root.props?.title as string | undefined,
55
- pageType: "page",
56
- }}
57
- />
58
- );
59
- }
60
-
61
- return (
62
- <WelcomeBlockRender
63
- heading="Welcome to your new Pantheon P1 Site."
64
- description="You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages."
65
- ctaLabel="Sign-in to P1"
66
- ctaHref="/p1"
67
- footnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
68
- loggedInHeading="Welcome to your new Pantheon P1 Site."
69
- loggedInDescription="You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site."
70
- loggedInCtaLabel="Edit this page with P1 Visual Editor"
71
- loggedInCtaHref="/p1"
72
- loggedInSecondaryLabel="Go to P1 Dashboard"
73
- loggedInFootnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
74
- showLogo={true}
75
- />
76
- );
77
- }
18
+ export const generateMetadata = published.generateHomeMetadata;
19
+ export default published.HomePage;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The app's published-page routes, built once and shared by both route files.
3
+ *
4
+ * One instance on purpose: the factory memoizes per-request work (the CCR query
5
+ * fetchers), and "/" and the catch-all should share that rather than each
6
+ * building their own.
7
+ */
8
+
9
+ import { createPublishedPage } from "@pantheon-systems/p1-next-sdk/server";
10
+ import { REMOTE_DATASOURCE_FETCHERS } from "../lib/remote-datasource-fetchers";
11
+ import { ContentUnavailable } from "../components/content-unavailable";
12
+ import { resolvePageMetadata } from "../lib/page-seo";
13
+ import { Client } from "./[...puckPath]/client";
14
+ import { WelcomeBlock } from "./welcome-block";
15
+
16
+ export const published = createPublishedPage({
17
+ Client,
18
+ Unavailable: ContentUnavailable,
19
+ Fallback: WelcomeBlock,
20
+ fetchers: REMOTE_DATASOURCE_FETCHERS,
21
+ resolveMetadata: resolvePageMetadata,
22
+ titles: { home: "P1 Starter Kit" },
23
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The home page's state before any content exists — a freshly scaffolded site,
3
+ * or one with no backend configured yet.
4
+ */
5
+
6
+ import { WelcomeBlockRender } from "../components/puck/welcome-block-render";
7
+
8
+ export function WelcomeBlock() {
9
+ return (
10
+ <WelcomeBlockRender
11
+ heading="Welcome to your new Pantheon P1 Site."
12
+ description="You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages."
13
+ ctaLabel="Sign-in to P1"
14
+ ctaHref="/p1"
15
+ footnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
16
+ loggedInHeading="Welcome to your new Pantheon P1 Site."
17
+ loggedInDescription="You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site."
18
+ loggedInCtaLabel="Edit this page with P1 Visual Editor"
19
+ loggedInCtaHref="/p1"
20
+ loggedInSecondaryLabel="Go to P1 Dashboard"
21
+ loggedInFootnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
22
+ showLogo={true}
23
+ />
24
+ );
25
+ }
@@ -4,6 +4,8 @@ import React from "react";
4
4
  import { LDProvider } from "launchdarkly-react-client-sdk";
5
5
  import { useP1Auth } from "@pantheon-systems/puck-css";
6
6
 
7
+ import { buildFlagContext } from "../lib/chatbot-flag/flag-context";
8
+
7
9
  /**
8
10
  * Wraps the editor with a LaunchDarkly client-side provider so the `p1-chatbot`
9
11
  * flag can be evaluated at runtime. The client-side ID is public by design.
@@ -28,14 +30,7 @@ export function ChatbotFlagProvider({
28
30
  // re-identify on context change. This provider mounts inside <P1App> (after
29
31
  // auth), so the authenticated user is available here; the anonymous fallback
30
32
  // only applies if it ever renders pre-auth.
31
- //
32
- // Key on the always-present, stable user id — email is optional on AuthUser, so
33
- // keying on it would silently drop emailless users into the anonymous branch and
34
- // lose per-user rollout stickiness. (LaunchDarkly also favors a non-PII key.)
35
- // Email is kept as a targeting attribute.
36
- const context = user
37
- ? { kind: "user" as const, key: user.id, email: user.email }
38
- : { kind: "user" as const, key: "anonymous", anonymous: true };
33
+ const context = buildFlagContext(user);
39
34
 
40
35
  return (
41
36
  <LDProvider
@@ -1,13 +1,14 @@
1
1
  "use client";
2
2
 
3
3
  import { P1_ASSETS } from "../constants/assets";
4
+ import styles from "./welcome-block.module.css";
4
5
 
5
6
  export function P1Lockup() {
6
7
  return (
7
8
  <img
8
9
  src={P1_ASSETS.LOGO_URL}
9
10
  alt="Pantheon P1"
10
- className="h-7 w-auto block mb-6"
11
+ className={styles.lockup}
11
12
  />
12
13
  );
13
14
  }
@@ -16,8 +16,11 @@ export const imageBlock = {
16
16
  },
17
17
  },
18
18
  defaultProps: {
19
- src: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200&q=80",
20
- alt: "Mountain landscape",
19
+ // Placeholder art inline as a data URI, so a fresh block loads nothing off
20
+ // the network. Authors replace it with their own image.
21
+ src:
22
+ "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22675%22%20viewBox%3D%220%200%201200%20675%22%3E%3Crect%20width%3D%221200%22%20height%3D%22675%22%20fill%3D%22%23f1f1f3%22%2F%3E%3Cpath%20d%3D%22M0%20675L1200%200%22%20stroke%3D%22%23c8c8ce%22%20stroke-width%3D%2284%22%2F%3E%3C%2Fsvg%3E",
23
+ alt: "",
21
24
  caption: "",
22
25
  loading: "lazy",
23
26
  },
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useState } from "react";
4
4
  import { P1Lockup } from "../p1-lockup";
5
+ import styles from "../welcome-block.module.css";
5
6
 
6
7
  export interface WelcomeBlockRenderProps {
7
8
  heading?: string;
@@ -30,12 +31,6 @@ const LOGGED_IN_DEFAULTS = {
30
31
  "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
31
32
  };
32
33
 
33
- const BTN_PRIMARY =
34
- "inline-flex items-center justify-center h-12 px-6 gap-2 rounded-full border border-[#1a1a2e] bg-[#1a1a2e] text-white font-['Inter',system-ui,sans-serif] text-lg font-medium leading-none whitespace-nowrap cursor-pointer transition-colors duration-200 hover:bg-[#2d2d44] hover:border-[#2d2d44] focus-visible:outline focus-visible:outline-1 focus-visible:outline-blue-600 focus-visible:outline-offset-1 disabled:opacity-40 disabled:cursor-not-allowed";
35
-
36
- const BTN_SECONDARY =
37
- "inline-flex items-center justify-center h-12 px-6 gap-2 rounded-full border border-[#d0d0d8] bg-transparent text-[#1a1a2e] font-['Inter',system-ui,sans-serif] text-lg font-medium leading-none whitespace-nowrap cursor-pointer transition-colors duration-200 hover:bg-[rgba(26,26,46,0.06)] focus-visible:outline focus-visible:outline-1 focus-visible:outline-blue-600 focus-visible:outline-offset-1";
38
-
39
34
  export function WelcomeBlockRender(props: WelcomeBlockRenderProps) {
40
35
  const [isLoggedIn, setIsLoggedIn] = useState(false);
41
36
 
@@ -64,47 +59,45 @@ export function WelcomeBlockRender(props: WelcomeBlockRenderProps) {
64
59
 
65
60
  const footnoteHtml = (activeFootnote ?? "").replace(
66
61
  /\[([^\]]+)\]\(([^)]+)\)/g,
67
- '<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-600 underline">$1</a>',
62
+ `<a href="$2" target="_blank" rel="noopener noreferrer" class="${styles.link}">$1</a>`,
68
63
  );
69
64
 
70
65
  return (
71
- <div className="w-full max-w-[620px] mx-auto flex flex-col items-center text-center px-8 py-16 font-['Inter',system-ui,sans-serif] text-[#1a1a2e] min-h-screen justify-center">
72
- {props.showLogo !== false && <P1Lockup />}
73
- <h1 className="text-[2.5rem] leading-[1.08] font-semibold m-0 mb-4">
74
- {activeHeading}
75
- </h1>
76
- <p className="text-base leading-6 text-[#5a5a6e] max-w-[54ch] m-0">
77
- {activeDescription}
78
- </p>
79
- <div className="flex gap-3 mt-8 justify-center">
80
- <button
81
- className={BTN_PRIMARY}
82
- onClick={() => {
83
- if (!isLoggedIn) {
84
- localStorage.setItem("p1_return_to", window.location.pathname);
85
- }
86
- window.location.href = activeCtaHref || "/";
87
- }}
88
- >
89
- {activeCtaLabel}
90
- </button>
91
- {secondaryLabel && (
66
+ <div className={styles.surface}>
67
+ <div className={styles.inner}>
68
+ {props.showLogo !== false && <P1Lockup />}
69
+ <h1 className={styles.heading}>{activeHeading}</h1>
70
+ <p className={styles.description}>{activeDescription}</p>
71
+ <div className={styles.actions}>
92
72
  <button
93
- className={BTN_SECONDARY}
73
+ className={styles.button}
94
74
  onClick={() => {
95
- window.open(LOGGED_IN_DEFAULTS.secondaryHref, "_blank", "noopener,noreferrer");
75
+ if (!isLoggedIn) {
76
+ localStorage.setItem("p1_return_to", window.location.pathname);
77
+ }
78
+ window.location.href = activeCtaHref || "/";
96
79
  }}
97
80
  >
98
- {secondaryLabel}
81
+ {activeCtaLabel}
99
82
  </button>
83
+ {secondaryLabel && (
84
+ <button
85
+ className={`${styles.button} ${styles.buttonSecondary}`}
86
+ onClick={() => {
87
+ window.open(LOGGED_IN_DEFAULTS.secondaryHref, "_blank", "noopener,noreferrer");
88
+ }}
89
+ >
90
+ {secondaryLabel}
91
+ </button>
92
+ )}
93
+ </div>
94
+ {activeFootnote && (
95
+ <p
96
+ className={styles.footnote}
97
+ dangerouslySetInnerHTML={{ __html: footnoteHtml }}
98
+ />
100
99
  )}
101
100
  </div>
102
- {activeFootnote && (
103
- <p
104
- className="mt-12 text-sm text-[#5a5a6e]"
105
- dangerouslySetInnerHTML={{ __html: footnoteHtml }}
106
- />
107
- )}
108
101
  </div>
109
102
  );
110
103
  }