@delmaredigital/payload-puck 0.1.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.
Files changed (128) hide show
  1. package/LICENSE +73 -0
  2. package/README.md +1580 -0
  3. package/dist/AccordionClient.d.mts +24 -0
  4. package/dist/AccordionClient.d.ts +24 -0
  5. package/dist/AccordionClient.js +786 -0
  6. package/dist/AccordionClient.js.map +1 -0
  7. package/dist/AccordionClient.mjs +784 -0
  8. package/dist/AccordionClient.mjs.map +1 -0
  9. package/dist/AnimatedWrapper.d.mts +30 -0
  10. package/dist/AnimatedWrapper.d.ts +30 -0
  11. package/dist/AnimatedWrapper.js +379 -0
  12. package/dist/AnimatedWrapper.js.map +1 -0
  13. package/dist/AnimatedWrapper.mjs +377 -0
  14. package/dist/AnimatedWrapper.mjs.map +1 -0
  15. package/dist/admin/client.d.mts +108 -0
  16. package/dist/admin/client.d.ts +108 -0
  17. package/dist/admin/client.js +177 -0
  18. package/dist/admin/client.js.map +1 -0
  19. package/dist/admin/client.mjs +173 -0
  20. package/dist/admin/client.mjs.map +1 -0
  21. package/dist/admin/index.d.mts +157 -0
  22. package/dist/admin/index.d.ts +157 -0
  23. package/dist/admin/index.js +31 -0
  24. package/dist/admin/index.js.map +1 -0
  25. package/dist/admin/index.mjs +29 -0
  26. package/dist/admin/index.mjs.map +1 -0
  27. package/dist/api/index.d.mts +460 -0
  28. package/dist/api/index.d.ts +460 -0
  29. package/dist/api/index.js +588 -0
  30. package/dist/api/index.js.map +1 -0
  31. package/dist/api/index.mjs +578 -0
  32. package/dist/api/index.mjs.map +1 -0
  33. package/dist/components/index.css +339 -0
  34. package/dist/components/index.css.map +1 -0
  35. package/dist/components/index.d.mts +222 -0
  36. package/dist/components/index.d.ts +222 -0
  37. package/dist/components/index.js +9177 -0
  38. package/dist/components/index.js.map +1 -0
  39. package/dist/components/index.mjs +9130 -0
  40. package/dist/components/index.mjs.map +1 -0
  41. package/dist/config/config.editor.css +339 -0
  42. package/dist/config/config.editor.css.map +1 -0
  43. package/dist/config/config.editor.d.mts +153 -0
  44. package/dist/config/config.editor.d.ts +153 -0
  45. package/dist/config/config.editor.js +9400 -0
  46. package/dist/config/config.editor.js.map +1 -0
  47. package/dist/config/config.editor.mjs +9368 -0
  48. package/dist/config/config.editor.mjs.map +1 -0
  49. package/dist/config/index.d.mts +68 -0
  50. package/dist/config/index.d.ts +68 -0
  51. package/dist/config/index.js +2017 -0
  52. package/dist/config/index.js.map +1 -0
  53. package/dist/config/index.mjs +1991 -0
  54. package/dist/config/index.mjs.map +1 -0
  55. package/dist/editor/index.d.mts +784 -0
  56. package/dist/editor/index.d.ts +784 -0
  57. package/dist/editor/index.js +4517 -0
  58. package/dist/editor/index.js.map +1 -0
  59. package/dist/editor/index.mjs +4483 -0
  60. package/dist/editor/index.mjs.map +1 -0
  61. package/dist/fields/index.css +339 -0
  62. package/dist/fields/index.css.map +1 -0
  63. package/dist/fields/index.d.mts +600 -0
  64. package/dist/fields/index.d.ts +600 -0
  65. package/dist/fields/index.js +7739 -0
  66. package/dist/fields/index.js.map +1 -0
  67. package/dist/fields/index.mjs +7590 -0
  68. package/dist/fields/index.mjs.map +1 -0
  69. package/dist/index-CQu6SzDg.d.mts +327 -0
  70. package/dist/index-CoUQnyC3.d.ts +327 -0
  71. package/dist/index.d.mts +6 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.js +569 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/index.mjs +555 -0
  76. package/dist/index.mjs.map +1 -0
  77. package/dist/layouts/index.d.mts +96 -0
  78. package/dist/layouts/index.d.ts +96 -0
  79. package/dist/layouts/index.js +394 -0
  80. package/dist/layouts/index.js.map +1 -0
  81. package/dist/layouts/index.mjs +378 -0
  82. package/dist/layouts/index.mjs.map +1 -0
  83. package/dist/plugin/index.d.mts +289 -0
  84. package/dist/plugin/index.d.ts +289 -0
  85. package/dist/plugin/index.js +569 -0
  86. package/dist/plugin/index.js.map +1 -0
  87. package/dist/plugin/index.mjs +555 -0
  88. package/dist/plugin/index.mjs.map +1 -0
  89. package/dist/render/index.d.mts +109 -0
  90. package/dist/render/index.d.ts +109 -0
  91. package/dist/render/index.js +2146 -0
  92. package/dist/render/index.js.map +1 -0
  93. package/dist/render/index.mjs +2123 -0
  94. package/dist/render/index.mjs.map +1 -0
  95. package/dist/shared-DMAF1AcH.d.mts +545 -0
  96. package/dist/shared-DMAF1AcH.d.ts +545 -0
  97. package/dist/theme/index.d.mts +155 -0
  98. package/dist/theme/index.d.ts +155 -0
  99. package/dist/theme/index.js +201 -0
  100. package/dist/theme/index.js.map +1 -0
  101. package/dist/theme/index.mjs +186 -0
  102. package/dist/theme/index.mjs.map +1 -0
  103. package/dist/types-D7D3rZ1J.d.mts +116 -0
  104. package/dist/types-D7D3rZ1J.d.ts +116 -0
  105. package/dist/types-_6MvjyKv.d.mts +104 -0
  106. package/dist/types-_6MvjyKv.d.ts +104 -0
  107. package/dist/utils/index.d.mts +267 -0
  108. package/dist/utils/index.d.ts +267 -0
  109. package/dist/utils/index.js +426 -0
  110. package/dist/utils/index.js.map +1 -0
  111. package/dist/utils/index.mjs +412 -0
  112. package/dist/utils/index.mjs.map +1 -0
  113. package/dist/utils-DaRs9t0J.d.mts +85 -0
  114. package/dist/utils-gAvt0Vhw.d.ts +85 -0
  115. package/examples/README.md +240 -0
  116. package/examples/api/puck/pages/[id]/route.ts +64 -0
  117. package/examples/api/puck/pages/[id]/versions/route.ts +47 -0
  118. package/examples/api/puck/pages/route.ts +45 -0
  119. package/examples/app/(frontend)/page.tsx +94 -0
  120. package/examples/app/[...slug]/page.tsx +101 -0
  121. package/examples/app/pages/[id]/edit/page.tsx +148 -0
  122. package/examples/components/CustomBanner.tsx +368 -0
  123. package/examples/config/custom-config.ts +223 -0
  124. package/examples/config/payload.config.example.ts +64 -0
  125. package/examples/lib/puck-layouts.ts +258 -0
  126. package/examples/lib/puck-theme.ts +94 -0
  127. package/examples/styles/puck-theme.css +171 -0
  128. package/package.json +157 -0
