payload-section-builder 0.0.0-stage → 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 +21 -0
- package/README.md +128 -2
- package/dist/admin/SectionBridge.d.ts +9 -0
- package/dist/admin/SectionBridge.js +139 -0
- package/dist/admin/SectionBridge.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +3 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/overlay.d.ts +2 -0
- package/dist/exports/overlay.js +4 -0
- package/dist/exports/overlay.js.map +1 -0
- package/dist/exports/react.d.ts +2 -0
- package/dist/exports/react.js +4 -0
- package/dist/exports/react.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay/index.d.ts +16 -0
- package/dist/overlay/index.js +191 -0
- package/dist/overlay/index.js.map +1 -0
- package/dist/protocol.d.ts +43 -0
- package/dist/protocol.js +23 -0
- package/dist/protocol.js.map +1 -0
- package/dist/react/SectionOverlay.d.ts +8 -0
- package/dist/react/SectionOverlay.js +15 -0
- package/dist/react/SectionOverlay.js.map +1 -0
- package/package.json +119 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bram den Elzen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,129 @@
|
|
|
1
|
-
#
|
|
1
|
+
# payload-section-builder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Visual section builder for [Payload CMS](https://payloadcms.com). Editors click a section in the
|
|
4
|
+
live preview to edit it in the sidebar, and rearrange sections without leaving the preview.
|
|
5
|
+
|
|
6
|
+
> Status: phase 0 prototype. Selecting a section and moving it up or down work end to end.
|
|
7
|
+
> The section library, drag and drop and per-page-type rules follow in the next phases.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Payload admin (edit view) Preview iframe (your frontend)
|
|
13
|
+
┌──────────────────────────┐ live data ┌──────────────────────────────┐
|
|
14
|
+
│ Document form │ ──────────────▶ │ Sections with data attributes│
|
|
15
|
+
│ SectionBridge (UI field) │ ◀────────────── │ Overlay: outlines, toolbar │
|
|
16
|
+
└──────────────────────────┘ edit intents └──────────────────────────────┘
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- The **bridge** is mounted inside the document form as a UI field (it stores nothing). It applies
|
|
20
|
+
intents from the preview to the form with Payload's own form API, so drafts, validation and
|
|
21
|
+
autosave keep working.
|
|
22
|
+
- The **overlay** is framework-free. It only switches on inside the live preview after the admin
|
|
23
|
+
answers a handshake, so production pages are never affected.
|
|
24
|
+
- Messages carry a `source` and protocol `version`, and both sides check the origin.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
**Full guide for client projects: [docs/client-setup.md](docs/client-setup.md)** (install, your own sections, preview route, styling, troubleshooting).
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// payload.config.ts
|
|
33
|
+
import { sectionBuilder } from 'payload-section-builder'
|
|
34
|
+
|
|
35
|
+
export default buildConfig({
|
|
36
|
+
plugins: [
|
|
37
|
+
sectionBuilder({
|
|
38
|
+
collections: {
|
|
39
|
+
pages: { field: 'layout' }, // a top-level blocks field
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Preview page (React)
|
|
48
|
+
import { sectionAttributes, SectionOverlay } from 'payload-section-builder/react'
|
|
49
|
+
|
|
50
|
+
{data.layout?.map((section, index) => (
|
|
51
|
+
<section key={section.id} {...sectionAttributes(index, section.blockType)}>…</section>
|
|
52
|
+
))}
|
|
53
|
+
<SectionOverlay adminOrigin={process.env.NEXT_PUBLIC_SERVER_URL!} />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Not using React? Call `initSectionOverlay({ adminOrigin })` from `payload-section-builder/overlay`.
|
|
57
|
+
|
|
58
|
+
## Your sections, your styling
|
|
59
|
+
|
|
60
|
+
The plugin ships **no sections and no site styles**. Each client project owns both:
|
|
61
|
+
|
|
62
|
+
- **Sections** are ordinary Payload blocks in your own config. Use any fields you like.
|
|
63
|
+
- **Rendering** is your own components with your own CSS, Tailwind or anything else.
|
|
64
|
+
- **The only contract** is one spread on each section's root element: `{...sectionAttributes(index, block.blockType)}`.
|
|
65
|
+
This adds two `data-` attributes and changes nothing visually.
|
|
66
|
+
|
|
67
|
+
The plugin only handles the editing bridge between the preview and the admin. The one thing it draws is
|
|
68
|
+
the editor chrome (hover outline, selection outline, label and toolbar), and only inside the live preview.
|
|
69
|
+
To match your brand, override its CSS variables:
|
|
70
|
+
|
|
71
|
+
```css
|
|
72
|
+
:root {
|
|
73
|
+
--psb-accent: #0f766e;
|
|
74
|
+
--psb-accent-soft: rgba(15, 118, 110, .2);
|
|
75
|
+
--psb-toolbar-bg: #111;
|
|
76
|
+
--psb-font: 500 13px/1 'Inter', sans-serif;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You can also turn the default chrome styles off and style the `.psb-box`, `.psb-hover`, `.psb-selected`,
|
|
81
|
+
`.psb-label` and `.psb-toolbar` classes yourself: `<SectionOverlay adminOrigin={url} injectStyles={false} />`.
|
|
82
|
+
|
|
83
|
+
The sections in `dev/` exist only to exercise the plugin in the playground. They are not published.
|
|
84
|
+
|
|
85
|
+
## Entry points
|
|
86
|
+
|
|
87
|
+
| Import | Contents |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `payload-section-builder` | `sectionBuilder()` plugin, `sectionAttributes()` |
|
|
90
|
+
| `payload-section-builder/client` | `SectionBridge` admin component (referenced by the plugin) |
|
|
91
|
+
| `payload-section-builder/overlay` | Framework-free `initSectionOverlay()` |
|
|
92
|
+
| `payload-section-builder/react` | `<SectionOverlay />`, `sectionAttributes()` |
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
The `dev/` folder is a Payload + Next.js playground on SQLite (`dev/dev.db`, gitignored). It seeds a
|
|
97
|
+
user (`dev@payloadcms.com` / `test`) and a Home page with four sections.
|
|
98
|
+
|
|
99
|
+
Playground routes:
|
|
100
|
+
|
|
101
|
+
| Route | Shows |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `/` | The published page with slug `home` (`/home` redirects here) |
|
|
104
|
+
| `/[slug]` | Any other published page. Draft-only pages return 404 in production; in development they show a "not published yet" message. |
|
|
105
|
+
| `/preview/[id]` | Live preview, by document id, including unpublished drafts. Payload only shows the Live Preview button after a page has been saved once. |
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pnpm install
|
|
109
|
+
pnpm dev # http://localhost:3000/admin, open Pages > Home > Live Preview
|
|
110
|
+
pnpm typecheck
|
|
111
|
+
pnpm test:int # plugin config tests (vitest)
|
|
112
|
+
pnpm test:e2e # select and move a section in the browser (Playwright)
|
|
113
|
+
pnpm build # outputs dist/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Publishing
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm version patch # or minor; commit and tag
|
|
120
|
+
npm publish # the prepare script builds dist first
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`exports` already point at `dist`, so `npm publish` and `pnpm publish` produce the same package.
|
|
124
|
+
Check what will ship with `npm pack --dry-run`: every entry point needs a `.js` and a `.d.ts` file
|
|
125
|
+
in `dist/`.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type SectionBridgeProps = {
|
|
2
|
+
fieldPath: string;
|
|
3
|
+
previewOrigin?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Lives inside the document form. Receives intents from the preview overlay and
|
|
7
|
+
* applies them to the form, so drafts, validation and autosave keep working as usual.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SectionBridge: ({ fieldPath, previewOrigin }: SectionBridgeProps) => null;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useForm, useFormFields, useLivePreviewContext } from '@payloadcms/ui';
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { isEnvelope, wrap } from '../protocol.js';
|
|
5
|
+
const rowElementID = (path, index)=>`${path.split('.').join('-')}-row-${index}`;
|
|
6
|
+
/**
|
|
7
|
+
* Lives inside the document form. Receives intents from the preview overlay and
|
|
8
|
+
* applies them to the form, so drafts, validation and autosave keep working as usual.
|
|
9
|
+
*/ export const SectionBridge = ({ fieldPath, previewOrigin })=>{
|
|
10
|
+
const { dispatchFields, moveFieldRow } = useForm();
|
|
11
|
+
const rows = useFormFields(([fields])=>fields[fieldPath]?.rows);
|
|
12
|
+
const { iframeRef, loadedURL, popupRef, previewWindowType } = useLivePreviewContext();
|
|
13
|
+
const rowsRef = useRef([]);
|
|
14
|
+
rowsRef.current = rows ?? [];
|
|
15
|
+
const targetOrigin = previewOrigin ?? (loadedURL ? new URL(loadedURL).origin : undefined);
|
|
16
|
+
const getPreviewWindow = useCallback(()=>{
|
|
17
|
+
if (previewWindowType === 'popup') {
|
|
18
|
+
return popupRef?.current;
|
|
19
|
+
}
|
|
20
|
+
return iframeRef.current?.contentWindow;
|
|
21
|
+
}, [
|
|
22
|
+
iframeRef,
|
|
23
|
+
popupRef,
|
|
24
|
+
previewWindowType
|
|
25
|
+
]);
|
|
26
|
+
const send = useCallback((message)=>{
|
|
27
|
+
const target = getPreviewWindow();
|
|
28
|
+
if (target && targetOrigin) {
|
|
29
|
+
target.postMessage(wrap(message), targetOrigin);
|
|
30
|
+
}
|
|
31
|
+
}, [
|
|
32
|
+
getPreviewWindow,
|
|
33
|
+
targetOrigin
|
|
34
|
+
]);
|
|
35
|
+
const selectRow = useCallback((index)=>{
|
|
36
|
+
const current = rowsRef.current;
|
|
37
|
+
if (index < 0 || index >= current.length) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Collapse every other section so the sidebar only shows the selected one.
|
|
41
|
+
dispatchFields({
|
|
42
|
+
type: 'SET_ALL_ROWS_COLLAPSED',
|
|
43
|
+
path: fieldPath,
|
|
44
|
+
updatedRows: current.map((row, i)=>({
|
|
45
|
+
...row,
|
|
46
|
+
collapsed: i !== index
|
|
47
|
+
}))
|
|
48
|
+
});
|
|
49
|
+
requestAnimationFrame(()=>{
|
|
50
|
+
const element = document.getElementById(rowElementID(fieldPath, index));
|
|
51
|
+
element?.scrollIntoView({
|
|
52
|
+
behavior: 'smooth',
|
|
53
|
+
block: 'start'
|
|
54
|
+
});
|
|
55
|
+
const firstInput = element?.querySelector('.blocks-field__row input, .blocks-field__row textarea, input, textarea');
|
|
56
|
+
firstInput?.focus({
|
|
57
|
+
preventScroll: true
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}, [
|
|
61
|
+
dispatchFields,
|
|
62
|
+
fieldPath
|
|
63
|
+
]);
|
|
64
|
+
useEffect(()=>{
|
|
65
|
+
const onMessage = (event)=>{
|
|
66
|
+
if (!isEnvelope(event.data)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (targetOrigin && event.origin !== targetOrigin) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (event.source !== getPreviewWindow()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const message = event.data;
|
|
76
|
+
switch(message.type){
|
|
77
|
+
case 'move-block':
|
|
78
|
+
{
|
|
79
|
+
const count = rowsRef.current.length;
|
|
80
|
+
const { from, to } = message;
|
|
81
|
+
if (from === to || from < 0 || to < 0 || from >= count || to >= count) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
moveFieldRow({
|
|
85
|
+
moveFromIndex: from,
|
|
86
|
+
moveToIndex: to,
|
|
87
|
+
path: fieldPath
|
|
88
|
+
});
|
|
89
|
+
send({
|
|
90
|
+
type: 'selection-changed',
|
|
91
|
+
index: to
|
|
92
|
+
});
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'overlay-hello':
|
|
96
|
+
{
|
|
97
|
+
send({
|
|
98
|
+
type: 'editor-ready',
|
|
99
|
+
blockCount: rowsRef.current.length
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'select-block':
|
|
104
|
+
{
|
|
105
|
+
selectRow(message.index);
|
|
106
|
+
send({
|
|
107
|
+
type: 'selection-changed',
|
|
108
|
+
index: message.index
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
window.addEventListener('message', onMessage);
|
|
115
|
+
return ()=>window.removeEventListener('message', onMessage);
|
|
116
|
+
}, [
|
|
117
|
+
fieldPath,
|
|
118
|
+
getPreviewWindow,
|
|
119
|
+
moveFieldRow,
|
|
120
|
+
selectRow,
|
|
121
|
+
send,
|
|
122
|
+
targetOrigin
|
|
123
|
+
]);
|
|
124
|
+
// Announce ourselves whenever a (new) preview page finishes loading.
|
|
125
|
+
useEffect(()=>{
|
|
126
|
+
if (loadedURL) {
|
|
127
|
+
send({
|
|
128
|
+
type: 'editor-ready',
|
|
129
|
+
blockCount: rowsRef.current.length
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}, [
|
|
133
|
+
loadedURL,
|
|
134
|
+
send
|
|
135
|
+
]);
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=SectionBridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/SectionBridge.tsx"],"sourcesContent":["'use client'\n\nimport type { Row } from 'payload'\n\nimport { useForm, useFormFields, useLivePreviewContext } from '@payloadcms/ui'\nimport { useCallback, useEffect, useRef } from 'react'\n\nimport type { AdminToPreviewMessage, PreviewToAdminMessage } from '../protocol.js'\n\nimport { isEnvelope, wrap } from '../protocol.js'\n\nexport type SectionBridgeProps = {\n fieldPath: string\n previewOrigin?: string\n}\n\nconst rowElementID = (path: string, index: number) => `${path.split('.').join('-')}-row-${index}`\n\n/**\n * Lives inside the document form. Receives intents from the preview overlay and\n * applies them to the form, so drafts, validation and autosave keep working as usual.\n */\nexport const SectionBridge = ({ fieldPath, previewOrigin }: SectionBridgeProps) => {\n const { dispatchFields, moveFieldRow } = useForm()\n const rows = useFormFields(([fields]) => fields[fieldPath]?.rows) as Row[] | undefined\n const { iframeRef, loadedURL, popupRef, previewWindowType } = useLivePreviewContext()\n\n const rowsRef = useRef<Row[]>([])\n rowsRef.current = rows ?? []\n\n const targetOrigin = previewOrigin ?? (loadedURL ? new URL(loadedURL).origin : undefined)\n\n const getPreviewWindow = useCallback((): null | Window | undefined => {\n if (previewWindowType === 'popup') {\n return popupRef?.current\n }\n return iframeRef.current?.contentWindow\n }, [iframeRef, popupRef, previewWindowType])\n\n const send = useCallback(\n (message: AdminToPreviewMessage) => {\n const target = getPreviewWindow()\n if (target && targetOrigin) {\n target.postMessage(wrap(message), targetOrigin)\n }\n },\n [getPreviewWindow, targetOrigin],\n )\n\n const selectRow = useCallback(\n (index: number) => {\n const current = rowsRef.current\n if (index < 0 || index >= current.length) {\n return\n }\n\n // Collapse every other section so the sidebar only shows the selected one.\n dispatchFields({\n type: 'SET_ALL_ROWS_COLLAPSED',\n path: fieldPath,\n updatedRows: current.map((row, i) => ({ ...row, collapsed: i !== index })),\n })\n\n requestAnimationFrame(() => {\n const element = document.getElementById(rowElementID(fieldPath, index))\n element?.scrollIntoView({ behavior: 'smooth', block: 'start' })\n const firstInput = element?.querySelector<HTMLElement>(\n '.blocks-field__row input, .blocks-field__row textarea, input, textarea',\n )\n firstInput?.focus({ preventScroll: true })\n })\n },\n [dispatchFields, fieldPath],\n )\n\n useEffect(() => {\n const onMessage = (event: MessageEvent) => {\n if (!isEnvelope(event.data)) {\n return\n }\n if (targetOrigin && event.origin !== targetOrigin) {\n return\n }\n if (event.source !== getPreviewWindow()) {\n return\n }\n\n const message = event.data as unknown as PreviewToAdminMessage\n\n switch (message.type) {\n case 'move-block': {\n const count = rowsRef.current.length\n const { from, to } = message\n if (from === to || from < 0 || to < 0 || from >= count || to >= count) {\n return\n }\n moveFieldRow({ moveFromIndex: from, moveToIndex: to, path: fieldPath })\n send({ type: 'selection-changed', index: to })\n break\n }\n case 'overlay-hello': {\n send({ type: 'editor-ready', blockCount: rowsRef.current.length })\n break\n }\n case 'select-block': {\n selectRow(message.index)\n send({ type: 'selection-changed', index: message.index })\n break\n }\n }\n }\n\n window.addEventListener('message', onMessage)\n return () => window.removeEventListener('message', onMessage)\n }, [fieldPath, getPreviewWindow, moveFieldRow, selectRow, send, targetOrigin])\n\n // Announce ourselves whenever a (new) preview page finishes loading.\n useEffect(() => {\n if (loadedURL) {\n send({ type: 'editor-ready', blockCount: rowsRef.current.length })\n }\n }, [loadedURL, send])\n\n return null\n}\n"],"names":["useForm","useFormFields","useLivePreviewContext","useCallback","useEffect","useRef","isEnvelope","wrap","rowElementID","path","index","split","join","SectionBridge","fieldPath","previewOrigin","dispatchFields","moveFieldRow","rows","fields","iframeRef","loadedURL","popupRef","previewWindowType","rowsRef","current","targetOrigin","URL","origin","undefined","getPreviewWindow","contentWindow","send","message","target","postMessage","selectRow","length","type","updatedRows","map","row","i","collapsed","requestAnimationFrame","element","document","getElementById","scrollIntoView","behavior","block","firstInput","querySelector","focus","preventScroll","onMessage","event","data","source","count","from","to","moveFromIndex","moveToIndex","blockCount","window","addEventListener","removeEventListener"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SectionBridge } from '../admin/SectionBridge.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { SectionBridge } from '../admin/SectionBridge.js'\n"],"names":["SectionBridge"],"mappings":"AAAA,SAASA,aAAa,QAAQ,4BAA2B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/exports/overlay.ts"],"sourcesContent":["export { initSectionOverlay, type SectionOverlayOptions } from '../overlay/index.js'\nexport { sectionAttributes } from '../protocol.js'\n"],"names":["initSectionOverlay","sectionAttributes"],"mappings":"AAAA,SAASA,kBAAkB,QAAoC,sBAAqB;AACpF,SAASC,iBAAiB,QAAQ,iBAAgB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/exports/react.ts"],"sourcesContent":["export { SectionOverlay, type SectionOverlayProps } from '../react/SectionOverlay.js'\nexport { sectionAttributes } from '../protocol.js'\n"],"names":["SectionOverlay","sectionAttributes"],"mappings":"AAAA,SAASA,cAAc,QAAkC,6BAA4B;AACrF,SAASC,iBAAiB,QAAQ,iBAAgB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CollectionSlug, Config } from 'payload';
|
|
2
|
+
export type SectionBuilderCollectionOptions = {
|
|
3
|
+
/** Name of the top-level blocks field that holds the sections, e.g. `layout`. */
|
|
4
|
+
field: string;
|
|
5
|
+
};
|
|
6
|
+
export type SectionBuilderConfig = {
|
|
7
|
+
/**
|
|
8
|
+
* Collections that get the visual section builder, and which blocks field it drives.
|
|
9
|
+
* Known slugs autocomplete; any string is accepted so the config still type-checks
|
|
10
|
+
* before `payload generate:types` has picked up a newly added collection.
|
|
11
|
+
*/
|
|
12
|
+
collections: Partial<Record<CollectionSlug, SectionBuilderCollectionOptions>> & Record<string, SectionBuilderCollectionOptions | undefined>;
|
|
13
|
+
/** Turns the editor off without changing the schema. */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Origin of the preview frontend, e.g. `https://www.client.com`. Messages are only
|
|
17
|
+
* accepted from and sent to this origin. Defaults to the origin of the loaded preview URL.
|
|
18
|
+
*/
|
|
19
|
+
previewOrigin?: string;
|
|
20
|
+
};
|
|
21
|
+
export { sectionAttributes } from './protocol.js';
|
|
22
|
+
export declare const BRIDGE_FIELD_NAME = "_sectionBuilderBridge";
|
|
23
|
+
export declare const sectionBuilder: (options: SectionBuilderConfig) => (config: Config) => Config;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export { sectionAttributes } from './protocol.js';
|
|
2
|
+
export const BRIDGE_FIELD_NAME = '_sectionBuilderBridge';
|
|
3
|
+
export const sectionBuilder = (options)=>(config)=>{
|
|
4
|
+
if (options.disabled) {
|
|
5
|
+
return config;
|
|
6
|
+
}
|
|
7
|
+
for (const [slug, collectionOptions] of Object.entries(options.collections)){
|
|
8
|
+
if (!collectionOptions) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const collection = config.collections?.find((c)=>c.slug === slug);
|
|
12
|
+
if (!collection) {
|
|
13
|
+
throw new Error(`[section-builder] Collection "${slug}" was not found in the Payload config.`);
|
|
14
|
+
}
|
|
15
|
+
const blocksField = collection.fields.find((field)=>'name' in field && field.name === collectionOptions.field);
|
|
16
|
+
if (!blocksField || blocksField.type !== 'blocks') {
|
|
17
|
+
throw new Error(`[section-builder] "${slug}.${collectionOptions.field}" must be a top-level blocks field.`);
|
|
18
|
+
}
|
|
19
|
+
// A UI field stores nothing; it only mounts the bridge inside the document form,
|
|
20
|
+
// where it can reach the form state and the live preview window.
|
|
21
|
+
collection.fields.push({
|
|
22
|
+
name: BRIDGE_FIELD_NAME,
|
|
23
|
+
type: 'ui',
|
|
24
|
+
admin: {
|
|
25
|
+
components: {
|
|
26
|
+
Field: {
|
|
27
|
+
clientProps: {
|
|
28
|
+
fieldPath: collectionOptions.field,
|
|
29
|
+
previewOrigin: options.previewOrigin
|
|
30
|
+
},
|
|
31
|
+
path: 'payload-section-builder/client#SectionBridge'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { CollectionSlug, Config } from 'payload'\n\nexport type SectionBuilderCollectionOptions = {\n /** Name of the top-level blocks field that holds the sections, e.g. `layout`. */\n field: string\n}\n\nexport type SectionBuilderConfig = {\n /**\n * Collections that get the visual section builder, and which blocks field it drives.\n * Known slugs autocomplete; any string is accepted so the config still type-checks\n * before `payload generate:types` has picked up a newly added collection.\n */\n collections: Partial<Record<CollectionSlug, SectionBuilderCollectionOptions>> &\n Record<string, SectionBuilderCollectionOptions | undefined>\n /** Turns the editor off without changing the schema. */\n disabled?: boolean\n /**\n * Origin of the preview frontend, e.g. `https://www.client.com`. Messages are only\n * accepted from and sent to this origin. Defaults to the origin of the loaded preview URL.\n */\n previewOrigin?: string\n}\n\nexport { sectionAttributes } from './protocol.js'\n\nexport const BRIDGE_FIELD_NAME = '_sectionBuilderBridge'\n\nexport const sectionBuilder =\n (options: SectionBuilderConfig) =>\n (config: Config): Config => {\n if (options.disabled) {\n return config\n }\n\n for (const [slug, collectionOptions] of Object.entries(options.collections)) {\n if (!collectionOptions) {\n continue\n }\n\n const collection = config.collections?.find((c) => c.slug === slug)\n\n if (!collection) {\n throw new Error(`[section-builder] Collection \"${slug}\" was not found in the Payload config.`)\n }\n\n const blocksField = collection.fields.find(\n (field) => 'name' in field && field.name === collectionOptions.field,\n )\n\n if (!blocksField || blocksField.type !== 'blocks') {\n throw new Error(\n `[section-builder] \"${slug}.${collectionOptions.field}\" must be a top-level blocks field.`,\n )\n }\n\n // A UI field stores nothing; it only mounts the bridge inside the document form,\n // where it can reach the form state and the live preview window.\n collection.fields.push({\n name: BRIDGE_FIELD_NAME,\n type: 'ui',\n admin: {\n components: {\n Field: {\n clientProps: {\n fieldPath: collectionOptions.field,\n previewOrigin: options.previewOrigin,\n },\n path: 'payload-section-builder/client#SectionBridge',\n },\n },\n },\n })\n }\n\n return config\n }\n"],"names":["sectionAttributes","BRIDGE_FIELD_NAME","sectionBuilder","options","config","disabled","slug","collectionOptions","Object","entries","collections","collection","find","c","Error","blocksField","fields","field","name","type","push","admin","components","Field","clientProps","fieldPath","previewOrigin","path"],"mappings":"AAwBA,SAASA,iBAAiB,QAAQ,gBAAe;AAEjD,OAAO,MAAMC,oBAAoB,wBAAuB;AAExD,OAAO,MAAMC,iBACX,CAACC,UACD,CAACC;QACC,IAAID,QAAQE,QAAQ,EAAE;YACpB,OAAOD;QACT;QAEA,KAAK,MAAM,CAACE,MAAMC,kBAAkB,IAAIC,OAAOC,OAAO,CAACN,QAAQO,WAAW,EAAG;YAC3E,IAAI,CAACH,mBAAmB;gBACtB;YACF;YAEA,MAAMI,aAAaP,OAAOM,WAAW,EAAEE,KAAK,CAACC,IAAMA,EAAEP,IAAI,KAAKA;YAE9D,IAAI,CAACK,YAAY;gBACf,MAAM,IAAIG,MAAM,CAAC,8BAA8B,EAAER,KAAK,sCAAsC,CAAC;YAC/F;YAEA,MAAMS,cAAcJ,WAAWK,MAAM,CAACJ,IAAI,CACxC,CAACK,QAAU,UAAUA,SAASA,MAAMC,IAAI,KAAKX,kBAAkBU,KAAK;YAGtE,IAAI,CAACF,eAAeA,YAAYI,IAAI,KAAK,UAAU;gBACjD,MAAM,IAAIL,MACR,CAAC,mBAAmB,EAAER,KAAK,CAAC,EAAEC,kBAAkBU,KAAK,CAAC,mCAAmC,CAAC;YAE9F;YAEA,iFAAiF;YACjF,iEAAiE;YACjEN,WAAWK,MAAM,CAACI,IAAI,CAAC;gBACrBF,MAAMjB;gBACNkB,MAAM;gBACNE,OAAO;oBACLC,YAAY;wBACVC,OAAO;4BACLC,aAAa;gCACXC,WAAWlB,kBAAkBU,KAAK;gCAClCS,eAAevB,QAAQuB,aAAa;4BACtC;4BACAC,MAAM;wBACR;oBACF;gBACF;YACF;QACF;QAEA,OAAOvB;IACT,EAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SectionOverlayOptions = {
|
|
2
|
+
/** Origin of the Payload admin, e.g. `https://cms.client.com`. Required for security. */
|
|
3
|
+
adminOrigin: string;
|
|
4
|
+
/**
|
|
5
|
+
* Inject the default editor chrome styles. Set to `false` to style the `.psb-*`
|
|
6
|
+
* classes yourself. Default: `true`.
|
|
7
|
+
*/
|
|
8
|
+
injectStyles?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Framework-free overlay for the preview page. Does nothing unless the page is
|
|
12
|
+
* inside the Payload live preview AND the admin has answered the handshake.
|
|
13
|
+
* Returns a cleanup function.
|
|
14
|
+
*/
|
|
15
|
+
export declare function initSectionOverlay({ adminOrigin, injectStyles, }: SectionOverlayOptions): () => void;
|
|
16
|
+
export { sectionAttributes } from '../protocol.js';
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { isEnvelope, SECTION_INDEX_ATTR, SECTION_TYPE_ATTR, wrap } from '../protocol.js';
|
|
2
|
+
const STYLE_ID = 'payload-section-builder-overlay-style';
|
|
3
|
+
/**
|
|
4
|
+
* Editor chrome only (outlines, label, toolbar), never your site's sections.
|
|
5
|
+
* Colours and fonts are CSS variables, so a client project can restyle the chrome
|
|
6
|
+
* without touching the plugin, or pass `injectStyles: false` and style the
|
|
7
|
+
* `.psb-*` classes itself.
|
|
8
|
+
*/ const CSS = `
|
|
9
|
+
:root {
|
|
10
|
+
--psb-accent: #3b5bdb;
|
|
11
|
+
--psb-accent-soft: rgba(59, 91, 219, .18);
|
|
12
|
+
--psb-on-accent: #fff;
|
|
13
|
+
--psb-toolbar-bg: #1c2333;
|
|
14
|
+
--psb-toolbar-fg: #fff;
|
|
15
|
+
--psb-focus: #91a7ff;
|
|
16
|
+
--psb-font: 500 13px/1 system-ui, sans-serif;
|
|
17
|
+
--psb-z: 2147483000;
|
|
18
|
+
}
|
|
19
|
+
.psb-box { position: fixed; pointer-events: none; z-index: var(--psb-z); border-radius: 4px; transition: opacity .12s; }
|
|
20
|
+
.psb-hover { outline: 2px dashed var(--psb-accent); outline-offset: -2px; }
|
|
21
|
+
.psb-selected { outline: 2px solid var(--psb-accent); outline-offset: -2px; box-shadow: 0 0 0 4px var(--psb-accent-soft); }
|
|
22
|
+
.psb-label { position: absolute; top: 0; left: 0; transform: translateY(-100%); background: var(--psb-accent);
|
|
23
|
+
color: var(--psb-on-accent); font: var(--psb-font); font-weight: 600; padding: 5px 8px; border-radius: 4px 4px 0 0; white-space: nowrap; }
|
|
24
|
+
.psb-toolbar { position: fixed; z-index: calc(var(--psb-z) + 1); display: flex; gap: 2px; background: var(--psb-toolbar-bg);
|
|
25
|
+
border-radius: 6px; padding: 3px; box-shadow: 0 4px 14px rgba(0,0,0,.25); }
|
|
26
|
+
.psb-toolbar button { all: unset; cursor: pointer; color: var(--psb-toolbar-fg); font: var(--psb-font); padding: 6px 9px; border-radius: 4px; }
|
|
27
|
+
.psb-toolbar button:hover { background: rgba(255,255,255,.14); }
|
|
28
|
+
.psb-toolbar button:focus-visible { outline: 2px solid var(--psb-focus); }
|
|
29
|
+
.psb-toolbar button[disabled] { opacity: .35; cursor: default; }
|
|
30
|
+
`;
|
|
31
|
+
const humanize = (blockType)=>blockType.replace(/[-_]/g, ' ').replace(/^\w/, (c)=>c.toUpperCase());
|
|
32
|
+
/**
|
|
33
|
+
* Framework-free overlay for the preview page. Does nothing unless the page is
|
|
34
|
+
* inside the Payload live preview AND the admin has answered the handshake.
|
|
35
|
+
* Returns a cleanup function.
|
|
36
|
+
*/ export function initSectionOverlay({ adminOrigin, injectStyles = true }) {
|
|
37
|
+
if (typeof window === 'undefined' || window.parent === window) {
|
|
38
|
+
return ()=>{};
|
|
39
|
+
}
|
|
40
|
+
const parentWindow = window.parent;
|
|
41
|
+
let active = false;
|
|
42
|
+
let blockCount = 0;
|
|
43
|
+
let hovered = null;
|
|
44
|
+
let selected = null;
|
|
45
|
+
let frame = 0;
|
|
46
|
+
const post = (message)=>parentWindow.postMessage(wrap(message), adminOrigin);
|
|
47
|
+
const style = document.createElement('style');
|
|
48
|
+
style.id = STYLE_ID;
|
|
49
|
+
style.textContent = CSS;
|
|
50
|
+
const hoverBox = document.createElement('div');
|
|
51
|
+
hoverBox.className = 'psb-box psb-hover';
|
|
52
|
+
const hoverLabel = document.createElement('div');
|
|
53
|
+
hoverLabel.className = 'psb-label';
|
|
54
|
+
hoverBox.append(hoverLabel);
|
|
55
|
+
const selectedBox = document.createElement('div');
|
|
56
|
+
selectedBox.className = 'psb-box psb-selected';
|
|
57
|
+
const toolbar = document.createElement('div');
|
|
58
|
+
toolbar.className = 'psb-toolbar';
|
|
59
|
+
toolbar.setAttribute('role', 'toolbar');
|
|
60
|
+
toolbar.setAttribute('aria-label', 'Section actions');
|
|
61
|
+
const upButton = document.createElement('button');
|
|
62
|
+
upButton.type = 'button';
|
|
63
|
+
upButton.textContent = '↑ Move up';
|
|
64
|
+
const downButton = document.createElement('button');
|
|
65
|
+
downButton.type = 'button';
|
|
66
|
+
downButton.textContent = '↓ Move down';
|
|
67
|
+
toolbar.append(upButton, downButton);
|
|
68
|
+
const sectionAt = (index)=>document.querySelector(`[${SECTION_INDEX_ATTR}="${index}"]`);
|
|
69
|
+
const sectionFrom = (target)=>target instanceof Element ? target.closest(`[${SECTION_INDEX_ATTR}]`) : null;
|
|
70
|
+
const indexOf = (element)=>Number(element.getAttribute(SECTION_INDEX_ATTR));
|
|
71
|
+
const place = (box, element)=>{
|
|
72
|
+
if (!element) {
|
|
73
|
+
box.style.opacity = '0';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const rect = element.getBoundingClientRect();
|
|
77
|
+
box.style.opacity = '1';
|
|
78
|
+
box.style.top = `${rect.top}px`;
|
|
79
|
+
box.style.left = `${rect.left}px`;
|
|
80
|
+
box.style.width = `${rect.width}px`;
|
|
81
|
+
box.style.height = `${rect.height}px`;
|
|
82
|
+
};
|
|
83
|
+
const render = ()=>{
|
|
84
|
+
const selectedElement = selected === null ? null : sectionAt(selected);
|
|
85
|
+
place(hoverBox, hovered && hovered !== selectedElement ? hovered : null);
|
|
86
|
+
place(selectedBox, selectedElement);
|
|
87
|
+
if (selectedElement && selected !== null) {
|
|
88
|
+
const rect = selectedElement.getBoundingClientRect();
|
|
89
|
+
toolbar.style.display = 'flex';
|
|
90
|
+
toolbar.style.top = `${Math.max(rect.top + 8, 8)}px`;
|
|
91
|
+
toolbar.style.left = `${Math.max(rect.right - toolbar.offsetWidth - 8, 8)}px`;
|
|
92
|
+
upButton.disabled = selected === 0;
|
|
93
|
+
downButton.disabled = selected >= blockCount - 1;
|
|
94
|
+
} else {
|
|
95
|
+
toolbar.style.display = 'none';
|
|
96
|
+
}
|
|
97
|
+
frame = requestAnimationFrame(render);
|
|
98
|
+
};
|
|
99
|
+
const activate = ()=>{
|
|
100
|
+
if (active) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
active = true;
|
|
104
|
+
document.documentElement.dataset.sectionBuilder = 'on';
|
|
105
|
+
if (injectStyles && !document.getElementById(STYLE_ID)) {
|
|
106
|
+
document.head.append(style);
|
|
107
|
+
}
|
|
108
|
+
document.body.append(hoverBox, selectedBox, toolbar);
|
|
109
|
+
frame = requestAnimationFrame(render);
|
|
110
|
+
};
|
|
111
|
+
const onMessage = (event)=>{
|
|
112
|
+
if (event.origin !== adminOrigin || event.source !== parentWindow || !isEnvelope(event.data)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const message = event.data;
|
|
116
|
+
if (message.type === 'editor-ready') {
|
|
117
|
+
blockCount = message.blockCount;
|
|
118
|
+
activate();
|
|
119
|
+
} else if (message.type === 'selection-changed') {
|
|
120
|
+
selected = message.index;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const onPointerOver = (event)=>{
|
|
124
|
+
if (active) {
|
|
125
|
+
hovered = sectionFrom(event.target);
|
|
126
|
+
if (hovered) {
|
|
127
|
+
hoverLabel.textContent = humanize(hovered.getAttribute(SECTION_TYPE_ATTR) ?? 'Section');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const onPointerLeave = ()=>{
|
|
132
|
+
hovered = null;
|
|
133
|
+
};
|
|
134
|
+
const onClick = (event)=>{
|
|
135
|
+
if (!active || toolbar.contains(event.target)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const section = sectionFrom(event.target);
|
|
139
|
+
if (!section) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Editing mode: clicks select sections instead of following links.
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
event.stopPropagation();
|
|
145
|
+
selected = indexOf(section);
|
|
146
|
+
post({
|
|
147
|
+
type: 'select-block',
|
|
148
|
+
index: selected
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
const move = (delta)=>{
|
|
152
|
+
if (selected === null) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const to = selected + delta;
|
|
156
|
+
if (to < 0 || to >= blockCount) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
post({
|
|
160
|
+
type: 'move-block',
|
|
161
|
+
from: selected,
|
|
162
|
+
to
|
|
163
|
+
});
|
|
164
|
+
selected = to; // optimistic; the admin confirms with selection-changed
|
|
165
|
+
};
|
|
166
|
+
upButton.addEventListener('click', ()=>move(-1));
|
|
167
|
+
downButton.addEventListener('click', ()=>move(1));
|
|
168
|
+
window.addEventListener('message', onMessage);
|
|
169
|
+
document.addEventListener('pointerover', onPointerOver);
|
|
170
|
+
document.documentElement.addEventListener('pointerleave', onPointerLeave);
|
|
171
|
+
document.addEventListener('click', onClick, true);
|
|
172
|
+
// Ask the admin to identify itself; it answers with editor-ready.
|
|
173
|
+
post({
|
|
174
|
+
type: 'overlay-hello'
|
|
175
|
+
});
|
|
176
|
+
return ()=>{
|
|
177
|
+
cancelAnimationFrame(frame);
|
|
178
|
+
window.removeEventListener('message', onMessage);
|
|
179
|
+
document.removeEventListener('pointerover', onPointerOver);
|
|
180
|
+
document.documentElement.removeEventListener('pointerleave', onPointerLeave);
|
|
181
|
+
document.removeEventListener('click', onClick, true);
|
|
182
|
+
hoverBox.remove();
|
|
183
|
+
selectedBox.remove();
|
|
184
|
+
toolbar.remove();
|
|
185
|
+
style.remove();
|
|
186
|
+
delete document.documentElement.dataset.sectionBuilder;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export { sectionAttributes } from '../protocol.js';
|
|
190
|
+
|
|
191
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/overlay/index.ts"],"sourcesContent":["import type { AdminToPreviewMessage, PreviewToAdminMessage } from '../protocol.js'\n\nimport { isEnvelope, SECTION_INDEX_ATTR, SECTION_TYPE_ATTR, wrap } from '../protocol.js'\n\nexport type SectionOverlayOptions = {\n /** Origin of the Payload admin, e.g. `https://cms.client.com`. Required for security. */\n adminOrigin: string\n /**\n * Inject the default editor chrome styles. Set to `false` to style the `.psb-*`\n * classes yourself. Default: `true`.\n */\n injectStyles?: boolean\n}\n\nconst STYLE_ID = 'payload-section-builder-overlay-style'\n\n/**\n * Editor chrome only (outlines, label, toolbar), never your site's sections.\n * Colours and fonts are CSS variables, so a client project can restyle the chrome\n * without touching the plugin, or pass `injectStyles: false` and style the\n * `.psb-*` classes itself.\n */\nconst CSS = `\n:root {\n --psb-accent: #3b5bdb;\n --psb-accent-soft: rgba(59, 91, 219, .18);\n --psb-on-accent: #fff;\n --psb-toolbar-bg: #1c2333;\n --psb-toolbar-fg: #fff;\n --psb-focus: #91a7ff;\n --psb-font: 500 13px/1 system-ui, sans-serif;\n --psb-z: 2147483000;\n}\n.psb-box { position: fixed; pointer-events: none; z-index: var(--psb-z); border-radius: 4px; transition: opacity .12s; }\n.psb-hover { outline: 2px dashed var(--psb-accent); outline-offset: -2px; }\n.psb-selected { outline: 2px solid var(--psb-accent); outline-offset: -2px; box-shadow: 0 0 0 4px var(--psb-accent-soft); }\n.psb-label { position: absolute; top: 0; left: 0; transform: translateY(-100%); background: var(--psb-accent);\n color: var(--psb-on-accent); font: var(--psb-font); font-weight: 600; padding: 5px 8px; border-radius: 4px 4px 0 0; white-space: nowrap; }\n.psb-toolbar { position: fixed; z-index: calc(var(--psb-z) + 1); display: flex; gap: 2px; background: var(--psb-toolbar-bg);\n border-radius: 6px; padding: 3px; box-shadow: 0 4px 14px rgba(0,0,0,.25); }\n.psb-toolbar button { all: unset; cursor: pointer; color: var(--psb-toolbar-fg); font: var(--psb-font); padding: 6px 9px; border-radius: 4px; }\n.psb-toolbar button:hover { background: rgba(255,255,255,.14); }\n.psb-toolbar button:focus-visible { outline: 2px solid var(--psb-focus); }\n.psb-toolbar button[disabled] { opacity: .35; cursor: default; }\n`\n\nconst humanize = (blockType: string) =>\n blockType.replace(/[-_]/g, ' ').replace(/^\\w/, (c) => c.toUpperCase())\n\n/**\n * Framework-free overlay for the preview page. Does nothing unless the page is\n * inside the Payload live preview AND the admin has answered the handshake.\n * Returns a cleanup function.\n */\nexport function initSectionOverlay({\n adminOrigin,\n injectStyles = true,\n}: SectionOverlayOptions): () => void {\n if (typeof window === 'undefined' || window.parent === window) {\n return () => {}\n }\n\n const parentWindow = window.parent\n let active = false\n let blockCount = 0\n let hovered: HTMLElement | null = null\n let selected: null | number = null\n let frame = 0\n\n const post = (message: PreviewToAdminMessage) => parentWindow.postMessage(wrap(message), adminOrigin)\n\n const style = document.createElement('style')\n style.id = STYLE_ID\n style.textContent = CSS\n\n const hoverBox = document.createElement('div')\n hoverBox.className = 'psb-box psb-hover'\n const hoverLabel = document.createElement('div')\n hoverLabel.className = 'psb-label'\n hoverBox.append(hoverLabel)\n\n const selectedBox = document.createElement('div')\n selectedBox.className = 'psb-box psb-selected'\n\n const toolbar = document.createElement('div')\n toolbar.className = 'psb-toolbar'\n toolbar.setAttribute('role', 'toolbar')\n toolbar.setAttribute('aria-label', 'Section actions')\n const upButton = document.createElement('button')\n upButton.type = 'button'\n upButton.textContent = '↑ Move up'\n const downButton = document.createElement('button')\n downButton.type = 'button'\n downButton.textContent = '↓ Move down'\n toolbar.append(upButton, downButton)\n\n const sectionAt = (index: number) =>\n document.querySelector<HTMLElement>(`[${SECTION_INDEX_ATTR}=\"${index}\"]`)\n\n const sectionFrom = (target: EventTarget | null) =>\n target instanceof Element ? target.closest<HTMLElement>(`[${SECTION_INDEX_ATTR}]`) : null\n\n const indexOf = (element: HTMLElement) => Number(element.getAttribute(SECTION_INDEX_ATTR))\n\n const place = (box: HTMLElement, element: HTMLElement | null) => {\n if (!element) {\n box.style.opacity = '0'\n return\n }\n const rect = element.getBoundingClientRect()\n box.style.opacity = '1'\n box.style.top = `${rect.top}px`\n box.style.left = `${rect.left}px`\n box.style.width = `${rect.width}px`\n box.style.height = `${rect.height}px`\n }\n\n const render = () => {\n const selectedElement = selected === null ? null : sectionAt(selected)\n place(hoverBox, hovered && hovered !== selectedElement ? hovered : null)\n place(selectedBox, selectedElement)\n\n if (selectedElement && selected !== null) {\n const rect = selectedElement.getBoundingClientRect()\n toolbar.style.display = 'flex'\n toolbar.style.top = `${Math.max(rect.top + 8, 8)}px`\n toolbar.style.left = `${Math.max(rect.right - toolbar.offsetWidth - 8, 8)}px`\n upButton.disabled = selected === 0\n downButton.disabled = selected >= blockCount - 1\n } else {\n toolbar.style.display = 'none'\n }\n\n frame = requestAnimationFrame(render)\n }\n\n const activate = () => {\n if (active) {\n return\n }\n active = true\n document.documentElement.dataset.sectionBuilder = 'on'\n if (injectStyles && !document.getElementById(STYLE_ID)) {\n document.head.append(style)\n }\n document.body.append(hoverBox, selectedBox, toolbar)\n frame = requestAnimationFrame(render)\n }\n\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== adminOrigin || event.source !== parentWindow || !isEnvelope(event.data)) {\n return\n }\n const message = event.data as unknown as AdminToPreviewMessage\n if (message.type === 'editor-ready') {\n blockCount = message.blockCount\n activate()\n } else if (message.type === 'selection-changed') {\n selected = message.index\n }\n }\n\n const onPointerOver = (event: PointerEvent) => {\n if (active) {\n hovered = sectionFrom(event.target)\n if (hovered) {\n hoverLabel.textContent = humanize(hovered.getAttribute(SECTION_TYPE_ATTR) ?? 'Section')\n }\n }\n }\n\n const onPointerLeave = () => {\n hovered = null\n }\n\n const onClick = (event: MouseEvent) => {\n if (!active || toolbar.contains(event.target as Node)) {\n return\n }\n const section = sectionFrom(event.target)\n if (!section) {\n return\n }\n // Editing mode: clicks select sections instead of following links.\n event.preventDefault()\n event.stopPropagation()\n selected = indexOf(section)\n post({ type: 'select-block', index: selected })\n }\n\n const move = (delta: -1 | 1) => {\n if (selected === null) {\n return\n }\n const to = selected + delta\n if (to < 0 || to >= blockCount) {\n return\n }\n post({ type: 'move-block', from: selected, to })\n selected = to // optimistic; the admin confirms with selection-changed\n }\n\n upButton.addEventListener('click', () => move(-1))\n downButton.addEventListener('click', () => move(1))\n\n window.addEventListener('message', onMessage)\n document.addEventListener('pointerover', onPointerOver)\n document.documentElement.addEventListener('pointerleave', onPointerLeave)\n document.addEventListener('click', onClick, true)\n\n // Ask the admin to identify itself; it answers with editor-ready.\n post({ type: 'overlay-hello' })\n\n return () => {\n cancelAnimationFrame(frame)\n window.removeEventListener('message', onMessage)\n document.removeEventListener('pointerover', onPointerOver)\n document.documentElement.removeEventListener('pointerleave', onPointerLeave)\n document.removeEventListener('click', onClick, true)\n hoverBox.remove()\n selectedBox.remove()\n toolbar.remove()\n style.remove()\n delete document.documentElement.dataset.sectionBuilder\n }\n}\n\nexport { sectionAttributes } from '../protocol.js'\n"],"names":["isEnvelope","SECTION_INDEX_ATTR","SECTION_TYPE_ATTR","wrap","STYLE_ID","CSS","humanize","blockType","replace","c","toUpperCase","initSectionOverlay","adminOrigin","injectStyles","window","parent","parentWindow","active","blockCount","hovered","selected","frame","post","message","postMessage","style","document","createElement","id","textContent","hoverBox","className","hoverLabel","append","selectedBox","toolbar","setAttribute","upButton","type","downButton","sectionAt","index","querySelector","sectionFrom","target","Element","closest","indexOf","element","Number","getAttribute","place","box","opacity","rect","getBoundingClientRect","top","left","width","height","render","selectedElement","display","Math","max","right","offsetWidth","disabled","requestAnimationFrame","activate","documentElement","dataset","sectionBuilder","getElementById","head","body","onMessage","event","origin","source","data","onPointerOver","onPointerLeave","onClick","contains","section","preventDefault","stopPropagation","move","delta","to","from","addEventListener","cancelAnimationFrame","removeEventListener","remove","sectionAttributes"],"mappings":"AAEA,SAASA,UAAU,EAAEC,kBAAkB,EAAEC,iBAAiB,EAAEC,IAAI,QAAQ,iBAAgB;AAYxF,MAAMC,WAAW;AAEjB;;;;;CAKC,GACD,MAAMC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;AAsBb,CAAC;AAED,MAAMC,WAAW,CAACC,YAChBA,UAAUC,OAAO,CAAC,SAAS,KAAKA,OAAO,CAAC,OAAO,CAACC,IAAMA,EAAEC,WAAW;AAErE;;;;CAIC,GACD,OAAO,SAASC,mBAAmB,EACjCC,WAAW,EACXC,eAAe,IAAI,EACG;IACtB,IAAI,OAAOC,WAAW,eAAeA,OAAOC,MAAM,KAAKD,QAAQ;QAC7D,OAAO,KAAO;IAChB;IAEA,MAAME,eAAeF,OAAOC,MAAM;IAClC,IAAIE,SAAS;IACb,IAAIC,aAAa;IACjB,IAAIC,UAA8B;IAClC,IAAIC,WAA0B;IAC9B,IAAIC,QAAQ;IAEZ,MAAMC,OAAO,CAACC,UAAmCP,aAAaQ,WAAW,CAACrB,KAAKoB,UAAUX;IAEzF,MAAMa,QAAQC,SAASC,aAAa,CAAC;IACrCF,MAAMG,EAAE,GAAGxB;IACXqB,MAAMI,WAAW,GAAGxB;IAEpB,MAAMyB,WAAWJ,SAASC,aAAa,CAAC;IACxCG,SAASC,SAAS,GAAG;IACrB,MAAMC,aAAaN,SAASC,aAAa,CAAC;IAC1CK,WAAWD,SAAS,GAAG;IACvBD,SAASG,MAAM,CAACD;IAEhB,MAAME,cAAcR,SAASC,aAAa,CAAC;IAC3CO,YAAYH,SAAS,GAAG;IAExB,MAAMI,UAAUT,SAASC,aAAa,CAAC;IACvCQ,QAAQJ,SAAS,GAAG;IACpBI,QAAQC,YAAY,CAAC,QAAQ;IAC7BD,QAAQC,YAAY,CAAC,cAAc;IACnC,MAAMC,WAAWX,SAASC,aAAa,CAAC;IACxCU,SAASC,IAAI,GAAG;IAChBD,SAASR,WAAW,GAAG;IACvB,MAAMU,aAAab,SAASC,aAAa,CAAC;IAC1CY,WAAWD,IAAI,GAAG;IAClBC,WAAWV,WAAW,GAAG;IACzBM,QAAQF,MAAM,CAACI,UAAUE;IAEzB,MAAMC,YAAY,CAACC,QACjBf,SAASgB,aAAa,CAAc,CAAC,CAAC,EAAEzC,mBAAmB,EAAE,EAAEwC,MAAM,EAAE,CAAC;IAE1E,MAAME,cAAc,CAACC,SACnBA,kBAAkBC,UAAUD,OAAOE,OAAO,CAAc,CAAC,CAAC,EAAE7C,mBAAmB,CAAC,CAAC,IAAI;IAEvF,MAAM8C,UAAU,CAACC,UAAyBC,OAAOD,QAAQE,YAAY,CAACjD;IAEtE,MAAMkD,QAAQ,CAACC,KAAkBJ;QAC/B,IAAI,CAACA,SAAS;YACZI,IAAI3B,KAAK,CAAC4B,OAAO,GAAG;YACpB;QACF;QACA,MAAMC,OAAON,QAAQO,qBAAqB;QAC1CH,IAAI3B,KAAK,CAAC4B,OAAO,GAAG;QACpBD,IAAI3B,KAAK,CAAC+B,GAAG,GAAG,GAAGF,KAAKE,GAAG,CAAC,EAAE,CAAC;QAC/BJ,IAAI3B,KAAK,CAACgC,IAAI,GAAG,GAAGH,KAAKG,IAAI,CAAC,EAAE,CAAC;QACjCL,IAAI3B,KAAK,CAACiC,KAAK,GAAG,GAAGJ,KAAKI,KAAK,CAAC,EAAE,CAAC;QACnCN,IAAI3B,KAAK,CAACkC,MAAM,GAAG,GAAGL,KAAKK,MAAM,CAAC,EAAE,CAAC;IACvC;IAEA,MAAMC,SAAS;QACb,MAAMC,kBAAkBzC,aAAa,OAAO,OAAOoB,UAAUpB;QAC7D+B,MAAMrB,UAAUX,WAAWA,YAAY0C,kBAAkB1C,UAAU;QACnEgC,MAAMjB,aAAa2B;QAEnB,IAAIA,mBAAmBzC,aAAa,MAAM;YACxC,MAAMkC,OAAOO,gBAAgBN,qBAAqB;YAClDpB,QAAQV,KAAK,CAACqC,OAAO,GAAG;YACxB3B,QAAQV,KAAK,CAAC+B,GAAG,GAAG,GAAGO,KAAKC,GAAG,CAACV,KAAKE,GAAG,GAAG,GAAG,GAAG,EAAE,CAAC;YACpDrB,QAAQV,KAAK,CAACgC,IAAI,GAAG,GAAGM,KAAKC,GAAG,CAACV,KAAKW,KAAK,GAAG9B,QAAQ+B,WAAW,GAAG,GAAG,GAAG,EAAE,CAAC;YAC7E7B,SAAS8B,QAAQ,GAAG/C,aAAa;YACjCmB,WAAW4B,QAAQ,GAAG/C,YAAYF,aAAa;QACjD,OAAO;YACLiB,QAAQV,KAAK,CAACqC,OAAO,GAAG;QAC1B;QAEAzC,QAAQ+C,sBAAsBR;IAChC;IAEA,MAAMS,WAAW;QACf,IAAIpD,QAAQ;YACV;QACF;QACAA,SAAS;QACTS,SAAS4C,eAAe,CAACC,OAAO,CAACC,cAAc,GAAG;QAClD,IAAI3D,gBAAgB,CAACa,SAAS+C,cAAc,CAACrE,WAAW;YACtDsB,SAASgD,IAAI,CAACzC,MAAM,CAACR;QACvB;QACAC,SAASiD,IAAI,CAAC1C,MAAM,CAACH,UAAUI,aAAaC;QAC5Cd,QAAQ+C,sBAAsBR;IAChC;IAEA,MAAMgB,YAAY,CAACC;QACjB,IAAIA,MAAMC,MAAM,KAAKlE,eAAeiE,MAAME,MAAM,KAAK/D,gBAAgB,CAAChB,WAAW6E,MAAMG,IAAI,GAAG;YAC5F;QACF;QACA,MAAMzD,UAAUsD,MAAMG,IAAI;QAC1B,IAAIzD,QAAQe,IAAI,KAAK,gBAAgB;YACnCpB,aAAaK,QAAQL,UAAU;YAC/BmD;QACF,OAAO,IAAI9C,QAAQe,IAAI,KAAK,qBAAqB;YAC/ClB,WAAWG,QAAQkB,KAAK;QAC1B;IACF;IAEA,MAAMwC,gBAAgB,CAACJ;QACrB,IAAI5D,QAAQ;YACVE,UAAUwB,YAAYkC,MAAMjC,MAAM;YAClC,IAAIzB,SAAS;gBACXa,WAAWH,WAAW,GAAGvB,SAASa,QAAQ+B,YAAY,CAAChD,sBAAsB;YAC/E;QACF;IACF;IAEA,MAAMgF,iBAAiB;QACrB/D,UAAU;IACZ;IAEA,MAAMgE,UAAU,CAACN;QACf,IAAI,CAAC5D,UAAUkB,QAAQiD,QAAQ,CAACP,MAAMjC,MAAM,GAAW;YACrD;QACF;QACA,MAAMyC,UAAU1C,YAAYkC,MAAMjC,MAAM;QACxC,IAAI,CAACyC,SAAS;YACZ;QACF;QACA,mEAAmE;QACnER,MAAMS,cAAc;QACpBT,MAAMU,eAAe;QACrBnE,WAAW2B,QAAQsC;QACnB/D,KAAK;YAAEgB,MAAM;YAAgBG,OAAOrB;QAAS;IAC/C;IAEA,MAAMoE,OAAO,CAACC;QACZ,IAAIrE,aAAa,MAAM;YACrB;QACF;QACA,MAAMsE,KAAKtE,WAAWqE;QACtB,IAAIC,KAAK,KAAKA,MAAMxE,YAAY;YAC9B;QACF;QACAI,KAAK;YAAEgB,MAAM;YAAcqD,MAAMvE;YAAUsE;QAAG;QAC9CtE,WAAWsE,IAAG,wDAAwD;IACxE;IAEArD,SAASuD,gBAAgB,CAAC,SAAS,IAAMJ,KAAK,CAAC;IAC/CjD,WAAWqD,gBAAgB,CAAC,SAAS,IAAMJ,KAAK;IAEhD1E,OAAO8E,gBAAgB,CAAC,WAAWhB;IACnClD,SAASkE,gBAAgB,CAAC,eAAeX;IACzCvD,SAAS4C,eAAe,CAACsB,gBAAgB,CAAC,gBAAgBV;IAC1DxD,SAASkE,gBAAgB,CAAC,SAAST,SAAS;IAE5C,kEAAkE;IAClE7D,KAAK;QAAEgB,MAAM;IAAgB;IAE7B,OAAO;QACLuD,qBAAqBxE;QACrBP,OAAOgF,mBAAmB,CAAC,WAAWlB;QACtClD,SAASoE,mBAAmB,CAAC,eAAeb;QAC5CvD,SAAS4C,eAAe,CAACwB,mBAAmB,CAAC,gBAAgBZ;QAC7DxD,SAASoE,mBAAmB,CAAC,SAASX,SAAS;QAC/CrD,SAASiE,MAAM;QACf7D,YAAY6D,MAAM;QAClB5D,QAAQ4D,MAAM;QACdtE,MAAMsE,MAAM;QACZ,OAAOrE,SAAS4C,eAAe,CAACC,OAAO,CAACC,cAAc;IACxD;AACF;AAEA,SAASwB,iBAAiB,QAAQ,iBAAgB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages exchanged between the Payload admin (the "bridge") and the
|
|
3
|
+
* live preview window (the "overlay"). Every message carries `source` and
|
|
4
|
+
* `version` so older client installs keep working when the protocol changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MESSAGE_SOURCE = "payload-section-builder";
|
|
7
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
8
|
+
/** Data attributes the frontend puts on each rendered section. */
|
|
9
|
+
export declare const SECTION_INDEX_ATTR = "data-section-index";
|
|
10
|
+
export declare const SECTION_TYPE_ATTR = "data-section-type";
|
|
11
|
+
export type PreviewToAdminMessage = {
|
|
12
|
+
index: number;
|
|
13
|
+
type: 'select-block';
|
|
14
|
+
} | {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
type: 'move-block';
|
|
18
|
+
} | {
|
|
19
|
+
type: 'overlay-hello';
|
|
20
|
+
};
|
|
21
|
+
export type AdminToPreviewMessage = {
|
|
22
|
+
blockCount: number;
|
|
23
|
+
type: 'editor-ready';
|
|
24
|
+
} | {
|
|
25
|
+
index: null | number;
|
|
26
|
+
type: 'selection-changed';
|
|
27
|
+
};
|
|
28
|
+
export type Envelope<T> = {
|
|
29
|
+
source: typeof MESSAGE_SOURCE;
|
|
30
|
+
version: number;
|
|
31
|
+
} & T;
|
|
32
|
+
export declare const wrap: <T extends object>(message: T) => Envelope<T>;
|
|
33
|
+
export declare const isEnvelope: (data: unknown) => data is Envelope<{
|
|
34
|
+
type: string;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Spread onto the root element of each rendered section:
|
|
38
|
+
* `<section {...sectionAttributes(index, block.blockType)}>`
|
|
39
|
+
*/
|
|
40
|
+
export declare const sectionAttributes: (index: number, blockType: string) => {
|
|
41
|
+
"data-section-index": number;
|
|
42
|
+
"data-section-type": string;
|
|
43
|
+
};
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages exchanged between the Payload admin (the "bridge") and the
|
|
3
|
+
* live preview window (the "overlay"). Every message carries `source` and
|
|
4
|
+
* `version` so older client installs keep working when the protocol changes.
|
|
5
|
+
*/ export const MESSAGE_SOURCE = 'payload-section-builder';
|
|
6
|
+
export const PROTOCOL_VERSION = 1;
|
|
7
|
+
/** Data attributes the frontend puts on each rendered section. */ export const SECTION_INDEX_ATTR = 'data-section-index';
|
|
8
|
+
export const SECTION_TYPE_ATTR = 'data-section-type';
|
|
9
|
+
export const wrap = (message)=>({
|
|
10
|
+
...message,
|
|
11
|
+
source: MESSAGE_SOURCE,
|
|
12
|
+
version: PROTOCOL_VERSION
|
|
13
|
+
});
|
|
14
|
+
export const isEnvelope = (data)=>typeof data === 'object' && data !== null && data.source === MESSAGE_SOURCE && typeof data.type === 'string';
|
|
15
|
+
/**
|
|
16
|
+
* Spread onto the root element of each rendered section:
|
|
17
|
+
* `<section {...sectionAttributes(index, block.blockType)}>`
|
|
18
|
+
*/ export const sectionAttributes = (index, blockType)=>({
|
|
19
|
+
[SECTION_INDEX_ATTR]: index,
|
|
20
|
+
[SECTION_TYPE_ATTR]: blockType
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol.ts"],"sourcesContent":["/**\n * Messages exchanged between the Payload admin (the \"bridge\") and the\n * live preview window (the \"overlay\"). Every message carries `source` and\n * `version` so older client installs keep working when the protocol changes.\n */\nexport const MESSAGE_SOURCE = 'payload-section-builder'\nexport const PROTOCOL_VERSION = 1\n\n/** Data attributes the frontend puts on each rendered section. */\nexport const SECTION_INDEX_ATTR = 'data-section-index'\nexport const SECTION_TYPE_ATTR = 'data-section-type'\n\nexport type PreviewToAdminMessage =\n | { index: number; type: 'select-block' }\n | { from: number; to: number; type: 'move-block' }\n | { type: 'overlay-hello' }\n\nexport type AdminToPreviewMessage =\n | { blockCount: number; type: 'editor-ready' }\n | { index: null | number; type: 'selection-changed' }\n\nexport type Envelope<T> = { source: typeof MESSAGE_SOURCE; version: number } & T\n\nexport const wrap = <T extends object>(message: T): Envelope<T> => ({\n ...message,\n source: MESSAGE_SOURCE,\n version: PROTOCOL_VERSION,\n})\n\nexport const isEnvelope = (data: unknown): data is Envelope<{ type: string }> =>\n typeof data === 'object' &&\n data !== null &&\n (data as { source?: unknown }).source === MESSAGE_SOURCE &&\n typeof (data as { type?: unknown }).type === 'string'\n\n/**\n * Spread onto the root element of each rendered section:\n * `<section {...sectionAttributes(index, block.blockType)}>`\n */\nexport const sectionAttributes = (index: number, blockType: string) => ({\n [SECTION_INDEX_ATTR]: index,\n [SECTION_TYPE_ATTR]: blockType,\n})\n"],"names":["MESSAGE_SOURCE","PROTOCOL_VERSION","SECTION_INDEX_ATTR","SECTION_TYPE_ATTR","wrap","message","source","version","isEnvelope","data","type","sectionAttributes","index","blockType"],"mappings":"AAAA;;;;CAIC,GACD,OAAO,MAAMA,iBAAiB,0BAAyB;AACvD,OAAO,MAAMC,mBAAmB,EAAC;AAEjC,gEAAgE,GAChE,OAAO,MAAMC,qBAAqB,qBAAoB;AACtD,OAAO,MAAMC,oBAAoB,oBAAmB;AAapD,OAAO,MAAMC,OAAO,CAAmBC,UAA6B,CAAA;QAClE,GAAGA,OAAO;QACVC,QAAQN;QACRO,SAASN;IACX,CAAA,EAAE;AAEF,OAAO,MAAMO,aAAa,CAACC,OACzB,OAAOA,SAAS,YAChBA,SAAS,QACT,AAACA,KAA8BH,MAAM,KAAKN,kBAC1C,OAAO,AAACS,KAA4BC,IAAI,KAAK,SAAQ;AAEvD;;;CAGC,GACD,OAAO,MAAMC,oBAAoB,CAACC,OAAeC,YAAuB,CAAA;QACtE,CAACX,mBAAmB,EAAEU;QACtB,CAACT,kBAAkB,EAAEU;IACvB,CAAA,EAAE"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type SectionOverlayProps = {
|
|
2
|
+
/** Origin of the Payload admin, e.g. `process.env.NEXT_PUBLIC_SERVER_URL`. */
|
|
3
|
+
adminOrigin: string;
|
|
4
|
+
/** Inject the default editor chrome styles. Default: `true`. */
|
|
5
|
+
injectStyles?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Mount once on preview pages. Renders nothing outside the Payload live preview. */
|
|
8
|
+
export declare const SectionOverlay: ({ adminOrigin, injectStyles }: SectionOverlayProps) => null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { initSectionOverlay } from '../overlay/index.js';
|
|
4
|
+
/** Mount once on preview pages. Renders nothing outside the Payload live preview. */ export const SectionOverlay = ({ adminOrigin, injectStyles })=>{
|
|
5
|
+
useEffect(()=>initSectionOverlay({
|
|
6
|
+
adminOrigin,
|
|
7
|
+
injectStyles
|
|
8
|
+
}), [
|
|
9
|
+
adminOrigin,
|
|
10
|
+
injectStyles
|
|
11
|
+
]);
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=SectionOverlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/SectionOverlay.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\n\nimport { initSectionOverlay } from '../overlay/index.js'\n\nexport type SectionOverlayProps = {\n /** Origin of the Payload admin, e.g. `process.env.NEXT_PUBLIC_SERVER_URL`. */\n adminOrigin: string\n /** Inject the default editor chrome styles. Default: `true`. */\n injectStyles?: boolean\n}\n\n/** Mount once on preview pages. Renders nothing outside the Payload live preview. */\nexport const SectionOverlay = ({ adminOrigin, injectStyles }: SectionOverlayProps) => {\n useEffect(() => initSectionOverlay({ adminOrigin, injectStyles }), [adminOrigin, injectStyles])\n return null\n}\n"],"names":["useEffect","initSectionOverlay","SectionOverlay","adminOrigin","injectStyles"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAQ,QAAO;AAEjC,SAASC,kBAAkB,QAAQ,sBAAqB;AASxD,mFAAmF,GACnF,OAAO,MAAMC,iBAAiB,CAAC,EAAEC,WAAW,EAAEC,YAAY,EAAuB;IAC/EJ,UAAU,IAAMC,mBAAmB;YAAEE;YAAaC;QAAa,IAAI;QAACD;QAAaC;KAAa;IAC9F,OAAO;AACT,EAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,121 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-section-builder",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Visual section builder for Payload CMS: select, edit and rearrange page sections from the live preview.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"psb-source": "./src/index.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./client": {
|
|
15
|
+
"psb-source": "./src/exports/client.ts",
|
|
16
|
+
"import": "./dist/exports/client.js",
|
|
17
|
+
"types": "./dist/exports/client.d.ts",
|
|
18
|
+
"default": "./dist/exports/client.js"
|
|
19
|
+
},
|
|
20
|
+
"./overlay": {
|
|
21
|
+
"psb-source": "./src/exports/overlay.ts",
|
|
22
|
+
"import": "./dist/exports/overlay.js",
|
|
23
|
+
"types": "./dist/exports/overlay.d.ts",
|
|
24
|
+
"default": "./dist/exports/overlay.js"
|
|
25
|
+
},
|
|
26
|
+
"./react": {
|
|
27
|
+
"psb-source": "./src/exports/react.ts",
|
|
28
|
+
"import": "./dist/exports/react.js",
|
|
29
|
+
"types": "./dist/exports/react.d.ts",
|
|
30
|
+
"default": "./dist/exports/react.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "pnpm clean && pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
40
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
41
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
42
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
43
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
44
|
+
"dev": "next dev dev --turbo",
|
|
45
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
46
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
47
|
+
"dev:payload": "cross-env NODE_OPTIONS=--conditions=psb-source PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
48
|
+
"generate:importmap": "pnpm dev:generate-importmap",
|
|
49
|
+
"generate:types": "pnpm dev:generate-types",
|
|
50
|
+
"lint": "eslint",
|
|
51
|
+
"lint:fix": "eslint ./src --fix",
|
|
52
|
+
"prepare": "pnpm build",
|
|
53
|
+
"test": "pnpm test:int && pnpm test:e2e",
|
|
54
|
+
"test:e2e": "playwright test",
|
|
55
|
+
"test:int": "vitest",
|
|
56
|
+
"typecheck": "tsc --noEmit -p dev/tsconfig.json"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
60
|
+
"@payloadcms/db-sqlite": "3.93.0",
|
|
61
|
+
"@payloadcms/eslint-config": "3.28.0",
|
|
62
|
+
"@payloadcms/live-preview-react": "3.93.0",
|
|
63
|
+
"@payloadcms/next": "3.93.0",
|
|
64
|
+
"@payloadcms/richtext-lexical": "3.93.0",
|
|
65
|
+
"@payloadcms/ui": "3.93.0",
|
|
66
|
+
"@playwright/test": "1.63.0",
|
|
67
|
+
"@swc-node/register": "1.10.9",
|
|
68
|
+
"@swc/cli": "0.6.0",
|
|
69
|
+
"@types/node": "22.19.9",
|
|
70
|
+
"@types/react": "19.2.14",
|
|
71
|
+
"@types/react-dom": "19.2.3",
|
|
72
|
+
"copyfiles": "2.4.1",
|
|
73
|
+
"cross-env": "^7.0.3",
|
|
74
|
+
"eslint": "^9.23.0",
|
|
75
|
+
"eslint-config-next": "16.3.3",
|
|
76
|
+
"graphql": "^16.8.1",
|
|
77
|
+
"next": "16.3.3",
|
|
78
|
+
"open": "^10.1.0",
|
|
79
|
+
"payload": "3.93.0",
|
|
80
|
+
"prettier": "^3.4.2",
|
|
81
|
+
"qs-esm": "8.0.1",
|
|
82
|
+
"react": "19.2.6",
|
|
83
|
+
"react-dom": "19.2.6",
|
|
84
|
+
"rimraf": "3.0.2",
|
|
85
|
+
"sharp": "0.35.4",
|
|
86
|
+
"sort-package-json": "^2.10.0",
|
|
87
|
+
"typescript": "5.7.3",
|
|
88
|
+
"vite-tsconfig-paths": "6.0.5",
|
|
89
|
+
"vitest": "4.0.18"
|
|
90
|
+
},
|
|
91
|
+
"peerDependencies": {
|
|
92
|
+
"payload": "^3.93.0",
|
|
93
|
+
"@payloadcms/ui": "^3.93.0",
|
|
94
|
+
"react": "^19.0.0"
|
|
95
|
+
},
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": "^22.23.4 || >=24.22.1",
|
|
98
|
+
"pnpm": "^9 || ^10 || ^11"
|
|
99
|
+
},
|
|
100
|
+
"packageManager": "pnpm@10.28.0",
|
|
101
|
+
"pnpm": {
|
|
102
|
+
"onlyBuiltDependencies": [
|
|
103
|
+
"sharp",
|
|
104
|
+
"esbuild",
|
|
105
|
+
"unrs-resolver"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"registry": "https://registry.npmjs.org/",
|
|
109
|
+
"dependencies": {},
|
|
110
|
+
"keywords": [
|
|
111
|
+
"payload",
|
|
112
|
+
"payload-plugin",
|
|
113
|
+
"page-builder",
|
|
114
|
+
"live-preview",
|
|
115
|
+
"visual-editing"
|
|
116
|
+
],
|
|
117
|
+
"repository": {
|
|
118
|
+
"type": "git",
|
|
119
|
+
"url": "git+https://github.com/bramdenelzen/payload-section-builder.git"
|
|
120
|
+
}
|
|
121
|
+
}
|