@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.
- package/LICENSE +73 -0
- package/README.md +1580 -0
- package/dist/AccordionClient.d.mts +24 -0
- package/dist/AccordionClient.d.ts +24 -0
- package/dist/AccordionClient.js +786 -0
- package/dist/AccordionClient.js.map +1 -0
- package/dist/AccordionClient.mjs +784 -0
- package/dist/AccordionClient.mjs.map +1 -0
- package/dist/AnimatedWrapper.d.mts +30 -0
- package/dist/AnimatedWrapper.d.ts +30 -0
- package/dist/AnimatedWrapper.js +379 -0
- package/dist/AnimatedWrapper.js.map +1 -0
- package/dist/AnimatedWrapper.mjs +377 -0
- package/dist/AnimatedWrapper.mjs.map +1 -0
- package/dist/admin/client.d.mts +108 -0
- package/dist/admin/client.d.ts +108 -0
- package/dist/admin/client.js +177 -0
- package/dist/admin/client.js.map +1 -0
- package/dist/admin/client.mjs +173 -0
- package/dist/admin/client.mjs.map +1 -0
- package/dist/admin/index.d.mts +157 -0
- package/dist/admin/index.d.ts +157 -0
- package/dist/admin/index.js +31 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/index.mjs +29 -0
- package/dist/admin/index.mjs.map +1 -0
- package/dist/api/index.d.mts +460 -0
- package/dist/api/index.d.ts +460 -0
- package/dist/api/index.js +588 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/index.mjs +578 -0
- package/dist/api/index.mjs.map +1 -0
- package/dist/components/index.css +339 -0
- package/dist/components/index.css.map +1 -0
- package/dist/components/index.d.mts +222 -0
- package/dist/components/index.d.ts +222 -0
- package/dist/components/index.js +9177 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +9130 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/config.editor.css +339 -0
- package/dist/config/config.editor.css.map +1 -0
- package/dist/config/config.editor.d.mts +153 -0
- package/dist/config/config.editor.d.ts +153 -0
- package/dist/config/config.editor.js +9400 -0
- package/dist/config/config.editor.js.map +1 -0
- package/dist/config/config.editor.mjs +9368 -0
- package/dist/config/config.editor.mjs.map +1 -0
- package/dist/config/index.d.mts +68 -0
- package/dist/config/index.d.ts +68 -0
- package/dist/config/index.js +2017 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +1991 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/editor/index.d.mts +784 -0
- package/dist/editor/index.d.ts +784 -0
- package/dist/editor/index.js +4517 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/index.mjs +4483 -0
- package/dist/editor/index.mjs.map +1 -0
- package/dist/fields/index.css +339 -0
- package/dist/fields/index.css.map +1 -0
- package/dist/fields/index.d.mts +600 -0
- package/dist/fields/index.d.ts +600 -0
- package/dist/fields/index.js +7739 -0
- package/dist/fields/index.js.map +1 -0
- package/dist/fields/index.mjs +7590 -0
- package/dist/fields/index.mjs.map +1 -0
- package/dist/index-CQu6SzDg.d.mts +327 -0
- package/dist/index-CoUQnyC3.d.ts +327 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +569 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +555 -0
- package/dist/index.mjs.map +1 -0
- package/dist/layouts/index.d.mts +96 -0
- package/dist/layouts/index.d.ts +96 -0
- package/dist/layouts/index.js +394 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/layouts/index.mjs +378 -0
- package/dist/layouts/index.mjs.map +1 -0
- package/dist/plugin/index.d.mts +289 -0
- package/dist/plugin/index.d.ts +289 -0
- package/dist/plugin/index.js +569 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/index.mjs +555 -0
- package/dist/plugin/index.mjs.map +1 -0
- package/dist/render/index.d.mts +109 -0
- package/dist/render/index.d.ts +109 -0
- package/dist/render/index.js +2146 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/index.mjs +2123 -0
- package/dist/render/index.mjs.map +1 -0
- package/dist/shared-DMAF1AcH.d.mts +545 -0
- package/dist/shared-DMAF1AcH.d.ts +545 -0
- package/dist/theme/index.d.mts +155 -0
- package/dist/theme/index.d.ts +155 -0
- package/dist/theme/index.js +201 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/index.mjs +186 -0
- package/dist/theme/index.mjs.map +1 -0
- package/dist/types-D7D3rZ1J.d.mts +116 -0
- package/dist/types-D7D3rZ1J.d.ts +116 -0
- package/dist/types-_6MvjyKv.d.mts +104 -0
- package/dist/types-_6MvjyKv.d.ts +104 -0
- package/dist/utils/index.d.mts +267 -0
- package/dist/utils/index.d.ts +267 -0
- package/dist/utils/index.js +426 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +412 -0
- package/dist/utils/index.mjs.map +1 -0
- package/dist/utils-DaRs9t0J.d.mts +85 -0
- package/dist/utils-gAvt0Vhw.d.ts +85 -0
- package/examples/README.md +240 -0
- package/examples/api/puck/pages/[id]/route.ts +64 -0
- package/examples/api/puck/pages/[id]/versions/route.ts +47 -0
- package/examples/api/puck/pages/route.ts +45 -0
- package/examples/app/(frontend)/page.tsx +94 -0
- package/examples/app/[...slug]/page.tsx +101 -0
- package/examples/app/pages/[id]/edit/page.tsx +148 -0
- package/examples/components/CustomBanner.tsx +368 -0
- package/examples/config/custom-config.ts +223 -0
- package/examples/config/payload.config.example.ts +64 -0
- package/examples/lib/puck-layouts.ts +258 -0
- package/examples/lib/puck-theme.ts +94 -0
- package/examples/styles/puck-theme.css +171 -0
- 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.
|