@avocadostudio-ai/preview-adapter 0.3.2 → 0.4.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 +154 -0
- package/dist/bridge-functions.js +317 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/preview-bridge-core.js +18 -7
- package/package.json +2 -2
- package/src/styles.css +79 -0
|
@@ -16,7 +16,106 @@ export declare function markdownToHtml(md: string): string;
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function withPreviewParams(href: string, currentSearch: string): string;
|
|
18
18
|
export declare function findBlockNode(blockId: string): HTMLElement | null;
|
|
19
|
+
/**
|
|
20
|
+
* True while a scroll this module started is still running or being corrected.
|
|
21
|
+
*
|
|
22
|
+
* The editor treats an `iframeScrolled` report as the reader taking over —
|
|
23
|
+
* it drops the field anchor and stops following a running chat stream — so our
|
|
24
|
+
* own scrolling must not be reported. One flag for every scroll we initiate,
|
|
25
|
+
* rather than a marker at each call site, because the one call site that had no
|
|
26
|
+
* marker (block focus, which is what "Go to" uses) is exactly the one that
|
|
27
|
+
* scrolls in response to a click somewhere else.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isProgrammaticScroll(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Bring a block on screen and *keep* it there while the page settles.
|
|
32
|
+
*
|
|
33
|
+
* One `scrollIntoView` cannot do this. It resolves its destination from the
|
|
34
|
+
* layout at the instant it is called, and the preview is hardly ever settled at
|
|
35
|
+
* that instant — selection chrome is mounting, hero images are swapping in,
|
|
36
|
+
* blocks above the target are still changing height. Measured against the demo
|
|
37
|
+
* site, the target moved 478–1152px between the call and the end of the
|
|
38
|
+
* animation, every time, which left it *entirely above the viewport*: "Go to"
|
|
39
|
+
* appeared to jump to a random section, and the block the panel was now editing
|
|
40
|
+
* was nowhere on screen.
|
|
41
|
+
*
|
|
42
|
+
* So: scroll, then watch. Correct whenever the block has drifted out of the
|
|
43
|
+
* band we aimed for, and stop as soon as it holds still. Corrections wait for
|
|
44
|
+
* the previous scroll to finish (a stable `scrollY`) so we never interrupt the
|
|
45
|
+
* smooth animation we started, and they use `auto` — a second smooth animation
|
|
46
|
+
* queued behind the first reads as a lurch.
|
|
47
|
+
*
|
|
48
|
+
* A reader who starts scrolling wins immediately. Being 300px off is a smaller
|
|
49
|
+
* insult than dragging the page back out from under someone's hand.
|
|
50
|
+
*/
|
|
51
|
+
export declare function scrollBlockIntoView(el: HTMLElement, options?: {
|
|
52
|
+
behavior?: ScrollBehavior;
|
|
53
|
+
block?: ScrollLogicalPosition;
|
|
54
|
+
}): void;
|
|
19
55
|
export declare function findEditableNode(parent: HTMLElement, editablePath: string): HTMLElement | null;
|
|
56
|
+
/**
|
|
57
|
+
* The node to point at for a field, which is not always the field's own node.
|
|
58
|
+
*
|
|
59
|
+
* Plenty of editable props are never rendered as an element. Alt text is an
|
|
60
|
+
* attribute on an image; a link target is an attribute on an anchor. A renderer
|
|
61
|
+
* emits `data-editable-target` for what it *draws*, so `cards[0].imageAlt` and
|
|
62
|
+
* `cards[0].ctaHref` match nothing at all, and asking to show somebody where
|
|
63
|
+
* the problem is has so far shown them nothing.
|
|
64
|
+
*
|
|
65
|
+
* Three steps, most specific first:
|
|
66
|
+
* 1. the field's own node, when it has one
|
|
67
|
+
* 2. for alt text, the image it describes — the only thing on the page that
|
|
68
|
+
* alt text *is* about, and the inverse of the `toAltPath` convention the
|
|
69
|
+
* renderers already follow
|
|
70
|
+
* 3. the nearest enclosing node: the card, the list item, the block. Less
|
|
71
|
+
* precise, and still the difference between "that one" and nothing.
|
|
72
|
+
*/
|
|
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;
|
|
20
119
|
/**
|
|
21
120
|
* True when React rendered this editable field as block-level element children
|
|
22
121
|
* (parsed markdown), not a plain text node.
|
|
@@ -61,6 +160,61 @@ export declare function setNestedLabelsVisibility(visible: boolean): void;
|
|
|
61
160
|
export declare function clearChildFocus(): void;
|
|
62
161
|
export declare function showSkeleton(afterBlockId: string | null, blockType: string): void;
|
|
63
162
|
export declare function removeSkeletons(): void;
|
|
163
|
+
/**
|
|
164
|
+
* The one misintegration the overlay can diagnose for itself.
|
|
165
|
+
*
|
|
166
|
+
* Selection is entirely `[data-block-id]`: a click resolves through
|
|
167
|
+
* `closest("[data-block-id]")`, and a null result is read as "clicked outside
|
|
168
|
+
* any block", which *clears* the selection. A site that added
|
|
169
|
+
* `data-editable-target` to its components but no block wrappers therefore
|
|
170
|
+
* frames, renders, styles and scrolls correctly, and silently deselects on
|
|
171
|
+
* every click — and the README has already trained the reader to expect a
|
|
172
|
+
* preview that ignores clicks, because that is what selection mode being off
|
|
173
|
+
* looks like.
|
|
174
|
+
*
|
|
175
|
+
* Editable targets with no wrapper around them is unambiguous: nobody adds the
|
|
176
|
+
* per-field attributes by accident. Say so once, loudly, in development.
|
|
177
|
+
*/
|
|
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;
|
|
64
218
|
export declare function ensureBlockBadges(): void;
|
|
65
219
|
export declare function clearListItemSelection(scope?: ParentNode): void;
|
|
66
220
|
export declare function removeOverlayControls(deleteConfirmTimer: {
|
package/dist/bridge-functions.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* communicate back to the editor through injectable callbacks, decoupled from any
|
|
7
7
|
* specific transport (postMessage vs direct).
|
|
8
8
|
*/
|
|
9
|
-
import { isImagePath, parseInline, parseRichTextBlocks, normalizeRichTextBody, resolveRichTextHeadingLevel } from "@avocadostudio-ai/shared";
|
|
9
|
+
import { isAltPath, isImagePath, toImagePath, parseInline, parseRichTextBlocks, normalizeRichTextBody, resolveRichTextHeadingLevel } from "@avocadostudio-ai/shared";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Markdown → HTML for the live-draft overlay
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -141,6 +141,108 @@ export function findBlockNode(blockId) {
|
|
|
141
141
|
}
|
|
142
142
|
return document.querySelector(`[data-block-id='${blockId}']`);
|
|
143
143
|
}
|
|
144
|
+
/** How long we keep correcting a scroll after asking for it. */
|
|
145
|
+
const SCROLL_SETTLE_BUDGET_MS = 1500;
|
|
146
|
+
let programmaticScrollUntil = 0;
|
|
147
|
+
/**
|
|
148
|
+
* True while a scroll this module started is still running or being corrected.
|
|
149
|
+
*
|
|
150
|
+
* The editor treats an `iframeScrolled` report as the reader taking over —
|
|
151
|
+
* it drops the field anchor and stops following a running chat stream — so our
|
|
152
|
+
* own scrolling must not be reported. One flag for every scroll we initiate,
|
|
153
|
+
* rather than a marker at each call site, because the one call site that had no
|
|
154
|
+
* marker (block focus, which is what "Go to" uses) is exactly the one that
|
|
155
|
+
* scrolls in response to a click somewhere else.
|
|
156
|
+
*/
|
|
157
|
+
export function isProgrammaticScroll() {
|
|
158
|
+
return Date.now() < programmaticScrollUntil;
|
|
159
|
+
}
|
|
160
|
+
/** Close enough to centred that another correction would only read as a twitch. */
|
|
161
|
+
const SCROLL_SETTLE_TOLERANCE_PX = 32;
|
|
162
|
+
/** Between samples: long enough that a smooth animation shows movement. */
|
|
163
|
+
const SCROLL_SETTLE_INTERVAL_MS = 120;
|
|
164
|
+
/**
|
|
165
|
+
* Bring a block on screen and *keep* it there while the page settles.
|
|
166
|
+
*
|
|
167
|
+
* One `scrollIntoView` cannot do this. It resolves its destination from the
|
|
168
|
+
* layout at the instant it is called, and the preview is hardly ever settled at
|
|
169
|
+
* that instant — selection chrome is mounting, hero images are swapping in,
|
|
170
|
+
* blocks above the target are still changing height. Measured against the demo
|
|
171
|
+
* site, the target moved 478–1152px between the call and the end of the
|
|
172
|
+
* animation, every time, which left it *entirely above the viewport*: "Go to"
|
|
173
|
+
* appeared to jump to a random section, and the block the panel was now editing
|
|
174
|
+
* was nowhere on screen.
|
|
175
|
+
*
|
|
176
|
+
* So: scroll, then watch. Correct whenever the block has drifted out of the
|
|
177
|
+
* band we aimed for, and stop as soon as it holds still. Corrections wait for
|
|
178
|
+
* the previous scroll to finish (a stable `scrollY`) so we never interrupt the
|
|
179
|
+
* smooth animation we started, and they use `auto` — a second smooth animation
|
|
180
|
+
* queued behind the first reads as a lurch.
|
|
181
|
+
*
|
|
182
|
+
* A reader who starts scrolling wins immediately. Being 300px off is a smaller
|
|
183
|
+
* insult than dragging the page back out from under someone's hand.
|
|
184
|
+
*/
|
|
185
|
+
export function scrollBlockIntoView(el, options) {
|
|
186
|
+
const block = options?.block ?? "center";
|
|
187
|
+
/** Where `block` wants the element's top edge, in viewport coordinates. */
|
|
188
|
+
const desiredTop = () => {
|
|
189
|
+
const height = Math.min(el.getBoundingClientRect().height, window.innerHeight);
|
|
190
|
+
if (block === "start")
|
|
191
|
+
return 0;
|
|
192
|
+
if (block === "end")
|
|
193
|
+
return Math.max(0, window.innerHeight - height);
|
|
194
|
+
return Math.max(0, Math.round((window.innerHeight - height) / 2));
|
|
195
|
+
};
|
|
196
|
+
/** `nearest` asks only that the block be on screen, so that is all we check. */
|
|
197
|
+
const offTarget = () => {
|
|
198
|
+
const rect = el.getBoundingClientRect();
|
|
199
|
+
if (block === "nearest")
|
|
200
|
+
return rect.bottom <= 0 || rect.top >= window.innerHeight;
|
|
201
|
+
return Math.abs(rect.top - desiredTop()) > SCROLL_SETTLE_TOLERANCE_PX;
|
|
202
|
+
};
|
|
203
|
+
const deadline = Date.now() + SCROLL_SETTLE_BUDGET_MS;
|
|
204
|
+
// A little past the last correction we could make, so the scroll it settles
|
|
205
|
+
// into is not reported to the editor as the reader's own.
|
|
206
|
+
programmaticScrollUntil = deadline + SCROLL_SETTLE_INTERVAL_MS * 3;
|
|
207
|
+
el.scrollIntoView({ behavior: options?.behavior ?? "smooth", block, inline: "nearest" });
|
|
208
|
+
let lastY = window.scrollY;
|
|
209
|
+
let timer = 0;
|
|
210
|
+
const stop = () => {
|
|
211
|
+
window.clearInterval(timer);
|
|
212
|
+
window.removeEventListener("wheel", handOver);
|
|
213
|
+
window.removeEventListener("touchstart", handOver);
|
|
214
|
+
window.removeEventListener("keydown", handOver);
|
|
215
|
+
};
|
|
216
|
+
// The reader took the wheel. Give the scroll position back to them at once,
|
|
217
|
+
// including the right to have their scrolling reported as theirs.
|
|
218
|
+
const handOver = () => {
|
|
219
|
+
programmaticScrollUntil = 0;
|
|
220
|
+
stop();
|
|
221
|
+
};
|
|
222
|
+
window.addEventListener("wheel", handOver, { passive: true });
|
|
223
|
+
window.addEventListener("touchstart", handOver, { passive: true });
|
|
224
|
+
window.addEventListener("keydown", handOver);
|
|
225
|
+
timer = window.setInterval(() => {
|
|
226
|
+
if (Date.now() > deadline || !el.isConnected)
|
|
227
|
+
return stop();
|
|
228
|
+
const y = window.scrollY;
|
|
229
|
+
const wasMoving = y !== lastY;
|
|
230
|
+
lastY = y;
|
|
231
|
+
// Still animating — let it land before judging where it landed.
|
|
232
|
+
if (wasMoving)
|
|
233
|
+
return;
|
|
234
|
+
if (!offTarget())
|
|
235
|
+
return stop();
|
|
236
|
+
el.scrollIntoView({ behavior: "auto", block, inline: "nearest" });
|
|
237
|
+
// No movement means the document cannot get any closer — a block near the
|
|
238
|
+
// end of the page can never be centred, and retrying would only burn the
|
|
239
|
+
// budget re-deciding that.
|
|
240
|
+
if (window.scrollY === y)
|
|
241
|
+
stop();
|
|
242
|
+
else
|
|
243
|
+
lastY = window.scrollY;
|
|
244
|
+
}, SCROLL_SETTLE_INTERVAL_MS);
|
|
245
|
+
}
|
|
144
246
|
export function findEditableNode(parent, editablePath) {
|
|
145
247
|
const nodes = parent.querySelectorAll("[data-editable-target]");
|
|
146
248
|
for (const node of nodes) {
|
|
@@ -149,6 +251,101 @@ export function findEditableNode(parent, editablePath) {
|
|
|
149
251
|
}
|
|
150
252
|
return null;
|
|
151
253
|
}
|
|
254
|
+
/** Drop the last `.segment` of a path: `cards[0].ctaHref` → `cards[0]`. */
|
|
255
|
+
function parentPath(editablePath) {
|
|
256
|
+
const cut = editablePath.lastIndexOf(".");
|
|
257
|
+
return cut > 0 ? editablePath.slice(0, cut) : null;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The node to point at for a field, which is not always the field's own node.
|
|
261
|
+
*
|
|
262
|
+
* Plenty of editable props are never rendered as an element. Alt text is an
|
|
263
|
+
* attribute on an image; a link target is an attribute on an anchor. A renderer
|
|
264
|
+
* emits `data-editable-target` for what it *draws*, so `cards[0].imageAlt` and
|
|
265
|
+
* `cards[0].ctaHref` match nothing at all, and asking to show somebody where
|
|
266
|
+
* the problem is has so far shown them nothing.
|
|
267
|
+
*
|
|
268
|
+
* Three steps, most specific first:
|
|
269
|
+
* 1. the field's own node, when it has one
|
|
270
|
+
* 2. for alt text, the image it describes — the only thing on the page that
|
|
271
|
+
* alt text *is* about, and the inverse of the `toAltPath` convention the
|
|
272
|
+
* renderers already follow
|
|
273
|
+
* 3. the nearest enclosing node: the card, the list item, the block. Less
|
|
274
|
+
* precise, and still the difference between "that one" and nothing.
|
|
275
|
+
*/
|
|
276
|
+
export function resolveEditableNode(parent, editablePath) {
|
|
277
|
+
const exact = findEditableNode(parent, editablePath);
|
|
278
|
+
if (exact)
|
|
279
|
+
return exact;
|
|
280
|
+
if (isAltPath(editablePath)) {
|
|
281
|
+
const image = findEditableNode(parent, toImagePath(editablePath));
|
|
282
|
+
if (image)
|
|
283
|
+
return image;
|
|
284
|
+
}
|
|
285
|
+
for (let path = parentPath(editablePath); path; path = parentPath(path)) {
|
|
286
|
+
const node = findEditableNode(parent, path);
|
|
287
|
+
if (node)
|
|
288
|
+
return node;
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
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
|
+
}
|
|
152
349
|
// Block-level tags that signal a field React rendered as parsed-markdown element
|
|
153
350
|
// children (e.g. RichText `body`, Tabs `content`) rather than a single text node.
|
|
154
351
|
const RICH_EDITABLE_SELECTOR = "p,ul,ol,li,blockquote,pre,table,h1,h2,h3,h4,h5,h6,hr";
|
|
@@ -334,6 +531,113 @@ export function showSkeleton(afterBlockId, blockType) {
|
|
|
334
531
|
export function removeSkeletons() {
|
|
335
532
|
document.querySelectorAll(".editor-skeleton-block").forEach((node) => node.remove());
|
|
336
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* The one misintegration the overlay can diagnose for itself.
|
|
536
|
+
*
|
|
537
|
+
* Selection is entirely `[data-block-id]`: a click resolves through
|
|
538
|
+
* `closest("[data-block-id]")`, and a null result is read as "clicked outside
|
|
539
|
+
* any block", which *clears* the selection. A site that added
|
|
540
|
+
* `data-editable-target` to its components but no block wrappers therefore
|
|
541
|
+
* frames, renders, styles and scrolls correctly, and silently deselects on
|
|
542
|
+
* every click — and the README has already trained the reader to expect a
|
|
543
|
+
* preview that ignores clicks, because that is what selection mode being off
|
|
544
|
+
* looks like.
|
|
545
|
+
*
|
|
546
|
+
* Editable targets with no wrapper around them is unambiguous: nobody adds the
|
|
547
|
+
* per-field attributes by accident. Say so once, loudly, in development.
|
|
548
|
+
*/
|
|
549
|
+
export function missingBlockWrapperWarning(blockCount, editableCount) {
|
|
550
|
+
if (blockCount > 0 || editableCount === 0)
|
|
551
|
+
return null;
|
|
552
|
+
return (`[avocado] The overlay found ${editableCount} data-editable-target attribute(s) and no ` +
|
|
553
|
+
"[data-block-id] wrapper. Nothing on this page can be selected: every click resolves to " +
|
|
554
|
+
"no block and clears the selection, whether or not selection mode is on.\n" +
|
|
555
|
+
"Wrap each block with getPreviewWrapperProps(editorMode, block.id, block.type) from " +
|
|
556
|
+
"@avocadostudio-ai/site-sdk/editor — it sets data-block-id, data-block-type and the " +
|
|
557
|
+
"editor-selectable class that the field pills are scoped to.");
|
|
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
|
+
}
|
|
337
641
|
export function ensureBlockBadges() {
|
|
338
642
|
document.querySelectorAll("[data-block-id]").forEach((node) => {
|
|
339
643
|
const blockType = node.getAttribute("data-block-type") ?? "Block";
|
|
@@ -440,7 +744,9 @@ export function applyAiFieldLoading(blockId, editablePath, active) {
|
|
|
440
744
|
}
|
|
441
745
|
}
|
|
442
746
|
export function cleanupOverlayElements() {
|
|
443
|
-
document
|
|
747
|
+
document
|
|
748
|
+
.querySelectorAll(".aifx-shimmer-overlay, .aifx-shimmer-sparkle, .editor-image-change-btn, .editor-integration-warning")
|
|
749
|
+
.forEach((el) => el.remove());
|
|
444
750
|
document.documentElement.removeAttribute("data-editor-active");
|
|
445
751
|
document.documentElement.removeAttribute("data-editor-selection-mode");
|
|
446
752
|
}
|
|
@@ -541,11 +847,11 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
541
847
|
}
|
|
542
848
|
}
|
|
543
849
|
for (const [path, value] of Object.entries(fields)) {
|
|
544
|
-
if (isImagePath(path))
|
|
545
|
-
continue;
|
|
546
850
|
const node = findEditableNode(block, path);
|
|
547
851
|
if (!node)
|
|
548
852
|
continue;
|
|
853
|
+
if (isImageEditable(node, path))
|
|
854
|
+
continue;
|
|
549
855
|
// Rich fields (RichText body, Tabs content, etc.) render as block-level
|
|
550
856
|
// React children. Writing innerHTML here desyncs React's vdom and the
|
|
551
857
|
// final router.refresh() then mis-reconciles the subtree (truncated text
|
|
@@ -710,7 +1016,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
710
1016
|
const parent = findBlockNode(parentBlockId);
|
|
711
1017
|
if (!parent)
|
|
712
1018
|
return;
|
|
713
|
-
const child =
|
|
1019
|
+
const child = resolveEditableNode(parent, editablePath);
|
|
714
1020
|
if (!child)
|
|
715
1021
|
return;
|
|
716
1022
|
child.classList.add("editor-child-highlight");
|
|
@@ -740,6 +1046,8 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
740
1046
|
const startInlineEdit = (args) => {
|
|
741
1047
|
if (!supportsInlineEditablePath(args.editablePath))
|
|
742
1048
|
return;
|
|
1049
|
+
if (!isInlineEditable(args.node))
|
|
1050
|
+
return;
|
|
743
1051
|
if (args.node.children.length > 0)
|
|
744
1052
|
return;
|
|
745
1053
|
const existing = state.inlineEditing;
|
|
@@ -754,7 +1062,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
754
1062
|
blockType: args.blockType,
|
|
755
1063
|
editablePath: args.editablePath,
|
|
756
1064
|
initialValue,
|
|
757
|
-
isMultiline: args.editablePath
|
|
1065
|
+
isMultiline: isMultilineEditable(args.node, args.editablePath)
|
|
758
1066
|
};
|
|
759
1067
|
args.node.setAttribute("contenteditable", "true");
|
|
760
1068
|
args.node.classList.add("editor-inline-editing");
|
|
@@ -1043,7 +1351,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1043
1351
|
}
|
|
1044
1352
|
document.querySelectorAll(".editor-selectable [data-editable-target]").forEach((el) => {
|
|
1045
1353
|
const path = el.getAttribute("data-editable-target") ?? "";
|
|
1046
|
-
if (!
|
|
1354
|
+
if (!isImageEditable(el, path))
|
|
1047
1355
|
return;
|
|
1048
1356
|
const block = el.closest("[data-block-id]");
|
|
1049
1357
|
if (!block)
|
|
@@ -1103,7 +1411,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1103
1411
|
state.pendingScrollAnchorY = null;
|
|
1104
1412
|
}
|
|
1105
1413
|
else {
|
|
1106
|
-
match
|
|
1414
|
+
scrollBlockIntoView(match);
|
|
1107
1415
|
}
|
|
1108
1416
|
}
|
|
1109
1417
|
if (shouldAnimate) {
|
|
@@ -1473,7 +1781,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1473
1781
|
}
|
|
1474
1782
|
applyBlockFocus(blockId, false, editablePath, { scrollIntoView: false });
|
|
1475
1783
|
let editableValue = null;
|
|
1476
|
-
if (editablePath &&
|
|
1784
|
+
if (editablePath && childNode && isImageEditable(childNode, editablePath)) {
|
|
1477
1785
|
const img = childNode.querySelector("img");
|
|
1478
1786
|
if (img?.src)
|
|
1479
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, 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, 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, parseListItemPath, ensureBlockBadges, setNestedLabelsVisibility, showSkeleton, removeSkeletons, clearChildFocus, clearAllHighlights, clearListItemSelection, applyAiFieldLoading, cleanupOverlayElements, withPreviewParams, } from "./bridge-functions.js";
|
|
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)
|
|
@@ -10,7 +10,6 @@ export function PreviewBridgeCore(props) {
|
|
|
10
10
|
}
|
|
11
11
|
function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathname, liveStore }) {
|
|
12
12
|
const stateRef = useRef(null);
|
|
13
|
-
const programmaticScrollUntilRef = useRef(0);
|
|
14
13
|
useEffect(() => {
|
|
15
14
|
// -- postMessage helpers ------------------------------------------------
|
|
16
15
|
const postToEditor = (type, payload) => {
|
|
@@ -50,6 +49,21 @@ function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathnam
|
|
|
50
49
|
clearListItemSelection();
|
|
51
50
|
ensureBlockBadges();
|
|
52
51
|
bridge.mountGlobalImageButtons();
|
|
52
|
+
/*
|
|
53
|
+
* Deferred a frame so a page still streaming in is not accused of having no
|
|
54
|
+
* blocks. Development only: in production the site owner cannot act on it
|
|
55
|
+
* and the visitor should never see it.
|
|
56
|
+
*/
|
|
57
|
+
if (process.env.NODE_ENV !== "production") {
|
|
58
|
+
requestAnimationFrame(() => {
|
|
59
|
+
const warnings = previewIntegrationWarnings(document.querySelectorAll("[data-block-id]").length, document.querySelectorAll("[data-editable-target]").length);
|
|
60
|
+
for (const warning of warnings)
|
|
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);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
53
67
|
// -- Mutation observer -------------------------------------------------
|
|
54
68
|
let detectNewBlocksRaf = null;
|
|
55
69
|
const scheduleDetectNewBlocks = () => {
|
|
@@ -255,10 +269,7 @@ function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathnam
|
|
|
255
269
|
const el = findBlockNode(blockId);
|
|
256
270
|
if (!el)
|
|
257
271
|
return false;
|
|
258
|
-
|
|
259
|
-
// interpret our own scroll as a user cancel.
|
|
260
|
-
programmaticScrollUntilRef.current = Date.now() + 800;
|
|
261
|
-
el.scrollIntoView({ behavior, block, inline: "nearest" });
|
|
272
|
+
scrollBlockIntoView(el, { behavior, block });
|
|
262
273
|
return true;
|
|
263
274
|
};
|
|
264
275
|
// The block may not yet be in the DOM if the iframe just re-rendered;
|
|
@@ -271,7 +282,7 @@ function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathnam
|
|
|
271
282
|
// -- Scroll handler ----------------------------------------------------
|
|
272
283
|
const onScroll = () => {
|
|
273
284
|
// Swallow the scroll event if it originated from our own scrollIntoView.
|
|
274
|
-
if (
|
|
285
|
+
if (isProgrammaticScroll())
|
|
275
286
|
return;
|
|
276
287
|
callbacks.onScroll();
|
|
277
288
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/preview-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.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
|
+
}
|