@pantheon-systems/create-p1-starter-kit 0.11.1 → 0.13.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,114 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { LogoutOutcome } from "@pantheon-systems/puck-css";
3
+ import { runWidgetLogout } from "../app/[...puckPath]/widget-logout";
4
+
5
+ // Records the effects in order, so each test asserts the whole sequence a
6
+ // logout attempt produces rather than one call in isolation.
7
+ function recorder(logout: () => Promise<LogoutOutcome>) {
8
+ const calls: string[] = [];
9
+ return {
10
+ calls,
11
+ fx: {
12
+ logout,
13
+ navigate: (url: string) => calls.push(`navigate:${url}`),
14
+ reload: () => calls.push("reload"),
15
+ setBusy: (busy: boolean) => calls.push(`busy:${busy}`),
16
+ setError: (message: string | null) =>
17
+ calls.push(`error:${message ?? "cleared"}`),
18
+ },
19
+ };
20
+ }
21
+
22
+ const LOGOUT_URL = "https://example.auth0.com/v2/logout?client_id=abc";
23
+
24
+ describe("runWidgetLogout", () => {
25
+ it("navigates to the Auth0 logout URL when the session ended", async () => {
26
+ const { calls, fx } = recorder(async () => ({
27
+ status: "signed_out",
28
+ logoutUrl: LOGOUT_URL,
29
+ }));
30
+
31
+ await runWidgetLogout(fx);
32
+
33
+ // Stays busy: the navigation replaces the page, so releasing the button
34
+ // would only flash it back to "Log out" on the way out.
35
+ expect(calls).toEqual([
36
+ "busy:true",
37
+ "error:cleared",
38
+ `navigate:${LOGOUT_URL}`,
39
+ ]);
40
+ });
41
+
42
+ it("keeps the menu open and shows why when logout failed", async () => {
43
+ const { calls, fx } = recorder(async () => ({
44
+ status: "error",
45
+ message: "Broker logout failed (503)",
46
+ }));
47
+
48
+ await runWidgetLogout(fx);
49
+
50
+ // Still signed in and retryable, so the button must come back.
51
+ expect(calls).toEqual([
52
+ "busy:true",
53
+ "error:cleared",
54
+ "error:Broker logout failed (503)",
55
+ "busy:false",
56
+ ]);
57
+ });
58
+
59
+ it("reloads when there was no session to end", async () => {
60
+ const { calls, fx } = recorder(async () => ({ status: "no_session" }));
61
+
62
+ await runWidgetLogout(fx);
63
+
64
+ expect(calls).toEqual(["busy:true", "error:cleared", "reload"]);
65
+ });
66
+
67
+ it("reports a thrown error instead of leaving the button stuck", async () => {
68
+ const { calls, fx } = recorder(async () => {
69
+ throw new Error("Failed to fetch");
70
+ });
71
+
72
+ await runWidgetLogout(fx);
73
+
74
+ expect(calls).toEqual([
75
+ "busy:true",
76
+ "error:cleared",
77
+ "error:Failed to fetch",
78
+ "busy:false",
79
+ ]);
80
+ });
81
+
82
+ it("falls back to a generic message when something non-Error is thrown", async () => {
83
+ const { calls, fx } = recorder(async () => {
84
+ throw "socket closed";
85
+ });
86
+
87
+ await runWidgetLogout(fx);
88
+
89
+ expect(calls).toEqual([
90
+ "busy:true",
91
+ "error:cleared",
92
+ "error:Logout failed",
93
+ "busy:false",
94
+ ]);
95
+ });
96
+
97
+ it("clears a previous failure before retrying", async () => {
98
+ const { calls, fx } = recorder(async () => ({
99
+ status: "signed_out",
100
+ logoutUrl: LOGOUT_URL,
101
+ }));
102
+
103
+ await runWidgetLogout(fx);
104
+ await runWidgetLogout(fx);
105
+
106
+ // The second attempt clears the slot again; a stale message must not sit
107
+ // under a logout that is now succeeding.
108
+ expect(calls.slice(3)).toEqual([
109
+ "busy:true",
110
+ "error:cleared",
111
+ `navigate:${LOGOUT_URL}`,
112
+ ]);
113
+ });
114
+ });
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { useEffect, useState } from "react";
4
4
  import type { Data } from "@puckeditor/core";
