@avocadostudio-ai/preview-adapter 0.3.3 → 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.
@@ -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: {
@@ -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.querySelectorAll(".aifx-shimmer-overlay, .aifx-shimmer-sparkle, .editor-image-change-btn").forEach((el) => el.remove());
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 === "body"
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 (!isImagePath(path))
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 && isImagePath(editablePath) && childNode) {
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, missingBlockWrapperWarning, scrollBlockIntoView, 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)
@@ -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 warning = missingBlockWrapperWarning(document.querySelectorAll("[data-block-id]").length, document.querySelectorAll("[data-editable-target]").length);
60
- if (warning)
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.3",
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.3.3"
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
+ }