@pantheon-systems/create-p1-starter-kit 0.4.4 → 0.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantheon-systems/create-p1-starter-kit",
3
- "version": "0.4.4",
3
+ "version": "0.6.0",
4
4
  "description": "Scaffold a new P1 starter project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,3 +14,14 @@ CSS_API_KEY=your-api-key
14
14
 
15
15
  # Branch is auto-detected (defaults to main) unless specified:
16
16
  # NEXT_PUBLIC_CSS_BRANCH_ID=branch-456
17
+
18
+ # --- Dev tools (disabled by default) ---
19
+ # Show the RoleSwitcher dropdown in the P1 editor for local testing of
20
+ # admin/editor/junior-editor permissions. Never enable this in production.
21
+ # NEXT_PUBLIC_ENABLE_ROLE_SWITCHER=true
22
+
23
+ # --- AI chatbot (optional) ---
24
+ # LaunchDarkly client-side ID used to evaluate the `p1-chatbot` flag. It is
25
+ # public by design (safe to expose in the browser). When unset, the chatbot
26
+ # stays hidden.
27
+ # NEXT_PUBLIC_LD_CLIENT_ID=your-launchdarkly-client-side-id
@@ -1,5 +1,20 @@
1
1
  # @pantheon-systems/p1-starter
2
2
 
3
+ ## 1.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - @pantheon-systems/puck-css@0.6.0
8
+ - @pantheon-systems/p1-next-sdk@0.6.0
9
+
10
+ ## 1.0.4
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [0bc7982]
15
+ - @pantheon-systems/puck-css@0.5.0
16
+ - @pantheon-systems/p1-next-sdk@0.5.0
17
+
3
18
  ## 1.0.3
4
19
 