package/README.md ADDED
@@ -0,0 +1,1580 @@
1
+ # @delmaredigital/payload-puck
2
+
3
+ A PayloadCMS plugin for integrating [Puck](https://puckeditor.com) visual page builder. Build pages visually with drag-and-drop components while leveraging Payload's content management capabilities.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Quick Start](#quick-start)
11
+ - [Styling Setup](#styling-setup)
12
+ - [Setup Checklist](#setup-checklist)
13
+ - [Core Concepts](#core-concepts)
14
+ - [Components](#components)
15
+ - [Configuration](#configuration)
16
+ - [Custom Fields](#custom-fields)
17
+ - [Theming](#theming)
18
+ - [Layouts](#layouts)
19
+ - [API Routes](#api-routes)
20
+ - [Plugin Options](#plugin-options)
21
+ - [Hybrid Integration](#hybrid-integration)
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ### Requirements
28
+
29
+ | Dependency | Version | Purpose |
30
+ |------------|---------|---------|
31
+ | `@measured/puck` | >= 0.20.0 | Visual editor core |
32
+ | `payload` | >= 3.0.0 | CMS backend |
33
+ | `next` | >= 15.4.0 | React framework |
34
+ | `react` | >= 18.0.0 | UI library |
35
+ | `@tailwindcss/typography` | >= 0.5.0 | RichText component styling |
36
+
37
+ > **⚠️ Don't skip the styling setup!** After Quick Start, you must configure Tailwind to scan this package. See [Styling Setup](#styling-setup).
38
+
39
+ ### Install
40
+
41
+ ```bash
42
+ pnpm add @delmaredigital/payload-puck @measured/puck
43
+ ```
44
+
45
+ Or install from GitHub:
46
+
47
+ ```bash
48
+ pnpm add github:delmaredigital/payload-puck#main @measured/puck
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Quick Start
54
+
55
+ > **📋 Important:** After completing these steps, continue to [Styling Setup](#styling-setup) and verify with the [Setup Checklist](#setup-checklist).
56
+
57
+ <details>
58
+ <summary><strong>Option A: Copy Boilerplate (Fastest)</strong></summary>
59
+
60
+ The package includes ready-to-use example files:
61
+
62
+ ```bash
63
+ # Copy API routes
64
+ cp -r node_modules/@delmaredigital/payload-puck/examples/api/puck src/app/api/
65
+
66
+ # Copy editor page
67
+ mkdir -p src/app/\(manage\)/pages/\[id\]/edit
68
+ cp node_modules/@delmaredigital/payload-puck/examples/app/pages/\[id\]/edit/page.tsx src/app/\(manage\)/pages/\[id\]/edit/
69
+
70
+ # Copy frontend routes (homepage + dynamic pages)
71
+ mkdir -p src/app/\(frontend\)
72
+ cp node_modules/@delmaredigital/payload-puck/examples/app/\(frontend\)/page.tsx src/app/\(frontend\)/
73
+ mkdir -p src/app/\(frontend\)/\[...slug\]
74
+ cp node_modules/@delmaredigital/payload-puck/examples/app/\[...slug\]/page.tsx src/app/\(frontend\)/\[...slug\]/
75
+
76
+ # Copy theme config (optional)
77
+ mkdir -p src/lib
78
+ cp node_modules/@delmaredigital/payload-puck/examples/lib/puck-theme.ts src/lib/
79
+ ```
80
+
81
+ See `examples/README.md` for detailed customization instructions.
82
+
83
+ </details>
84
+
85
+ <details>
86
+ <summary><strong>Option B: Manual Setup</strong></summary>
87
+
88
+ #### Step 1: Add the Plugin
89
+
90
+ ```typescript
91
+ // payload.config.ts
92
+ import { buildConfig } from 'payload'
93
+ import { createPuckPlugin } from '@delmaredigital/payload-puck/plugin'
94
+
95
+ export default buildConfig({
96
+ plugins: [
97
+ createPuckPlugin({
98
+ pagesCollection: 'pages', // Collection slug (default: 'pages')
99
+ }),
100
+ ],
101
+ // ...
102
+ })
103
+ ```
104
+
105
+ #### Step 2: Create API Routes
106
+
107
+ ```typescript
108
+ // app/api/puck/pages/route.ts
109
+ import { createPuckApiRoutes } from '@delmaredigital/payload-puck/api'
110
+ import { getPayload } from 'payload'
111
+ import config from '@payload-config'
112
+ import { headers } from 'next/headers'
113
+
114
+ export const { GET, POST } = createPuckApiRoutes({
115
+ collection: 'pages',
116
+ payloadConfig: config,
117
+ auth: {
118
+ authenticate: async (request) => {
119
+ const payload = await getPayload({ config })
120
+ const { user } = await payload.auth({ headers: await headers() })
121
+ if (!user) return { authenticated: false }
122
+ return { authenticated: true, user: { id: user.id } }
123
+ },
124
+ },
125
+ })
126
+ ```
127
+
128
+ ```typescript
129
+ // app/api/puck/pages/[id]/route.ts
130
+ import { createPuckApiRoutesWithId } from '@delmaredigital/payload-puck/api'
131
+ import config from '@payload-config'
132
+
133
+ export const { GET, PATCH, DELETE } = createPuckApiRoutesWithId({
134
+ collection: 'pages',
135
+ payloadConfig: config,
136
+ auth: {
137
+ authenticate: async (request) => {
138
+ // Same auth logic as above
139
+ },
140
+ },
141
+ })
142
+ ```
143
+
144
+ #### Step 3: Create the Editor Page
145
+
146
+ ```typescript
147
+ // app/pages/[id]/edit/page.tsx
148
+ 'use client'
149
+
150
+ import { PuckEditorView } from '@delmaredigital/payload-puck/editor'
151
+ import { editorConfig } from '@delmaredigital/payload-puck/config/editor'
152
+
153
+ export default function EditorPage() {
154
+ return (
155
+ <PuckEditorView
156
+ config={editorConfig}
157
+ collectionSlug="pages"
158
+ apiBasePath="/api/puck"
159
+ backUrl="/admin/collections/pages"
160
+ previewUrl={(slug) => `/${slug}`}
161
+ />
162
+ )
163
+ }
164
+ ```
165
+
166
+ The editor includes:
167
+ - **Save Draft** - Saves without publishing
168
+ - **Publish** - Publishes the page
169
+ - **Draft/Published badge** - Shows current document status
170
+ - **Unsaved changes warning** - Prevents accidental navigation
171
+ - **Heading Analyzer** - WCAG-compliant heading outline visualization (enabled by default)
172
+
173
+ **Heading Analyzer Plugin**
174
+
175
+ The heading analyzer plugin is included by default and displays a heading outline in the editor sidebar. It helps identify accessibility issues like skipped heading levels (e.g., jumping from H1 to H3).
176
+
177
+ To add additional plugins or disable the default:
178
+
179
+ ```typescript
180
+ // Add additional plugins (headingAnalyzer is still included)
181
+ <PuckEditorView
182
+ plugins={[myCustomPlugin]}
183
+ // ...
184
+ />
185
+
186
+ // Disable all default plugins
187
+ <PuckEditorView
188
+ plugins={false}
189
+ // ...
190
+ />
191
+ ```
192
+
193
+ #### Step 4: Create a Frontend Route
194
+
195
+ ```typescript
196
+ // app/(frontend)/[...slug]/page.tsx
197
+ import { getPayload } from 'payload'
198
+ import config from '@payload-config'
199
+ import { PageRenderer } from '@delmaredigital/payload-puck/render'
200
+ import { baseConfig } from '@delmaredigital/payload-puck/config'
201
+ import { notFound } from 'next/navigation'
202
+
203
+ export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
204
+ const { slug } = await params
205
+ const payload = await getPayload({ config })
206
+
207
+ const { docs } = await payload.find({
208
+ collection: 'pages',
209
+ where: { slug: { equals: slug.join('/') } },
210
+ limit: 1,
211
+ })
212
+
213
+ const page = docs[0]
214
+ if (!page) notFound()
215
+
216
+ return <PageRenderer config={baseConfig} data={page.puckData} />
217
+ }
218
+ ```
219
+
220
+ #### Step 5: Enable Version History (Optional)
221
+
222
+ ```typescript
223
+ // app/api/puck/pages/[id]/versions/route.ts
224
+ import { createPuckApiRoutesVersions } from '@delmaredigital/payload-puck/api'
225
+ import config from '@payload-config'
226
+
227
+ export const { GET, POST } = createPuckApiRoutesVersions({
228
+ collection: 'pages',
229
+ payloadConfig: config,
230
+ auth: {
231
+ authenticate: async (request) => {
232
+ // Same auth logic as your main routes
233
+ },
234
+ },
235
+ })
236
+ ```
237
+
238
+ The History button automatically appears when this route exists.
239
+
240
+ </details>
241
+
242
+ ---
243
+
244
+ ## Styling Setup
245
+
246
+ > **⚠️ This section is critical.** Without proper styling setup, components will render with broken or missing styles.
247
+
248
+ <details>
249
+ <summary><strong>⚠️ Tailwind Typography</strong> — Required for RichText component</summary>
250
+
251
+ The RichText component uses the `prose` class from `@tailwindcss/typography`. **Without this, rich text content will be unstyled** — no proper heading sizes, list styles, blockquote formatting, etc.
252
+
253
+ ```bash
254
+ pnpm add @tailwindcss/typography
255
+ ```
256
+
257
+ **Tailwind v4** — Add the `@plugin` directive to your main CSS file:
258
+
259
+ ```css
260
+ @import "tailwindcss";
261
+ @plugin "@tailwindcss/typography";
262
+ ```
263
+
264
+ **Tailwind v3** — Add to your Tailwind config:
265
+
266
+ ```javascript
267
+ // tailwind.config.js
268
+ module.exports = {
269
+ plugins: [
270
+ require('@tailwindcss/typography'),
271
+ ],
272
+ }
273
+ ```
274
+
275
+ </details>
276
+
277
+ <details>
278
+ <summary><strong>⚠️ Tailwind Source Scanning</strong> — Required for component styles</summary>
279
+
280
+ **Without this, Tailwind won't include the plugin's CSS classes in your build.** Components will have missing styles.
281
+
282
+ The `@source` directive (v4) or `content` path (v3) tells Tailwind to scan the package for class names.
283
+
284
+ **Tailwind v4**
285
+
286
+ The path is relative to your CSS file's location and must resolve to your project's `node_modules`:
287
+
288
+ | CSS file location | `@source` path |
289
+ |-------------------|----------------|
290
+ | `globals.css` (root) | `./node_modules/@delmaredigital/payload-puck` |
291
+ | `src/globals.css` | `../node_modules/@delmaredigital/payload-puck` |
292
+ | `src/styles/tailwind.css` | `../../node_modules/@delmaredigital/payload-puck` |
293
+
294
+ ```css
295
+ @import "tailwindcss";
296
+ @plugin "@tailwindcss/typography";
297
+
298
+ /* Adjust path based on your CSS file location */
299
+ @source "../node_modules/@delmaredigital/payload-puck";
300
+
301
+ @theme inline {
302
+ --color-background: var(--background);
303
+ --color-foreground: var(--foreground);
304
+ --color-primary: var(--primary);
305
+ --color-primary-foreground: var(--primary-foreground);
306
+ --color-secondary: var(--secondary);
307
+ --color-secondary-foreground: var(--secondary-foreground);
308
+ --color-muted: var(--muted);
309
+ --color-muted-foreground: var(--muted-foreground);
310
+ --color-accent: var(--accent);
311
+ --color-accent-foreground: var(--accent-foreground);
312
+ --color-destructive: var(--destructive);
313
+ --color-destructive-foreground: var(--destructive-foreground);
314
+ --color-border: var(--border);
315
+ --color-input: var(--input);
316
+ --color-ring: var(--ring);
317
+ --color-popover: var(--popover);
318
+ --color-popover-foreground: var(--popover-foreground);
319
+ --color-card: var(--card);
320
+ --color-card-foreground: var(--card-foreground);
321
+ --radius-sm: calc(var(--radius) - 4px);
322
+ --radius-md: calc(var(--radius) - 2px);
323
+ --radius-lg: var(--radius);
324
+ --radius-xl: calc(var(--radius) + 4px);
325
+ }
326
+ ```
327
+
328
+ **Tailwind v3**
329
+
330
+ ```javascript
331
+ // tailwind.config.js
332
+ module.exports = {
333
+ content: [
334
+ './src/**/*.{js,ts,jsx,tsx}',
335
+ './node_modules/@delmaredigital/payload-puck/**/*.{js,mjs,jsx,tsx}',
336
+ ],
337
+ }
338
+ ```
339
+
340
+ </details>
341
+
342
+ <details>
343
+ <summary><strong>⚠️ Theme CSS Variables</strong> — Required if not using shadcn/ui</summary>
344
+
345
+ The field components use [shadcn/ui](https://ui.shadcn.com)-style CSS variables.
346
+
347
+ **If you use shadcn/ui:** No action needed — components inherit your existing theme.
348
+
349
+ **If you don't use shadcn/ui:** Copy the theme boilerplate:
350
+
351
+ ```bash
352
+ cp node_modules/@delmaredigital/payload-puck/examples/styles/puck-theme.css src/styles/
353
+ ```
354
+
355
+ Then import it:
356
+
357
+ ```css
358
+ @import './puck-theme.css';
359
+ ```
360
+
361
+ <details>
362
+ <summary>CSS Variable Reference</summary>
363
+
364
+ | Variable | Purpose |
365
+ |----------|---------|
366
+ | `--background` | Page background color |
367
+ | `--foreground` | Default text color |
368
+ | `--primary` | Primary buttons, active states |
369
+ | `--primary-foreground` | Text on primary backgrounds |
370
+ | `--secondary` | Secondary buttons |
371
+ | `--secondary-foreground` | Text on secondary backgrounds |
372
+ | `--muted` | Subtle backgrounds, disabled states |
373
+ | `--muted-foreground` | Muted text |
374
+ | `--accent` | Hover states |
375
+ | `--accent-foreground` | Text on accent backgrounds |
376
+ | `--destructive` | Error messages, delete buttons |
377
+ | `--destructive-foreground` | Text on destructive backgrounds |
378
+ | `--border` | General borders |
379
+ | `--input` | Form input borders |
380
+ | `--ring` | Focus rings |
381
+ | `--popover` | Dropdown/modal backgrounds |
382
+ | `--popover-foreground` | Text in popovers |
383
+ | `--card` | Card backgrounds |
384
+ | `--card-foreground` | Text in cards |
385
+ | `--radius` | Base border radius |
386
+
387
+ </details>
388
+
389
+ </details>
390
+
391
+ ---
392
+
393
+ ## Setup Checklist
394
+
395
+ Use this checklist to verify your setup is complete.
396
+
397
+ ### ✅ Core Setup
398
+
399
+ - [ ] Install packages: `@delmaredigital/payload-puck` and `@measured/puck`
400
+ - [ ] Add `createPuckPlugin()` to your Payload config
401
+ - [ ] Create API routes (`/api/puck/pages` and `/api/puck/pages/[id]`)
402
+ - [ ] Create the editor page component with `PuckEditorView`
403
+ - [ ] Create a frontend route with `PageRenderer`
404
+
405
+ ### ⚠️ Styling Setup
406
+
407
+ - [ ] Install and configure `@tailwindcss/typography`
408
+ - [ ] Add Tailwind `@source` directive (v4) or `content` path (v3)
409
+ - [ ] Set up theme CSS variables (if not using shadcn/ui)
410
+
411
+ ### ⚙️ Collection Config
412
+
413
+ - [ ] Enable `versions: { drafts: true }` on your pages collection
414
+
415
+ ### 📦 Optional
416
+
417
+ - [ ] Version history API route (`/api/puck/pages/[id]/versions`)
418
+ - [ ] Custom theme configuration via `ThemeProvider`
419
+ - [ ] Custom layouts with headers/footers
420
+ - [ ] Custom components
421
+
422
+ ---
423
+
424
+ <details>
425
+ <summary><strong>Core Concepts</strong> — Server vs Client configs, Draft system</summary>
426
+
427
+ ### Server vs Client Configuration
428
+
429
+ The plugin provides two configurations to handle React Server Components:
430
+
431
+ | Config | Import | Use Case |
432
+ |--------|--------|----------|
433
+ | `baseConfig` | `@delmaredigital/payload-puck/config` | Server-safe rendering with `PageRenderer` |
434
+ | `editorConfig` | `@delmaredigital/payload-puck/config/editor` | Client-side editing with full TipTap support |
435
+
436
+ ```typescript
437
+ // Server component - use baseConfig
438
+ import { baseConfig } from '@delmaredigital/payload-puck/config'
439
+ <PageRenderer config={baseConfig} data={page.puckData} />
440
+
441
+ // Client component - use editorConfig
442
+ import { editorConfig } from '@delmaredigital/payload-puck/config/editor'
443
+ <PuckEditor config={editorConfig} ... />
444
+ ```
445
+
446
+ ### Draft System
447
+
448
+ The editor uses Payload's native draft system.
449
+
450
+ > **⚠️ Required:** Without `drafts: true`, the Save Draft and Publish buttons won't work correctly.
451
+
452
+ ```typescript
453
+ {
454
+ slug: 'pages',
455
+ versions: {
456
+ drafts: true,
457
+ },
458
+ }
459
+ ```
460
+
461
+ </details>
462
+
463
+ ---
464
+
465
+ <details>
466
+ <summary><strong>Components</strong> — Layout, Typography, Media, Interactive</summary>
467
+
468
+ ### Layout
469
+
470
+ | Component | Description | Responsive Controls |
471
+ |-----------|-------------|---------------------|
472
+ | **Container** | Content wrapper with max-width and background options | Dimensions, Padding, Margin, Visibility |
473
+ | **Flex** | Flexible box layout with direction and alignment | Dimensions, Padding, Margin, Visibility |
474
+ | **Grid** | CSS Grid layout with responsive columns | Dimensions, Padding, Margin, Visibility |
475
+ | **Section** | Full-width section with slot for nested content | Dimensions, Padding, Margin, Visibility |
476
+ | **Spacer** | Vertical/horizontal spacing element | Visibility |
477
+ | **Template** | Reusable component arrangements - save and load templates | Dimensions, Padding, Margin, Visibility |
478
+
479
+ ### Typography
480
+
481
+ | Component | Description | Responsive Controls |
482
+ |-----------|-------------|---------------------|
483
+ | **Heading** | H1-H6 headings with size and alignment options | — |
484
+ | **Text** | Paragraph text with styling options | — |
485
+ | **RichText** | TipTap-powered WYSIWYG editor | — |
486
+
487
+ > **⚠️ RichText requires `@tailwindcss/typography`** — see [Styling Setup](#styling-setup). Without it, content renders without proper formatting.
488
+
489
+ ### Media
490
+
491
+ | Component | Description | Responsive Controls |
492
+ |-----------|-------------|---------------------|
493
+ | **Image** | Responsive image with alt text and sizing | Visibility |
494
+
495
+ ### Interactive
496
+
497
+ | Component | Description | Responsive Controls |
498
+ |-----------|-------------|---------------------|
499
+ | **Button** | Styled button/link with variants | — |
500
+ | **Card** | Content card with optional image header | — |
501
+ | **Divider** | Horizontal rule with style options | — |
502
+ | **Accordion** | Expandable content sections | — |
503
+
504
+ ### Responsive Controls
505
+
506
+ Components with responsive controls allow you to customize their behavior at different breakpoints (mobile, tablet, desktop). Available controls:
507
+
508
+ - **Dimensions** - Width, max-width, height constraints per breakpoint
509
+ - **Padding** - Inner spacing per breakpoint
510
+ - **Margin** - Outer spacing per breakpoint
511
+ - **Visibility** - Show/hide components at specific breakpoints
512
+
513
+ Breakpoints follow Tailwind CSS conventions:
514
+ | Breakpoint | Min Width | Description |
515
+ |------------|-----------|-------------|
516
+ | base | 0px | Mobile (default) |
517
+ | sm | 640px | Small tablets |
518
+ | md | 768px | Tablets |
519
+ | lg | 1024px | Laptops |
520
+ | xl | 1280px | Desktops |
521
+
522
+ ### Template Component
523
+
524
+ The Template component allows saving groups of components as reusable templates. The plugin automatically creates a `puck-templates` collection in Payload.
525
+
526
+ **Saving a template:**
527
+ 1. Add a Template component to your page
528
+ 2. Add child components inside the Template slot
529
+ 3. Click "Save as Template" and give it a name/category
530
+
531
+ **Loading a template:**
532
+ 1. Add a Template component to your page
533
+ 2. Select a saved template from the dropdown
534
+ 3. The template's components are loaded into the slot
535
+
536
+ </details>
537
+
538
+ ---
539
+
540
+ <details>
541
+ <summary><strong>Configuration</strong> — Merging configs, Custom components</summary>
542
+
543
+ ### Merging Custom Components
544
+
545
+ ```typescript
546
+ import { mergeConfigs } from '@delmaredigital/payload-puck/config'
547
+ import { baseConfig } from '@delmaredigital/payload-puck/config'
548
+ import { MyCustomComponent } from './components/MyCustomComponent'
549
+
550
+ const customConfig = mergeConfigs({
551
+ base: baseConfig,
552
+ components: {
553
+ MyCustomComponent,
554
+ },
555
+ categories: {
556
+ custom: { title: 'Custom', components: ['MyCustomComponent'] },
557
+ },
558
+ exclude: ['Spacer'], // Optionally remove components
559
+ })
560
+ ```
561
+
562
+ ### Creating Custom Components
563
+
564
+ Components need two variants to work across server rendering and the editor:
565
+
566
+ | File | Purpose | Used By |
567
+ |------|---------|---------|
568
+ | `MyComponent.server.tsx` | Server-safe render (no hooks/interactivity) | `baseConfig` → `PageRenderer` |
569
+ | `MyComponent.tsx` or `.editor.tsx` | Full interactivity + field definitions | `editorConfig` → `PuckEditor` |
570
+
571
+ **Server variant** (`MyComponent.server.tsx`):
572
+ - No `'use client'` directive
573
+ - No React hooks (`useState`, `useEffect`, etc.)
574
+ - No event handlers that require client JS
575
+ - **If component has slots**: Must include `fields: { content: { type: 'slot' } }` (Puck needs this to transform slot data into a renderable component)
576
+ - Other fields can be omitted (not used in rendering)
577
+
578
+ **Editor variant** (`MyComponent.tsx`):
579
+ - Can use `'use client'` if needed
580
+ - Full interactivity with hooks
581
+ - Includes all `fields` for the Puck sidebar
582
+
583
+ <details>
584
+ <summary><strong>Example: Interactive Component</strong></summary>
585
+
586
+ ```tsx
587
+ // components/Tabs.server.tsx - Server-safe version
588
+ import type { ComponentConfig } from '@measured/puck'
589
+
590
+ export interface TabsProps {
591
+ items: { title: string; content: string }[]
592
+ defaultTab: number
593
+ }
594
+
595
+ export const TabsConfig: ComponentConfig<TabsProps> = {
596
+ label: 'Tabs',
597
+ defaultProps: {
598
+ items: [{ title: 'Tab 1', content: 'Content 1' }],
599
+ defaultTab: 0,
600
+ },
601
+ // No fields - server version only renders
602
+ render: ({ items, defaultTab }) => (
603
+ <div>
604
+ {/* Render only the default tab statically */}
605
+ <div className="flex border-b">
606
+ {items.map((item, i) => (
607
+ <div key={i} className={i === defaultTab ? 'border-b-2 border-primary' : ''}>
608
+ {item.title}
609
+ </div>
610
+ ))}
611
+ </div>
612
+ <div>{items[defaultTab]?.content}</div>
613
+ </div>
614
+ ),
615
+ }
616
+ ```
617
+
618
+ ```tsx
619
+ // components/Tabs.tsx - Editor version with interactivity
620
+ 'use client'
621
+
622
+ import type { ComponentConfig } from '@measured/puck'
623
+ import { useState } from 'react'
624
+
625
+ export interface TabsProps {
626
+ items: { title: string; content: string }[]
627
+ defaultTab: number
628
+ }
629
+
630
+ export const TabsConfig: ComponentConfig<TabsProps> = {
631
+ label: 'Tabs',
632
+ fields: {
633
+ items: {
634
+ type: 'array',
635
+ label: 'Tabs',
636
+ arrayFields: {
637
+ title: { type: 'text', label: 'Title' },
638
+ content: { type: 'textarea', label: 'Content' },
639
+ },
640
+ },
641
+ defaultTab: { type: 'number', label: 'Default Tab' },
642
+ },
643
+ defaultProps: {
644
+ items: [{ title: 'Tab 1', content: 'Content 1' }],
645
+ defaultTab: 0,
646
+ },
647
+ render: ({ items, defaultTab }) => {
648
+ const [activeTab, setActiveTab] = useState(defaultTab)
649
+
650
+ return (
651
+ <div>
652
+ <div className="flex border-b">
653
+ {items.map((item, i) => (
654
+ <button
655
+ key={i}
656
+ onClick={() => setActiveTab(i)}
657
+ className={i === activeTab ? 'border-b-2 border-primary' : ''}
658
+ >
659
+ {item.title}
660
+ </button>
661
+ ))}
662
+ </div>
663
+ <div>{items[activeTab]?.content}</div>
664
+ </div>
665
+ )
666
+ },
667
+ }
668
+ ```
669
+
670
+ Then register both:
671
+ ```tsx
672
+ // In your custom baseConfig
673
+ import { TabsConfig } from './components/Tabs.server'
674
+
675
+ // In your custom editorConfig
676
+ import { TabsConfig } from './components/Tabs'
677
+ ```
678
+ </details>
679
+
680
+ <details>
681
+ <summary><strong>Example: Component with Slot (nested components)</strong></summary>
682
+
683
+ ```tsx
684
+ // components/Card.server.tsx - Server-safe version WITH slot field
685
+ import type { ComponentConfig } from '@measured/puck'
686
+
687
+ export interface CardProps {
688
+ content: unknown // Slot for nested components
689
+ title: string
690
+ }
691
+
692
+ export const CardConfig: ComponentConfig<CardProps> = {
693
+ label: 'Card',
694
+ // CRITICAL: Slot field MUST be defined for Puck to transform data into component
695
+ fields: {
696
+ content: { type: 'slot' },
697
+ },
698
+ defaultProps: {
699
+ content: [],
700
+ title: 'Card Title',
701
+ },
702
+ render: ({ content: Content, title }) => (
703
+ <div className="border rounded-lg p-4">
704
+ <h3 className="font-bold mb-2">{title}</h3>
705
+ <Content /> {/* Renders nested components */}
706
+ </div>
707
+ ),
708
+ }
709
+ ```
710
+
711
+ ```tsx
712
+ // components/Card.tsx - Editor version with all fields
713
+ import type { ComponentConfig } from '@measured/puck'
714
+
715
+ export interface CardProps {
716
+ content: unknown
717
+ title: string
718
+ }
719
+
720
+ export const CardConfig: ComponentConfig<CardProps> = {
721
+ label: 'Card',
722
+ fields: {
723
+ title: { type: 'text', label: 'Title' },
724
+ content: { type: 'slot' },
725
+ },
726
+ defaultProps: {
727
+ content: [],
728
+ title: 'Card Title',
729
+ },
730
+ render: ({ content: Content, title }) => (
731
+ <div className="border rounded-lg p-4">
732
+ <h3 className="font-bold mb-2">{title}</h3>
733
+ <Content />
734
+ </div>
735
+ ),
736
+ }
737
+ ```
738
+
739
+ **Why slots need the field definition:** Puck stores slot content as an array of component data. The `fields: { content: { type: 'slot' } }` tells Puck to transform this array into a renderable `<Content />` component before passing it to `render()`. Without this, you'll get "Element type is invalid: got array" errors.
740
+ </details>
741
+
742
+ </details>
743
+
744
+ ---
745
+
746
+ <details>
747
+ <summary><strong>Custom Fields</strong> — 19 field types with usage examples</summary>
748
+
749
+ All fields are imported from `@delmaredigital/payload-puck/fields`.
750
+
751
+ ### Field Reference
752
+
753
+ | Field | Description |
754
+ |-------|-------------|
755
+ | **MediaField** | Payload media library integration with upload/browse |
756
+ | **TiptapField** | Rich text editor with formatting toolbar |
757
+ | **ColorPickerField** | Color picker with presets and transparency |
758
+ | **BackgroundField** | Backgrounds with solid colors, gradients, and images |
759
+ | **PaddingField** | Visual padding editor with per-side controls |
760
+ | **MarginField** | Visual margin editor with per-side controls |
761
+ | **BorderField** | Border editor with width, style, color, radius |
762
+ | **DimensionsField** | Width/height with min/max constraints |
763
+ | **SizeField** | Preset sizes (sm, md, lg) or custom values |
764
+ | **AlignmentField** | Text alignment (left, center, right, justify) |
765
+ | **JustifyContentField** | Flexbox justify-content options |
766
+ | **AlignItemsField** | Flexbox align-items options |
767
+ | **VerticalAlignmentField** | Vertical alignment (top, center, bottom) |
768
+ | **LockedTextField** | Protected text field with edit confirmation |
769
+ | **LockedRadioField** | Protected radio field with edit confirmation |
770
+ | **ResponsiveField** | Per-breakpoint values for responsive design |
771
+ | **ResponsiveVisibilityField** | Show/hide toggle per breakpoint (Divi/Elementor-style) |
772
+ | **AnimationField** | Entrance animations with easing/duration |
773
+ | **TransformField** | CSS transforms (rotate, scale, translate) |
774
+ | **GradientEditor** | Visual gradient builder |
775
+
776
+ ### Usage Examples
777
+
778
+ <details>
779
+ <summary><strong>MediaField</strong></summary>
780
+
781
+ Integrates with Payload's media collection:
782
+
783
+ ```typescript
784
+ import { createMediaField } from '@delmaredigital/payload-puck/fields'
785
+
786
+ const config = {
787
+ components: {
788
+ Hero: {
789
+ fields: {
790
+ backgroundImage: createMediaField({
791
+ label: 'Background Image',
792
+ collection: 'media', // Default: 'media'
793
+ }),
794
+ },
795
+ },
796
+ },
797
+ }
798
+ ```
799
+ </details>
800
+
801
+ <details>
802
+ <summary><strong>TiptapField</strong></summary>
803
+
804
+ Rich text editor with formatting toolbar:
805
+
806
+ ```typescript
807
+ import { createTiptapField } from '@delmaredigital/payload-puck/fields'
808
+
809
+ const config = {
810
+ components: {
811
+ TextBlock: {
812
+ fields: {
813
+ content: createTiptapField({ label: 'Content' }),
814
+ },
815
+ },
816
+ },
817
+ }
818
+ ```
819
+ </details>
820
+
821
+ <details>
822
+ <summary><strong>ColorPickerField</strong></summary>
823
+
824
+ Color picker with presets and alpha channel:
825
+
826
+ ```typescript
827
+ import { createColorPickerField } from '@delmaredigital/payload-puck/fields'
828
+
829
+ const config = {
830
+ components: {
831
+ Section: {
832
+ fields: {
833
+ backgroundColor: createColorPickerField({
834
+ label: 'Background Color',
835
+ }),
836
+ },
837
+ },
838
+ },
839
+ }
840
+ ```
841
+ </details>
842
+
843
+ <details>
844
+ <summary><strong>BackgroundField</strong></summary>
845
+
846
+ Full background editor with solid colors, gradients, and images:
847
+
848
+ ```typescript
849
+ import { createBackgroundField, backgroundValueToCSS } from '@delmaredigital/payload-puck/fields'
850
+
851
+ const config = {
852
+ components: {
853
+ Section: {
854
+ fields: {
855
+ background: createBackgroundField({ label: 'Background' }),
856
+ },
857
+ render: ({ background }) => (
858
+ <section style={{ background: backgroundValueToCSS(background) }}>
859
+ {/* content */}
860
+ </section>
861
+ ),
862
+ },
863
+ },
864
+ }
865
+ ```
866
+ </details>
867
+
868
+ <details>
869
+ <summary><strong>DimensionsField</strong></summary>
870
+
871
+ Width/height with min/max constraints and alignment:
872
+
873
+ ```typescript
874
+ import { createDimensionsField, dimensionsValueToCSS } from '@delmaredigital/payload-puck/fields'
875
+
876
+ const config = {
877
+ components: {
878
+ Container: {
879
+ fields: {
880
+ dimensions: createDimensionsField({ label: 'Size' }),
881
+ },
882
+ render: ({ dimensions, children }) => (
883
+ <div style={dimensionsValueToCSS(dimensions)}>
884
+ {children}
885
+ </div>
886
+ ),
887
+ },
888
+ },
889
+ }
890
+ ```
891
+ </details>
892
+
893
+ <details>
894
+ <summary><strong>PaddingField & MarginField</strong></summary>
895
+
896
+ Visual spacing editors:
897
+
898
+ ```typescript
899
+ import { createPaddingField, createMarginField } from '@delmaredigital/payload-puck/fields'
900
+
901
+ const config = {
902
+ components: {
903
+ Box: {
904
+ fields: {
905
+ padding: createPaddingField({ label: 'Padding' }),
906
+ margin: createMarginField({ label: 'Margin' }),
907
+ },
908
+ },
909
+ },
910
+ }
911
+ ```
912
+ </details>
913
+
914
+ <details>
915
+ <summary><strong>LockedTextField & LockedRadioField</strong></summary>
916
+
917
+ Protected fields that require confirmation before editing:
918
+
919
+ ```typescript
920
+ import {
921
+ createLockedTextField,
922
+ createLockedRadioField,
923
+ lockedSlugField, // Pre-built slug field
924
+ lockedHomepageField, // Pre-built homepage toggle
925
+ } from '@delmaredigital/payload-puck/fields'
926
+
927
+ // Use pre-built fields
928
+ const config = {
929
+ root: {
930
+ fields: {
931
+ slug: lockedSlugField,
932
+ isHomepage: lockedHomepageField,
933
+ },
934
+ },
935
+ }
936
+
937
+ // Or create custom locked fields
938
+ const customLockedField = createLockedTextField({
939
+ label: 'API Key',
940
+ placeholder: 'Enter API key',
941
+ warningMessage: 'Changing this will break integrations.',
942
+ })
943
+ ```
944
+ </details>
945
+
946
+ <details>
947
+ <summary><strong>AnimationField</strong></summary>
948
+
949
+ Entrance animations with customizable timing:
950
+
951
+ ```typescript
952
+ import { createAnimationField, getEntranceAnimationClasses } from '@delmaredigital/payload-puck/fields'
953
+
954
+ const config = {
955
+ components: {
956
+ AnimatedSection: {
957
+ fields: {
958
+ animation: createAnimationField({ label: 'Entrance Animation' }),
959
+ },
960
+ render: ({ animation, children }) => (
961
+ <div className={getEntranceAnimationClasses(animation)}>
962
+ {children}
963
+ </div>
964
+ ),
965
+ },
966
+ },
967
+ }
968
+ ```
969
+ </details>
970
+
971
+ <details>
972
+ <summary><strong>ResponsiveField</strong></summary>
973
+
974
+ Values that change per breakpoint:
975
+
976
+ ```typescript
977
+ import { createResponsiveField, BREAKPOINTS } from '@delmaredigital/payload-puck/fields'
978
+
979
+ const config = {
980
+ components: {
981
+ Grid: {
982
+ fields: {
983
+ columns: createResponsiveField({
984
+ label: 'Columns',
985
+ field: { type: 'number' },
986
+ defaultValue: { sm: 1, md: 2, lg: 3 },
987
+ }),
988
+ },
989
+ },
990
+ },
991
+ }
992
+ ```
993
+ </details>
994
+
995
+ <details>
996
+ <summary><strong>ResponsiveVisibilityField</strong></summary>
997
+
998
+ Show/hide components at different breakpoints (like Divi/Elementor):
999
+
1000
+ ```typescript
1001
+ import { createResponsiveVisibilityField, visibilityValueToCSS } from '@delmaredigital/payload-puck/fields'
1002
+
1003
+ const config = {
1004
+ components: {
1005
+ MobileOnlyBanner: {
1006
+ fields: {
1007
+ visibility: createResponsiveVisibilityField({ label: 'Visibility' }),
1008
+ },
1009
+ render: ({ visibility, children }) => {
1010
+ const visibilityCSS = visibilityValueToCSS(visibility, 'mobile-banner')
1011
+ return (
1012
+ <>
1013
+ {visibilityCSS && <style>{visibilityCSS}</style>}
1014
+ <div className="mobile-banner">{children}</div>
1015
+ </>
1016
+ )
1017
+ },
1018
+ },
1019
+ },
1020
+ }
1021
+ ```
1022
+
1023
+ The field displays device icons with visibility toggles. Green = visible, Red = hidden at that breakpoint.
1024
+ </details>
1025
+
1026
+ ### CSS Helper Functions
1027
+
1028
+ Convert field values to CSS:
1029
+
1030
+ ```typescript
1031
+ import {
1032
+ backgroundValueToCSS,
1033
+ dimensionsValueToCSS,
1034
+ animationValueToCSS,
1035
+ transformValueToCSS,
1036
+ gradientValueToCSS,
1037
+ sizeValueToCSS,
1038
+ // Responsive helpers
1039
+ responsiveValueToCSS,
1040
+ visibilityValueToCSS,
1041
+ } from '@delmaredigital/payload-puck/fields'
1042
+
1043
+ const styles = {
1044
+ background: backgroundValueToCSS(background),
1045
+ ...dimensionsValueToCSS(dimensions),
1046
+ animation: animationValueToCSS(animation),
1047
+ transform: transformValueToCSS(transform),
1048
+ }
1049
+
1050
+ // Responsive values generate CSS media queries
1051
+ const uniqueId = 'my-component-123'
1052
+ const { baseStyles, mediaQueryCSS } = responsiveValueToCSS(
1053
+ dimensions, // ResponsiveValue<T> or T
1054
+ dimensionsValueToCSS, // Converter function
1055
+ uniqueId // CSS class selector
1056
+ )
1057
+
1058
+ // Visibility generates show/hide media queries
1059
+ const visibilityCSS = visibilityValueToCSS(visibility, uniqueId)
1060
+
1061
+ // Render with media queries
1062
+ return (
1063
+ <>
1064
+ {mediaQueryCSS && <style>{mediaQueryCSS}</style>}
1065
+ {visibilityCSS && <style>{visibilityCSS}</style>}
1066
+ <div className={uniqueId} style={baseStyles}>{children}</div>
1067
+ </>
1068
+ )
1069
+ ```
1070
+
1071
+ </details>
1072
+
1073
+ ---
1074
+
1075
+ <details>
1076
+ <summary><strong>Theming</strong> — Button variants, color presets, focus rings</summary>
1077
+
1078
+ Customize button styles, color presets, and focus rings to match your design system.
1079
+
1080
+ ### Basic Usage
1081
+
1082
+ Wrap your `PageRenderer` with `ThemeProvider` to apply custom theming:
1083
+
1084
+ ```typescript
1085
+ import { PageRenderer } from '@delmaredigital/payload-puck/render'
1086
+ import { ThemeProvider } from '@delmaredigital/payload-puck/theme'
1087
+
1088
+ <ThemeProvider theme={{
1089
+ buttonVariants: {
1090
+ default: { classes: 'bg-primary text-primary-foreground hover:bg-primary/90' },
1091
+ secondary: { classes: 'bg-secondary text-secondary-foreground hover:bg-secondary/90' },
1092
+ },
1093
+ focusRingColor: 'focus:ring-primary',
1094
+ }}>
1095
+ <PageRenderer config={baseConfig} data={page.puckData} />
1096
+ </ThemeProvider>
1097
+ ```
1098
+
1099
+ ### Using the Example Theme
1100
+
1101
+ ```typescript
1102
+ import { ThemeProvider, exampleTheme } from '@delmaredigital/payload-puck/theme'
1103
+
1104
+ <ThemeProvider theme={exampleTheme}>
1105
+ <PageRenderer data={page.puckData} />
1106
+ </ThemeProvider>
1107
+
1108
+ // Or customize specific values
1109
+ <ThemeProvider theme={{
1110
+ ...exampleTheme,
1111
+ focusRingColor: 'focus:ring-brand',
1112
+ }}>
1113
+ <PageRenderer data={page.puckData} />
1114
+ </ThemeProvider>
1115
+ ```
1116
+
1117
+ ### Theme Options
1118
+
1119
+ | Option | Description |
1120
+ |--------|-------------|
1121
+ | `buttonVariants` | Button component variant styles (default, secondary, outline, ghost) |
1122
+ | `ctaButtonVariants` | CallToAction button styles (primary, secondary, outline) |
1123
+ | `ctaBackgroundStyles` | CallToAction background styles (default, dark, light) |
1124
+ | `colorPresets` | Color picker preset swatches |
1125
+ | `extendColorPresets` | If true, adds to defaults instead of replacing |
1126
+ | `focusRingColor` | Focus ring class (e.g., `focus:ring-primary`) |
1127
+
1128
+ ### Custom Color Presets
1129
+
1130
+ ```typescript
1131
+ <ThemeProvider theme={{
1132
+ colorPresets: [
1133
+ { hex: '#3b82f6', label: 'Brand Blue' },
1134
+ { hex: '#10b981', label: 'Success' },
1135
+ { hex: '#ef4444', label: 'Danger' },
1136
+ ],
1137
+ extendColorPresets: false, // Replace defaults
1138
+ }}>
1139
+ <PageRenderer data={page.puckData} />
1140
+ </ThemeProvider>
1141
+ ```
1142
+
1143
+ ### Direct Theme Imports
1144
+
1145
+ ```typescript
1146
+ import {
1147
+ ThemeProvider,
1148
+ useTheme,
1149
+ getVariantClasses,
1150
+ DEFAULT_THEME,
1151
+ } from '@delmaredigital/payload-puck/theme'
1152
+
1153
+ function MyButton({ variant }) {
1154
+ const theme = useTheme()
1155
+ const classes = getVariantClasses(theme.buttonVariants, variant)
1156
+ return <button className={classes}>Click me</button>
1157
+ }
1158
+ ```
1159
+
1160
+ </details>
1161
+
1162
+ ---
1163
+
1164
+ <details>
1165
+ <summary><strong>Layouts</strong> — Page structure, headers/footers, responsive controls</summary>
1166
+
1167
+ The layout system controls page structure, max-width constraints, and optional header/footer rendering. Layouts are selected per-page in the Puck editor's "Page Setup" panel.
1168
+
1169
+ ### Built-in Layouts
1170
+
1171
+ | Layout | Description |
1172
+ |--------|-------------|
1173
+ | **Default** | Standard page with max-width container (1200px) |
1174
+ | **Landing** | Full-width sections, no container constraints |
1175
+ | **Full Width** | Edge-to-edge content |
1176
+
1177
+ ### Defining Custom Layouts
1178
+
1179
+ ```typescript
1180
+ // lib/puck-layouts.ts
1181
+ import type { LayoutDefinition } from '@delmaredigital/payload-puck/layouts'
1182
+ import { SiteHeader } from '@/components/header'
1183
+ import { SiteFooter } from '@/components/footer'
1184
+
1185
+ export const siteLayouts: LayoutDefinition[] = [
1186
+ {
1187
+ value: 'default',
1188
+ label: 'Default',
1189
+ description: 'Standard page with header and footer',
1190
+ maxWidth: '1200px',
1191
+ header: SiteHeader,
1192
+ footer: SiteFooter,
1193
+ editorBackground: '#ffffff',
1194
+ editorDarkMode: false,
1195
+ stickyHeaderHeight: 80,
1196
+ // Default background for frontend (overridden by pageBackground in Puck)
1197
+ styles: {
1198
+ wrapper: {
1199
+ background: 'var(--site-bg)',
1200
+ backgroundAttachment: 'fixed',
1201
+ },
1202
+ },
1203
+ },
1204
+ {
1205
+ value: 'landing',
1206
+ label: 'Landing',
1207
+ description: 'Full-width landing page',
1208
+ fullWidth: true,
1209
+ editorBackground: '#f8fafc',
1210
+ styles: {
1211
+ wrapper: {
1212
+ background: 'linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%)',
1213
+ },
1214
+ },
1215
+ },
1216
+ {
1217
+ value: 'full-width',
1218
+ label: 'Full Width',
1219
+ description: 'Edge-to-edge content',
1220
+ fullWidth: true,
1221
+ },
1222
+ ]
1223
+ ```
1224
+
1225
+ > **Background priority:** If a user sets `pageBackground` in the Puck editor, it overrides the layout's `styles.wrapper.background`. This allows layouts to define sensible defaults while letting individual pages customize their appearance.
1226
+
1227
+ ### Using Layouts in the Editor
1228
+
1229
+ ```typescript
1230
+ import { PuckEditor } from '@delmaredigital/payload-puck/editor'
1231
+ import { siteLayouts } from '@/lib/puck-layouts'
1232
+
1233
+ <PuckEditor
1234
+ config={editorConfig}
1235
+ pageId={page.id}
1236
+ initialData={page.puckData}
1237
+ layouts={siteLayouts}
1238
+ />
1239
+ ```
1240
+
1241
+ ### Using Layouts on the Frontend
1242
+
1243
+ ```typescript
1244
+ import { PageRenderer } from '@delmaredigital/payload-puck/render'
1245
+ import { LayoutWrapper, DEFAULT_LAYOUTS } from '@delmaredigital/payload-puck/layouts'
1246
+
1247
+ export default async function Page({ params }) {
1248
+ const page = await getPage(params.slug)
1249
+ const layout = DEFAULT_LAYOUTS.find(l => l.value === page.puckData?.root?.props?.pageLayout)
1250
+
1251
+ return (
1252
+ <LayoutWrapper layout={layout}>
1253
+ <PageRenderer config={baseConfig} data={page.puckData} />
1254
+ </LayoutWrapper>
1255
+ )
1256
+ }
1257
+ ```
1258
+
1259
+ ### Layout Definition Options
1260
+
1261
+ | Option | Type | Default | Description |
1262
+ |--------|------|---------|-------------|
1263
+ | `value` | `string` | — | Unique identifier |
1264
+ | `label` | `string` | — | Display name in editor |
1265
+ | `description` | `string` | — | Optional description |
1266
+ | `maxWidth` | `string` | — | Container max-width (e.g., `'1200px'`) |
1267
+ | `fullWidth` | `boolean` | `false` | If true, no container constraints |
1268
+ | `classes` | `object` | — | CSS classes for wrapper/container/content |
1269
+ | `styles` | `object` | — | Inline styles for wrapper/container/content |
1270
+ | `header` | `ComponentType` | — | Header component for editor preview and frontend |
1271
+ | `footer` | `ComponentType` | — | Footer component for editor preview and frontend |
1272
+ | `editorBackground` | `string` | `'#ffffff'` | Background color/gradient for editor preview |
1273
+ | `editorDarkMode` | `boolean` | `false` | Whether to use dark mode styling in editor preview |
1274
+ | `stickyHeaderHeight` | `number` | — | Height in px of sticky/fixed header for proper content offset |
1275
+ | `stickyFooter` | `boolean` | `true` | Push footer to bottom of viewport even with minimal content. Set to `false` to let footer flow naturally after content |
1276
+
1277
+ ### Page-Level Settings
1278
+
1279
+ The editor automatically includes page-level controls that allow overriding layout defaults per-page:
1280
+
1281
+ | Field | Options | Description |
1282
+ |-------|---------|-------------|
1283
+ | `showHeader` | `default`, `show`, `hide` | Override header visibility for this page |
1284
+ | `showFooter` | `default`, `show`, `hide` | Override footer visibility for this page |
1285
+ | `pageBackground` | Background field | Custom page background color/gradient/image |
1286
+ | `pageMaxWidth` | Select | Override layout's max-width constraint |
1287
+
1288
+ These settings appear in the editor's "Page Setup" panel and are applied in both the editor preview and frontend rendering.
1289
+
1290
+ </details>
1291
+
1292
+ ---
1293
+
1294
+ <details>
1295
+ <summary><strong>API Routes</strong> — Auth configuration, root props mapping</summary>
1296
+
1297
+ ### Auth Configuration
1298
+
1299
+ The API routes require an `auth` configuration with permission hooks:
1300
+
1301
+ ```typescript
1302
+ const auth = {
1303
+ // Required: Authenticate the request
1304
+ authenticate: async (request) => {
1305
+ const session = await getSession(request)
1306
+ if (!session?.user) return { authenticated: false }
1307
+ return { authenticated: true, user: session.user }
1308
+ },
1309
+
1310
+ // Optional permission hooks
1311
+ canList: async (user) => ({ allowed: true }),
1312
+ canView: async (user, pageId) => ({ allowed: true }),
1313
+ canEdit: async (user, pageId) => ({ allowed: user.role === 'editor' }),
1314
+ canPublish: async (user, pageId) => ({ allowed: user.role === 'admin' }),
1315
+ canDelete: async (user, pageId) => ({ allowed: user.role === 'admin' }),
1316
+ }
1317
+ ```
1318
+
1319
+ ### Root Props Mapping
1320
+
1321
+ Sync Puck root.props to Payload fields automatically:
1322
+
1323
+ ```typescript
1324
+ createPuckApiRoutesWithId({
1325
+ // ...
1326
+ rootPropsMapping: [
1327
+ { from: 'title', to: 'meta.title' },
1328
+ { from: 'description', to: 'meta.description' },
1329
+ { from: 'pageLayout', to: 'pageLayout' },
1330
+ ],
1331
+ })
1332
+ ```
1333
+
1334
+ </details>
1335
+
1336
+ ---
1337
+
1338
+ <details>
1339
+ <summary><strong>Plugin Options</strong> — Collection config, access control</summary>
1340
+
1341
+ ```typescript
1342
+ createPuckPlugin({
1343
+ // Collection slug for pages (default: 'pages')
1344
+ pagesCollection: 'pages',
1345
+
1346
+ // Auto-generate the Pages collection (default: true)
1347
+ autoGenerateCollection: true,
1348
+
1349
+ // Override collection config
1350
+ collectionOverrides: {
1351
+ admin: {
1352
+ defaultColumns: ['title', 'slug', 'updatedAt'],
1353
+ },
1354
+ },
1355
+
1356
+ // Custom access control
1357
+ access: {
1358
+ read: () => true,
1359
+ create: ({ req }) => !!req.user,
1360
+ update: ({ req }) => !!req.user,
1361
+ delete: ({ req }) => !!req.user,
1362
+ },
1363
+ })
1364
+ ```
1365
+
1366
+ </details>
1367
+
1368
+ ---
1369
+
1370
+ <details>
1371
+ <summary><strong>Hybrid Integration</strong> — Add Puck to existing collections with legacy blocks</summary>
1372
+
1373
+ If you have an existing Pages collection with legacy Payload blocks, you can add Puck support without replacing your collection.
1374
+
1375
+ ### Automatic (Recommended)
1376
+
1377
+ If you already have a `pages` collection defined, the plugin automatically adds only the Puck-specific fields that don't already exist:
1378
+
1379
+ ```typescript
1380
+ // payload.config.ts
1381
+ import { buildConfig } from 'payload'
1382
+ import { createPuckPlugin } from '@delmaredigital/payload-puck/plugin'
1383
+
1384
+ export default buildConfig({
1385
+ collections: [
1386
+ {
1387
+ slug: 'pages',
1388
+ fields: [
1389
+ { name: 'title', type: 'text', required: true },
1390
+ { name: 'slug', type: 'text', required: true },
1391
+ { name: 'layout', type: 'blocks', blocks: [HeroBlock, CTABlock] },
1392
+ ],
1393
+ },
1394
+ ],
1395
+ plugins: [
1396
+ createPuckPlugin({ pagesCollection: 'pages' }),
1397
+ ],
1398
+ })
1399
+ ```
1400
+
1401
+ The plugin adds: `puckData`, `editorVersion`, `pageLayout`, `meta` (SEO fields), and the "Edit with Puck" button.
1402
+
1403
+ **Smart detection:** The `editorVersion` field automatically detects whether existing pages use legacy blocks or Puck data. Pages with legacy blocks are marked `'legacy'`, pages with Puck content are marked `'puck'`, and new empty pages use the configured default. This prevents migrations from incorrectly overwriting existing content.
1404
+
1405
+ ### Manual with `getPuckFields()`
1406
+
1407
+ For full control, disable auto-generation and use `getPuckFields()`:
1408
+
1409
+ ```typescript
1410
+ // collections/Pages.ts
1411
+ import type { CollectionConfig } from 'payload'
1412
+ import { getPuckFields } from '@delmaredigital/payload-puck'
1413
+
1414
+ export const Pages: CollectionConfig = {
1415
+ slug: 'pages',
1416
+ versions: { drafts: true },
1417
+ fields: [
1418
+ { name: 'title', type: 'text', required: true },
1419
+ { name: 'slug', type: 'text', required: true },
1420
+ { name: 'layout', type: 'blocks', blocks: [HeroBlock, CTABlock] },
1421
+
1422
+ ...getPuckFields({
1423
+ includeSEO: false,
1424
+ includeConversion: true,
1425
+ // Custom conversion types (optional - defaults to standard types)
1426
+ conversionTypeOptions: [
1427
+ { label: 'Registration', value: 'registration' },
1428
+ { label: 'Donation', value: 'donation' },
1429
+ { label: 'Course Start', value: 'course_start' },
1430
+ { label: 'Custom', value: 'custom' },
1431
+ ],
1432
+ includeEditorVersion: true,
1433
+ includePageLayout: true,
1434
+ includeIsHomepage: false,
1435
+ // Custom layouts (only value/label needed for the field)
1436
+ layouts: [
1437
+ { value: 'default', label: 'Default' },
1438
+ { value: 'landing', label: 'Landing' },
1439
+ ],
1440
+ }),
1441
+ ],
1442
+ }
1443
+ ```
1444
+
1445
+ ```typescript
1446
+ // payload.config.ts
1447
+ createPuckPlugin({
1448
+ pagesCollection: 'pages',
1449
+ autoGenerateCollection: false,
1450
+ })
1451
+ ```
1452
+
1453
+ ### Individual Field Imports
1454
+
1455
+ ```typescript
1456
+ import {
1457
+ puckDataField,
1458
+ editorVersionField,
1459
+ createPageLayoutField,
1460
+ seoFieldGroup,
1461
+ conversionFieldGroup,
1462
+ } from '@delmaredigital/payload-puck'
1463
+
1464
+ const Pages: CollectionConfig = {
1465
+ slug: 'pages',
1466
+ fields: [
1467
+ puckDataField,
1468
+ editorVersionField,
1469
+ createPageLayoutField(myCustomLayouts),
1470
+ ],
1471
+ }
1472
+ ```
1473
+
1474
+ ### Rendering Hybrid Pages
1475
+
1476
+ **Option A: Using `HybridPageRenderer` (Recommended)**
1477
+
1478
+ The `HybridPageRenderer` component handles the branching logic for you:
1479
+
1480
+ ```typescript
1481
+ // app/(frontend)/[...slug]/page.tsx
1482
+ import { HybridPageRenderer } from '@delmaredigital/payload-puck/render'
1483
+ import { puckConfig } from '@/puck/config'
1484
+ import { siteLayouts } from '@/lib/puck-layouts'
1485
+ import { LegacyBlockRenderer } from '@/components/LegacyBlockRenderer'
1486
+
1487
+ export default async function Page({ params }) {
1488
+ const page = await getPage(params.slug)
1489
+
1490
+ return (
1491
+ <HybridPageRenderer
1492
+ page={page}
1493
+ config={puckConfig}
1494
+ layouts={siteLayouts}
1495
+ legacyRenderer={(blocks) => <LegacyBlockRenderer blocks={blocks} />}
1496
+ />
1497
+ )
1498
+ }
1499
+ ```
1500
+
1501
+ **Option B: Manual Branching**
1502
+
1503
+ For more control, handle the branching yourself:
1504
+
1505
+ ```typescript
1506
+ // app/(frontend)/[...slug]/page.tsx
1507
+ import { PageRenderer } from '@delmaredigital/payload-puck/render'
1508
+ import { puckConfig } from '@/puck/config'
1509
+ import { siteLayouts } from '@/lib/puck-layouts'
1510
+ import { LegacyBlockRenderer } from '@/components/LegacyBlockRenderer'
1511
+
1512
+ export default async function Page({ params }) {
1513
+ const page = await getPage(params.slug)
1514
+
1515
+ if (page.editorVersion === 'puck' && page.puckData) {
1516
+ return (
1517
+ <PageRenderer
1518
+ config={puckConfig}
1519
+ data={page.puckData}
1520
+ layouts={siteLayouts}
1521
+ />
1522
+ )
1523
+ }
1524
+
1525
+ return <LegacyBlockRenderer blocks={page.layout} />
1526
+ }
1527
+ ```
1528
+
1529
+ ### Available Field Exports
1530
+
1531
+ | Export | Description |
1532
+ |--------|-------------|
1533
+ | `getPuckFields(options)` | Returns array of Puck fields based on options |
1534
+ | `puckDataField` | JSON field for Puck editor data (hidden) |
1535
+ | `editorVersionField` | Select field: 'legacy' \| 'puck' (auto-detects based on content) |
1536
+ | `createEditorVersionField(default, sidebar, legacyBlocksFieldName)` | Factory for custom editor version field |
1537
+ | `pageLayoutField` | Layout selector with DEFAULT_LAYOUTS |
1538
+ | `createPageLayoutField(layouts, sidebar)` | Factory for custom layout options |
1539
+ | `isHomepageField` | Checkbox for homepage designation |
1540
+ | `seoFieldGroup` | Group named `meta` with title, description, image, noindex, etc. |
1541
+ | `conversionFieldGroup` | Group for conversion tracking analytics (default types) |
1542
+ | `createConversionFieldGroup(types, sidebar)` | Factory for custom conversion types |
1543
+ | `DEFAULT_CONVERSION_TYPES` | Default conversion types array |
1544
+ | `generatePuckEditField(slug, config)` | Creates the "Edit with Puck" UI button |
1545
+
1546
+ ### Available Render Exports
1547
+
1548
+ | Export | Description |
1549
+ |--------|-------------|
1550
+ | `PageRenderer` | Renders Puck data with layout support |
1551
+ | `HybridPageRenderer` | Renders either Puck or legacy pages based on `editorVersion` |
1552
+
1553
+ </details>
1554
+
1555
+ ---
1556
+
1557
+ ## License
1558
+
1559
+ This project is licensed under the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/).
1560
+
1561
+ ### What This Means
1562
+
1563
+ **✅ Free for:**
1564
+ - Personal projects and hobby use
1565
+ - Open source projects
1566
+ - Educational and research purposes
1567
+ - Evaluation and testing
1568
+ - Nonprofit organizations
1569
+ - Government institutions
1570
+
1571
+ **💼 Commercial use:**
1572
+ Requires a separate commercial license. If you're using this in a commercial product or service, please contact us for licensing options.
1573
+
1574
+ **📧 Commercial Licensing:** [hello@delmaredigital.com](mailto:hello@delmaredigital.com)
1575
+
1576
+ ---
1577
+
1578
+ ## About
1579
+
1580
+ Built by [Delmare Digital](https://delmaredigital.com) — custom software solutions for growing businesses.