@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.
- package/package.json +5 -3
- package/template/__tests__/chatbot-flag-context.test.ts +55 -0
- package/template/__tests__/data-list-block-sub-components.test.tsx +246 -0
- package/template/__tests__/data-list-block-utils.test.ts +84 -0
- package/template/__tests__/data-list-block.test.ts +198 -0
- package/template/__tests__/published-page-routes.test.ts +86 -0
- package/template/app/[...puckPath]/page.tsx +10 -117
- package/template/app/p1/(editor)/[[...p1]]/editor-client.tsx +23 -73
- package/template/app/page.tsx +11 -69
- package/template/app/published-pages.tsx +23 -0
- package/template/app/welcome-block.tsx +25 -0
- package/template/components/ChatbotFlagProvider.tsx +3 -8
- package/template/components/p1-lockup.tsx +2 -1
- package/template/components/puck/image-block.tsx +5 -2
- package/template/components/puck/welcome-block-render.tsx +30 -37
- package/template/components/welcome-block.module.css +124 -0
- package/template/eslint.config.js +9 -1
- package/template/lib/chatbot-flag/flag-context.ts +32 -0
- package/template/package.json +3 -3
- package/template/vitest.config.ts +17 -20
- package/template/__tests__/published-page-404.test.ts +0 -65
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* Styling for the welcome block and the sign-in screen that shares its layout.
|
|
2
|
+
Both render inside a host site whose own body styles they must not inherit —
|
|
3
|
+
a dark-themed host would otherwise leave this dark text on a dark background —
|
|
4
|
+
so the surface sets its own background as well as its foreground. */
|
|
5
|
+
|
|
6
|
+
.surface {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
width: 100%;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
padding: 4rem 2rem;
|
|
14
|
+
background: #ffffff;
|
|
15
|
+
color: #1a1a2e;
|
|
16
|
+
font-family: "Inter", system-ui, sans-serif;
|
|
17
|
+
text-align: center;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.inner {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
align-items: center;
|
|
24
|
+
width: 100%;
|
|
25
|
+
max-width: 620px;
|
|
26
|
+
margin: 0 auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.lockup {
|
|
30
|
+
display: block;
|
|
31
|
+
height: 1.75rem;
|
|
32
|
+
width: auto;
|
|
33
|
+
margin-bottom: 1.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.heading {
|
|
37
|
+
margin: 0 0 1rem;
|
|
38
|
+
font-size: 2.5rem;
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
line-height: 1.08;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.description {
|
|
44
|
+
max-width: 54ch;
|
|
45
|
+
margin: 0;
|
|
46
|
+
font-size: 1rem;
|
|
47
|
+
line-height: 1.5rem;
|
|
48
|
+
color: #5a5a6e;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.actions {
|
|
52
|
+
display: flex;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
gap: 0.75rem;
|
|
55
|
+
margin-top: 2rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.button {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
gap: 0.5rem;
|
|
63
|
+
height: 3rem;
|
|
64
|
+
padding: 0 1.5rem;
|
|
65
|
+
border: 1px solid #1a1a2e;
|
|
66
|
+
border-radius: 9999px;
|
|
67
|
+
background: #1a1a2e;
|
|
68
|
+
color: #ffffff;
|
|
69
|
+
font-family: "Inter", system-ui, sans-serif;
|
|
70
|
+
font-size: 1.125rem;
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
line-height: 1;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
transition:
|
|
76
|
+
background-color 0.2s,
|
|
77
|
+
border-color 0.2s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.button:hover {
|
|
81
|
+
background: #2d2d44;
|
|
82
|
+
border-color: #2d2d44;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.button:focus-visible {
|
|
86
|
+
outline: 1px solid #2563eb;
|
|
87
|
+
outline-offset: 1px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.button:disabled {
|
|
91
|
+
opacity: 0.4;
|
|
92
|
+
cursor: not-allowed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.buttonSecondary {
|
|
96
|
+
border-color: #d0d0d8;
|
|
97
|
+
background: #ffffff;
|
|
98
|
+
color: #1a1a2e;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.buttonSecondary:hover {
|
|
102
|
+
background: rgba(26, 26, 46, 0.06);
|
|
103
|
+
border-color: #d0d0d8;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.footnote {
|
|
107
|
+
margin-top: 3rem;
|
|
108
|
+
font-size: 0.875rem;
|
|
109
|
+
color: #5a5a6e;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.link {
|
|
113
|
+
color: #2563eb;
|
|
114
|
+
text-decoration: underline;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.error {
|
|
118
|
+
margin-top: 1rem;
|
|
119
|
+
padding: 0.5rem 0.75rem;
|
|
120
|
+
border-radius: 0.375rem;
|
|
121
|
+
background: #fef2f2;
|
|
122
|
+
color: #dc2626;
|
|
123
|
+
font-size: 0.875rem;
|
|
124
|
+
}
|
|
@@ -38,6 +38,9 @@ export default tseslint.config(
|
|
|
38
38
|
{
|
|
39
39
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.mjs'],
|
|
40
40
|
plugins: { import: importPlugin },
|
|
41
|
+
// Packages alias their own source as @/… in tsconfig paths. Without this the
|
|
42
|
+
// import plugin reads those as third-party and sorts them after relative ones.
|
|
43
|
+
settings: { 'import/internal-regex': '^@/' },
|
|
41
44
|
languageOptions: {
|
|
42
45
|
ecmaVersion: 'latest',
|
|
43
46
|
sourceType: 'module',
|
|
@@ -71,7 +74,12 @@ export default tseslint.config(
|
|
|
71
74
|
'import/extensions': 'off',
|
|
72
75
|
'import/prefer-default-export': 'off',
|
|
73
76
|
'import/no-unresolved': 'off',
|
|
74
|
-
'
|
|
77
|
+
// 'internal' is absent from the rule's default group list, so aliased
|
|
78
|
+
// imports would otherwise rank below every relative import.
|
|
79
|
+
'import/order': [
|
|
80
|
+
'warn',
|
|
81
|
+
{ groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'] },
|
|
82
|
+
],
|
|
75
83
|
'import/no-extraneous-dependencies': 'warn',
|
|
76
84
|
|
|
77
85
|
// General ESLint rules
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AuthUser } from "@pantheon-systems/puck-css";
|
|
2
|
+
|
|
3
|
+
export interface FlagContext {
|
|
4
|
+
kind: "user";
|
|
5
|
+
key: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
anonymous?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the LaunchDarkly evaluation context for the signed-in user.
|
|
12
|
+
*
|
|
13
|
+
* Keyed on the lowercased email because LaunchDarkly matches individual targets on
|
|
14
|
+
* the key alone, and the rest of P1 identifies people to LaunchDarkly the same way.
|
|
15
|
+
* Keying on anything else makes an email entered in the targeting UI match nothing,
|
|
16
|
+
* with no error to say so.
|
|
17
|
+
*
|
|
18
|
+
* Falls back to the user id when email is absent, which AuthUser allows, so those
|
|
19
|
+
* users stay individually targetable instead of sharing the anonymous context.
|
|
20
|
+
*/
|
|
21
|
+
export function buildFlagContext(user: AuthUser | null): FlagContext {
|
|
22
|
+
if (!user) {
|
|
23
|
+
return { kind: "user", key: "anonymous", anonymous: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const email = user.email?.trim();
|
|
27
|
+
return {
|
|
28
|
+
kind: "user",
|
|
29
|
+
key: email ? email.toLowerCase() : user.id,
|
|
30
|
+
...(email && { email }),
|
|
31
|
+
};
|
|
32
|
+
}
|
package/template/package.json
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@pantheon-systems/cpub-react-sdk": "^5.2.1",
|
|
18
|
-
"@pantheon-systems/css-client": "^0.
|
|
18
|
+
"@pantheon-systems/css-client": "^0.14.0",
|
|
19
19
|
"@pantheon-systems/p1-ai-chat": "^0.6.0",
|
|
20
20
|
"@pantheon-systems/p1-media": "^0.4.5",
|
|
21
|
-
"@pantheon-systems/p1-next-sdk": "^0.
|
|
21
|
+
"@pantheon-systems/p1-next-sdk": "^0.14.0",
|
|
22
22
|
"@pantheon-systems/pds-toolkit-react": "2.0.0-alpha.66",
|
|
23
|
-
"@pantheon-systems/puck-css": "^0.
|
|
23
|
+
"@pantheon-systems/puck-css": "^0.14.0",
|
|
24
24
|
"@puckeditor/core": "^0.21.1",
|
|
25
25
|
"@tailwindcss/postcss": "^4.2.2",
|
|
26
26
|
"@tailwindcss/typography": "^0.5.16",
|
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
import { resolve } from "path";
|
|
2
1
|
import { defineConfig } from "vitest/config";
|
|
3
2
|
import react from "@vitejs/plugin-react";
|
|
4
3
|
|
|
5
4
|
export default defineConfig({
|
|
6
5
|
plugins: [react()],
|
|
7
|
-
resolve: {
|
|
8
|
-
alias: {
|
|
9
|
-
"@pantheon-systems/puck-css/fields": resolve(
|
|
10
|
-
__dirname,
|
|
11
|
-
"../..",
|
|
12
|
-
"packages/puck-css/src/data/fields.tsx",
|
|
13
|
-
),
|
|
14
|
-
"@pantheon-systems/pds-toolkit-react": resolve(
|
|
15
|
-
__dirname,
|
|
16
|
-
"../..",
|
|
17
|
-
"packages/puck-css/src/__mocks__/@pantheon-systems/pds-toolkit-react.ts",
|
|
18
|
-
),
|
|
19
|
-
"@puckeditor/core": resolve(
|
|
20
|
-
__dirname,
|
|
21
|
-
"../..",
|
|
22
|
-
"packages/puck-css/src/__mocks__/@puckeditor/core.ts",
|
|
23
|
-
),
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
6
|
test: {
|
|
27
7
|
environment: "node",
|
|
8
|
+
// Inlining below means the first test to import puck-css pays for Vite
|
|
9
|
+
// transforming that whole graph — seconds on a cold machine, and the default
|
|
10
|
+
// 5s timeout fails it on slower CI runners while every later test in the file
|
|
11
|
+
// runs in milliseconds.
|
|
12
|
+
testTimeout: 30_000,
|
|
13
|
+
server: {
|
|
14
|
+
deps: {
|
|
15
|
+
// pds-toolkit-react's entry point imports its own dist/index.css, and
|
|
16
|
+
// puck-css imports pds-toolkit-react. Left external, vitest hands that
|
|
17
|
+
// chain to Node's ESM loader, which has no loader for .css; inlining
|
|
18
|
+
// routes it through Vite instead. Both must be listed — inlining only the
|
|
19
|
+
// leaf leaves puck-css external, and Node then loads the whole subtree
|
|
20
|
+
// without consulting this list. The patterns match a resolved path, so
|
|
21
|
+
// they allow pnpm's `@pantheon-systems+puck-css` directory form too.
|
|
22
|
+
inline: [/@pantheon-systems[/+]puck-css/, /@pantheon-systems[/+]pds-toolkit-react/],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
28
25
|
typecheck: {
|
|
29
26
|
tsconfig: "./tsconfig.test.json",
|
|
30
27
|
},
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const { loadPublishedPage, notFound } = vi.hoisted(() => ({
|
|
4
|
-
loadPublishedPage: vi.fn(),
|
|
5
|
-
notFound: vi.fn(() => {
|
|
6
|
-
throw new Error("NEXT_NOT_FOUND");
|
|
7
|
-
}),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
vi.mock("@pantheon-systems/p1-next-sdk/server", () => ({
|
|
11
|
-
loadPublishedPage,
|
|
12
|
-
loadRouteTemplateKeys: vi.fn().mockResolvedValue([]),
|
|
13
|
-
createCssQueryFetchers: vi.fn().mockReturnValue([]),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock("next/navigation", () => ({ notFound }));
|
|
17
|
-
|
|
18
|
-
vi.mock("@pantheon-systems/puck-css/server", () => ({
|
|
19
|
-
pagePathFromCatchAllSegments: (segments: string[]) => `/${segments.join("/")}`,
|
|
20
|
-
loadRemoteDatasourceContext: vi.fn().mockResolvedValue({}),
|
|
21
|
-
extractReferencedDatasourceIds: vi.fn().mockReturnValue([]),
|
|
22
|
-
resolveDataTemplates: vi.fn(async (d: unknown) => d),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
vi.mock("../lib/remote-datasource-fetchers", () => ({
|
|
26
|
-
REMOTE_DATASOURCE_FETCHERS: [],
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
vi.mock("../app/[...puckPath]/client", () => ({ Client: () => null }));
|
|
30
|
-
vi.mock("../components/content-unavailable", () => ({
|
|
31
|
-
ContentUnavailable: () => null,
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
import Page from "../app/[...puckPath]/page";
|
|
35
|
-
|
|
36
|
-
const render = (...segments: string[]) =>
|
|
37
|
-
Page({ params: Promise.resolve({ puckPath: segments }) });
|
|
38
|
-
|
|
39
|
-
describe("catch-all route — missing vs unavailable", () => {
|
|
40
|
-
beforeEach(() => vi.clearAllMocks());
|
|
41
|
-
|
|
42
|
-
// The route is statically renderable, so a 200 here would write every junk URL
|
|
43
|
-
// a crawler probes into the response cache as a successful page.
|
|
44
|
-
it("404s a path with no published page", async () => {
|
|
45
|
-
loadPublishedPage.mockResolvedValue({ status: "missing" });
|
|
46
|
-
await expect(render("nope")).rejects.toThrow("NEXT_NOT_FOUND");
|
|
47
|
-
expect(notFound).toHaveBeenCalled();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// 404ing here would deindex a live page over a transient backend blip.
|
|
51
|
-
it("does not 404 when the backend is unreachable", async () => {
|
|
52
|
-
loadPublishedPage.mockResolvedValue({ status: "unavailable" });
|
|
53
|
-
await render("real-page");
|
|
54
|
-
expect(notFound).not.toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("renders published content", async () => {
|
|
58
|
-
loadPublishedPage.mockResolvedValue({
|
|
59
|
-
status: "ok",
|
|
60
|
-
data: { root: { props: { title: "Hi" } }, content: [] },
|
|
61
|
-
});
|
|
62
|
-
await render("blog");
|
|
63
|
-
expect(notFound).not.toHaveBeenCalled();
|
|
64
|
-
});
|
|
65
|
-
});
|