5
- import { RenderClient } from "@pantheon-systems/puck-css";
5
+ import { RenderClient, performLogout, P1_LOGGED_IN_KEY } from "@pantheon-systems/puck-css";
6
6
  import config from "../../puck.config";
7
+ import { runWidgetLogout } from "./widget-logout";
7
8
 
8
9
  function EditIcon() {
9
10
  return (
@@ -35,10 +36,12 @@ function LogoutIcon() {
35
36
  export function P1EditWidget({ route }: { route: string }) {
36
37
  const [hasToken, setHasToken] = useState(false);
37
38
  const [open, setOpen] = useState(false);
39
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
40
+ const [logoutError, setLogoutError] = useState<string | null>(null);
38
41
 
39
42
  useEffect(() => {
40
43
  const flag = typeof localStorage !== "undefined"
41
- ? localStorage.getItem("p1_logged_in")
44
+ ? localStorage.getItem(P1_LOGGED_IN_KEY)
42
45
  : null;
43
46
  setHasToken(!!flag);
44
47
  }, []);
@@ -63,6 +66,22 @@ export function P1EditWidget({ route }: { route: string }) {
63
66
 
64
67
  const editHref = `/p1${route === "/" ? "" : route}`;
65
68
 
69
+ const handleLogout = () =>
70
+ runWidgetLogout({
71
+ logout: () =>
72
+ performLogout({
73
+ cssBaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL ?? "http://localhost:8787",
74
+ }),
75
+ navigate: (url) => {
76
+ window.location.href = url;
77
+ },
78
+ reload: () => {
79
+ window.location.reload();
80
+ },
81
+ setBusy: setIsLoggingOut,
82
+ setError: setLogoutError,
83
+ });
84
+
66
85
  return (
67
86
  <div
68
87
  data-p1-widget
@@ -145,12 +164,8 @@ export function P1EditWidget({ route }: { route: string }) {
145
164
  <div style={{ height: 1, background: "#e0e0e0", margin: "6px 4px" }} />
146
165
  <button
147
166
  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
- }}
167
+ disabled={isLoggingOut}
168
+ onClick={handleLogout}
154
169
  style={{
155
170
  display: "flex",
156
171
  alignItems: "center",
@@ -163,15 +178,28 @@ export function P1EditWidget({ route }: { route: string }) {
163
178
  textDecoration: "none",
164
179
  background: "transparent",
165
180
  border: "none",
166
- cursor: "pointer",
181
+ cursor: isLoggingOut ? "not-allowed" : "pointer",
167
182
  fontFamily: "inherit",
168
183
  }}
169
184
  onMouseEnter={(e) => { e.currentTarget.style.background = "#f5f5f5"; }}
170
185
  onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
171
186
  >
172
187
  <span style={{ color: "#888", flex: "0 0 auto" }}><LogoutIcon /></span>
173
- Log out
188
+ {isLoggingOut ? "Logging out…" : "Log out"}
174
189
  </button>
190
+ {logoutError && (
191
+ <p
192
+ role="alert"
193
+ style={{
194
+ margin: "2px 10px 6px",
195
+ fontSize: 12,
196
+ lineHeight: 1.4,
197
+ color: "#b3261e",
198
+ }}
199
+ >
200
+ {logoutError}
201
+ </p>
202
+ )}
175
203
  </div>
176
204
  )}
177
205
  </div>
@@ -21,7 +21,7 @@ import { ContentUnavailable } from "../../components/content-unavailable";
21
21
  import { resolvePageMetadata } from "../../lib/page-seo";
22
22
  import { Client } from "./client";
23
23
 
24
- const getCssQueryFetchers = cache(() => createCssQueryFetchers());
24
+ const getCcrQueryFetchers = cache(() => createCssQueryFetchers());
25
25
 
26
26
  // Document namespaces that live alongside pages but are never routable.
27
27
  const INTERNAL_PATH_PREFIXES = ["/_registry", "/_redirects"];
@@ -101,11 +101,11 @@ export default async function Page({
101
101
 
102
102
  const data = result.data;
103
103
 
104
- const [routeTemplateKeys, cssQueryFetchers] = await Promise.all([
104
+ const [routeTemplateKeys, ccrQueryFetchers] = await Promise.all([
105
105
  loadRouteTemplateKeys(),
106
- getCssQueryFetchers(),
106
+ getCcrQueryFetchers(),
107
107
  ]);
108
- const builtinFetchers = [...REMOTE_DATASOURCE_FETCHERS, ...cssQueryFetchers];
108
+ const builtinFetchers = [...REMOTE_DATASOURCE_FETCHERS, ...ccrQueryFetchers];
109
109
 
110
110
  const referencedDatasourceIds = extractReferencedDatasourceIds(data);
111
111
  const context = await loadRemoteDatasourceContext({
@@ -0,0 +1,36 @@
1
+ import type { LogoutOutcome } from "@pantheon-systems/puck-css";
2
+
3
+ export type WidgetLogoutEffects = {
4
+ logout: () => Promise<LogoutOutcome>;
5
+ navigate: (url: string) => void;
6
+ reload: () => void;
7
+ setBusy: (busy: boolean) => void;
8
+ setError: (message: string | null) => void;
9
+ };
10
+
11
+ /**
12
+ * Runs one logout attempt for the widget. Kept outside the component so each
13
+ * outcome — navigate away, stay and explain, reload — can be exercised without
14
+ * a browser.
15
+ */
16
+ export async function runWidgetLogout(fx: WidgetLogoutEffects): Promise<void> {
17
+ fx.setBusy(true);
18
+ fx.setError(null);
19
+ try {
20
+ const outcome = await fx.logout();
21
+ if (outcome.status === "signed_out") {
22
+ fx.navigate(outcome.logoutUrl);
23
+ return; // navigation takes over
24
+ }
25
+ if (outcome.status === "error") {
26
+ // Still signed in and retryable — keep the menu open and say why.
27
+ fx.setError(outcome.message);
28
+ fx.setBusy(false);
29
+ return;
30
+ }
31
+ fx.reload(); // no_session: drop any stale widget state
32
+ } catch (err) {
33
+ fx.setError(err instanceof Error ? err.message : "Logout failed");
34
+ fx.setBusy(false);
35
+ }
36
+ }
@@ -14,6 +14,7 @@ import {
14
14
  wrapConfigForEditorPreview,
15
15
  P1QueryProvider,
16
16
  editorPathHref,
17
+ EditorReloadOverlay,
17
18
  } from "@pantheon-systems/puck-css";
18
19
  import { DatasourceRegistryProvider, DatasourceDataProvider } from "@pantheon-systems/puck-css/fields";
19
20
  import { LoadingMessage } from "@pantheon-systems/puck-css/pds";
@@ -120,7 +121,6 @@ export function EditorClientWrapper() {
120
121
  const pathname = usePathname();
121
122
  const path = editorPagePathFromUrlPath(pathname);
122
123
  const [userRole, setUserRole] = useState<ContentRole>('editor');
123
- const lastGoodStateRef = React.useRef<{ puckKey: string; puckProps: any } | null>(null);
124
124
 
125
125
  if (!p1Config) {
126
126
  return (
@@ -145,7 +145,7 @@ export function EditorClientWrapper() {
145
145
  loginFallback={<P1SignInPage />}
146
146
  >
147
147
  <ChatbotFlagProvider>
148
- <EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
148
+ <EditorContent path={path} />
149
149
  </ChatbotFlagProvider>
150
150
  </P1App>
151
151
  {process.env.NEXT_PUBLIC_ENABLE_ROLE_SWITCHER === 'true' && (
@@ -206,13 +206,7 @@ function RoleSwitcher({
206
206
  );
207
207
  }
208
208
 
209
- function EditorContent({
210
- path,
211
- lastGoodStateRef,
212
- }: {
213
- path: string;
214
- lastGoodStateRef: React.MutableRefObject<{ puckKey: string; puckProps: any } | null>;
215
- }) {
209
+ function EditorContent({ path }: { path: string }) {
216
210
  const router = useRouter();
217
211
  const { getToken } = useP1Auth();
218
212
  const { data: editorCtx } = useEditorContext(path);
@@ -280,7 +274,7 @@ function EditorContent({
280
274
  [getToken],
281
275
  );
282
276
 
283
- const { loading, error, puckKey, puckProps } = useP1Editor({
277
+ const { loading, reloading, hasContent, error, puckKey, puckProps } = useP1Editor({
284
278
  documentPath: path,
285
279
  puckConfig: editorConfig,
286
280
  additionalPlugins,
@@ -305,24 +299,17 @@ function EditorContent({
305
299
  },
306
300
  });
307
301
 
308
- // Update last good state when loading completes successfully (ref passed from parent)
309
- React.useEffect(() => {
310
- if (!loading && !error) {
311
- lastGoodStateRef.current = { puckKey, puckProps };
312
- }
313
- }, [loading, error, puckKey, puckProps]);
314
-
315
302
  if (redirecting) {
316
303
  return <LoadingMessage message="Redirecting" data-testid="editor-redirecting" />;
317
304
  }
318
305
 
319
- // Show full loading screen only on first load (no previous state)
320
- if (loading && !lastGoodStateRef.current) {
306
+ if (loading) {
321
307
  return <LoadingMessage message="Loading document" data-testid="editor-loading" />;
322
308
  }
323
309
 
324
- // Show error only if we have no previous state to fall back to
325
- if (error && !lastGoodStateRef.current) {
310
+ // A failed load with a document already on screen keeps that document; only a
311
+ // failure with nothing to fall back on takes over the view.
312
+ if (error && !hasContent) {
326
313
  return (
327
314
  <div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
328
315
  <h3>Error loading document</h3>
@@ -331,57 +318,13 @@ function EditorContent({
331
318
  );
332
319
  }
333
320
 
334
- // Use current state if loaded, otherwise keep showing last good state
335
- const displayState = (!loading && !error)
336
- ? { puckKey, puckProps }
337
- : lastGoodStateRef.current ?? { puckKey, puckProps };
338
-
339
321
  return (
340
322
  <div className="puck-editor-theme" style={{ position: "relative" }}>
341
- {/* Loading overlay - shown during branch switch */}
342
- {loading && lastGoodStateRef.current && (
343
- <div
344
- style={{
345
- position: "fixed",
346
- top: "50%",
347
- left: "50%",
348
- transform: "translate(-50%, -50%)",
349
- zIndex: 9999,
350
- background: "rgba(255, 255, 255, 0.95)",
351
- padding: "1rem 2rem",
352
- borderRadius: "8px",
353
- boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
354
- fontFamily: "system-ui",
355
- fontSize: "14px",
356
- color: "#333",
357
- fontWeight: 500,
358
- display: "flex",
359
- alignItems: "center",
360
- gap: "0.75rem",
361
- }}
362
- >
363
- <div
364
- style={{
365
- width: "16px",
366
- height: "16px",
367
- border: "2px solid #e0e0e0",
368
- borderTopColor: "#2563eb",
369
- borderRadius: "50%",
370
- animation: "spin 0.6s linear infinite",
371
- }}
372
- />
373
- Switching workstream...
374
- <style>{`
375
- @keyframes spin {
376
- to { transform: rotate(360deg); }
377
- }
378
- `}</style>
379
- </div>
380
- )}
323
+ <EditorReloadOverlay reloading={reloading} />
381
324
  <DatasourceRegistryProvider registry={editorCtx?.remoteDatasourceRegistry ?? []}>
382
325
  <DatasourceDataProvider context={remoteDatasourceContext}>
383
326
  {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
384
- <Puck key={`${displayState.puckKey}-${chatbotEnabled ? "ai" : "no-ai"}`} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
327
+ <Puck key={`${puckKey}-${chatbotEnabled ? "ai" : "no-ai"}`} {...puckProps as any} _experimentalFullScreenCanvas={true} />
385
328
  </DatasourceDataProvider>
386
329
  </DatasourceRegistryProvider>
387
330
  </div>
@@ -1,9 +1,13 @@
1
1
  @import "tailwindcss";
2
2
  @plugin "@tailwindcss/typography";
3
- @source "../../../packages/puck-css/src";
3
+ /* Tailwind must scan puck-css's own components for the utility classes they use.
4
+ node_modules-relative so the same path resolves in this monorepo (pnpm symlink)
5
+ and in a scaffolded project; dist rather than src because the published package
6
+ ships only compiled output. */
7
+ @source "../node_modules/@pantheon-systems/puck-css/dist";
4
8
 
5
9
  /* Scoped via :has() rather than a bare `body {...}` selector so this reset
6
- stays out of Puck's canvas-preview iframe (PCC-3499 / PCC-3513).
10
+ stays out of Puck's canvas-preview iframe.
7
11
  Puck's collectStyles() copies every parent <style>/<link> into the canvas
8
12
  iframe verbatim (querySelectorAll('style, link[rel="stylesheet"]'), no
9
13
  exclusion API) — so a bare `body {...}` rule here would also match inside
@@ -1,4 +1,4 @@
1
- # Optional: syncs this site's Puck component registry to the CSS backend
1
+ # Optional: syncs this site's Puck component registry to the backend
2
2
  # headlessly on every push, without anyone needing to open the editor.
3
3
  #
4
4
  # This file is NOT active until you copy it into .github/workflows/ yourself
@@ -12,11 +12,12 @@
12
12
  # 3. Copy this file to .github/workflows/sync-puck-registry.yml.
13
13
  #
14
14
  # Triggers on push to any branch that touches puck.config.tsx or
15
- # components/puck/**. The sync script resolves the CSS branch from the pushed
16
- # git branch's name: the repo's default branch always targets the site's main
17
- # CSS branch (whatever the git branch is called), any other ref matches a CSS
18
- # branch by name. A push on a non-default branch with no matching CSS branch
19
- # is not an error: the script logs a skip and exits 0.
15
+ # components/puck/**. The sync script resolves the target branch from the
16
+ # pushed git branch's name: the repo's default branch always targets the
17
+ # site's main branch (whatever the git branch is called), any other ref
18
+ # matches a branch on the site by name. A push on a non-default branch with
19
+ # no matching branch on the site is not an error: the script logs a skip and
20
+ # exits 0.
20
21
  #
21
22
  # CSS_DEFAULT_BRANCH defaults to "main" if omitted — repos whose default
22
23
  # branch has another name (master, trunk) need the line below (or must set
@@ -33,10 +34,17 @@ on:
33
34
  workflow_dispatch:
34
35
  inputs:
35
36
  branch_id:
36
- description: 'CSS branch ID/name to sync against (blank = match the current git branch, else site main)'
37
+ description: 'Branch ID/name to sync against (blank = match the current git branch, else site main)'
37
38
  required: false
38
39
  default: ''
39
40
 
41
+ # A branch pushed several times in quick succession only needs its newest
42
+ # component set synced. Without this, every push starts its own run and they
43
+ # race each other into the same registry documents.
44
+ concurrency:
45
+ group: sync-puck-registry-${{ github.ref }}
46
+ cancel-in-progress: true
47
+
40
48
  jobs:
41
49
  sync-registry:
42
50
  runs-on: ubuntu-latest
@@ -6,7 +6,31 @@ import react from 'eslint-plugin-react';
6
6
  import reactHooks from 'eslint-plugin-react-hooks';
7
7
  import prettierConfig from 'eslint-config-prettier';
8
8
 
9
+ /**
10
+ * Test-file relaxations.
11
+ *
12
+ * Append this AFTER the base/react/worker layers so it wins. Each rule here is
13
+ * off because the pattern it flags is idiomatic in tests, not debt — measured
14
+ * against the repo on 2026-08-05, where 79% of no-non-null-assertion and 77% of
15
+ * no-empty-function findings were in test files.
16
+ *
17
+ * Rules deliberately NOT relaxed: no-floating-promises and no-misused-promises
18
+ * (an unawaited promise in a test is a silently passing test),
19
+ * no-unnecessary-condition (it found real dead assertions), and no-unused-vars.
20
+ */
21
+
22
+ const TEST_FILES = [
23
+ '**/*.{test,spec}.{js,jsx,mjs,cjs,ts,tsx}',
24
+ '**/tests/**/*.{js,jsx,mjs,cjs,ts,tsx}',
25
+ '**/test/**/*.{js,jsx,mjs,cjs,ts,tsx}',
26
+ '**/__tests__/**/*.{js,jsx,mjs,cjs,ts,tsx}',
27
+ '**/__mocks__/**/*.{js,jsx,mjs,cjs,ts,tsx}',
28
+ '**/test-stubs/**/*.{js,jsx,mjs,cjs,ts,tsx}',
29
+ '**/*.setup.{js,mjs,cjs,ts}',
30
+ ];
31
+
9
32
  export default tseslint.config(
33
+ // @pantheon-systems/eslint-config/base
10
34
  eslint.configs.recommended,
11
35
  ...tseslint.configs.recommended,
12
36
  ...tseslint.configs.strict,
@@ -42,12 +66,14 @@ export default tseslint.config(
42
66
  '@typescript-eslint/consistent-type-imports': 'warn',
43
67
  '@typescript-eslint/explicit-function-return-type': 'off',
44
68
  '@typescript-eslint/explicit-module-boundary-types': 'off',
69
+
45
70
  // Import rules
46
71
  'import/extensions': 'off',
47
72
  'import/prefer-default-export': 'off',
48
73
  'import/no-unresolved': 'off',
49
74
  'import/order': 'warn',
50
75
  'import/no-extraneous-dependencies': 'warn',
76
+
51
77
  // General ESLint rules
52
78
  'no-console': 'off',
53
79
  'no-debugger': 'error',
@@ -75,6 +101,7 @@ export default tseslint.config(
75
101
  ],
76
102
  },
77
103
  ],
104
+
78
105
  // Stricter rules (warn to resolve over time)
79
106
  '@typescript-eslint/consistent-generic-constructors': 'warn',
80
107
  '@typescript-eslint/consistent-indexed-object-style': 'warn',
@@ -146,6 +173,7 @@ export default tseslint.config(
146
173
  '**/.puppeteerrc.cjs',
147
174
  ],
148
175
  },
176
+ // @pantheon-systems/eslint-config/react
149
177
  {
150
178
  files: ['**/*.{ts,tsx,jsx}'],
151
179
  plugins: {
@@ -173,5 +201,40 @@ export default tseslint.config(
173
201
  'react-hooks/exhaustive-deps': 'warn',
174
202
  },
175
203
  },
204
+ // @pantheon-systems/eslint-config/prettier
176
205
  prettierConfig,
206
+ // @pantheon-systems/eslint-config/tests
207
+ {
208
+ files: TEST_FILES,
209
+ rules: {
210
+ // `x!` after a known-good arrange step asserts the fixture, it doesn't hide a bug.
211
+ '@typescript-eslint/no-non-null-assertion': 'off',
212
+ // `() => {}` is the entire point of a stub.
213
+ '@typescript-eslint/no-empty-function': 'off',
214
+ // Passing an unbound method to vi.spyOn / expect is the documented API.
215
+ '@typescript-eslint/unbound-method': 'off',
216
+ // Test helpers are read at the call site; annotating their returns is noise.
217
+ '@typescript-eslint/explicit-function-return-type': 'off',
218
+ // `async` with no await is how you satisfy an interface in a fake.
219
+ '@typescript-eslint/require-await': 'off',
220
+ // Mock payloads are structurally untyped by nature.
221
+ '@typescript-eslint/no-explicit-any': 'off',
222
+ '@typescript-eslint/no-unsafe-assignment': 'off',
223
+ '@typescript-eslint/no-unsafe-member-access': 'off',
224
+ '@typescript-eslint/no-unsafe-call': 'off',
225
+ '@typescript-eslint/no-unsafe-argument': 'off',
226
+ '@typescript-eslint/no-unsafe-return': 'off',
227
+ '@typescript-eslint/no-useless-constructor': 'off',
228
+ // Stand-ins for `cloudflare:*` built-ins have to be classes to stand in
229
+ // for classes, even with nothing in them.
230
+ '@typescript-eslint/no-extraneous-class': 'off',
231
+ // `vi.importActual<typeof import('mod')>('mod')` is vitest's documented
232
+ // shape and cannot be hoisted to a top-level type import.
233
+ '@typescript-eslint/consistent-type-imports': ['warn', { disallowTypeAnnotations: false }],
234
+ // Inline fixtures and assertion chains run long. The formatter will own
235
+ // line length once it lands; until then this is the only rule that would
236
+ // force hand-wrapping code a formatter is about to rewrite.
237
+ 'max-len': 'off',
238
+ },
239
+ },
177
240
  );
@@ -0,0 +1,43 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ .claude/
9
+
10
+ # testing
11
+ /coverage
12
+
13
+ # next.js
14
+ /.next/
15
+ /out/
16
+
17
+ # typescript
18
+ *.tsbuildinfo
19
+ next-env.d.ts
20
+
21
+ .env
22
+ !.env.example
23
+
24
+ # production
25
+ /build
26
+
27
+ # misc
28
+ .DS_Store
29
+ *.pem
30
+
31
+ # debug
32
+ npm-debug.log*
33
+ yarn-debug.log*
34
+ yarn-error.log*
35
+
36
+ # local env files
37
+ .env.local
38
+ .env.development.local
39
+ .env.test.local
40
+ .env.production.local
41
+
42
+ # vercel
43
+ .vercel
@@ -6,7 +6,7 @@ import type { DraftRequestChannel } from "@pantheon-systems/p1-ai-chat";
6
6
  *
7
7
  * The page does not exist yet: the chat proposes the page template it should start from and
8
8
  * creates it once the user agrees, because a document's template can only be set as it is
9
- * created (PCC-3555). Until then the request carries the title and path the dialog collected.
9
+ * created. Until then the request carries the title and path the dialog collected.
10
10
  */
11
11
  export function createGenerateWithAIHandler(
12
12
  draftRequests: DraftRequestChannel,
@@ -37,7 +37,7 @@ const carriesTemplate = (value: unknown): value is string =>
37
37
  typeof value === "string" && value.includes("{{");
38
38
 
39
39
  /**
40
- * Produces the per-page <head> Metadata for a route (PCC-3407). Title,
40
+ * Produces the per-page <head> Metadata for a route. Title,
41
41
  * description and the free-text metadata fields are template-allowed.
42
42
  */
43
43
  export async function resolvePageMetadata({
@@ -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.11.1",
19
- "@pantheon-systems/p1-ai-chat": "^0.5.1",
20
- "@pantheon-systems/p1-media": "^0.4.4",
21
- "@pantheon-systems/p1-next-sdk": "^0.11.1",
18
+ "@pantheon-systems/css-client": "^0.13.0",
19
+ "@pantheon-systems/p1-ai-chat": "^0.6.0",
20
+ "@pantheon-systems/p1-media": "^0.4.5",
21
+ "@pantheon-systems/p1-next-sdk": "^0.13.0",
22
22
  "@pantheon-systems/pds-toolkit-react": "2.0.0-alpha.66",
23
- "@pantheon-systems/puck-css": "^0.11.1",
23
+ "@pantheon-systems/puck-css": "^0.13.0",
24
24
  "@puckeditor/core": "^0.21.1",
25
25
  "@tailwindcss/postcss": "^4.2.2",
26
26
  "@tailwindcss/typography": "^0.5.16",
@@ -56,7 +56,7 @@ describe("validateEnv", () => {
56
56
  });
57
57
 
58
58
  it("defaults defaultBranchName to 'main' when CSS_DEFAULT_BRANCH is not set", () => {
59
- // Safe because the CSS main content branch is always literally named
59
+ // Safe because the site's main content branch is always literally named
60
60
  // "main": a push override of "main" resolves to the same branch either
61
61
  // by name match or by isMain, so the default only adds semantics.
62
62
  const result = validateEnv(baseEnv());
@@ -145,8 +145,8 @@ describe("resolveBranchId", () => {
145
145
 
146
146
  describe("resolveBranchId default-branch semantics", () => {
147
147
  // In CI the override is always the pushed git ref's name, so a repo whose
148
- // default branch is not literally named "main" would never match the CSS
149
- // main branch (whose name is always "main") — the sync silently skips on
148
+ // default branch is not literally named "main" would never match the
149
+ // site's main branch (whose name is always "main") — the sync silently skips on
150
150
  // every default-branch push. When the caller also supplies the repo's
151
151
  // default branch name, an override equal to it must resolve via isMain.
152
152
  const branches = [