@payload-onpage/react 0.0.0-stage → 0.2.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 +13 -2
- package/dist/OnPageClient.d.ts +18 -0
- package/dist/OnPageClient.d.ts.map +1 -0
- package/dist/OnPageClient.js +51 -0
- package/dist/OnPageClient.js.map +1 -0
- package/dist/annotations.d.ts +55 -0
- package/dist/annotations.d.ts.map +1 -0
- package/dist/annotations.js +60 -0
- package/dist/annotations.js.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +19 -0
- package/dist/context.js.map +1 -0
- package/dist/dom.d.ts +42 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +95 -0
- package/dist/dom.js.map +1 -0
- package/dist/editor/FieldHost.d.ts +5 -0
- package/dist/editor/FieldHost.d.ts.map +1 -0
- package/dist/editor/FieldHost.js +36 -0
- package/dist/editor/FieldHost.js.map +1 -0
- package/dist/editor/Ghosts.d.ts +6 -0
- package/dist/editor/Ghosts.d.ts.map +1 -0
- package/dist/editor/Ghosts.js +37 -0
- package/dist/editor/Ghosts.js.map +1 -0
- package/dist/editor/OnPageEditor.d.ts +25 -0
- package/dist/editor/OnPageEditor.d.ts.map +1 -0
- package/dist/editor/OnPageEditor.js +211 -0
- package/dist/editor/OnPageEditor.js.map +1 -0
- package/dist/editor/useGhosts.d.ts +24 -0
- package/dist/editor/useGhosts.d.ts.map +1 -0
- package/dist/editor/useGhosts.js +34 -0
- package/dist/editor/useGhosts.js.map +1 -0
- package/dist/editor/usePageEvents.d.ts +3 -0
- package/dist/editor/usePageEvents.d.ts.map +1 -0
- package/dist/editor/usePageEvents.js +164 -0
- package/dist/editor/usePageEvents.js.map +1 -0
- package/dist/editor-kit.d.ts +19 -0
- package/dist/editor-kit.d.ts.map +1 -0
- package/dist/editor-kit.js +17 -0
- package/dist/editor-kit.js.map +1 -0
- package/dist/editors/CheckboxEditor.d.ts +3 -0
- package/dist/editors/CheckboxEditor.d.ts.map +1 -0
- package/dist/editors/CheckboxEditor.js +10 -0
- package/dist/editors/CheckboxEditor.js.map +1 -0
- package/dist/editors/ColorEditor.d.ts +4 -0
- package/dist/editors/ColorEditor.d.ts.map +1 -0
- package/dist/editors/ColorEditor.js +21 -0
- package/dist/editors/ColorEditor.js.map +1 -0
- package/dist/editors/DateEditor.d.ts +3 -0
- package/dist/editors/DateEditor.d.ts.map +1 -0
- package/dist/editors/DateEditor.js +28 -0
- package/dist/editors/DateEditor.js.map +1 -0
- package/dist/editors/InlineText.d.ts +4 -0
- package/dist/editors/InlineText.d.ts.map +1 -0
- package/dist/editors/InlineText.js +77 -0
- package/dist/editors/InlineText.js.map +1 -0
- package/dist/editors/NumberEditor.d.ts +4 -0
- package/dist/editors/NumberEditor.d.ts.map +1 -0
- package/dist/editors/NumberEditor.js +23 -0
- package/dist/editors/NumberEditor.js.map +1 -0
- package/dist/editors/RelationshipEditor.d.ts +3 -0
- package/dist/editors/RelationshipEditor.d.ts.map +1 -0
- package/dist/editors/RelationshipEditor.js +60 -0
- package/dist/editors/RelationshipEditor.js.map +1 -0
- package/dist/editors/SelectEditor.d.ts +4 -0
- package/dist/editors/SelectEditor.d.ts.map +1 -0
- package/dist/editors/SelectEditor.js +36 -0
- package/dist/editors/SelectEditor.js.map +1 -0
- package/dist/editors/UploadEditor.d.ts +3 -0
- package/dist/editors/UploadEditor.d.ts.map +1 -0
- package/dist/editors/UploadEditor.js +48 -0
- package/dist/editors/UploadEditor.js.map +1 -0
- package/dist/editors/index.d.ts +13 -0
- package/dist/editors/index.d.ts.map +1 -0
- package/dist/editors/index.js +23 -0
- package/dist/editors/index.js.map +1 -0
- package/dist/editors/useDebouncedSearch.d.ts +17 -0
- package/dist/editors/useDebouncedSearch.d.ts.map +1 -0
- package/dist/editors/useDebouncedSearch.js +50 -0
- package/dist/editors/useDebouncedSearch.js.map +1 -0
- package/dist/hub.d.ts +14 -0
- package/dist/hub.d.ts.map +1 -0
- package/dist/hub.js +20 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +12 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +83 -0
- package/dist/layout.js.map +1 -0
- package/dist/locators.d.ts +19 -0
- package/dist/locators.d.ts.map +1 -0
- package/dist/locators.js +215 -0
- package/dist/locators.js.map +1 -0
- package/dist/messages.d.ts +100 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +112 -0
- package/dist/messages.js.map +1 -0
- package/dist/registry.d.ts +15 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +29 -0
- package/dist/registry.js.map +1 -0
- package/dist/renderers.d.ts +7 -0
- package/dist/renderers.d.ts.map +1 -0
- package/dist/renderers.js +12 -0
- package/dist/renderers.js.map +1 -0
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +49 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/styles.d.ts +8 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +122 -0
- package/dist/styles.js.map +1 -0
- package/dist/targets.d.ts +18 -0
- package/dist/targets.d.ts.map +1 -0
- package/dist/targets.js +86 -0
- package/dist/targets.js.map +1 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/BlockPicker.d.ts +3 -0
- package/dist/ui/BlockPicker.d.ts.map +1 -0
- package/dist/ui/BlockPicker.js +27 -0
- package/dist/ui/BlockPicker.js.map +1 -0
- package/dist/ui/Dialog.d.ts +22 -0
- package/dist/ui/Dialog.d.ts.map +1 -0
- package/dist/ui/Dialog.js +83 -0
- package/dist/ui/Dialog.js.map +1 -0
- package/dist/ui/Dock.d.ts +7 -0
- package/dist/ui/Dock.d.ts.map +1 -0
- package/dist/ui/Dock.js +104 -0
- package/dist/ui/Dock.js.map +1 -0
- package/dist/ui/Overlay.d.ts +2 -0
- package/dist/ui/Overlay.d.ts.map +1 -0
- package/dist/ui/Overlay.js +150 -0
- package/dist/ui/Overlay.js.map +1 -0
- package/dist/ui/PostButton.d.ts +10 -0
- package/dist/ui/PostButton.d.ts.map +1 -0
- package/dist/ui/PostButton.js +7 -0
- package/dist/ui/PostButton.js.map +1 -0
- package/dist/ui/PreviewToolbar.d.ts +15 -0
- package/dist/ui/PreviewToolbar.d.ts.map +1 -0
- package/dist/ui/PreviewToolbar.js +45 -0
- package/dist/ui/PreviewToolbar.js.map +1 -0
- package/dist/ui/SettingsPanel.d.ts +11 -0
- package/dist/ui/SettingsPanel.d.ts.map +1 -0
- package/dist/ui/SettingsPanel.js +130 -0
- package/dist/ui/SettingsPanel.js.map +1 -0
- package/dist/useEvent.d.ts +7 -0
- package/dist/useEvent.d.ts.map +1 -0
- package/dist/useEvent.js +15 -0
- package/dist/useEvent.js.map +1 -0
- package/package.json +71 -4
- package/src/OnPageClient.tsx +67 -0
- package/src/annotations.ts +83 -0
- package/src/context.tsx +23 -0
- package/src/dom.ts +118 -0
- package/src/editor/FieldHost.tsx +66 -0
- package/src/editor/Ghosts.tsx +55 -0
- package/src/editor/OnPageEditor.tsx +315 -0
- package/src/editor/useGhosts.ts +64 -0
- package/src/editor/usePageEvents.ts +161 -0
- package/src/editor-kit.ts +27 -0
- package/src/editors/CheckboxEditor.tsx +31 -0
- package/src/editors/ColorEditor.tsx +64 -0
- package/src/editors/DateEditor.tsx +64 -0
- package/src/editors/InlineText.tsx +106 -0
- package/src/editors/NumberEditor.tsx +69 -0
- package/src/editors/RelationshipEditor.tsx +153 -0
- package/src/editors/SelectEditor.tsx +74 -0
- package/src/editors/UploadEditor.tsx +130 -0
- package/src/editors/index.ts +35 -0
- package/src/editors/useDebouncedSearch.ts +54 -0
- package/src/hub.ts +32 -0
- package/src/index.ts +55 -0
- package/src/layout.ts +83 -0
- package/src/locators.ts +210 -0
- package/src/messages.ts +120 -0
- package/src/registry.ts +43 -0
- package/src/renderers.ts +15 -0
- package/src/sanitize.ts +52 -0
- package/src/styles.ts +122 -0
- package/src/targets.ts +101 -0
- package/src/types.ts +132 -0
- package/src/ui/BlockPicker.tsx +49 -0
- package/src/ui/Dialog.tsx +159 -0
- package/src/ui/Dock.tsx +240 -0
- package/src/ui/Overlay.tsx +324 -0
- package/src/ui/PostButton.tsx +26 -0
- package/src/ui/PreviewToolbar.tsx +123 -0
- package/src/ui/SettingsPanel.tsx +279 -0
- package/src/useEvent.ts +16 -0
package/src/styles.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor's own styles, injected once. Everything is prefixed `po-` and set
|
|
3
|
+
* explicitly, so the host site's CSS (and its absence) doesn't matter.
|
|
4
|
+
*/
|
|
5
|
+
export const PO_Z = 2147483000
|
|
6
|
+
|
|
7
|
+
export const PO_CSS = `
|
|
8
|
+
.po-layer{position:fixed;inset:0;pointer-events:none;z-index:${PO_Z};font:13px/1.35 system-ui,-apple-system,"Segoe UI",sans-serif;color:#0f172a}
|
|
9
|
+
.po-layer *{box-sizing:border-box}
|
|
10
|
+
.po-outline{position:fixed;border:2px solid #4f7cff;border-radius:4px;pointer-events:none;transition:all .06s ease-out}
|
|
11
|
+
.po-outline.is-block{border-style:dashed;border-color:#4f7cff99}
|
|
12
|
+
.po-outline.is-item{border-color:#22a06b}
|
|
13
|
+
.po-chip{position:fixed;transform:translateY(-100%);background:#3b63e0;color:#fff;font-size:11px;font-weight:600;padding:2px 6px;border-radius:4px 4px 0 0;white-space:nowrap;pointer-events:none}
|
|
14
|
+
.po-chip.is-item{background:#167a45}
|
|
15
|
+
.po-bar{position:fixed;display:flex;gap:2px;padding:3px;background:#0f172a;border-radius:8px;box-shadow:0 6px 24px #0f172a40;pointer-events:auto}
|
|
16
|
+
.po-bar.is-item{background:#14532d}
|
|
17
|
+
.po-btn{all:unset;display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:28px;height:28px;padding:0 8px;border-radius:6px;color:#fff;font:600 12px/1 system-ui,-apple-system,sans-serif;cursor:pointer;white-space:nowrap}
|
|
18
|
+
.po-btn:hover{background:#ffffff1f}
|
|
19
|
+
.po-btn:focus-visible{outline:2px solid #93c5fd;outline-offset:1px}
|
|
20
|
+
.po-btn[disabled]{opacity:.35;cursor:default;background:none}
|
|
21
|
+
.po-btn.is-danger:hover{background:#ef444455}
|
|
22
|
+
.po-add{position:fixed;transform:translate(-50%,-50%);pointer-events:auto}
|
|
23
|
+
.po-add .po-btn{background:#3b63e0;box-shadow:0 2px 10px #4f7cff66;height:24px;font-size:11px}
|
|
24
|
+
.po-add .po-btn:hover{background:#2f5bd9}
|
|
25
|
+
.po-add.is-item .po-btn{background:#167a45;box-shadow:0 2px 10px #167a4555}
|
|
26
|
+
.po-panel{position:fixed;width:320px;max-height:min(70vh,560px);overflow:auto;background:#fff;color:#0f172a;border-radius:12px;box-shadow:0 20px 60px #0f172a40,0 0 0 1px #0f172a14;pointer-events:auto;padding:14px}
|
|
27
|
+
.po-panel .po-h3{margin:0 0 10px;font:700 13px/1.2 system-ui,sans-serif}
|
|
28
|
+
.po-panel .po-h4{margin:14px 0 6px;font:600 11px/1.2 system-ui,sans-serif;text-transform:uppercase;letter-spacing:.06em;color:#64748b}
|
|
29
|
+
.po-row{display:flex;flex-direction:column;gap:4px;margin:0 0 10px}
|
|
30
|
+
.po-row label{font-weight:600;font-size:12px}
|
|
31
|
+
.po-row small{color:#64748b;font-size:11px}
|
|
32
|
+
.po-input,.po-select{all:unset;box-sizing:border-box;width:100%;height:32px;padding:0 10px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;font:13px system-ui,sans-serif;color:#0f172a}
|
|
33
|
+
.po-select{appearance:auto;-webkit-appearance:auto}
|
|
34
|
+
.po-input:focus,.po-select:focus{border-color:#4f7cff;box-shadow:0 0 0 3px #4f7cff33}
|
|
35
|
+
.po-check{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600}
|
|
36
|
+
.po-seg{display:flex;flex-wrap:wrap;gap:4px}
|
|
37
|
+
.po-seg button{all:unset;padding:5px 9px;border:1px solid #cbd5e1;border-radius:7px;font:12px system-ui,sans-serif;cursor:pointer;background:#fff}
|
|
38
|
+
.po-seg button[aria-pressed="true"]{background:#0f172a;color:#fff;border-color:#0f172a}
|
|
39
|
+
.po-modal-bg{position:fixed;inset:0;background:#0f172a66;pointer-events:auto;display:flex;align-items:center;justify-content:center;padding:24px}
|
|
40
|
+
.po-modal{width:min(880px,100%);max-height:85vh;display:flex;flex-direction:column;background:#fff;border-radius:14px;box-shadow:0 30px 80px #0f172a55;overflow:hidden}
|
|
41
|
+
.po-modal header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #e2e8f0}
|
|
42
|
+
.po-modal header .po-h3{margin:0;flex:1;font:700 14px system-ui,sans-serif}
|
|
43
|
+
.po-modal .po-body{padding:14px 16px;overflow:auto}
|
|
44
|
+
.po-modal .po-btn{color:#0f172a}
|
|
45
|
+
.po-modal .po-btn:hover{background:#f1f5f9}
|
|
46
|
+
.po-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
|
|
47
|
+
.po-tile{all:unset;display:flex;flex-direction:column;gap:6px;cursor:pointer;border-radius:10px;padding:6px;border:2px solid transparent}
|
|
48
|
+
.po-tile:hover,.po-tile:focus-visible{border-color:#4f7cff;background:#f8fafc}
|
|
49
|
+
.po-tile img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:6px;background:#f1f5f9}
|
|
50
|
+
.po-tile span{font-size:11px;color:#475569;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
51
|
+
.po-blocks{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
|
|
52
|
+
.po-blocks button{all:unset;padding:14px;border:1px solid #e2e8f0;border-radius:10px;cursor:pointer;font:600 13px system-ui,sans-serif}
|
|
53
|
+
.po-blocks button:hover,.po-blocks button:focus-visible{border-color:#4f7cff;background:#f5f8ff}
|
|
54
|
+
.po-dock{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:4px;padding:6px;background:#0f172a;color:#fff;border-radius:12px;box-shadow:0 12px 40px #0f172a55;pointer-events:auto;max-width:calc(100vw - 24px);flex-wrap:wrap}
|
|
55
|
+
.po-dock .po-status{padding:0 10px;font-size:12px;opacity:.85;white-space:nowrap}
|
|
56
|
+
.po-dock .po-status.is-bad{color:#fca5a5;opacity:1}
|
|
57
|
+
.po-dock .po-sep{width:1px;height:20px;background:#ffffff26;margin:0 4px}
|
|
58
|
+
.po-dock .po-primary{background:#3b63e0}
|
|
59
|
+
.po-dock .po-primary:hover{background:#2f5bd9}
|
|
60
|
+
.po-chip-locale{padding:2px 7px;border-radius:999px;background:#ffffff1f;font-size:11px;font-weight:700;text-transform:uppercase}
|
|
61
|
+
.po-notice{position:fixed;left:50%;bottom:72px;transform:translateX(-50%);max-width:min(560px,calc(100vw - 24px));padding:8px 12px;border-radius:10px;background:#fffbeb;color:#78350f;border:1px solid #fcd34d;font-size:12px;pointer-events:auto}
|
|
62
|
+
.po-doc-layer{position:absolute;top:0;left:0;width:0;height:0;overflow:visible;z-index:${PO_Z};pointer-events:none;font:13px/1.35 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
63
|
+
.po-inline{position:absolute;pointer-events:auto;outline:2px solid #4f7cff;outline-offset:2px;border-radius:2px;background:transparent;white-space:pre-wrap;word-break:break-word;caret-color:currentColor;cursor:text}
|
|
64
|
+
.po-inline:focus{outline-color:#4f7cff}
|
|
65
|
+
.po-rt{position:absolute;pointer-events:auto}
|
|
66
|
+
.po-rt-surface{outline:2px solid #4f7cff;outline-offset:6px;border-radius:2px;min-height:1.5em}
|
|
67
|
+
.po-rt-surface:focus{outline:2px solid #4f7cff}
|
|
68
|
+
.po-rt-toolbar{position:absolute;left:0;top:-48px;display:flex;gap:2px;padding:3px;background:#0f172a;border-radius:8px;box-shadow:0 6px 24px #0f172a40}
|
|
69
|
+
.po-rt-toolbar .po-btn[aria-pressed="true"]{background:#ffffff2e}
|
|
70
|
+
.po-pop{position:fixed;min-width:220px;max-width:340px;background:#fff;color:#0f172a;border-radius:10px;box-shadow:0 14px 40px #0f172a40,0 0 0 1px #0f172a14;pointer-events:auto;padding:10px}
|
|
71
|
+
.po-pop .po-row{margin-bottom:8px}
|
|
72
|
+
.po-pop-list{display:flex;flex-direction:column;gap:2px;max-height:260px;overflow:auto}
|
|
73
|
+
.po-pop-list button{all:unset;padding:7px 9px;border-radius:7px;cursor:pointer;font:13px system-ui,sans-serif}
|
|
74
|
+
.po-pop-list button:hover,.po-pop-list button:focus-visible,.po-pop-list button[aria-selected="true"]{background:#eef2ff}
|
|
75
|
+
.po-ghost{position:absolute;pointer-events:none;white-space:pre-wrap;word-break:break-word}
|
|
76
|
+
.po-docs{font-size:11px;padding:2px 7px;border-radius:999px;background:#ffffff1f}
|
|
77
|
+
.po-frame-bg{position:fixed;inset:0;background:#0f172acc;pointer-events:auto;display:flex;flex-direction:column;align-items:center;padding:56px 16px 16px}
|
|
78
|
+
.po-frame-bg iframe{background:#fff;border:0;border-radius:12px;box-shadow:0 30px 80px #0008;height:100%;max-width:100%}
|
|
79
|
+
.po-badge{padding:2px 8px;border-radius:999px;background:#f59e0b;color:#111;font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:.04em}
|
|
80
|
+
.po-b{font-weight:700}.po-i{font-style:italic}.po-u{text-decoration:underline}
|
|
81
|
+
.po-opaque{display:inline-flex;align-items:center;gap:6px;margin:6px 0;padding:6px 10px;border:1px dashed #94a3b8;border-radius:8px;background:#f8fafc;color:#475569;font:12px system-ui,sans-serif;user-select:none}
|
|
82
|
+
html[data-onpage-editing] [data-onpage-field]{cursor:pointer}
|
|
83
|
+
[data-onpage-hidden="true"]{visibility:hidden!important}
|
|
84
|
+
.po-preview{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:6px;padding:6px;background:#0f172a;color:#fff;border-radius:12px;box-shadow:0 12px 40px #0f172a55;pointer-events:auto;max-width:calc(100vw - 24px);flex-wrap:wrap}
|
|
85
|
+
.po-preview .po-status{padding:0 8px;font-size:12px;opacity:.85}
|
|
86
|
+
.po-rel-item{display:flex;align-items:center;gap:6px;padding:4px 6px;border-radius:6px;background:#f1f5f9;font-size:12px;margin:0 0 4px}
|
|
87
|
+
.po-rel-item button{all:unset;cursor:pointer;margin-left:auto;padding:0 4px;color:#64748b}
|
|
88
|
+
|
|
89
|
+
.po-dialog-head{display:flex;align-items:center;gap:8px;margin:0 0 10px}
|
|
90
|
+
.po-modal .po-dialog-head{margin:0;padding:14px 16px;border-bottom:1px solid #e2e8f0}
|
|
91
|
+
.po-dialog-head .po-h3{flex:1;margin:0;font:700 14px/1.2 system-ui,sans-serif}
|
|
92
|
+
.po-close{color:#0f172a;min-width:24px;height:24px}
|
|
93
|
+
.po-close:hover{background:#f1f5f9}
|
|
94
|
+
.po-bar-label{cursor:default;opacity:.8}
|
|
95
|
+
.po-muted{color:#64748b;margin:6px 0}
|
|
96
|
+
.po-error{color:#b91c1c;margin:6px 0}
|
|
97
|
+
.po-center{text-align:center;margin-top:12px}
|
|
98
|
+
.po-actions{display:flex;gap:6px}
|
|
99
|
+
.po-inline-row{flex-direction:row;align-items:center}
|
|
100
|
+
.po-search{width:240px}
|
|
101
|
+
.po-picker{cursor:pointer;display:flex;align-items:center}
|
|
102
|
+
.po-group{border:0;margin:0;padding:0}
|
|
103
|
+
.po-primary,.po-pop .po-primary,.po-panel .po-primary{background:#3b63e0;color:#fff}
|
|
104
|
+
.po-primary:hover{background:#2f5bd9}
|
|
105
|
+
.po-pop .po-btn,.po-panel .po-btn{color:#0f172a}
|
|
106
|
+
.po-pop .po-btn.po-primary,.po-panel .po-btn.po-primary{color:#fff}
|
|
107
|
+
.po-frame-close{all:unset;position:absolute;inset:0;cursor:pointer}
|
|
108
|
+
.po-frame-bg iframe{position:relative}
|
|
109
|
+
.po-input[aria-invalid="true"]{border-color:#dc2626}
|
|
110
|
+
html[data-onpage-editing] [data-onpage-focusable]:focus-visible{outline:2px solid #4f7cff;outline-offset:2px}
|
|
111
|
+
@media (prefers-reduced-motion:reduce){.po-outline{transition:none}}
|
|
112
|
+
`
|
|
113
|
+
|
|
114
|
+
let injected = false
|
|
115
|
+
export function injectStyles() {
|
|
116
|
+
if (injected || typeof document === 'undefined') return
|
|
117
|
+
injected = true
|
|
118
|
+
const el = document.createElement('style')
|
|
119
|
+
el.setAttribute('data-onpage-styles', '')
|
|
120
|
+
el.textContent = PO_CSS
|
|
121
|
+
document.head.appendChild(el)
|
|
122
|
+
}
|
package/src/targets.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps an index of the page's targets current: scans with the locator on
|
|
5
|
+
* mount and again whenever the page's DOM changes (a server refresh, a client
|
|
6
|
+
* re-render, lazy content), ignoring the editor's own UI. Rescans are
|
|
7
|
+
* debounced and run when the browser is idle.
|
|
8
|
+
*/
|
|
9
|
+
import { targetKey, type Locator, type Target, type TargetKind } from '@payload-onpage/core'
|
|
10
|
+
import { useEffect, useState } from 'react'
|
|
11
|
+
import { ATTR } from './annotations.ts'
|
|
12
|
+
import { isOurs } from './dom.ts'
|
|
13
|
+
import type { TargetIndex } from './types.ts'
|
|
14
|
+
|
|
15
|
+
const FIELD_KINDS = new Set<TargetKind>(['text', 'textarea', 'richtext', 'media', 'field'])
|
|
16
|
+
export const isFieldTarget = (t: Target): boolean => FIELD_KINDS.has(t.kind)
|
|
17
|
+
export const isRowTarget = (t: Target): boolean => t.kind === 'item'
|
|
18
|
+
|
|
19
|
+
export function buildIndex(all: Target[]): TargetIndex {
|
|
20
|
+
const byEl = new Map<Element, Target[]>()
|
|
21
|
+
const byPath = new Map<string, Target[]>()
|
|
22
|
+
// Annotations first: they are precise, inferred targets are a fallback.
|
|
23
|
+
const sorted = [...all].sort(
|
|
24
|
+
(a, b) => Number(a.source !== 'annotation') - Number(b.source !== 'annotation'),
|
|
25
|
+
)
|
|
26
|
+
for (const t of sorted) {
|
|
27
|
+
byEl.set(t.el, [...(byEl.get(t.el) ?? []), t])
|
|
28
|
+
const k = targetKey(t.ref, t.path)
|
|
29
|
+
byPath.set(k, [...(byPath.get(k) ?? []), t])
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
all: sorted,
|
|
33
|
+
at: (el) => byEl.get(el) ?? [],
|
|
34
|
+
find: (key, path, kinds) =>
|
|
35
|
+
(byPath.get(targetKey(key, path)) ?? []).find(
|
|
36
|
+
(t) => t.el.isConnected && (!kinds || kinds.includes(t.kind)),
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The innermost field target and all row targets (innermost first) at or above `el`. */
|
|
42
|
+
export function targetsAt(
|
|
43
|
+
index: TargetIndex,
|
|
44
|
+
el: Element | null,
|
|
45
|
+
): { field?: Target; rows: Target[] } {
|
|
46
|
+
let field: Target | undefined
|
|
47
|
+
const rows: Target[] = []
|
|
48
|
+
for (let node = el; node; node = node.parentElement) {
|
|
49
|
+
for (const t of index.at(node)) {
|
|
50
|
+
if (!field && isFieldTarget(t)) field = t
|
|
51
|
+
if (isRowTarget(t)) rows.push(t)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { field, rows }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const WATCHED = [ATTR.ref, ATTR.field, ATTR.row, ATTR.list, 'alt']
|
|
58
|
+
const idle = (fn: () => void): (() => void) => {
|
|
59
|
+
if (typeof requestIdleCallback === 'function') {
|
|
60
|
+
const h = requestIdleCallback(fn, { timeout: 300 })
|
|
61
|
+
return () => cancelIdleCallback(h)
|
|
62
|
+
}
|
|
63
|
+
const h = setTimeout(fn, 50)
|
|
64
|
+
return () => clearTimeout(h)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useTargets(locator: Locator, root: () => ParentNode | null): TargetIndex {
|
|
68
|
+
const [index, setIndex] = useState<TargetIndex>(() => buildIndex([]))
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const r = root()
|
|
71
|
+
if (!r) return
|
|
72
|
+
let debounce: ReturnType<typeof setTimeout> | undefined
|
|
73
|
+
let cancelIdle: (() => void) | undefined
|
|
74
|
+
const scan = () => setIndex(buildIndex(locator.scan(r)))
|
|
75
|
+
scan()
|
|
76
|
+
const observer = new MutationObserver((records) => {
|
|
77
|
+
if (
|
|
78
|
+
records.every((m) =>
|
|
79
|
+
isOurs(m.target instanceof Element ? m.target : m.target.parentElement),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
clearTimeout(debounce)
|
|
84
|
+
cancelIdle?.()
|
|
85
|
+
debounce = setTimeout(() => (cancelIdle = idle(scan)), 120)
|
|
86
|
+
})
|
|
87
|
+
observer.observe(r, {
|
|
88
|
+
subtree: true,
|
|
89
|
+
childList: true,
|
|
90
|
+
characterData: true,
|
|
91
|
+
attributes: true,
|
|
92
|
+
attributeFilter: WATCHED,
|
|
93
|
+
})
|
|
94
|
+
return () => {
|
|
95
|
+
observer.disconnect()
|
|
96
|
+
clearTimeout(debounce)
|
|
97
|
+
cancelIdle?.()
|
|
98
|
+
}
|
|
99
|
+
}, [locator, root])
|
|
100
|
+
return index
|
|
101
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** Public types of the editor UI. */
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
import type {
|
|
4
|
+
DocRef,
|
|
5
|
+
EditorSession,
|
|
6
|
+
MediaLibrary,
|
|
7
|
+
Renderer,
|
|
8
|
+
SchemaField,
|
|
9
|
+
SessionSnapshot,
|
|
10
|
+
Target,
|
|
11
|
+
TargetKind,
|
|
12
|
+
} from '@payload-onpage/core'
|
|
13
|
+
import type { MessageKey, Translate } from './messages.ts'
|
|
14
|
+
|
|
15
|
+
export interface EditingState {
|
|
16
|
+
/** refKey of the document */
|
|
17
|
+
key: string
|
|
18
|
+
path: string
|
|
19
|
+
/** Editor id from the registry ("text", "richText", …). */
|
|
20
|
+
editor: string
|
|
21
|
+
point?: { x: number; y: number }
|
|
22
|
+
/** The element that was clicked (several elements may show one field). */
|
|
23
|
+
el?: Element
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Panel =
|
|
27
|
+
| { kind: 'settings'; key: string; path: string; title: string }
|
|
28
|
+
| { kind: 'blocks'; key: string; listPath: string; index: number }
|
|
29
|
+
| null
|
|
30
|
+
|
|
31
|
+
export interface Selection {
|
|
32
|
+
key: string
|
|
33
|
+
path: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Props every field editor receives. */
|
|
37
|
+
export interface FieldEditorProps {
|
|
38
|
+
docKey: string
|
|
39
|
+
path: string
|
|
40
|
+
field: SchemaField
|
|
41
|
+
/** The element showing the field (re-resolved after each refresh). */
|
|
42
|
+
element: () => Element | null
|
|
43
|
+
value: unknown
|
|
44
|
+
/** Commit a value (one undo step, autosaved). */
|
|
45
|
+
commit: (value: unknown) => void
|
|
46
|
+
/** Live preview while editing (no history entry, not saved). */
|
|
47
|
+
preview: (value: unknown) => void
|
|
48
|
+
/** Close the editor (after commit, or to cancel: transient previews are reverted). */
|
|
49
|
+
close: () => void
|
|
50
|
+
/** Leave a stand-in showing the new content until the page re-renders (refresh mode). */
|
|
51
|
+
ghost: (g: { html: string; className?: string; style?: Partial<CSSStyleDeclaration> }) => void
|
|
52
|
+
/** True when the page renders this document from editor state (instant); false = server refresh. */
|
|
53
|
+
live: boolean
|
|
54
|
+
point?: { x: number; y: number }
|
|
55
|
+
/** UI strings. */
|
|
56
|
+
t: Translate
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A field editor (the FieldEditor port). `inline` editors replace the element
|
|
61
|
+
* in place (text, rich text); `popover` editors open beside it or as a dialog
|
|
62
|
+
* (select, date, relationship, image…). Register your own by id and refer to it
|
|
63
|
+
* from a field with `admin: { custom: { onpage: { editor: 'my-id' } } }`, or by
|
|
64
|
+
* field type ("json", "code"…) to replace or add a default.
|
|
65
|
+
*/
|
|
66
|
+
export interface FieldEditorDef {
|
|
67
|
+
mode: 'inline' | 'popover'
|
|
68
|
+
component: ComponentType<FieldEditorProps>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type EditorRegistry = Record<string, FieldEditorDef>
|
|
72
|
+
|
|
73
|
+
/** Navigation targets. `exit` and `mode` are endpoints the editor POSTs to. */
|
|
74
|
+
export interface Links {
|
|
75
|
+
exit: string
|
|
76
|
+
admin?: (ref: DocRef) => string
|
|
77
|
+
mode?: (mode: 'edit' | 'view') => string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TargetIndex {
|
|
81
|
+
all: Target[]
|
|
82
|
+
/** Targets whose element is exactly `el`. */
|
|
83
|
+
at: (el: Element) => Target[]
|
|
84
|
+
find: (key: string, path: string, kinds?: TargetKind[]) => Target | undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** What extensions and field editors can use from the editor (`useOnPage()`). */
|
|
88
|
+
export interface OnPageContextValue {
|
|
89
|
+
session: EditorSession
|
|
90
|
+
snap: SessionSnapshot
|
|
91
|
+
renderer: Renderer
|
|
92
|
+
media?: MediaLibrary
|
|
93
|
+
editors: EditorRegistry
|
|
94
|
+
targets: TargetIndex
|
|
95
|
+
links: Links
|
|
96
|
+
t: Translate
|
|
97
|
+
panel: Panel
|
|
98
|
+
setPanel: (p: Panel) => void
|
|
99
|
+
editing: EditingState | null
|
|
100
|
+
setEditing: (e: EditingState | null) => void
|
|
101
|
+
selected: Selection | null
|
|
102
|
+
setSelected: (s: Selection | null) => void
|
|
103
|
+
/** Documents shown on the current page (the session may hold others from earlier pages). */
|
|
104
|
+
docKeys: string[]
|
|
105
|
+
/** Is this document rendered from session state (client mode)? */
|
|
106
|
+
isLive: (key: string) => boolean
|
|
107
|
+
/** Client-mode documents register here (they don't need a server refresh). */
|
|
108
|
+
registerClientDoc: (key: string) => () => void
|
|
109
|
+
/** The page is re-rendering after a save (refresh mode). */
|
|
110
|
+
rendering: boolean
|
|
111
|
+
/** Open the right editor for a field (registry resolution); false if none applies. */
|
|
112
|
+
edit: (
|
|
113
|
+
key: string,
|
|
114
|
+
path: string,
|
|
115
|
+
opts?: { point?: { x: number; y: number }; kind?: TargetKind; anchor?: Element },
|
|
116
|
+
) => boolean
|
|
117
|
+
/** Re-render documents after an operation outside autosave (publish, reload). */
|
|
118
|
+
rerender: (refs: DocRef[]) => Promise<void>
|
|
119
|
+
/** Show a short message in the dock. */
|
|
120
|
+
notify: (
|
|
121
|
+
key: MessageKey | (string & {}),
|
|
122
|
+
vars?: Record<string, string | number>,
|
|
123
|
+
bad?: boolean,
|
|
124
|
+
) => void
|
|
125
|
+
addGhost: (g: {
|
|
126
|
+
key: string
|
|
127
|
+
path: string
|
|
128
|
+
html: string
|
|
129
|
+
className?: string
|
|
130
|
+
style?: Partial<CSSStyleDeclaration>
|
|
131
|
+
}) => void
|
|
132
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { fieldAtPath, insertItem, newBlockRow, updateArray } from '@payload-onpage/core'
|
|
4
|
+
import { useOnPage } from '../context.tsx'
|
|
5
|
+
import { Dialog } from './Dialog.tsx'
|
|
6
|
+
|
|
7
|
+
/** Choose a block type to insert at a position in a blocks field. */
|
|
8
|
+
export function BlockPicker() {
|
|
9
|
+
const { panel, setPanel, snap, session, setSelected, t } = useOnPage()
|
|
10
|
+
if (panel?.kind !== 'blocks') return null
|
|
11
|
+
const state = snap.docs[panel.key]
|
|
12
|
+
if (!state?.schema || !state.doc) return null
|
|
13
|
+
const { key, listPath, index } = panel
|
|
14
|
+
const field = fieldAtPath(state.schema.fields, state.doc, listPath)?.field
|
|
15
|
+
const blocks = field?.type === 'blocks' ? (field.blocks ?? []) : []
|
|
16
|
+
|
|
17
|
+
const choose = (slug: string) => {
|
|
18
|
+
const block = blocks.find((b) => b.slug === slug)
|
|
19
|
+
if (!block) return
|
|
20
|
+
session.apply(key, (d) =>
|
|
21
|
+
updateArray(d, listPath, (a) => insertItem(a, index, newBlockRow(block))),
|
|
22
|
+
)
|
|
23
|
+
setPanel(null)
|
|
24
|
+
setSelected({ key, path: `${listPath}.${index}` })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Dialog
|
|
29
|
+
title={t('blocks.title')}
|
|
30
|
+
onClose={() => setPanel(null)}
|
|
31
|
+
t={t}
|
|
32
|
+
width={640}
|
|
33
|
+
testId="onpage-block-picker"
|
|
34
|
+
>
|
|
35
|
+
<div className="po-blocks">
|
|
36
|
+
{blocks.map((b) => (
|
|
37
|
+
<button
|
|
38
|
+
key={b.slug}
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={() => choose(b.slug)}
|
|
41
|
+
data-testid={`onpage-block-${b.slug}`}
|
|
42
|
+
>
|
|
43
|
+
{b.labels.singular}
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</Dialog>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one dialog primitive: labelled, focus moves in on open and back on
|
|
5
|
+
* close, Tab stays inside (modal), Escape closes. `variant="popover"` is
|
|
6
|
+
* anchored next to an element and closes on an outside click.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from 'react'
|
|
9
|
+
import { focusableIn, popoverPosition } from '../dom.ts'
|
|
10
|
+
import { useLayoutTick } from '../layout.ts'
|
|
11
|
+
import type { Translate } from '../messages.ts'
|
|
12
|
+
|
|
13
|
+
export interface DialogProps {
|
|
14
|
+
title: string
|
|
15
|
+
onClose: () => void
|
|
16
|
+
children: ReactNode
|
|
17
|
+
t: Translate
|
|
18
|
+
/** modal: centered with a backdrop. panel: fixed side panel. popover: next to `anchor`. */
|
|
19
|
+
variant?: 'modal' | 'panel' | 'popover'
|
|
20
|
+
anchor?: () => Element | null
|
|
21
|
+
width?: number
|
|
22
|
+
testId?: string
|
|
23
|
+
/** Extra header content (search box, actions). */
|
|
24
|
+
actions?: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Dialog({
|
|
28
|
+
title,
|
|
29
|
+
onClose,
|
|
30
|
+
children,
|
|
31
|
+
t,
|
|
32
|
+
variant = 'modal',
|
|
33
|
+
anchor,
|
|
34
|
+
width,
|
|
35
|
+
testId,
|
|
36
|
+
actions,
|
|
37
|
+
}: DialogProps) {
|
|
38
|
+
const box = useRef<HTMLDivElement>(null)
|
|
39
|
+
const titleId = useId()
|
|
40
|
+
const [height, setHeight] = useState(240)
|
|
41
|
+
useLayoutTick() // popovers follow their anchor
|
|
42
|
+
|
|
43
|
+
// Focus in on open, back on close.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const previous = document.activeElement as HTMLElement | null
|
|
46
|
+
const el = box.current
|
|
47
|
+
if (el)
|
|
48
|
+
(
|
|
49
|
+
el.querySelector<HTMLElement>('[autofocus],[data-autofocus]') ??
|
|
50
|
+
focusableIn(el)[0] ??
|
|
51
|
+
el
|
|
52
|
+
).focus({ preventScroll: true })
|
|
53
|
+
return () => previous?.focus?.({ preventScroll: true })
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
// Popovers flip above their anchor when there's no room below: track their height.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const el = box.current
|
|
59
|
+
if (variant !== 'popover' || !el) return
|
|
60
|
+
const ro = new ResizeObserver(() => setHeight(el.offsetHeight))
|
|
61
|
+
ro.observe(el)
|
|
62
|
+
return () => ro.disconnect()
|
|
63
|
+
}, [variant])
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const onKey = (e: KeyboardEvent) => {
|
|
67
|
+
if (e.key === 'Escape') {
|
|
68
|
+
e.stopPropagation()
|
|
69
|
+
onClose()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
if (e.key !== 'Tab' || variant !== 'modal' || !box.current) return
|
|
73
|
+
const items = focusableIn(box.current)
|
|
74
|
+
if (!items.length) return
|
|
75
|
+
const first = items[0]
|
|
76
|
+
const last = items[items.length - 1]
|
|
77
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
78
|
+
e.preventDefault()
|
|
79
|
+
last.focus()
|
|
80
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
81
|
+
e.preventDefault()
|
|
82
|
+
first.focus()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const onDown = (e: PointerEvent) => {
|
|
86
|
+
if (variant === 'popover' && box.current && !box.current.contains(e.target as Node)) onClose()
|
|
87
|
+
}
|
|
88
|
+
document.addEventListener('keydown', onKey, true)
|
|
89
|
+
// Deferred, so the click that opened a popover doesn't close it.
|
|
90
|
+
const timer = setTimeout(() => document.addEventListener('pointerdown', onDown, true))
|
|
91
|
+
return () => {
|
|
92
|
+
clearTimeout(timer)
|
|
93
|
+
document.removeEventListener('keydown', onKey, true)
|
|
94
|
+
document.removeEventListener('pointerdown', onDown, true)
|
|
95
|
+
}
|
|
96
|
+
}, [onClose, variant])
|
|
97
|
+
|
|
98
|
+
const header = (
|
|
99
|
+
<header className="po-dialog-head">
|
|
100
|
+
<h2 className="po-h3" id={titleId}>
|
|
101
|
+
{title}
|
|
102
|
+
</h2>
|
|
103
|
+
{actions}
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
className="po-btn po-close"
|
|
107
|
+
onClick={onClose}
|
|
108
|
+
aria-label={t('common.close')}
|
|
109
|
+
>
|
|
110
|
+
✕
|
|
111
|
+
</button>
|
|
112
|
+
</header>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (variant === 'modal') {
|
|
116
|
+
return (
|
|
117
|
+
// Backdrop: a press outside the dialog closes it (Escape works from the keyboard).
|
|
118
|
+
<div
|
|
119
|
+
className="po-modal-bg"
|
|
120
|
+
onPointerDown={(e) => e.target === e.currentTarget && onClose()}
|
|
121
|
+
data-testid={testId}
|
|
122
|
+
>
|
|
123
|
+
<div
|
|
124
|
+
ref={box}
|
|
125
|
+
className="po-modal"
|
|
126
|
+
role="dialog"
|
|
127
|
+
aria-modal="true"
|
|
128
|
+
aria-labelledby={titleId}
|
|
129
|
+
tabIndex={-1}
|
|
130
|
+
style={width ? { width: `min(${width}px, 100%)` } : undefined}
|
|
131
|
+
>
|
|
132
|
+
{header}
|
|
133
|
+
<div className="po-body">{children}</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
const style: CSSProperties =
|
|
139
|
+
variant === 'popover'
|
|
140
|
+
? {
|
|
141
|
+
...popoverPosition(anchor?.() ?? null, { width: width ?? 300, height }),
|
|
142
|
+
width: width ?? 300,
|
|
143
|
+
}
|
|
144
|
+
: { top: 72, right: 16 }
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
ref={box}
|
|
148
|
+
className={variant === 'popover' ? 'po-pop' : 'po-panel'}
|
|
149
|
+
role="dialog"
|
|
150
|
+
aria-labelledby={titleId}
|
|
151
|
+
tabIndex={-1}
|
|
152
|
+
style={style}
|
|
153
|
+
data-testid={testId}
|
|
154
|
+
>
|
|
155
|
+
{header}
|
|
156
|
+
{children}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|