@delmaredigital/payload-puck 0.6.29 → 0.7.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/README.md +15 -3
- package/dist/editor/PuckEditorImpl.client.js +40 -5
- package/dist/editor/components/IframeWrapper.d.ts +6 -2
- package/dist/editor/components/IframeWrapper.js +92 -127
- package/dist/editor/plugins/VersionHistoryPanel.d.ts +6 -2
- package/dist/editor/plugins/VersionHistoryPanel.js +3 -2
- package/dist/editor/plugins/versionHistoryPlugin.d.ts +4 -3
- package/dist/editor/utils/index.d.ts +1 -0
- package/dist/editor/utils/index.js +1 -0
- package/dist/editor/utils/isDataEqual.d.ts +15 -0
- package/dist/editor/utils/isDataEqual.js +57 -0
- package/dist/endpoints/styles.js +22 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +19 -15
package/README.md
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
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
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://demo.delmaredigital.com"><img src="https://img.shields.io/badge/Live_Demo-Try_It_Now-2ea44f?style=for-the-badge&logo=vercel&logoColor=white" alt="Live Demo - Try It Now"></a>
|
|
7
|
-
|
|
8
6
|
<a href="https://github.com/delmaredigital/dd-starter"><img src="https://img.shields.io/badge/Starter_Template-Use_This-blue?style=for-the-badge&logo=github&logoColor=white" alt="Starter Template - Use This"></a>
|
|
9
7
|
</p>
|
|
10
8
|
|
|
@@ -34,7 +32,8 @@ pnpm add @delmaredigital/payload-puck @puckeditor/core
|
|
|
34
32
|
|
|
35
33
|
| Dependency | Version |
|
|
36
34
|
|------------|---------|
|
|
37
|
-
|
|
|
35
|
+
| `node` | >= 20.9.0 |
|
|
36
|
+
| `@puckeditor/core` | >= 0.23.0 |
|
|
38
37
|
| `payload` | >= 3.69.0 |
|
|
39
38
|
| `@payloadcms/next` | >= 3.69.0 |
|
|
40
39
|
| `next` | >= 15.4.8 (see security note below) |
|
|
@@ -44,6 +43,19 @@ pnpm add @delmaredigital/payload-puck @puckeditor/core
|
|
|
44
43
|
|
|
45
44
|
> **Security:** If your app uses Next.js middleware (or proxy.ts) to protect dynamic routes, use `next` >= 15.5.16 / 16.2.5 to pick up the fix for [CVE-2026-44574](https://github.com/vercel/next.js/security/advisories/GHSA-492v-c6pp-mqqv) (middleware bypass via dynamic route parameter injection). Turbopack users need >= 15.5.18 / 16.2.6.
|
|
46
45
|
|
|
46
|
+
### Upgrading to 0.7.0 (breaking)
|
|
47
|
+
|
|
48
|
+
`0.7.0` raises two floors. Both are a one-line change for most projects:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add @puckeditor/core@^0.23.0 # peer floor moved from >=0.21.0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **`@puckeditor/core` now requires >= 0.23.0.** The Puck plugins this package bundles are versioned in lockstep with Puck core and import it from the host, so running them against an older core is not a supported combination. Puck 0.23 ships a rewritten canvas drag-and-drop engine and a redesigned outline — **the editing experience changes visibly**, even though no API you call has changed. Worth a pass through your editor after upgrading. See the [Puck 0.23 release notes](https://puckeditor.com/blog/puck-023).
|
|
55
|
+
- **Node 18 is no longer supported;** the floor is now `>=20.9.0`. Node 18 is end-of-life and Puck core 0.23 itself requires `>=20.0.0`.
|
|
56
|
+
|
|
57
|
+
No exports, props, or configuration options were removed or renamed. Full detail in the [changelog](./CHANGELOG.md).
|
|
58
|
+
|
|
47
59
|
---
|
|
48
60
|
|
|
49
61
|
## Quick Start
|
|
@@ -16,6 +16,7 @@ import { IframeWrapper } from './components/IframeWrapper.js';
|
|
|
16
16
|
import { PreviewModal } from './components/PreviewModal.js';
|
|
17
17
|
import { DarkModeStyles } from './components/DarkModeStyles.js';
|
|
18
18
|
import { useUnsavedChanges } from './hooks/useUnsavedChanges.js';
|
|
19
|
+
import { isDataEqual } from './utils/isDataEqual.js';
|
|
19
20
|
import { createVersionHistoryPlugin } from './plugins/versionHistoryPlugin.js';
|
|
20
21
|
import { ThemeProvider } from '../theme/index.js';
|
|
21
22
|
import { usePuckConfig } from '../views/PuckConfigContext.js';
|
|
@@ -117,6 +118,11 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
117
118
|
]);
|
|
118
119
|
// Use a ref to track latest data without causing re-renders
|
|
119
120
|
const latestDataRef = useRef(dataWithSlug);
|
|
121
|
+
// Track the last loaded/saved data so we can distinguish real user edits from
|
|
122
|
+
// no-op onChange dispatches (e.g. Puck's mount-time resolve pass). Refreshed at
|
|
123
|
+
// every markClean() site (save / publish) so subsequent edits diff against the
|
|
124
|
+
// most recently persisted state.
|
|
125
|
+
const savedDataRef = useRef(dataWithSlug);
|
|
120
126
|
// Get editor stylesheets from PuckConfigProvider context (as fallback)
|
|
121
127
|
const { editorStylesheets: contextStylesheets, editorCss: contextCss } = usePuckConfig();
|
|
122
128
|
// Props take precedence over context
|
|
@@ -135,8 +141,10 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
135
141
|
const fromBase = baseStylesheets || [];
|
|
136
142
|
const fromLayout = currentLayout?.editorStylesheets || [];
|
|
137
143
|
return [
|
|
138
|
-
...
|
|
139
|
-
|
|
144
|
+
...new Set([
|
|
145
|
+
...fromBase,
|
|
146
|
+
...fromLayout
|
|
147
|
+
])
|
|
140
148
|
];
|
|
141
149
|
}, [
|
|
142
150
|
baseStylesheets,
|
|
@@ -237,6 +245,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
237
245
|
setSaveError(null); // Clear any previous error
|
|
238
246
|
// After saving as draft, update status to draft (shows "Unpublished Changes" if was published)
|
|
239
247
|
setDocumentStatus('draft');
|
|
248
|
+
savedDataRef.current = typedData;
|
|
240
249
|
markClean();
|
|
241
250
|
onSaveSuccess?.(data);
|
|
242
251
|
} catch (error) {
|
|
@@ -285,6 +294,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
285
294
|
setSaveError(null); // Clear any previous error
|
|
286
295
|
setDocumentStatus('published'); // Update status after successful publish
|
|
287
296
|
setWasPublished(true); // Mark as having been published
|
|
297
|
+
savedDataRef.current = typedData;
|
|
288
298
|
markClean();
|
|
289
299
|
onSaveSuccess?.(data);
|
|
290
300
|
} catch (error) {
|
|
@@ -338,9 +348,19 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
338
348
|
// Handle data change
|
|
339
349
|
const handleChange = useCallback((data)=>{
|
|
340
350
|
latestDataRef.current = data;
|
|
341
|
-
|
|
351
|
+
// Only mark dirty when the data actually differs from the last loaded/saved
|
|
352
|
+
// state. Puck fires onChange for no-op mount-time resolves (which can differ
|
|
353
|
+
// from the loaded data only by undefined-valued keys); treating those as
|
|
354
|
+
// edits produces a spurious "Unsaved" flag on load. isDataEqual is
|
|
355
|
+
// undefined-tolerant so those resolves compare equal and clear the flag.
|
|
356
|
+
if (isDataEqual(data, savedDataRef.current)) {
|
|
357
|
+
markClean();
|
|
358
|
+
} else {
|
|
359
|
+
markDirty();
|
|
360
|
+
}
|
|
342
361
|
onChangeProp?.(data);
|
|
343
362
|
}, [
|
|
363
|
+
markClean,
|
|
344
364
|
markDirty,
|
|
345
365
|
onChangeProp
|
|
346
366
|
]);
|
|
@@ -413,6 +433,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
413
433
|
setLastSaved(new Date());
|
|
414
434
|
setSaveError(null);
|
|
415
435
|
setDocumentStatus('draft');
|
|
436
|
+
savedDataRef.current = data;
|
|
416
437
|
markClean();
|
|
417
438
|
onSaveSuccess?.(data);
|
|
418
439
|
} catch (error) {
|
|
@@ -503,18 +524,32 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
503
524
|
const defaultPlugins = [
|
|
504
525
|
headingAnalyzer
|
|
505
526
|
];
|
|
527
|
+
// Restoring a version has already persisted that data server-side, so it
|
|
528
|
+
// becomes the new saved baseline. Without this, the `setData` dispatch the
|
|
529
|
+
// restore performs fires an onChange that diffs against the pre-restore
|
|
530
|
+
// baseline and immediately re-flags the (already saved) document as dirty.
|
|
531
|
+
const handleRestoreSuccess = useCallback((restoredData)=>{
|
|
532
|
+
if (restoredData) {
|
|
533
|
+
const typedData = restoredData;
|
|
534
|
+
savedDataRef.current = typedData;
|
|
535
|
+
latestDataRef.current = typedData;
|
|
536
|
+
}
|
|
537
|
+
markClean();
|
|
538
|
+
}, [
|
|
539
|
+
markClean
|
|
540
|
+
]);
|
|
506
541
|
// Version history plugin for the plugin rail
|
|
507
542
|
const versionHistoryPlugin = useMemo(()=>{
|
|
508
543
|
if (!pageId) return null;
|
|
509
544
|
return createVersionHistoryPlugin({
|
|
510
545
|
pageId,
|
|
511
546
|
apiEndpoint,
|
|
512
|
-
onRestoreSuccess:
|
|
547
|
+
onRestoreSuccess: handleRestoreSuccess
|
|
513
548
|
});
|
|
514
549
|
}, [
|
|
515
550
|
pageId,
|
|
516
551
|
apiEndpoint,
|
|
517
|
-
|
|
552
|
+
handleRestoreSuccess
|
|
518
553
|
]);
|
|
519
554
|
// Fetch AI prompts client-side when prompts collection is enabled
|
|
520
555
|
// This allows prompts to update in real-time when edited via the prompt editor panel
|
|
@@ -78,14 +78,18 @@ export interface IframeWrapperProps {
|
|
|
78
78
|
*/
|
|
79
79
|
defaultLayout?: string;
|
|
80
80
|
/**
|
|
81
|
-
* Stylesheet URLs to
|
|
81
|
+
* Stylesheet URLs to render inside the iframe.
|
|
82
82
|
* These are merged from PuckConfigProvider and layout-specific settings.
|
|
83
83
|
* Use this to provide frontend CSS (Tailwind, CSS variables, etc.) that
|
|
84
84
|
* header/footer components need for proper styling.
|
|
85
|
+
*
|
|
86
|
+
* Rendered as ordinary React children (not host-document resources), so
|
|
87
|
+
* this CSS only ever affects the iframe's own document, never the
|
|
88
|
+
* Payload admin page it's embedded in.
|
|
85
89
|
*/
|
|
86
90
|
editorStylesheets?: string[];
|
|
87
91
|
/**
|
|
88
|
-
* Raw CSS to
|
|
92
|
+
* Raw CSS to render inside the iframe.
|
|
89
93
|
* Merged from PuckConfigProvider and layout-specific settings.
|
|
90
94
|
* Useful for CSS variables or style overrides.
|
|
91
95
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { memo, useEffect, useMemo, useState, createContext, useContext } from 'react';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useEffect, useMemo, useState, useCallback, createContext, useContext } from 'react';
|
|
4
4
|
import { createUsePuck } from '@puckeditor/core';
|
|
5
5
|
import { backgroundValueToCSS } from '../../fields/shared.js';
|
|
6
6
|
// Create usePuck hook for accessing editor state
|
|
@@ -63,8 +63,6 @@ const usePuck = createUsePuck();
|
|
|
63
63
|
* ```
|
|
64
64
|
*/ export const IframeWrapper = /*#__PURE__*/ memo(function IframeWrapper({ children, document: iframeDoc, layouts, layoutStyles, layoutKey = 'pageLayout', defaultLayout = 'default', editorStylesheets, editorCss, previewDarkModeOverride }) {
|
|
65
65
|
const appState = usePuck((s)=>s.appState);
|
|
66
|
-
// Track stylesheet loading state to force re-render when styles are ready
|
|
67
|
-
const [stylesLoaded, setStylesLoaded] = useState(false);
|
|
68
66
|
// Check if we're in interactive mode (links should be clickable)
|
|
69
67
|
const isInteractive = appState.ui.previewMode === 'interactive';
|
|
70
68
|
// Read layout value and page-level overrides from root props
|
|
@@ -106,6 +104,44 @@ const usePuck = createUsePuck();
|
|
|
106
104
|
const layoutConfig = layoutConfigMap[layoutValue] || layoutConfigMap[defaultLayout] || DEFAULT_LAYOUT_CONFIG;
|
|
107
105
|
// Calculate isDark for context provider (same logic as in useEffect)
|
|
108
106
|
const isDark = previewDarkModeOverride ?? layoutConfig.isDark;
|
|
107
|
+
// Resolve relative stylesheet URLs to absolute URLs. Puck's iframe uses
|
|
108
|
+
// srcDoc, whose relative-URL resolution can't be relied on to match the
|
|
109
|
+
// host origin, so resolve explicitly against window.location.origin.
|
|
110
|
+
const resolvedStylesheets = useMemo(()=>{
|
|
111
|
+
if (!editorStylesheets || editorStylesheets.length === 0) return [];
|
|
112
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
113
|
+
return editorStylesheets.map((href)=>href.startsWith('/') ? `${origin}${href}` : href);
|
|
114
|
+
}, [
|
|
115
|
+
editorStylesheets
|
|
116
|
+
]);
|
|
117
|
+
// Track which stylesheet URLs have finished loading (or errored, which we
|
|
118
|
+
// also count as "settled" so a broken URL can't block rendering forever).
|
|
119
|
+
// This accumulates across the component's lifetime rather than resetting
|
|
120
|
+
// per-layout: if a layout switches back to a previously-loaded stylesheet,
|
|
121
|
+
// it's already known-loaded and doesn't need to be waited on again.
|
|
122
|
+
const [loadedHrefs, setLoadedHrefs] = useState(()=>new Set());
|
|
123
|
+
const markLoaded = useCallback((href)=>{
|
|
124
|
+
setLoadedHrefs((prev)=>prev.has(href) ? prev : new Set(prev).add(href));
|
|
125
|
+
}, []);
|
|
126
|
+
// Gate rendering children until every currently-relevant stylesheet has
|
|
127
|
+
// settled, to avoid a flash of unstyled content. Vacuously true when there
|
|
128
|
+
// are no stylesheets to wait for.
|
|
129
|
+
const stylesReady = resolvedStylesheets.every((href)=>loadedHrefs.has(href));
|
|
130
|
+
// Safety net: a <link>'s onLoad can fail to fire for a resource that's
|
|
131
|
+
// already complete in the browser cache before React attaches the
|
|
132
|
+
// handler (a known browser quirk, and the exact reason the pre-refactor
|
|
133
|
+
// implementation carried this same fallback). Without it, a missed event
|
|
134
|
+
// leaves `stylesReady` false forever, permanently blanking the iframe --
|
|
135
|
+
// worse than the FOUC this whole mechanism exists to prevent. `markLoaded`
|
|
136
|
+
// is idempotent, so this is a harmless no-op if the real event already
|
|
137
|
+
// fired first.
|
|
138
|
+
useEffect(()=>{
|
|
139
|
+
const timers = resolvedStylesheets.map((href)=>setTimeout(()=>markLoaded(href), 2000));
|
|
140
|
+
return ()=>timers.forEach(clearTimeout);
|
|
141
|
+
}, [
|
|
142
|
+
resolvedStylesheets,
|
|
143
|
+
markLoaded
|
|
144
|
+
]);
|
|
109
145
|
useEffect(()=>{
|
|
110
146
|
if (!iframeDoc) return;
|
|
111
147
|
const body = iframeDoc.body;
|
|
@@ -138,97 +174,6 @@ const usePuck = createUsePuck();
|
|
|
138
174
|
html.setAttribute('data-theme', 'light');
|
|
139
175
|
body.style.color = '#1f2937'; // gray-800
|
|
140
176
|
}
|
|
141
|
-
// Inject external stylesheets (Tailwind CSS, CSS variables, etc.)
|
|
142
|
-
// These provide the styles needed for header/footer components
|
|
143
|
-
if (editorStylesheets && editorStylesheets.length > 0) {
|
|
144
|
-
let pendingLoads = 0;
|
|
145
|
-
let loadedCount = 0;
|
|
146
|
-
const checkAllLoaded = ()=>{
|
|
147
|
-
loadedCount++;
|
|
148
|
-
if (loadedCount >= pendingLoads) {
|
|
149
|
-
// All stylesheets loaded - force browser to recalculate styles
|
|
150
|
-
// This is necessary because the DOM was already rendered before CSS loaded
|
|
151
|
-
setStylesLoaded(true);
|
|
152
|
-
// Force a browser repaint after styles load
|
|
153
|
-
// Use multiple techniques to ensure CSS is applied to existing elements
|
|
154
|
-
requestAnimationFrame(()=>{
|
|
155
|
-
if (!html || !body) return;
|
|
156
|
-
// Technique 1: Re-apply theme classes (mimics what dark mode toggle does)
|
|
157
|
-
const isDark = previewDarkModeOverride ?? layoutConfig.isDark;
|
|
158
|
-
if (isDark) {
|
|
159
|
-
html.classList.remove('dark');
|
|
160
|
-
void html.offsetHeight; // Force reflow
|
|
161
|
-
html.classList.add('dark');
|
|
162
|
-
} else {
|
|
163
|
-
html.classList.remove('light');
|
|
164
|
-
void html.offsetHeight; // Force reflow
|
|
165
|
-
html.classList.add('light');
|
|
166
|
-
}
|
|
167
|
-
// Technique 2: Toggle visibility to force repaint
|
|
168
|
-
body.style.visibility = 'hidden';
|
|
169
|
-
void body.offsetHeight;
|
|
170
|
-
body.style.visibility = '';
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
// Get origin for resolving relative URLs
|
|
175
|
-
// Puck's iframe may use srcdoc which doesn't have a proper base URL,
|
|
176
|
-
// so relative paths like '/api/puck/styles' won't resolve correctly
|
|
177
|
-
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
178
|
-
// Track which stylesheets have been counted to avoid double-counting
|
|
179
|
-
const loadedIndexes = new Set();
|
|
180
|
-
const markLoaded = (index)=>{
|
|
181
|
-
if (loadedIndexes.has(index)) return;
|
|
182
|
-
loadedIndexes.add(index);
|
|
183
|
-
checkAllLoaded();
|
|
184
|
-
};
|
|
185
|
-
editorStylesheets.forEach((href, index)=>{
|
|
186
|
-
const linkId = `puck-editor-stylesheet-${index}`;
|
|
187
|
-
const existingLink = iframeDoc.getElementById(linkId);
|
|
188
|
-
if (!existingLink) {
|
|
189
|
-
pendingLoads++;
|
|
190
|
-
const link = iframeDoc.createElement('link');
|
|
191
|
-
link.id = linkId;
|
|
192
|
-
link.rel = 'stylesheet';
|
|
193
|
-
// Resolve relative URLs to absolute URLs for iframe compatibility
|
|
194
|
-
link.href = href.startsWith('/') ? `${origin}${href}` : href;
|
|
195
|
-
// Track when stylesheet loads
|
|
196
|
-
link.onload = ()=>markLoaded(index);
|
|
197
|
-
link.onerror = ()=>markLoaded(index); // Count errors too to avoid hanging
|
|
198
|
-
iframeDoc.head.appendChild(link);
|
|
199
|
-
// Fallback: if onload doesn't fire within 2 seconds, force trigger
|
|
200
|
-
// This handles edge cases with cached resources or browser quirks
|
|
201
|
-
setTimeout(()=>{
|
|
202
|
-
if (!loadedIndexes.has(index)) {
|
|
203
|
-
markLoaded(index);
|
|
204
|
-
}
|
|
205
|
-
}, 2000);
|
|
206
|
-
} else if (!stylesLoaded) {
|
|
207
|
-
// Link exists - assume it's already loaded
|
|
208
|
-
pendingLoads++;
|
|
209
|
-
// Immediately mark as loaded since it's already in the DOM
|
|
210
|
-
requestAnimationFrame(()=>markLoaded(index));
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
// If no new stylesheets to load, mark as loaded
|
|
214
|
-
if (pendingLoads === 0 && !stylesLoaded) {
|
|
215
|
-
setStylesLoaded(true);
|
|
216
|
-
}
|
|
217
|
-
} else if (!stylesLoaded) {
|
|
218
|
-
// No stylesheets to load
|
|
219
|
-
setStylesLoaded(true);
|
|
220
|
-
}
|
|
221
|
-
// Inject custom CSS (CSS variables, overrides, etc.)
|
|
222
|
-
if (editorCss) {
|
|
223
|
-
const CUSTOM_CSS_ID = 'puck-editor-custom-css';
|
|
224
|
-
let style = iframeDoc.getElementById(CUSTOM_CSS_ID);
|
|
225
|
-
if (!style) {
|
|
226
|
-
style = iframeDoc.createElement('style');
|
|
227
|
-
style.id = CUSTOM_CSS_ID;
|
|
228
|
-
iframeDoc.head.appendChild(style);
|
|
229
|
-
}
|
|
230
|
-
style.textContent = editorCss;
|
|
231
|
-
}
|
|
232
177
|
// Inject richtext-output styles into the iframe for proper heading/list rendering
|
|
233
178
|
const RICHTEXT_STYLES_ID = 'puck-richtext-output-styles';
|
|
234
179
|
if (!iframeDoc.getElementById(RICHTEXT_STYLES_ID)) {
|
|
@@ -349,9 +294,6 @@ const usePuck = createUsePuck();
|
|
|
349
294
|
iframeDoc,
|
|
350
295
|
layoutConfig,
|
|
351
296
|
pageBackground,
|
|
352
|
-
editorStylesheets,
|
|
353
|
-
editorCss,
|
|
354
|
-
stylesLoaded,
|
|
355
297
|
previewDarkModeOverride
|
|
356
298
|
]);
|
|
357
299
|
// Get header/footer components from layout config
|
|
@@ -363,6 +305,26 @@ const usePuck = createUsePuck();
|
|
|
363
305
|
// 'hide' = always hide
|
|
364
306
|
const shouldShowHeader = showHeaderOverride === 'hide' ? false : showHeaderOverride === 'show' ? !!LayoutHeader : !!LayoutHeader;
|
|
365
307
|
const shouldShowFooter = showFooterOverride === 'hide' ? false : showFooterOverride === 'show' ? !!LayoutFooter : !!LayoutFooter;
|
|
308
|
+
// Stylesheet <link>/<style> elements, rendered as ordinary React children.
|
|
309
|
+
// Because these render inside the iframe's own portaled tree (not the host
|
|
310
|
+
// document), this CSS only ever applies inside the iframe -- it can never
|
|
311
|
+
// reach the Payload admin page. Being ordinary (non-resource) elements,
|
|
312
|
+
// React's normal reconciliation adds/removes/updates them correctly on
|
|
313
|
+
// every render, including layout switches that change which stylesheets
|
|
314
|
+
// apply -- no special keying tricks needed.
|
|
315
|
+
const styleElements = /*#__PURE__*/ _jsxs(_Fragment, {
|
|
316
|
+
children: [
|
|
317
|
+
resolvedStylesheets.map((href)=>/*#__PURE__*/ _jsx("link", {
|
|
318
|
+
rel: "stylesheet",
|
|
319
|
+
href: href,
|
|
320
|
+
onLoad: ()=>markLoaded(href),
|
|
321
|
+
onError: ()=>markLoaded(href)
|
|
322
|
+
}, href)),
|
|
323
|
+
editorCss ? /*#__PURE__*/ _jsx("style", {
|
|
324
|
+
children: editorCss
|
|
325
|
+
}) : null
|
|
326
|
+
]
|
|
327
|
+
});
|
|
366
328
|
// If we have header or footer to show, wrap in flex container to ensure proper layout
|
|
367
329
|
if (shouldShowHeader || shouldShowFooter) {
|
|
368
330
|
// Calculate content padding for sticky headers (only if header is actually shown)
|
|
@@ -378,38 +340,41 @@ const usePuck = createUsePuck();
|
|
|
378
340
|
const headerFooterStyle = isInteractive ? {} : {
|
|
379
341
|
pointerEvents: 'none'
|
|
380
342
|
};
|
|
381
|
-
|
|
382
|
-
// This ensures Tailwind classes are applied after the stylesheet loads
|
|
383
|
-
return /*#__PURE__*/ _jsx(PuckPreviewThemeContext.Provider, {
|
|
343
|
+
return /*#__PURE__*/ _jsxs(PuckPreviewThemeContext.Provider, {
|
|
384
344
|
value: isDark,
|
|
385
|
-
children:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
345
|
+
children: [
|
|
346
|
+
styleElements,
|
|
347
|
+
stylesReady && /*#__PURE__*/ _jsxs("div", {
|
|
348
|
+
style: {
|
|
349
|
+
display: 'flex',
|
|
350
|
+
flexDirection: 'column',
|
|
351
|
+
minHeight: '100vh'
|
|
352
|
+
},
|
|
353
|
+
children: [
|
|
354
|
+
shouldShowHeader && LayoutHeader && /*#__PURE__*/ _jsx("div", {
|
|
355
|
+
style: headerFooterStyle,
|
|
356
|
+
children: /*#__PURE__*/ _jsx(LayoutHeader, {})
|
|
357
|
+
}),
|
|
358
|
+
/*#__PURE__*/ _jsx("div", {
|
|
359
|
+
style: contentStyle,
|
|
360
|
+
children: children
|
|
361
|
+
}),
|
|
362
|
+
shouldShowFooter && LayoutFooter && /*#__PURE__*/ _jsx("div", {
|
|
363
|
+
style: headerFooterStyle,
|
|
364
|
+
children: /*#__PURE__*/ _jsx(LayoutFooter, {})
|
|
365
|
+
})
|
|
366
|
+
]
|
|
367
|
+
})
|
|
368
|
+
]
|
|
406
369
|
});
|
|
407
370
|
}
|
|
408
|
-
|
|
409
|
-
return /*#__PURE__*/ _jsx(PuckPreviewThemeContext.Provider, {
|
|
371
|
+
return /*#__PURE__*/ _jsxs(PuckPreviewThemeContext.Provider, {
|
|
410
372
|
value: isDark,
|
|
411
|
-
children:
|
|
412
|
-
|
|
413
|
-
|
|
373
|
+
children: [
|
|
374
|
+
styleElements,
|
|
375
|
+
stylesReady && /*#__PURE__*/ _jsx("div", {
|
|
376
|
+
children: children
|
|
377
|
+
})
|
|
378
|
+
]
|
|
414
379
|
});
|
|
415
380
|
});
|
|
@@ -29,9 +29,13 @@ export interface VersionHistoryPanelProps {
|
|
|
29
29
|
*/
|
|
30
30
|
apiEndpoint?: string;
|
|
31
31
|
/**
|
|
32
|
-
* Callback after successful restore (e.g., to mark editor as clean)
|
|
32
|
+
* Callback after successful restore (e.g., to mark editor as clean).
|
|
33
|
+
*
|
|
34
|
+
* Receives the restored data that was dispatched into the editor, so the
|
|
35
|
+
* caller can treat it as the new "last saved" baseline — the restore endpoint
|
|
36
|
+
* has already persisted it server-side.
|
|
33
37
|
*/
|
|
34
|
-
onRestoreSuccess?: () => void;
|
|
38
|
+
onRestoreSuccess?: (restoredData?: Data) => void;
|
|
35
39
|
}
|
|
36
40
|
/**
|
|
37
41
|
* Version history panel for the Puck plugin rail
|
|
@@ -256,8 +256,9 @@ const styles = {
|
|
|
256
256
|
// Show success message
|
|
257
257
|
setSuccessMessage(`Restored version from ${formatDate(version.updatedAt)}`);
|
|
258
258
|
setTimeout(()=>setSuccessMessage(null), 3000);
|
|
259
|
-
// Notify parent to mark as clean
|
|
260
|
-
|
|
259
|
+
// Notify parent to mark as clean, handing over the restored data so it
|
|
260
|
+
// becomes the new saved baseline (the restore already persisted it).
|
|
261
|
+
onRestoreSuccess?.(restoredDoc?.puckData);
|
|
261
262
|
// Refresh version list
|
|
262
263
|
fetchVersions();
|
|
263
264
|
} catch (err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Plugin } from '@puckeditor/core';
|
|
1
|
+
import type { Data, Plugin } from '@puckeditor/core';
|
|
2
2
|
export interface VersionHistoryPluginOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Page ID to fetch versions for
|
|
@@ -10,9 +10,10 @@ export interface VersionHistoryPluginOptions {
|
|
|
10
10
|
*/
|
|
11
11
|
apiEndpoint?: string;
|
|
12
12
|
/**
|
|
13
|
-
* Callback after successful restore (e.g., to mark editor as clean)
|
|
13
|
+
* Callback after successful restore (e.g., to mark editor as clean).
|
|
14
|
+
* Receives the restored data that was dispatched into the editor.
|
|
14
15
|
*/
|
|
15
|
-
onRestoreSuccess?: () => void;
|
|
16
|
+
onRestoreSuccess?: (restoredData?: Data) => void;
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
19
|
* Creates a Puck plugin for version history
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undefined-tolerant deep equality for Puck data.
|
|
3
|
+
*
|
|
4
|
+
* Puck's mount-time resolve pass (`resolveAndCommitData`) can dispatch a `replace`
|
|
5
|
+
* even when the net content is unchanged — e.g. a resolved node differs from the
|
|
6
|
+
* loaded node only by an `undefined`-valued key. Strict deep-equal libraries such
|
|
7
|
+
* as `fast-equals` treat `{}` and `{ k: undefined }` as different, so that no-op
|
|
8
|
+
* resolve looks like a real edit and spuriously marks the document dirty.
|
|
9
|
+
*
|
|
10
|
+
* This comparison treats an absent key and an `undefined`-valued key as equal, so
|
|
11
|
+
* a resolve that only adds/removes `undefined` values compares equal to the loaded
|
|
12
|
+
* data. Object key order is irrelevant (as with any deep equal); array order is
|
|
13
|
+
* significant (Puck content order is meaningful).
|
|
14
|
+
*/
|
|
15
|
+
export declare function isDataEqual(a: unknown, b: unknown): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undefined-tolerant deep equality for Puck data.
|
|
3
|
+
*
|
|
4
|
+
* Puck's mount-time resolve pass (`resolveAndCommitData`) can dispatch a `replace`
|
|
5
|
+
* even when the net content is unchanged — e.g. a resolved node differs from the
|
|
6
|
+
* loaded node only by an `undefined`-valued key. Strict deep-equal libraries such
|
|
7
|
+
* as `fast-equals` treat `{}` and `{ k: undefined }` as different, so that no-op
|
|
8
|
+
* resolve looks like a real edit and spuriously marks the document dirty.
|
|
9
|
+
*
|
|
10
|
+
* This comparison treats an absent key and an `undefined`-valued key as equal, so
|
|
11
|
+
* a resolve that only adds/removes `undefined` values compares equal to the loaded
|
|
12
|
+
* data. Object key order is irrelevant (as with any deep equal); array order is
|
|
13
|
+
* significant (Puck content order is meaningful).
|
|
14
|
+
*/ export function isDataEqual(a, b) {
|
|
15
|
+
if (Object.is(a, b)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
// NaN is handled by Object.is above; remaining primitives that differ are unequal.
|
|
19
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const aIsArray = Array.isArray(a);
|
|
23
|
+
const bIsArray = Array.isArray(b);
|
|
24
|
+
if (aIsArray !== bIsArray) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (aIsArray && bIsArray) {
|
|
28
|
+
if (a.length !== b.length) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
for(let i = 0; i < a.length; i++){
|
|
32
|
+
if (!isDataEqual(a[i], b[i])) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const aObj = a;
|
|
39
|
+
const bObj = b;
|
|
40
|
+
// Compare the union of keys, treating a missing key and an `undefined` value as
|
|
41
|
+
// equivalent so that `{}` and `{ k: undefined }` are considered equal.
|
|
42
|
+
const keys = new Set([
|
|
43
|
+
...Object.keys(aObj),
|
|
44
|
+
...Object.keys(bObj)
|
|
45
|
+
]);
|
|
46
|
+
for (const key of keys){
|
|
47
|
+
const aVal = aObj[key];
|
|
48
|
+
const bVal = bObj[key];
|
|
49
|
+
if (aVal === undefined && bVal === undefined) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!isDataEqual(aVal, bVal)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
package/dist/endpoints/styles.js
CHANGED
|
@@ -76,7 +76,7 @@ const cssCache = new Map();
|
|
|
76
76
|
* @param cssFilePath - Path to CSS file relative to project root
|
|
77
77
|
* @returns PayloadHandler that serves compiled CSS
|
|
78
78
|
*/ export function createStylesHandler(cssFilePath) {
|
|
79
|
-
return async ()=>{
|
|
79
|
+
return async (req)=>{
|
|
80
80
|
try {
|
|
81
81
|
const fullPath = join(process.cwd(), cssFilePath);
|
|
82
82
|
// Check if file exists
|
|
@@ -92,13 +92,30 @@ const cssCache = new Map();
|
|
|
92
92
|
// Get file modification time for cache invalidation
|
|
93
93
|
const stats = statSync(fullPath);
|
|
94
94
|
const mtime = stats.mtimeMs;
|
|
95
|
+
// ETag derived from mtime - changes whenever the source file changes,
|
|
96
|
+
// whether from a dev-mode edit or a version upgrade recompiling output.
|
|
97
|
+
const etag = `"${mtime}"`;
|
|
98
|
+
// If the browser's cached copy is still fresh, tell it so without
|
|
99
|
+
// doing any file read/compilation work.
|
|
100
|
+
const ifNoneMatch = req.headers?.get('if-none-match');
|
|
101
|
+
if (ifNoneMatch === etag) {
|
|
102
|
+
return new Response(null, {
|
|
103
|
+
status: 304,
|
|
104
|
+
headers: {
|
|
105
|
+
ETag: etag,
|
|
106
|
+
'Cache-Control': 'no-cache'
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
95
110
|
// Check cache
|
|
96
111
|
const cached = cssCache.get(cssFilePath);
|
|
97
112
|
if (cached && cached.mtime === mtime) {
|
|
98
113
|
return new Response(cached.css, {
|
|
99
114
|
headers: {
|
|
100
115
|
'Content-Type': 'text/css',
|
|
101
|
-
'Cache-Control': '
|
|
116
|
+
'Cache-Control': 'no-cache',
|
|
117
|
+
ETag: etag,
|
|
118
|
+
'Last-Modified': new Date(mtime).toUTCString(),
|
|
102
119
|
'X-Puck-Cache': 'hit'
|
|
103
120
|
}
|
|
104
121
|
});
|
|
@@ -114,7 +131,9 @@ const cssCache = new Map();
|
|
|
114
131
|
return new Response(compiledCss, {
|
|
115
132
|
headers: {
|
|
116
133
|
'Content-Type': 'text/css',
|
|
117
|
-
'Cache-Control': '
|
|
134
|
+
'Cache-Control': 'no-cache',
|
|
135
|
+
ETag: etag,
|
|
136
|
+
'Last-Modified': new Date(mtime).toUTCString(),
|
|
118
137
|
'X-Puck-Cache': 'miss'
|
|
119
138
|
}
|
|
120
139
|
});
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.7.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-version.js - do not edit manually
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.7.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delmaredigital/payload-puck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Puck visual page builder plugin for Payload CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -104,20 +104,22 @@
|
|
|
104
104
|
"scripts": {
|
|
105
105
|
"dev-publish": "pnpm build && pnpm version prerelease --preid=dev --no-git-tag-version && pnpm publish --registry http://localhost:4873 --no-git-checks --tag dev",
|
|
106
106
|
"generate-version": "node scripts/generate-version.js",
|
|
107
|
-
"
|
|
108
|
-
"build": "pnpm build:swc && pnpm build:types && pnpm copyfiles",
|
|
107
|
+
"build": "pnpm generate-version && pnpm build:swc && pnpm build:types && pnpm copyfiles",
|
|
109
108
|
"build:swc": "swc ./src -d ./dist --strip-leading-paths",
|
|
110
109
|
"build:types": "tsc --outDir dist",
|
|
111
110
|
"copyfiles": "copyfiles -u 1 \"src/**/*.css\" dist",
|
|
112
111
|
"dev": "pnpm generate-version && swc ./src -d ./dist --strip-leading-paths --watch",
|
|
113
|
-
"typecheck": "tsc --noEmit",
|
|
112
|
+
"typecheck": "pnpm generate-version && tsc --noEmit && tsc -p tsconfig.test.json",
|
|
113
|
+
"test": "pnpm generate-version && vitest run",
|
|
114
|
+
"test:watch": "pnpm generate-version && vitest",
|
|
115
|
+
"test:coverage": "pnpm generate-version && vitest run --coverage",
|
|
114
116
|
"clean": "rm -rf dist",
|
|
115
117
|
"prepublishOnly": "pnpm build"
|
|
116
118
|
},
|
|
117
119
|
"peerDependencies": {
|
|
118
120
|
"@payloadcms/next": ">=3.69.0",
|
|
119
121
|
"@payloadcms/ui": ">=3.69.0",
|
|
120
|
-
"@puckeditor/core": ">=0.
|
|
122
|
+
"@puckeditor/core": ">=0.23.0",
|
|
121
123
|
"@tailwindcss/postcss": ">=4.0.0",
|
|
122
124
|
"next": ">=15.4.8",
|
|
123
125
|
"payload": ">=3.69.0",
|
|
@@ -152,9 +154,9 @@
|
|
|
152
154
|
}
|
|
153
155
|
},
|
|
154
156
|
"dependencies": {
|
|
155
|
-
"@puckeditor/cloud-client": "^0.
|
|
156
|
-
"@puckeditor/plugin-ai": "^0.
|
|
157
|
-
"@puckeditor/plugin-heading-analyzer": "^0.
|
|
157
|
+
"@puckeditor/cloud-client": "^0.8.2",
|
|
158
|
+
"@puckeditor/plugin-ai": "^0.8.2",
|
|
159
|
+
"@puckeditor/plugin-heading-analyzer": "^0.23.0",
|
|
158
160
|
"@radix-ui/react-popover": "^1.1.15",
|
|
159
161
|
"@tiptap/core": "^3.20.1",
|
|
160
162
|
"@tiptap/extension-color": "^3.20.1",
|
|
@@ -171,20 +173,22 @@
|
|
|
171
173
|
"lucide-react": "^0.469.0"
|
|
172
174
|
},
|
|
173
175
|
"devDependencies": {
|
|
174
|
-
"@payloadcms/next": "^3.
|
|
175
|
-
"@payloadcms/ui": "^3.
|
|
176
|
-
"@puckeditor/core": "^0.
|
|
176
|
+
"@payloadcms/next": "^3.87.1",
|
|
177
|
+
"@payloadcms/ui": "^3.87.1",
|
|
178
|
+
"@puckeditor/core": "^0.23.0",
|
|
177
179
|
"@swc/cli": "^0.6.0",
|
|
178
180
|
"@swc/core": "^1.15.18",
|
|
179
181
|
"@types/node": "^24.12.0",
|
|
180
182
|
"@types/react": "^19.2.14",
|
|
181
183
|
"@types/react-dom": "^19.2.3",
|
|
184
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
182
185
|
"copyfiles": "^2.4.1",
|
|
183
|
-
"next": "^16.
|
|
184
|
-
"payload": "^3.
|
|
186
|
+
"next": "^16.3.0",
|
|
187
|
+
"payload": "^3.87.1",
|
|
185
188
|
"react": "^19.2.4",
|
|
186
189
|
"react-dom": "^19.2.4",
|
|
187
|
-
"typescript": "^5.9.3"
|
|
190
|
+
"typescript": "^5.9.3",
|
|
191
|
+
"vitest": "^3.2.7"
|
|
188
192
|
},
|
|
189
193
|
"keywords": [
|
|
190
194
|
"payload",
|
|
@@ -196,7 +200,7 @@
|
|
|
196
200
|
"cms"
|
|
197
201
|
],
|
|
198
202
|
"engines": {
|
|
199
|
-
"node": "
|
|
203
|
+
"node": ">=20.9.0"
|
|
200
204
|
},
|
|
201
205
|
"author": "Delmare Digital",
|
|
202
206
|
"repository": {
|