5
20
  ### Patch Changes
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const appDir = resolve(__dirname, "..");
8
+
9
+ describe("auth route does not force re-authentication on every login", () => {
10
+ const content = readFileSync(
11
+ resolve(appDir, "app/p1/auth/[...action]/route.ts"),
12
+ "utf-8",
13
+ );
14
+
15
+ it("does not hardcode an OAuth prompt override", () => {
16
+ // prompt: 'login' forces Google's full re-auth screen on every broker
17
+ // login, even with a live Google session — see PCC-3391.
18
+ expect(content).not.toMatch(/prompt\s*:\s*['"]login['"]/);
19
+ });
20
+
21
+ it("uses select_account so users can still switch Google accounts", () => {
22
+ // Omitting `prompt` entirely silently re-authenticates whichever Google
23
+ // account has a live session, with no way to pick a different one on
24
+ // logout/login. select_account shows a lightweight account chooser
25
+ // (one click if already signed in) without forcing full re-auth.
26
+ expect(content).toMatch(/prompt\s*:\s*['"]select_account['"]/);
27
+ });
28
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../lib/chatbot-flag/feature-gate";
3
+
4
+ describe("shouldShowChatbot", () => {
5
+ it("is off when the flag is disabled, even with an agent URL", () => {
6
+ expect(shouldShowChatbot(false, "https://agent.example")).toBe(false);
7
+ });
8
+
9
+ it("is off when the agent URL is missing, even with the flag enabled", () => {
10
+ expect(shouldShowChatbot(true, undefined)).toBe(false);
11
+ expect(shouldShowChatbot(true, "")).toBe(false);
12
+ });
13
+
14
+ it("is on only when the flag is enabled AND the agent URL is set", () => {
15
+ expect(shouldShowChatbot(true, "https://agent.example")).toBe(true);
16
+ });
17
+
18
+ it("defaults off when the flag value is undefined (LD not yet resolved / offline)", () => {
19
+ expect(shouldShowChatbot(undefined, "https://agent.example")).toBe(false);
20
+ });
21
+
22
+ it("exposes the p1-chatbot flag key", () => {
23
+ expect(CHATBOT_FLAG_KEY).toBe("p1-chatbot");
24
+ });
25
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const appDir = resolve(__dirname, "..");
8
+
9
+ describe("editor-client gates the chatbot behind the p1-chatbot flag", () => {
10
+ const content = readFileSync(
11
+ resolve(appDir, "app/p1/[[...p1]]/editor-client.tsx"),
12
+ "utf-8",
13
+ );
14
+
15
+ it("reads LaunchDarkly flags via useFlags", () => {
16
+ expect(content).toContain("useFlags");
17
+ });
18
+
19
+ it("gates the AI plugin through shouldShowChatbot", () => {
20
+ expect(content).toContain("shouldShowChatbot");
21
+ });
22
+
23
+ it("wraps the editor in the chatbot flag provider", () => {
24
+ expect(content).toContain("ChatbotFlagProvider");
25
+ });
26
+
27
+ it("imports the plugin from the published @pantheon-systems/p1-ai-chat package", () => {
28
+ expect(content).toContain("@pantheon-systems/p1-ai-chat");
29
+ });
30
+ });
31
+
32
+ describe("chatbot flag provider is a client-side LaunchDarkly gate", () => {
33
+ const content = readFileSync(
34
+ resolve(appDir, "components/ChatbotFlagProvider.tsx"),
35
+ "utf-8",
36
+ );
37
+
38
+ it("initializes from the public client-side ID env var", () => {
39
+ expect(content).toContain("NEXT_PUBLIC_LD_CLIENT_ID");
40
+ });
41
+
42
+ it("uses the launchdarkly-react-client-sdk", () => {
43
+ expect(content).toContain("launchdarkly-react-client-sdk");
44
+ });
45
+ });
@@ -27,6 +27,10 @@ describe("editor-client uses P1 plugins", () => {
27
27
  it("wraps with P1QueryProvider for TanStack React Query", () => {
28
28
  expect(content).toContain("P1QueryProvider");
29
29
  });
30
+
31
+ it("gates RoleSwitcher behind NEXT_PUBLIC_ENABLE_ROLE_SWITCHER", () => {
32
+ expect(content).toContain("NEXT_PUBLIC_ENABLE_ROLE_SWITCHER");
33
+ });
30
34
  });
31
35
 
32
36
  describe("API handler passes fetcher config", () => {
@@ -1,9 +1,183 @@
1
1
  "use client";
2
2
 
3
+ import { useEffect, useState } from "react";
3
4
  import type { Data } from "@puckeditor/core";
4
5
  import { RenderClient } from "@pantheon-systems/puck-css";
5
6
  import config from "../../puck.config";
6
7
 
8
+ function EditIcon() {
9
+ return (
10
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={16} height={16} aria-hidden>
11
+ <path d="M12 20h9" />
12
+ <path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
13
+ </svg>
14
+ );
15
+ }
16
+
17
+ function ChevronIcon() {
18
+ return (
19
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={14} height={14} aria-hidden>
20
+ <path d="M6 9l6 6 6-6" />
21
+ </svg>
22
+ );
23
+ }
24
+
25
+ function LogoutIcon() {
26
+ return (
27
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={16} height={16} aria-hidden>
28
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
29
+ <polyline points="16 17 21 12 16 7" />
30
+ <line x1="21" y1="12" x2="9" y2="12" />
31
+ </svg>
32
+ );
33
+ }
34
+
35
+ export function P1EditWidget({ route }: { route: string }) {
36
+ const [hasToken, setHasToken] = useState(false);
37
+ const [open, setOpen] = useState(false);
38
+
39
+ useEffect(() => {
40
+ const flag = typeof localStorage !== "undefined"
41
+ ? localStorage.getItem("p1_logged_in")
42
+ : null;
43
+ setHasToken(!!flag);
44
+ }, []);
45
+
46
+ useEffect(() => {
47
+ if (!open) return;
48
+ const onDoc = (e: PointerEvent) => {
49
+ if (!(e.target as HTMLElement).closest("[data-p1-widget]")) setOpen(false);
50
+ };
51
+ const onKey = (e: KeyboardEvent) => {
52
+ if (e.key === "Escape") setOpen(false);
53
+ };
54
+ document.addEventListener("pointerdown", onDoc);
55
+ document.addEventListener("keydown", onKey);
56
+ return () => {
57
+ document.removeEventListener("pointerdown", onDoc);
58
+ document.removeEventListener("keydown", onKey);
59
+ };
60
+ }, [open]);
61
+
62
+ if (!hasToken) return null;
63
+
64
+ const editHref = `/p1${route === "/" ? "" : route}`;
65
+
66
+ return (
67
+ <div
68
+ data-p1-widget
69
+ style={{
70
+ position: "fixed",
71
+ top: 16,
72
+ right: 16,
73
+ zIndex: 99999,
74
+ fontFamily: "system-ui, -apple-system, sans-serif",
75
+ }}
76
+ >
77
+ <button
78
+ onClick={() => setOpen((o) => !o)}
79
+ aria-expanded={open}
80
+ aria-haspopup="menu"
81
+ style={{
82
+ display: "inline-flex",
83
+ alignItems: "center",
84
+ gap: 8,
85
+ height: 38,
86
+ padding: "4px 12px 4px 12px",
87
+ background: "#fff",
88
+ border: "1px solid #e0e0e0",
89
+ borderRadius: 999,
90
+ cursor: "pointer",
91
+ fontSize: 14,
92
+ fontWeight: 500,
93
+ color: "#1a1a1a",
94
+ boxShadow: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.06)",
95
+ }}
96
+ >
97
+ <span style={{ fontWeight: 600 }}>P1</span>
98
+ <span
99
+ style={{
100
+ display: "inline-flex",
101
+ color: "#888",
102
+ transition: "transform 200ms ease",
103
+ transform: open ? "rotate(180deg)" : "none",
104
+ }}
105
+ >
106
+ <ChevronIcon />
107
+ </span>
108
+ </button>
109
+
110
+ {open && (
111
+ <div
112
+ role="menu"
113
+ style={{
114
+ position: "absolute",
115
+ top: "calc(100% + 8px)",
116
+ right: 0,
117
+ minWidth: 200,
118
+ background: "#fff",
119
+ border: "1px solid #e0e0e0",
120
+ borderRadius: 8,
121
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
122
+ padding: 6,
123
+ }}
124
+ >
125
+ <a
126
+ href={editHref}
127
+ role="menuitem"
128
+ style={{
129
+ display: "flex",
130
+ alignItems: "center",
131
+ gap: 10,
132
+ width: "100%",
133
+ padding: "9px 10px",
134
+ borderRadius: 4,
135
+ fontSize: 14,
136
+ color: "#1a1a1a",
137
+ textDecoration: "none",
138
+ }}
139
+ onMouseEnter={(e) => { e.currentTarget.style.background = "#f5f5f5"; }}
140
+ onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
141
+ >
142
+ <span style={{ color: "#888", flex: "0 0 auto" }}><EditIcon /></span>
143
+ Edit this page
144
+ </a>
145
+ <div style={{ height: 1, background: "#e0e0e0", margin: "6px 4px" }} />
146
+ <button
147
+ role="menuitem"
148
+ onClick={() => {
149
+ localStorage.removeItem("p1_logged_in");
150
+ localStorage.removeItem("p1_auth_token");
151
+ localStorage.removeItem("css_broker_token");
152
+ window.location.reload();
153
+ }}
154
+ style={{
155
+ display: "flex",
156
+ alignItems: "center",
157
+ gap: 10,
158
+ width: "100%",
159
+ padding: "9px 10px",
160
+ borderRadius: 4,
161
+ fontSize: 14,
162
+ color: "#1a1a1a",
163
+ textDecoration: "none",
164
+ background: "transparent",
165
+ border: "none",
166
+ cursor: "pointer",
167
+ fontFamily: "inherit",
168
+ }}
169
+ onMouseEnter={(e) => { e.currentTarget.style.background = "#f5f5f5"; }}
170
+ onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
171
+ >
172
+ <span style={{ color: "#888", flex: "0 0 auto" }}><LogoutIcon /></span>
173
+ Log out
174
+ </button>
175
+ </div>
176
+ )}
177
+ </div>
178
+ );
179
+ }
180
+
7
181
  export function Client({
8
182
  data,
9
183
  pageMetadata,
@@ -18,6 +192,7 @@ export function Client({
18
192
  return (
19
193
  <>
20
194
  <RenderClient config={config} data={data} />
195
+ {pageMetadata && <P1EditWidget route={pageMetadata.route} />}
21
196
  {pageMetadata && (
22
197
  <footer className="mt-16 border-t border-gray-200 py-4 text-center text-sm text-gray-500">
23
198
  Rendered with{" "}
@@ -20,7 +20,9 @@ const initPromise = ensureInitialized({
20
20
  p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
21
21
  p1ApiKey: process.env.CSS_API_KEY,
22
22
  p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
23
- p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
23
+ // Default to "main" when unset: server components (no user token) need a
24
+ // branch to list/read documents (e.g. the /structure routes table).
25
+ p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID ?? "main",
24
26
  });
25
27
 
26
28
  export async function generateMetadata({
@@ -110,7 +112,7 @@ export default async function Page({
110
112
  Open the Page Editor
111
113
  </a>
112
114
  <a
113
- href="https://staging.content.pantheon.io/dashboard/sites"
115
+ href={`${process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL || "https://content.pantheon.io"}/dashboard/sites`}
114
116
  target="_blank"
115
117
  rel="noopener noreferrer"
116
118
  className="rounded-lg border border-gray-300 px-5 py-3 text-sm font-medium text-gray-900 hover:bg-gray-100"
@@ -8,18 +8,56 @@ import {
8
8
  createNextConfig,
9
9
  useP1Editor,
10
10
  useP1Plugins,
11
+ useP1Auth,
11
12
  wrapConfigForEditorPreview,
12
13
  P1QueryProvider,
13
14
  editorPathHref,
14
15
  } from "@pantheon-systems/puck-css";
15
16
  import { P1NextRouterProvider } from "@pantheon-systems/p1-next-sdk";
17
+ import { createAIChatPlugin } from "@pantheon-systems/p1-ai-chat";
18
+ import { useFlags } from "launchdarkly-react-client-sdk";
16
19
  import type { Checkpoint } from "@pantheon-systems/puck-css";
17
20
  import type { ContentRole } from "@pantheon-systems/puck-css";
21
+ import { P1_ASSETS } from "../../../constants/assets";
18
22
 
19
23
  import "@pantheon-systems/puck-css/styles.css";
20
24
  import "@pantheon-systems/puck-css/pds/styles.css";
21
25
 
26
+ import { ChatbotFlagProvider } from "../../../components/ChatbotFlagProvider";
27
+ import { P1Lockup } from "../../../components/p1-lockup";
22
28
  import config from "../../../puck.config";
29
+ import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../../../lib/chatbot-flag/feature-gate";
30
+
31
+ const DEFAULT_PAGE_DATA = {
32
+ root: { props: { title: "New page" } },
33
+ content: [],
34
+ zones: {},
35
+ };
36
+
37
+ const DEFAULT_ROOT_PAGE_DATA = {
38
+ root: { props: { title: "Welcome | P1 site" } },
39
+ content: [
40
+ {
41
+ type: "P1WelcomeBlock",
42
+ props: {
43
+ id: "seed-welcome",
44
+ heading: "Welcome to your new Pantheon P1 Site.",
45
+ 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.",
46
+ ctaLabel: "Sign-in to P1",
47
+ ctaHref: "/p1",
48
+ footnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
49
+ loggedInHeading: "Welcome to your new Pantheon P1 Site.",
50
+ 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.",
51
+ loggedInCtaLabel: "Edit this page with P1 Visual Editor",
52
+ loggedInCtaHref: "/p1",
53
+ loggedInSecondaryLabel: "Go to P1 Dashboard",
54
+ loggedInFootnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
55
+ showLogo: true,
56
+ },
57
+ },
58
+ ],
59
+ zones: {},
60
+ };
23
61
 
24
62
  let p1Config: ReturnType<typeof createNextConfig> | null = null;
25
63
  let p1ConfigError: string | null = null;
@@ -34,6 +72,41 @@ const editorConfig = wrapConfigForEditorPreview(config);
34
72
 
35
73
  const ROLES: ContentRole[] = ['admin', 'editor', 'junior-editor'];
36
74
 
75
+ function P1SignInPage() {
76
+ const { login, isLoading, error } = useP1Auth();
77
+
78
+ return (
79
+ <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">
80
+ <P1Lockup />
81
+
82
+ <h1 className="text-[2.5rem] leading-[1.08] font-semibold m-0 mb-3" style={{ fontSize: '2.5rem' }}>
83
+ Your Collaborative Website Management Workspace.
84
+ </h1>
85
+ <p className="text-base leading-6 text-[#5a5a6e] max-w-[54ch] m-0">
86
+ Log in to your Pantheon P1 account to edit your P1 powered website.
87
+ If you don&apos;t have yet a Pantheon P1 account, contact us{" "}
88
+ <a href="https://pantheon.io/contact-us" className="text-blue-600 underline">here</a>.
89
+ </p>
90
+
91
+ <div className="flex gap-3 mt-8 justify-center">
92
+ <button
93
+ 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"
94
+ onClick={() => void login()}
95
+ disabled={isLoading}
96
+ >
97
+ {isLoading ? "Signing in..." : "Continue"}
98
+ </button>
99
+ </div>
100
+
101
+ {error && (
102
+ <p className="mt-4 text-sm text-red-600 bg-red-50 px-3 py-2 rounded-md">
103
+ {error}
104
+ </p>
105
+ )}
106
+ </div>
107
+ );
108
+ }
109
+
37
110
  export function EditorClientWrapper({ path }: { path: string }) {
38
111
  const [userRole, setUserRole] = useState<ContentRole>('editor');
39
112
  const lastGoodStateRef = React.useRef<{ puckKey: string; puckProps: any } | null>(null);
@@ -58,11 +131,15 @@ export function EditorClientWrapper({ path }: { path: string }) {
58
131
  <P1NextRouterProvider>
59
132
  <P1App
60
133
  config={{ ...p1Config, userRole }}
61
- loginPageProps={{ title: "P1 Starter", subtitle: "Sign in to edit" }}
134
+ loginFallback={<P1SignInPage />}
62
135
  >
63
- <EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
136
+ <ChatbotFlagProvider>
137
+ <EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
138
+ </ChatbotFlagProvider>
64
139
  </P1App>
65
- <RoleSwitcher currentRole={userRole} onRoleChange={setUserRole} />
140
+ {process.env.NEXT_PUBLIC_ENABLE_ROLE_SWITCHER === 'true' && (
141
+ <RoleSwitcher currentRole={userRole} onRoleChange={setUserRole} />
142
+ )}
66
143
  </P1NextRouterProvider>
67
144
  </P1QueryProvider>
68
145
  );
@@ -126,7 +203,33 @@ function EditorContent({
126
203
  lastGoodStateRef: React.MutableRefObject<{ puckKey: string; puckProps: any } | null>;
127
204
  }) {
128
205
  const router = useRouter();
206
+ const { getToken } = useP1Auth();
129
207
  const p1Plugins = useP1Plugins(path, config);
208
+ const flags = useFlags();
209
+ const agentUrl = process.env.NEXT_PUBLIC_AGENT_URL;
210
+ const chatbotEnabled = shouldShowChatbot(flags[CHATBOT_FLAG_KEY], agentUrl);
211
+ const aiPlugin = React.useMemo(
212
+ () =>
213
+ chatbotEnabled && agentUrl
214
+ ? createAIChatPlugin({ agentUrl })
215
+ : null,
216
+ [chatbotEnabled, agentUrl],
217
+ );
218
+ const additionalPlugins = React.useMemo(
219
+ () => (aiPlugin ? [...p1Plugins, aiPlugin] : p1Plugins),
220
+ [p1Plugins, aiPlugin],
221
+ ) as typeof p1Plugins;
222
+
223
+ const [redirecting, setRedirecting] = React.useState(false);
224
+
225
+ React.useEffect(() => {
226
+ const returnTo = localStorage.getItem("p1_return_to");
227
+ if (returnTo) {
228
+ localStorage.removeItem("p1_return_to");
229
+ setRedirecting(true);
230
+ router.push(returnTo);
231
+ }
232
+ }, [router]);
130
233
 
131
234
  const handleDocumentSelect = useCallback(
132
235
  (docPath: string) => {
@@ -135,15 +238,33 @@ function EditorContent({
135
238
  [router],
136
239
  );
137
240
 
241
+ const handleDocumentNotFound = useCallback(
242
+ async (docPath: string, _error: Error) => {
243
+ const initialData = docPath === "/" ? DEFAULT_ROOT_PAGE_DATA : DEFAULT_PAGE_DATA;
244
+ const token = await getToken();
245
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
246
+ if (token) headers["Authorization"] = `Bearer ${token}`;
247
+ const res = await fetch("/p1/api/structure/page", {
248
+ method: "POST",
249
+ headers,
250
+ body: JSON.stringify({ path: docPath, initialData }),
251
+ });
252
+ return res.ok;
253
+ },
254
+ [getToken],
255
+ );
256
+
138
257
  const { loading, error, puckKey, puckProps } = useP1Editor({
139
258
  documentPath: path,
140
259
  puckConfig: editorConfig,
141
- additionalPlugins: p1Plugins,
260
+ additionalPlugins,
261
+ onDocumentNotFound: handleDocumentNotFound,
142
262
  pluginOptions: {
143
263
  onDocumentSelect: handleDocumentSelect,
144
264
  selectedDocumentPath: path,
145
265
  siteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
146
266
  dashboardUrl: process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL,
267
+ logoUrl: P1_ASSETS.LOGO_URL,
147
268
  },
148
269
  overrideOptions: {
149
270
  showDefaultPublish: false,
@@ -163,6 +284,14 @@ function EditorContent({
163
284
  }
164
285
  }, [loading, error, puckKey, puckProps]);
165
286
 
287
+ if (redirecting) {
288
+ return (
289
+ <div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
290
+ Redirecting...
291
+ </div>
292
+ );
293
+ }
294
+
166
295
  // Show full loading screen only on first load (no previous state)
167
296
  if (loading && !lastGoodStateRef.current) {
168
297
  return (
@@ -230,7 +359,7 @@ function EditorContent({
230
359
  </div>
231
360
  )}
232
361
  {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
233
- <Puck key={displayState.puckKey} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
362
+ <Puck key={`${displayState.puckKey}-${chatbotEnabled ? "ai" : "no-ai"}`} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
234
363
  </div>
235
364
  );
236
365
  }
@@ -2,16 +2,16 @@ import "@puckeditor/core/puck.css";
2
2
  import { createP1Pages } from "@pantheon-systems/p1-next-sdk/server";
3
3
  import config from "../../../puck.config";
4
4
  import { EditorClientWrapper } from "./editor-client";
5
- import { RenderClientWrapper } from "./render-client";
6
5
 
7
6
  const pages = createP1Pages({
8
7
  config,
9
8
  p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
10
9
  p1ApiKey: process.env.CSS_API_KEY,
11
10
  p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
12
- p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
11
+ // Default to "main" when unset: server components (no user token) need a
12
+ // branch to list/read documents (e.g. the /p1/structure routes table).
13
+ p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID ?? "main",
13
14
  EditorClient: EditorClientWrapper,
14
- RenderClient: RenderClientWrapper,
15
15
  });
16
16
 
17
17
  export default pages.Page;
@@ -3,7 +3,7 @@ import { createP1AuthHandler } from "@pantheon-systems/p1-next-sdk/server";
3
3
  const handler = createP1AuthHandler({
4
4
  p1ApiKey: process.env.CSS_API_KEY,
5
5
  p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
6
- prompt: 'login',
6
+ prompt: "select_account",
7
7
  });
8
8
 
9
9
  export const { POST } = handler;
@@ -1,6 +1,5 @@
1
- import Link from "next/link";
1
+ import { WelcomeBlockRender } from "../components/puck/welcome-block-render";
2
2
  import {
3
- listRoutes,
4
3
  ensureInitialized,
5
4
  getPage,
6
5
  listRouteTemplateKeysFromDatabase,
@@ -12,13 +11,14 @@ import {
12
11
  import type { Metadata } from "next";
13
12
  import { REMOTE_DATASOURCE_FETCHERS } from "../lib/remote-datasource-fetchers";
14
13
  import { Client } from "./[...puckPath]/client";
15
- import { CollectionNav } from "./collection-nav";
16
14
 
17
15
  const initPromise = ensureInitialized({
18
16
  p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
19
17
  p1ApiKey: process.env.CSS_API_KEY,
20
18
  p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
21
- p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
19
+ // Default to "main" when unset: server components (no user token) need a
20
+ // branch to list/read documents (e.g. the /structure routes table).
21
+ p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID ?? "main",
22
22
  });
23
23
 
24
24
  export async function generateMetadata(): Promise<Metadata> {
@@ -76,63 +76,21 @@ export default async function HomePage() {
76
76
  );
77
77
  }
78
78
 
79
- const routes = await listRoutes();
80
-
81
- const staticPages = routes.filter((r) => r.kind === "static");
82
- const templates = routes.filter((r) => r.kind === "template");
83
-
84
79
  return (
85
- <main className="flex min-h-screen items-center justify-center bg-gray-50">
86
- <div className="mx-auto max-w-lg px-6 py-16 text-center">
87
- <h1 className="text-3xl font-bold tracking-tight text-gray-900">
88
- Welcome to the P1 Starter Kit
89
- </h1>
90
- <p className="mt-4 text-gray-600">
91
- Build and manage pages with the visual editor, or head to the
92
- dashboard to manage your site.
93
- </p>
94
-
95
- <nav className="mt-10 flex flex-col gap-3">
96
- <Link
97
- href="/p1"
98
- className="rounded-lg bg-gray-900 px-5 py-3 text-sm font-medium text-white hover:bg-gray-700"
99
- >
100
- Open the Page Editor
101
- </Link>
102
- <a
103
- href="https://staging.content.pantheon.io/dashboard/sites"
104
- target="_blank"
105
- rel="noopener noreferrer"
106
- className="rounded-lg border border-gray-300 px-5 py-3 text-sm font-medium text-gray-900 hover:bg-gray-100"
107
- >
108
- P1 Dashboard &rarr;
109
- </a>
110
- </nav>
111
-
112
- {(staticPages.length > 0 || templates.length > 0) && (
113
- <div className="mt-10 text-left">
114
- <h2 className="text-lg font-semibold text-gray-900 mb-4">Pages</h2>
115
- <ul className="space-y-3">
116
- {staticPages.map((route) => (
117
- <li key={route.path}>
118
- <Link
119
- href={route.path}
120
- className="text-sm font-mono text-blue-600 hover:underline"
121
- >
122
- {route.path}
123
- </Link>
124
- </li>
125
- ))}
126
- {templates.map((route) => (
127
- <li key={route.path}>
128
- <CollectionNav templatePath={route.path} />
129
- </li>
130
- ))}
131
- </ul>
132
- </div>
133
- )}
134
- </div>
135
- </main>
80
+ <WelcomeBlockRender
81
+ heading="Welcome to your new Pantheon P1 Site."
82
+ 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."
83
+ ctaLabel="Sign-in to P1"
84
+ ctaHref="/p1"
85
+ footnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
86
+ loggedInHeading="Welcome to your new Pantheon P1 Site."
87
+ 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."
88
+ loggedInCtaLabel="Edit this page with P1 Visual Editor"
89
+ loggedInCtaHref="/p1"
90
+ loggedInSecondaryLabel="Go to P1 Dashboard"
91
+ loggedInFootnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
92
+ showLogo={true}
93
+ />
136
94
  );
137
95
  }
138
96
 
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { LDProvider } from "launchdarkly-react-client-sdk";
5
+ import { useP1Auth } from "@pantheon-systems/puck-css";
6
+
7
+ /**
8
+ * Wraps the editor with a LaunchDarkly client-side provider so the `p1-chatbot`
9
+ * flag can be evaluated at runtime. The client-side ID is public by design.
10
+ *
11
+ * When NEXT_PUBLIC_LD_CLIENT_ID is unset (local dev / offline), LaunchDarkly is
12
+ * not initialized and children render without a provider — `useFlags()` then
13
+ * returns no flags, so the chatbot defaults to hidden.
14
+ */
15
+ export function ChatbotFlagProvider({
16
+ children,
17
+ }: {
18
+ children: React.ReactNode;
19
+ }) {
20
+ const clientSideID = process.env.NEXT_PUBLIC_LD_CLIENT_ID;
21
+ const { user } = useP1Auth();
22
+
23
+ if (!clientSideID) {
24
+ return <>{children}</>;
25
+ }
26
+
27
+ // LaunchDarkly evaluates the flag for the context present at mount and does not
28
+ // re-identify on context change. This provider mounts inside <P1App> (after
29
+ // auth), so the authenticated user is available here; the anonymous fallback
30
+ // 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 };
39
+
40
+ return (
41
+ <LDProvider
42
+ clientSideID={clientSideID}
43
+ context={context}
44
+ reactOptions={{ useCamelCaseFlagKeys: false }}
45
+ >
46
+ {children}
47
+ </LDProvider>
48
+ );
49
+ }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ export function PantheonMark() {
4
+ return (
5
+ <svg
6
+ className="h-7 w-auto block"
7
+ viewBox="0 0 105 230"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ aria-hidden="true"
10
+ >
11
+ <polygon fill="#FFDC28" points="17.8,13.4 35.7,56.4 13,56.4 20.5,75.4 66.6,75.4" />
12
+ <polygon fill="#FFDC28" points="78.4,170.1 70.8,151.2 60.3,151.2 38.3,97.9 28.9,97.9 50.8,151.2 24,151.2 73.6,213.2 55.7,170.1" />
13
+ <path fill="#23232D" d="M84.6,94.3c0.6,0,1.9-0.7,1.9-7.3s-1.3-7.3-1.9-7.3H52.8l6,14.6C58.8,94.3,84.6,94.3,84.6,94.3z" />
14
+ <path fill="#23232D" d="M66.1,111.8l21.3,0c0.6,0,1.9-0.7,1.9-7.3s-1.3-7.3-1.9-7.3l-27.4,0L66.1,111.8z" />
15
+ <path fill="#23232D" d="M84.6,132.2H55.9l6,14.6h22.7c0.6,0,1.9-0.7,1.9-7.3S85.1,132.2,84.6,132.2z" />
16
+ <path fill="#23232D" d="M87.4,114.7H48.7l6,14.6h32.7c0.6,0,1.9-0.7,1.9-7.3S88,114.7,87.4,114.7L87.4,114.7z" />
17
+ <path fill="#23232D" d="M31.1,111.9l-6.8-17.6h15.9l7.4,17.6l15.2-0.1L49.5,79.7H16.5c-2.5,0-3.9,0-5.1,3.8c-1.4,4.5-1.5,13.1-1.5,29.7s0.2,25.2,1.5,29.7c1.1,3.8,2.5,3.8,5.1,3.8l29,0l-14.4-35L31.1,111.9L31.1,111.9z" />
18
+ <path fill="#23232D" d="M91.7,143h-1.2v-0.8h3.4v0.8h-1.2v3.5h-1L91.7,143L91.7,143z M96.3,146.5l-1.1-3.3v3.3h-0.9v-4.3h1.3l1.1,3.3l1.1-3.3H99v4.3h-0.9v-3.3l-1,3.3H96.3L96.3,146.5z" />
19
+ </svg>
20
+ );
21
+ }
22
+
23
+ export function P1Lockup() {
24
+ return (
25
+ <div className="inline-flex items-center gap-2.5 mb-6 px-2 -mx-2 rounded-full">
26
+ <PantheonMark />
27
+ <span className="w-px h-5 bg-gray-200" />
28
+ <span className="font-['Inter_Tight','Inter',system-ui,sans-serif] font-semibold text-[19px] tracking-[0.01em] text-[#1a1a2e] leading-none">
29
+ P1
30
+ </span>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { P1Lockup } from "../p1-lockup";
5
+
6
+ export interface WelcomeBlockRenderProps {
7
+ heading?: string;
8
+ description?: string;
9
+ ctaLabel?: string;
10
+ ctaHref?: string;
11
+ footnote?: string;
12
+ loggedInHeading?: string;
13
+ loggedInDescription?: string;
14
+ loggedInCtaLabel?: string;
15
+ loggedInCtaHref?: string;
16
+ loggedInSecondaryLabel?: string;
17
+ loggedInFootnote?: string;
18
+ showLogo?: boolean;
19
+ }
20
+
21
+ const LOGGED_IN_DEFAULTS = {
22
+ heading: "Welcome to your new Pantheon P1 Site.",
23
+ description:
24
+ "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.",
25
+ ctaLabel: "Edit this page with P1 Visual Editor",
26
+ ctaHref: "/p1",
27
+ secondaryLabel: "Go to P1 Dashboard",
28
+ secondaryHref: process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL || "https://content.pantheon.io",
29
+ footnote:
30
+ "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
31
+ };
32
+
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
+ export function WelcomeBlockRender(props: WelcomeBlockRenderProps) {
40
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
41
+
42
+ useEffect(() => {
43
+ setIsLoggedIn(!!localStorage.getItem("p1_logged_in"));
44
+ }, []);
45
+
46
+ const activeHeading = isLoggedIn
47
+ ? (props.loggedInHeading || LOGGED_IN_DEFAULTS.heading)
48
+ : props.heading;
49
+ const activeDescription = isLoggedIn
50
+ ? (props.loggedInDescription || LOGGED_IN_DEFAULTS.description)
51
+ : props.description;
52
+ const activeCtaLabel = isLoggedIn
53
+ ? (props.loggedInCtaLabel || LOGGED_IN_DEFAULTS.ctaLabel)
54
+ : props.ctaLabel;
55
+ const activeCtaHref = isLoggedIn
56
+ ? (props.loggedInCtaHref || LOGGED_IN_DEFAULTS.ctaHref)
57
+ : props.ctaHref;
58
+ const activeFootnote = isLoggedIn
59
+ ? (props.loggedInFootnote || LOGGED_IN_DEFAULTS.footnote)
60
+ : props.footnote;
61
+ const secondaryLabel = isLoggedIn
62
+ ? (props.loggedInSecondaryLabel || LOGGED_IN_DEFAULTS.secondaryLabel)
63
+ : null;
64
+
65
+ const footnoteHtml = (activeFootnote ?? "").replace(
66
+ /\[([^\]]+)\]\(([^)]+)\)/g,
67
+ '<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-600 underline">$1</a>',
68
+ );
69
+
70
+ 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 && (
92
+ <button
93
+ className={BTN_SECONDARY}
94
+ onClick={() => {
95
+ window.open(LOGGED_IN_DEFAULTS.secondaryHref, "_blank", "noopener,noreferrer");
96
+ }}
97
+ >
98
+ {secondaryLabel}
99
+ </button>
100
+ )}
101
+ </div>
102
+ {activeFootnote && (
103
+ <p
104
+ className="mt-12 text-sm text-[#5a5a6e]"
105
+ dangerouslySetInnerHTML={{ __html: footnoteHtml }}
106
+ />
107
+ )}
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,45 @@
1
+ import { WelcomeBlockRender } from "./welcome-block-render";
2
+
3
+ export const welcomeBlock = {
4
+ label: "P1 Welcome",
5
+ fields: {
6
+ heading: { type: "text" as const, label: "Heading (signed out)" },
7
+ description: { type: "textarea" as const, label: "Description (signed out)" },
8
+ ctaLabel: { type: "text" as const, label: "Primary button label (signed out)" },
9
+ ctaHref: { type: "text" as const, label: "Primary button link (signed out)" },
10
+ footnote: { type: "textarea" as const, label: "Footnote (signed out)" },
11
+ loggedInHeading: { type: "text" as const, label: "Heading (signed in)" },
12
+ loggedInDescription: { type: "textarea" as const, label: "Description (signed in)" },
13
+ loggedInCtaLabel: { type: "text" as const, label: "Primary button label (signed in)" },
14
+ loggedInCtaHref: { type: "text" as const, label: "Primary button link (signed in)" },
15
+ loggedInSecondaryLabel: { type: "text" as const, label: "Secondary button label (signed in)" },
16
+ loggedInFootnote: { type: "textarea" as const, label: "Footnote (signed in)" },
17
+ showLogo: {
18
+ type: "radio" as const,
19
+ label: "Show P1 logo",
20
+ options: [
21
+ { label: "Yes", value: true },
22
+ { label: "No", value: false },
23
+ ],
24
+ },
25
+ },
26
+ defaultProps: {
27
+ heading: "Welcome to your new Pantheon P1 Site.",
28
+ description:
29
+ "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.",
30
+ ctaLabel: "Sign-in to P1",
31
+ ctaHref: "/p1",
32
+ footnote:
33
+ "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
34
+ loggedInHeading: "Welcome to your new Pantheon P1 Site.",
35
+ loggedInDescription:
36
+ "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.",
37
+ loggedInCtaLabel: "Edit this page with P1 Visual Editor",
38
+ loggedInCtaHref: "/p1",
39
+ loggedInSecondaryLabel: "Go to P1 Dashboard",
40
+ loggedInFootnote:
41
+ "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
42
+ showLogo: true,
43
+ },
44
+ render: WelcomeBlockRender,
45
+ };
@@ -0,0 +1,3 @@
1
+ export const P1_ASSETS = {
2
+ LOGO_URL: "/images/p1_logo.svg",
3
+ } as const;
@@ -0,0 +1,17 @@
1
+ /** LaunchDarkly flag that gates the AI chatbot. Short-lived — removed once the
2
+ * chatbot ships to everyone in the alpha. */
3
+ export const CHATBOT_FLAG_KEY = "p1-chatbot";
4
+
5
+ /**
6
+ * Whether the AI chatbot plugin should be mounted in the editor.
7
+ *
8
+ * Requires both the `p1-chatbot` LaunchDarkly flag to be enabled and an agent
9
+ * URL to be configured. Defaults off when the flag is `undefined` (LD not yet
10
+ * resolved, unset client ID, or offline), so the chatbot stays hidden by default.
11
+ */
12
+ export function shouldShowChatbot(
13
+ flagEnabled: boolean | undefined,
14
+ agentUrl: string | undefined,
15
+ ): boolean {
16
+ return Boolean(flagEnabled && agentUrl);
17
+ }
@@ -11,12 +11,14 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@pantheon-systems/cpub-react-sdk": "^5.2.1",
14
- "@pantheon-systems/css-client": "^0.4.4",
15
- "@pantheon-systems/p1-next-sdk": "^0.4.4",
16
- "@pantheon-systems/puck-css": "^0.4.4",
14
+ "@pantheon-systems/p1-ai-chat": "^0.1.0",
15
+ "@pantheon-systems/p1-next-sdk": "^0.6.0",
16
+ "@pantheon-systems/pds-toolkit-react": "2.0.0-alpha.12",
17
+ "@pantheon-systems/puck-css": "^0.6.0",
17
18
  "@puckeditor/core": "^0.21.1",
18
19
  "@tailwindcss/postcss": "^4.2.2",
19
20
  "classnames": "^2.5.1",
21
+ "launchdarkly-react-client-sdk": "^3.9.2",
20
22
  "next": "^16.2.6",
21
23
  "postcss": "^8.5.12",
22
24
  "react": "^19.2.5",
@@ -0,0 +1,5 @@
1
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0 3C0 1.34315 1.34315 0 3 0H29C30.6569 0 32 1.34315 32 3V29C32 30.6569 30.6569 32 29 32H3C1.34315 32 0 30.6569 0 29V3Z" fill="#171717"/>
3
+ <path d="M6 7H16.9111V8.82232H18.7335V17.9112H16.9111V19.7335H9.64464V25.189H6.01139V7H6ZM15.0888 10.6333H9.63325V16.0888H15.0888V10.6333Z" fill="#FFDC28"/>
4
+ <path d="M20.5449 7H26.0005V25.1777H22.3672V10.6333H20.5449V7Z" fill="#FFDC28"/>
5
+ </svg>
@@ -10,6 +10,7 @@ import { paragraphBlock } from "./components/puck/paragraph-block";
10
10
  import { quoteBlock } from "./components/puck/quote-block";
11
11
  import { puckRoot } from "./components/puck/root";
12
12
  import { spacerBlock } from "./components/puck/spacer-block";
13
+ import { welcomeBlock } from "./components/puck/welcome-block";
13
14
 
14
15
  export const config = {
15
16
  categories: {
@@ -33,6 +34,10 @@ export const config = {
33
34
  title: "Actions",
34
35
  components: ["ButtonBlock"],
35
36
  },
37
+ pages: {
38
+ title: "Page Sections",
39
+ components: ["P1WelcomeBlock"],
40
+ },
36
41
  },
37
42
  root: puckRoot,
38
43
  components: {
@@ -45,6 +50,7 @@ export const config = {
45
50
  DividerBlock: dividerBlock,
46
51
  SpacerBlock: spacerBlock,
47
52
  ButtonBlock: buttonBlock,
53
+ P1WelcomeBlock: welcomeBlock,
48
54
  },
49
55
  } as Config;
50
56
 
@@ -1,9 +0,0 @@
1
- "use client";
2
-
3
- import type { Data } from "@puckeditor/core";
4
- import { RenderClient } from "@pantheon-systems/puck-css";
5
- import config from "../../../puck.config";
6
-
7
- export function RenderClientWrapper({ data }: { data: Data }) {
8
- return <RenderClient config={config} data={data} />;
9
- }