@avocadostudio-ai/preview-adapter 0.3.3 → 0.5.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/dist/bridge-functions.d.ts +84 -0
- package/dist/bridge-functions.js +149 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/preview-bridge-core.js +6 -3
- package/package.json +2 -2
- package/src/styles.css +79 -0
|
@@ -71,6 +71,51 @@ export declare function findEditableNode(parent: HTMLElement, editablePath: stri
|
|
|
71
71
|
* precise, and still the difference between "that one" and nothing.
|
|
72
72
|
*/
|
|
73
73
|
export declare function resolveEditableNode(parent: HTMLElement, editablePath: string): HTMLElement | null;
|
|
74
|
+
/**
|
|
75
|
+
* Does this marked node draw an image?
|
|
76
|
+
*
|
|
77
|
+
* Ask the node first. `data-editable-kind`, which `editableProps` emits from
|
|
78
|
+
* whatever the site's own block manifest says, is a statement by the only party
|
|
79
|
+
* that knows. `isImagePath` is a guess from the prop's *name* against Avocado's
|
|
80
|
+
* naming for its own catalogue — `imageUrl`, `*.src` — and it is right about
|
|
81
|
+
* Avocado's blocks for the circular reason that Avocado named them. A site
|
|
82
|
+
* calling the field `photoUrl` or `heroSrc` gets an image picker in the
|
|
83
|
+
* property panel, which reads the manifest, and no button in the preview, which
|
|
84
|
+
* does not; nothing anywhere reports a disagreement.
|
|
85
|
+
*
|
|
86
|
+
* The guess stays as the fallback: every built-in renderer, and every
|
|
87
|
+
* integration written before the attribute existed, emits a path and no kind.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isImageEditable(node: HTMLElement, editablePath: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* May this marked node be typed into?
|
|
92
|
+
*
|
|
93
|
+
* The other half of `inlineEditable: false`, which until now existed only in the
|
|
94
|
+
* property panel: the manifest could say a string is not something anybody edits
|
|
95
|
+
* on the page, and the preview had no way to hear it. Marking such a field was
|
|
96
|
+
* therefore all-or-nothing — mark it and accept that a double-click starts a
|
|
97
|
+
* text edit, or leave it unmarked and lose the hover pill, the field selection
|
|
98
|
+
* and, when it holds a picture, the Change button.
|
|
99
|
+
*
|
|
100
|
+
* The field this was written for is the built-in blocks' `icon`: an emoji, a
|
|
101
|
+
* symbol, or the URL of a small image. Everything else renders as nothing at
|
|
102
|
+
* all, so typing the word the planner reaches for — "rocket" — into a marked
|
|
103
|
+
* emoji makes the icon disappear, with the value stored and invisible.
|
|
104
|
+
*
|
|
105
|
+
* The attribute is read from the marked element, so a site declares it once
|
|
106
|
+
* where it declares the target.
|
|
107
|
+
*/
|
|
108
|
+
export declare function isInlineEditable(node: HTMLElement): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Should Enter insert a newline rather than committing the edit?
|
|
111
|
+
*
|
|
112
|
+
* `body` is grandfathered: it is the name Avocado's own long-text fields use,
|
|
113
|
+
* and it was the entire rule before `data-editable-multiline` existed — the same
|
|
114
|
+
* shape of mistake as guessing an image from `imageUrl`, and a site whose prose
|
|
115
|
+
* field is called `intro` or `beschreibung` got a single-line editor for a
|
|
116
|
+
* paragraph. A node that states its own answer is believed either way.
|
|
117
|
+
*/
|
|
118
|
+
export declare function isMultilineEditable(node: HTMLElement, editablePath: string): boolean;
|
|
74
119
|
/**
|
|
75
120
|
* True when React rendered this editable field as block-level element children
|
|
76
121
|
* (parsed markdown), not a plain text node.
|
|
@@ -131,6 +176,45 @@ export declare function removeSkeletons(): void;
|
|
|
131
176
|
* per-field attributes by accident. Say so once, loudly, in development.
|
|
132
177
|
*/
|
|
133
178
|
export declare function missingBlockWrapperWarning(blockCount: number, editableCount: number): string | null;
|
|
179
|
+
/**
|
|
180
|
+
* The other half of a half-finished integration — and the likelier half.
|
|
181
|
+
*
|
|
182
|
+
* `missingBlockWrapperWarning` names the case where the fields are marked and
|
|
183
|
+
* the blocks are not. This names its mirror, which is what following the
|
|
184
|
+
* integration guide to its end and stopping one step early actually produces:
|
|
185
|
+
* every block wrapped, no field marked. Wrapping blocks is a single call in the
|
|
186
|
+
* preview route; marking fields means touching the site's own components, so it
|
|
187
|
+
* is the step that gets deferred and then forgotten.
|
|
188
|
+
*
|
|
189
|
+
* What it costs is invisible rather than broken. Selection works. Highlighting,
|
|
190
|
+
* scroll-to-block, the badges, the property panel and every edit made through
|
|
191
|
+
* it all work, because the panel is built from the block manifest and never
|
|
192
|
+
* looks at the page. What is silently off is everything the overlay finds by
|
|
193
|
+
* walking the DOM: inline text editing, the field pills on hover, the image
|
|
194
|
+
* Change/Remove buttons, and the live-draft path that streams a field into the
|
|
195
|
+
* node that draws it. A reviewer sees a preview that responds to clicks and
|
|
196
|
+
* concludes it is wired.
|
|
197
|
+
*
|
|
198
|
+
* Like its mirror this is an all-or-nothing test, not a coverage check: one
|
|
199
|
+
* marked field means the integrator knows the attribute exists, and which of
|
|
200
|
+
* their fields deserve it is their call, not ours.
|
|
201
|
+
*/
|
|
202
|
+
export declare function missingEditableTargetsWarning(blockCount: number, editableCount: number): string | null;
|
|
203
|
+
/** Every integration problem the overlay can diagnose from the page alone. */
|
|
204
|
+
export declare function previewIntegrationWarnings(blockCount: number, editableCount: number): string[];
|
|
205
|
+
/**
|
|
206
|
+
* Put the diagnosis where the person is looking.
|
|
207
|
+
*
|
|
208
|
+
* Both warnings above describe a preview that looks like it works, so the
|
|
209
|
+
* console they would print to is the one nobody opens — and in the editor it is
|
|
210
|
+
* the *iframe's* console, two context menus away from the window in front of
|
|
211
|
+
* the user. The symptom is "the button is missing from the preview"; the
|
|
212
|
+
* explanation has to be in the preview.
|
|
213
|
+
*
|
|
214
|
+
* Development only, and dismissible: it sits over the site's own design, and
|
|
215
|
+
* once read it has done its work.
|
|
216
|
+
*/
|
|
217
|
+
export declare function showIntegrationWarnings(messages: string[]): void;
|
|
134
218
|
export declare function ensureBlockBadges(): void;
|
|
135
219
|
export declare function clearListItemSelection(scope?: ParentNode): void;
|
|
136
220
|
export declare function removeOverlayControls(deleteConfirmTimer: {
|
package/dist/bridge-functions.js
CHANGED
|
@@ -289,6 +289,63 @@ export function resolveEditableNode(parent, editablePath) {
|
|
|
289
289
|
}
|
|
290
290
|
return null;
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Does this marked node draw an image?
|
|
294
|
+
*
|
|
295
|
+
* Ask the node first. `data-editable-kind`, which `editableProps` emits from
|
|
296
|
+
* whatever the site's own block manifest says, is a statement by the only party
|
|
297
|
+
* that knows. `isImagePath` is a guess from the prop's *name* against Avocado's
|
|
298
|
+
* naming for its own catalogue — `imageUrl`, `*.src` — and it is right about
|
|
299
|
+
* Avocado's blocks for the circular reason that Avocado named them. A site
|
|
300
|
+
* calling the field `photoUrl` or `heroSrc` gets an image picker in the
|
|
301
|
+
* property panel, which reads the manifest, and no button in the preview, which
|
|
302
|
+
* does not; nothing anywhere reports a disagreement.
|
|
303
|
+
*
|
|
304
|
+
* The guess stays as the fallback: every built-in renderer, and every
|
|
305
|
+
* integration written before the attribute existed, emits a path and no kind.
|
|
306
|
+
*/
|
|
307
|
+
export function isImageEditable(node, editablePath) {
|
|
308
|
+
const declared = node.getAttribute("data-editable-kind");
|
|
309
|
+
if (declared)
|
|
310
|
+
return declared === "image";
|
|
311
|
+
return isImagePath(editablePath);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* May this marked node be typed into?
|
|
315
|
+
*
|
|
316
|
+
* The other half of `inlineEditable: false`, which until now existed only in the
|
|
317
|
+
* property panel: the manifest could say a string is not something anybody edits
|
|
318
|
+
* on the page, and the preview had no way to hear it. Marking such a field was
|
|
319
|
+
* therefore all-or-nothing — mark it and accept that a double-click starts a
|
|
320
|
+
* text edit, or leave it unmarked and lose the hover pill, the field selection
|
|
321
|
+
* and, when it holds a picture, the Change button.
|
|
322
|
+
*
|
|
323
|
+
* The field this was written for is the built-in blocks' `icon`: an emoji, a
|
|
324
|
+
* symbol, or the URL of a small image. Everything else renders as nothing at
|
|
325
|
+
* all, so typing the word the planner reaches for — "rocket" — into a marked
|
|
326
|
+
* emoji makes the icon disappear, with the value stored and invisible.
|
|
327
|
+
*
|
|
328
|
+
* The attribute is read from the marked element, so a site declares it once
|
|
329
|
+
* where it declares the target.
|
|
330
|
+
*/
|
|
331
|
+
export function isInlineEditable(node) {
|
|
332
|
+
return node.getAttribute("data-editable-inline") !== "false";
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Should Enter insert a newline rather than committing the edit?
|
|
336
|
+
*
|
|
337
|
+
* `body` is grandfathered: it is the name Avocado's own long-text fields use,
|
|
338
|
+
* and it was the entire rule before `data-editable-multiline` existed — the same
|
|
339
|
+
* shape of mistake as guessing an image from `imageUrl`, and a site whose prose
|
|
340
|
+
* field is called `intro` or `beschreibung` got a single-line editor for a
|
|
341
|
+
* paragraph. A node that states its own answer is believed either way.
|
|
342
|
+
*/
|
|
343
|
+
export function isMultilineEditable(node, editablePath) {
|
|
344
|
+
const declared = node.getAttribute("data-editable-multiline");
|
|
345
|
+
if (declared)
|
|
346
|
+
return declared !== "false";
|
|
347
|
+
return editablePath === "body";
|
|
348
|
+
}
|
|
292
349
|
// Block-level tags that signal a field React rendered as parsed-markdown element
|
|
293
350
|
// children (e.g. RichText `body`, Tabs `content`) rather than a single text node.
|
|
294
351
|
const RICH_EDITABLE_SELECTOR = "p,ul,ol,li,blockquote,pre,table,h1,h2,h3,h4,h5,h6,hr";
|
|
@@ -499,6 +556,88 @@ export function missingBlockWrapperWarning(blockCount, editableCount) {
|
|
|
499
556
|
"@avocadostudio-ai/site-sdk/editor — it sets data-block-id, data-block-type and the " +
|
|
500
557
|
"editor-selectable class that the field pills are scoped to.");
|
|
501
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* The other half of a half-finished integration — and the likelier half.
|
|
561
|
+
*
|
|
562
|
+
* `missingBlockWrapperWarning` names the case where the fields are marked and
|
|
563
|
+
* the blocks are not. This names its mirror, which is what following the
|
|
564
|
+
* integration guide to its end and stopping one step early actually produces:
|
|
565
|
+
* every block wrapped, no field marked. Wrapping blocks is a single call in the
|
|
566
|
+
* preview route; marking fields means touching the site's own components, so it
|
|
567
|
+
* is the step that gets deferred and then forgotten.
|
|
568
|
+
*
|
|
569
|
+
* What it costs is invisible rather than broken. Selection works. Highlighting,
|
|
570
|
+
* scroll-to-block, the badges, the property panel and every edit made through
|
|
571
|
+
* it all work, because the panel is built from the block manifest and never
|
|
572
|
+
* looks at the page. What is silently off is everything the overlay finds by
|
|
573
|
+
* walking the DOM: inline text editing, the field pills on hover, the image
|
|
574
|
+
* Change/Remove buttons, and the live-draft path that streams a field into the
|
|
575
|
+
* node that draws it. A reviewer sees a preview that responds to clicks and
|
|
576
|
+
* concludes it is wired.
|
|
577
|
+
*
|
|
578
|
+
* Like its mirror this is an all-or-nothing test, not a coverage check: one
|
|
579
|
+
* marked field means the integrator knows the attribute exists, and which of
|
|
580
|
+
* their fields deserve it is their call, not ours.
|
|
581
|
+
*/
|
|
582
|
+
export function missingEditableTargetsWarning(blockCount, editableCount) {
|
|
583
|
+
if (editableCount > 0 || blockCount === 0)
|
|
584
|
+
return null;
|
|
585
|
+
return (`[avocado] The overlay found ${blockCount} [data-block-id] wrapper(s) and no ` +
|
|
586
|
+
"data-editable-target attribute. Selection and the property panel work; inline text " +
|
|
587
|
+
"editing, the field pills and the image Change/Remove buttons are all off, because every " +
|
|
588
|
+
"one of them is found by walking the marked fields in the page.\n" +
|
|
589
|
+
"Mark the element that draws each editable prop with editableProps(path, { kind }) from " +
|
|
590
|
+
"@avocadostudio-ai/site-sdk/editor — e.g. editableProps(\"imageUrl\", { kind: \"image\" }) " +
|
|
591
|
+
"on the wrapper around an image, editableProps(\"heading\") on the element holding it.");
|
|
592
|
+
}
|
|
593
|
+
/** Every integration problem the overlay can diagnose from the page alone. */
|
|
594
|
+
export function previewIntegrationWarnings(blockCount, editableCount) {
|
|
595
|
+
return [
|
|
596
|
+
missingBlockWrapperWarning(blockCount, editableCount),
|
|
597
|
+
missingEditableTargetsWarning(blockCount, editableCount),
|
|
598
|
+
].filter((w) => w !== null);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Put the diagnosis where the person is looking.
|
|
602
|
+
*
|
|
603
|
+
* Both warnings above describe a preview that looks like it works, so the
|
|
604
|
+
* console they would print to is the one nobody opens — and in the editor it is
|
|
605
|
+
* the *iframe's* console, two context menus away from the window in front of
|
|
606
|
+
* the user. The symptom is "the button is missing from the preview"; the
|
|
607
|
+
* explanation has to be in the preview.
|
|
608
|
+
*
|
|
609
|
+
* Development only, and dismissible: it sits over the site's own design, and
|
|
610
|
+
* once read it has done its work.
|
|
611
|
+
*/
|
|
612
|
+
export function showIntegrationWarnings(messages) {
|
|
613
|
+
document.querySelector(".editor-integration-warning")?.remove();
|
|
614
|
+
if (messages.length === 0)
|
|
615
|
+
return;
|
|
616
|
+
const banner = document.createElement("div");
|
|
617
|
+
banner.className = "editor-integration-warning";
|
|
618
|
+
banner.setAttribute("role", "alert");
|
|
619
|
+
for (const message of messages) {
|
|
620
|
+
const [headline, ...rest] = message.replace(/^\[avocado] /, "").split("\n");
|
|
621
|
+
const item = document.createElement("div");
|
|
622
|
+
item.className = "editor-integration-warning__item";
|
|
623
|
+
const title = document.createElement("strong");
|
|
624
|
+
title.textContent = headline ?? "";
|
|
625
|
+
item.append(title);
|
|
626
|
+
for (const line of rest) {
|
|
627
|
+
const p = document.createElement("p");
|
|
628
|
+
p.textContent = line;
|
|
629
|
+
item.append(p);
|
|
630
|
+
}
|
|
631
|
+
banner.append(item);
|
|
632
|
+
}
|
|
633
|
+
const dismiss = document.createElement("button");
|
|
634
|
+
dismiss.type = "button";
|
|
635
|
+
dismiss.className = "editor-integration-warning__dismiss";
|
|
636
|
+
dismiss.textContent = "Dismiss";
|
|
637
|
+
dismiss.addEventListener("click", () => banner.remove());
|
|
638
|
+
banner.append(dismiss);
|
|
639
|
+
document.body.append(banner);
|
|
640
|
+
}
|
|
502
641
|
export function ensureBlockBadges() {
|
|
503
642
|
document.querySelectorAll("[data-block-id]").forEach((node) => {
|
|
504
643
|
const blockType = node.getAttribute("data-block-type") ?? "Block";
|
|
@@ -605,7 +744,9 @@ export function applyAiFieldLoading(blockId, editablePath, active) {
|
|
|
605
744
|
}
|
|
606
745
|
}
|
|
607
746
|
export function cleanupOverlayElements() {
|
|
608
|
-
document
|
|
747
|
+
document
|
|
748
|
+
.querySelectorAll(".aifx-shimmer-overlay, .aifx-shimmer-sparkle, .editor-image-change-btn, .editor-integration-warning")
|
|
749
|
+
.forEach((el) => el.remove());
|
|
609
750
|
document.documentElement.removeAttribute("data-editor-active");
|
|
610
751
|
document.documentElement.removeAttribute("data-editor-selection-mode");
|
|
611
752
|
}
|
|
@@ -706,11 +847,11 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
706
847
|
}
|
|
707
848
|
}
|
|
708
849
|
for (const [path, value] of Object.entries(fields)) {
|
|
709
|
-
if (isImagePath(path))
|
|
710
|
-
continue;
|
|
711
850
|
const node = findEditableNode(block, path);
|
|
712
851
|
if (!node)
|
|
713
852
|
continue;
|
|
853
|
+
if (isImageEditable(node, path))
|
|
854
|
+
continue;
|
|
714
855
|
// Rich fields (RichText body, Tabs content, etc.) render as block-level
|
|
715
856
|
// React children. Writing innerHTML here desyncs React's vdom and the
|
|
716
857
|
// final router.refresh() then mis-reconciles the subtree (truncated text
|
|
@@ -905,6 +1046,8 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
905
1046
|
const startInlineEdit = (args) => {
|
|
906
1047
|
if (!supportsInlineEditablePath(args.editablePath))
|
|
907
1048
|
return;
|
|
1049
|
+
if (!isInlineEditable(args.node))
|
|
1050
|
+
return;
|
|
908
1051
|
if (args.node.children.length > 0)
|
|
909
1052
|
return;
|
|
910
1053
|
const existing = state.inlineEditing;
|
|
@@ -919,7 +1062,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
919
1062
|
blockType: args.blockType,
|
|
920
1063
|
editablePath: args.editablePath,
|
|
921
1064
|
initialValue,
|
|
922
|
-
isMultiline: args.editablePath
|
|
1065
|
+
isMultiline: isMultilineEditable(args.node, args.editablePath)
|
|
923
1066
|
};
|
|
924
1067
|
args.node.setAttribute("contenteditable", "true");
|
|
925
1068
|
args.node.classList.add("editor-inline-editing");
|
|
@@ -1208,7 +1351,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1208
1351
|
}
|
|
1209
1352
|
document.querySelectorAll(".editor-selectable [data-editable-target]").forEach((el) => {
|
|
1210
1353
|
const path = el.getAttribute("data-editable-target") ?? "";
|
|
1211
|
-
if (!
|
|
1354
|
+
if (!isImageEditable(el, path))
|
|
1212
1355
|
return;
|
|
1213
1356
|
const block = el.closest("[data-block-id]");
|
|
1214
1357
|
if (!block)
|
|
@@ -1638,7 +1781,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1638
1781
|
}
|
|
1639
1782
|
applyBlockFocus(blockId, false, editablePath, { scrollIntoView: false });
|
|
1640
1783
|
let editableValue = null;
|
|
1641
|
-
if (editablePath &&
|
|
1784
|
+
if (editablePath && childNode && isImageEditable(childNode, editablePath)) {
|
|
1642
1785
|
const img = childNode.querySelector("img");
|
|
1643
1786
|
if (img?.src)
|
|
1644
1787
|
editableValue = img.src;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { PreviewBridge } from "./preview-bridge.tsx";
|
|
|
2
2
|
export { PreviewBridgeCore } from "./preview-bridge-core.tsx";
|
|
3
3
|
export type { PreviewBridgeConfig, PreviewBridgeCoreProps } from "./preview-bridge-core.tsx";
|
|
4
4
|
export { getPreviewWrapperProps } from "./selectable.ts";
|
|
5
|
-
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.ts";
|
|
5
|
+
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, missingEditableTargetsWarning, previewIntegrationWarnings, } from "./bridge-functions.ts";
|
|
6
6
|
export type { BridgeCallbacks, BridgeState, BridgeFunctions } from "./bridge-functions.ts";
|
|
7
7
|
export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.tsx";
|
|
8
8
|
export type { LivePreviewPage, LivePreviewBridgeApi } from "./live-preview-store.tsx";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { PreviewBridge } from "./preview-bridge.js";
|
|
2
2
|
export { PreviewBridgeCore } from "./preview-bridge-core.js";
|
|
3
3
|
export { getPreviewWrapperProps } from "./selectable.js";
|
|
4
|
-
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.js";
|
|
4
|
+
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, missingEditableTargetsWarning, previewIntegrationWarnings, } from "./bridge-functions.js";
|
|
5
5
|
export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
|
-
import { createBridgeFunctions, createBridgeState, findBlockNode, isProgrammaticScroll, parseListItemPath, ensureBlockBadges,
|
|
4
|
+
import { createBridgeFunctions, createBridgeState, findBlockNode, isProgrammaticScroll, parseListItemPath, ensureBlockBadges, previewIntegrationWarnings, showIntegrationWarnings, scrollBlockIntoView, setNestedLabelsVisibility, showSkeleton, removeSkeletons, clearChildFocus, clearAllHighlights, clearListItemSelection, applyAiFieldLoading, cleanupOverlayElements, withPreviewParams, } from "./bridge-functions.js";
|
|
5
5
|
export function PreviewBridgeCore(props) {
|
|
6
6
|
// When running standalone (no editor origin) or not embedded in an iframe, render nothing.
|
|
7
7
|
if (!props.editorOrigin || typeof window !== "undefined" && window.parent === window)
|
|
@@ -56,9 +56,12 @@ function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathnam
|
|
|
56
56
|
*/
|
|
57
57
|
if (process.env.NODE_ENV !== "production") {
|
|
58
58
|
requestAnimationFrame(() => {
|
|
59
|
-
const
|
|
60
|
-
|
|
59
|
+
const warnings = previewIntegrationWarnings(document.querySelectorAll("[data-block-id]").length, document.querySelectorAll("[data-editable-target]").length);
|
|
60
|
+
for (const warning of warnings)
|
|
61
61
|
console.error(warning);
|
|
62
|
+
// Both of these describe a preview that *looks* correct, so the iframe's
|
|
63
|
+
// console is the last place they would be found. Say it in the frame.
|
|
64
|
+
showIntegrationWarnings(warnings);
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
// -- Mutation observer -------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/preview-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"src/styles.css"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@avocadostudio-ai/shared": "^0.
|
|
35
|
+
"@avocadostudio-ai/shared": "^0.5.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"next": ">=15.0.0",
|
package/src/styles.css
CHANGED
|
@@ -1152,6 +1152,27 @@
|
|
|
1152
1152
|
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18);
|
|
1153
1153
|
}
|
|
1154
1154
|
|
|
1155
|
+
/*
|
|
1156
|
+
* Hovering the *block* reveals its image buttons, not hovering the image.
|
|
1157
|
+
*
|
|
1158
|
+
* An image is very often not the top thing over its own area, and both ways it
|
|
1159
|
+
* happens are ordinary design, not mistakes. A full-bleed photo behind a
|
|
1160
|
+
* headline has the copy column painted over it; a card that is one big click
|
|
1161
|
+
* target has a stretched `::after` over the whole card, photo included. In
|
|
1162
|
+
* both, the marked element never receives `:hover`, so the button it holds
|
|
1163
|
+
* never became visible — the image simply had no Change button, which is
|
|
1164
|
+
* exactly how this reads as a bug in the button.
|
|
1165
|
+
*
|
|
1166
|
+
* `:hover` propagates to ancestors, and whatever covers the image is inside
|
|
1167
|
+
* the same block wrapper, so the wrapper is hovered in every one of these
|
|
1168
|
+
* cases. On a grid this reveals each card's button at once, which is the
|
|
1169
|
+
* better behaviour anyway: hover a block, see what it lets you change.
|
|
1170
|
+
*
|
|
1171
|
+
* Clicking then works without further help. The button carries z-index 24 and
|
|
1172
|
+
* its marked element sets no z-index of its own, so it competes — and wins —
|
|
1173
|
+
* in the block's own stacking context against the copy that covers it.
|
|
1174
|
+
*/
|
|
1175
|
+
[data-editor-active] .editor-selectable:hover .editor-image-change-btn,
|
|
1155
1176
|
[data-editor-active] [data-editable-target]:hover > .editor-image-change-btn,
|
|
1156
1177
|
[data-editor-active] [data-editable-target]:focus-within > .editor-image-change-btn {
|
|
1157
1178
|
opacity: 1;
|
|
@@ -1205,3 +1226,61 @@
|
|
|
1205
1226
|
animation: none !important;
|
|
1206
1227
|
}
|
|
1207
1228
|
}
|
|
1229
|
+
|
|
1230
|
+
/* ---------------------------------------------------------------------------
|
|
1231
|
+
Integration diagnostics
|
|
1232
|
+
|
|
1233
|
+
Development only, injected by the bridge. Both warnings it can raise describe
|
|
1234
|
+
a preview that renders and responds to clicks and is missing half of what the
|
|
1235
|
+
overlay does, so the message has to appear over the page itself rather than
|
|
1236
|
+
in the iframe's console.
|
|
1237
|
+
--------------------------------------------------------------------------- */
|
|
1238
|
+
|
|
1239
|
+
.editor-integration-warning {
|
|
1240
|
+
position: fixed;
|
|
1241
|
+
z-index: 2147483000;
|
|
1242
|
+
inset: 12px 12px auto 12px;
|
|
1243
|
+
max-width: 720px;
|
|
1244
|
+
margin-inline: auto;
|
|
1245
|
+
display: flex;
|
|
1246
|
+
flex-direction: column;
|
|
1247
|
+
gap: 10px;
|
|
1248
|
+
padding: 14px 16px;
|
|
1249
|
+
border-radius: 12px;
|
|
1250
|
+
border: 1px solid rgba(180, 83, 9, 0.45);
|
|
1251
|
+
background: #fffbeb;
|
|
1252
|
+
color: #78350f;
|
|
1253
|
+
font: 13px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
1254
|
+
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.editor-integration-warning__item {
|
|
1258
|
+
display: flex;
|
|
1259
|
+
flex-direction: column;
|
|
1260
|
+
gap: 6px;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
.editor-integration-warning__item strong {
|
|
1264
|
+
font-weight: 650;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.editor-integration-warning__item p {
|
|
1268
|
+
margin: 0;
|
|
1269
|
+
color: #92400e;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
.editor-integration-warning__dismiss {
|
|
1273
|
+
align-self: flex-start;
|
|
1274
|
+
padding: 5px 12px;
|
|
1275
|
+
border: 1px solid rgba(120, 53, 15, 0.35);
|
|
1276
|
+
border-radius: 8px;
|
|
1277
|
+
background: transparent;
|
|
1278
|
+
color: inherit;
|
|
1279
|
+
font: inherit;
|
|
1280
|
+
font-weight: 600;
|
|
1281
|
+
cursor: pointer;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.editor-integration-warning__dismiss:hover {
|
|
1285
|
+
background: rgba(120, 53, 15, 0.08);
|
|
1286
|
+
}
|