@avocadostudio-ai/preview-adapter 0.20.0 → 0.21.1

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.
@@ -258,8 +258,9 @@ export function attachBridge(config) {
258
258
  const blockId = String(msg.payload.blockId ?? "");
259
259
  const editablePath = String(msg.payload.editablePath ?? "");
260
260
  const active = Boolean(msg.payload.active);
261
- state.activeShimmer = active ? { blockId, editablePath } : null;
262
- applyAiFieldLoading(blockId, editablePath, active);
261
+ const scope = msg.payload.scope === "page" ? "page" : "block";
262
+ state.activeShimmer = active ? { blockId, editablePath, scope } : null;
263
+ applyAiFieldLoading(blockId, editablePath, active, scope);
263
264
  }
264
265
  if (msg.type === "scrollToBlock") {
265
266
  const blockId = String(msg.payload.blockId ?? "");
@@ -231,7 +231,17 @@ export declare function removeOverlayControls(deleteConfirmTimer: {
231
231
  current: number | null;
232
232
  }): void;
233
233
  export declare function clearAllHighlights(): void;
234
- export declare function applyAiFieldLoading(blockId: string, editablePath: string, active: boolean): void;
234
+ /** What the editor says is being worked on: one block, or the whole page. */
235
+ export type AiLoadingScope = "block" | "page";
236
+ /**
237
+ * True when everything `applyAiFieldLoading` would paint for this target is
238
+ * already on the page. React re-renders drop the block overlay without
239
+ * touching the page frame, so the two are checked separately.
240
+ */
241
+ export declare function isAiFieldLoadingApplied(blockId: string, editablePath: string, scope?: AiLoadingScope): boolean;
242
+ /** Remove every shimmer this module painted and give blocks their position back. */
243
+ export declare function clearAiFieldLoading(): void;
244
+ export declare function applyAiFieldLoading(blockId: string, editablePath: string, active: boolean, scope?: AiLoadingScope): void;
235
245
  export declare function cleanupOverlayElements(): void;
236
246
  /** Callbacks for communicating user actions back to the editor/widget. */
237
247
  export type BridgeCallbacks = {
@@ -359,6 +369,7 @@ export type BridgeState = {
359
369
  activeShimmer: {
360
370
  blockId: string;
361
371
  editablePath: string;
372
+ scope: AiLoadingScope;
362
373
  } | null;
363
374
  };
364
375
  export declare function createBridgeState(): BridgeState;
@@ -703,72 +703,109 @@ export function removeOverlayControls(deleteConfirmTimer) {
703
703
  export function clearAllHighlights() {
704
704
  document.querySelectorAll(".editor-highlight").forEach((node) => node.classList.remove("editor-highlight"));
705
705
  }
706
- export function applyAiFieldLoading(blockId, editablePath, active) {
707
- // Early-out: if shimmer is already active on the same target, skip DOM rebuild
708
- if (active) {
709
- const existing = document.querySelector(".aifx-shimmer-overlay");
710
- const existingBlockId = existing?.getAttribute("data-aifx-block-id") ?? "";
711
- const existingPath = existing?.getAttribute("data-aifx-editable-path") ?? "";
712
- if (existingBlockId === (blockId ?? "") && existingPath === (editablePath ?? "")) {
713
- return;
714
- }
706
+ const AIFX_KEY_ATTR = "data-aifx-key";
707
+ const AIFX_PREV_POSITION_ATTR = "data-aifx-prev-position";
708
+ const AIFX_SPARKLE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>';
709
+ function aiLoadingKey(scope, blockId, editablePath) {
710
+ return `${scope}|${blockId}|${editablePath}`;
711
+ }
712
+ function createSparkle() {
713
+ const sparkle = document.createElement("div");
714
+ sparkle.className = "aifx-shimmer-sparkle";
715
+ sparkle.innerHTML = AIFX_SPARKLE_SVG;
716
+ return sparkle;
717
+ }
718
+ /**
719
+ * True when everything `applyAiFieldLoading` would paint for this target is
720
+ * already on the page. React re-renders drop the block overlay without
721
+ * touching the page frame, so the two are checked separately.
722
+ */
723
+ export function isAiFieldLoadingApplied(blockId, editablePath, scope = "block") {
724
+ const key = aiLoadingKey(scope, blockId, editablePath);
725
+ if (scope === "page") {
726
+ const frame = document.querySelector(".aifx-page-shimmer");
727
+ if (frame?.getAttribute(AIFX_KEY_ATTR) !== key)
728
+ return false;
715
729
  }
716
- // Remove all existing shimmer overlays and restore inline styles
717
- document.querySelectorAll(".aifx-shimmer-sparkle").forEach((el) => el.remove());
718
- document.querySelectorAll(".aifx-shimmer-overlay").forEach((el) => {
719
- const parent = el.parentElement;
720
- el.remove();
721
- if (parent) {
722
- const prevOverflow = parent.getAttribute("data-aifx-prev-overflow");
723
- if (prevOverflow !== null) {
724
- if (prevOverflow.length > 0)
725
- parent.style.overflow = prevOverflow;
726
- else
727
- parent.style.removeProperty("overflow");
728
- parent.removeAttribute("data-aifx-prev-overflow");
729
- }
730
- const prevPosition = parent.getAttribute("data-aifx-prev-position");
731
- if (prevPosition !== null) {
732
- if (prevPosition.length > 0)
733
- parent.style.position = prevPosition;
734
- else
735
- parent.style.removeProperty("position");
736
- parent.removeAttribute("data-aifx-prev-position");
737
- }
738
- if (!parent.style.cssText.trim())
739
- parent.removeAttribute("style");
740
- }
730
+ if (!blockId)
731
+ return scope === "page" || !document.querySelector(".aifx-shimmer-overlay");
732
+ const node = findBlockNode(blockId);
733
+ if (!node)
734
+ return false;
735
+ const overlay = Array.from(node.children).find((child) => child.classList.contains("aifx-shimmer-overlay"));
736
+ return overlay?.getAttribute(AIFX_KEY_ATTR) === key;
737
+ }
738
+ /** Remove every shimmer this module painted and give blocks their position back. */
739
+ export function clearAiFieldLoading() {
740
+ document
741
+ .querySelectorAll(".aifx-shimmer-overlay, .aifx-shimmer-sparkle, .aifx-page-shimmer")
742
+ .forEach((el) => el.remove());
743
+ document.querySelectorAll(`[${AIFX_PREV_POSITION_ATTR}]`).forEach((node) => {
744
+ const prev = node.getAttribute(AIFX_PREV_POSITION_ATTR) ?? "";
745
+ if (prev)
746
+ node.style.position = prev;
747
+ else
748
+ node.style.removeProperty("position");
749
+ node.removeAttribute(AIFX_PREV_POSITION_ATTR);
750
+ if (!node.style.cssText.trim())
751
+ node.removeAttribute("style");
741
752
  });
742
- if (active) {
743
- const scopedBlock = (blockId ? findBlockNode(blockId) : null) ??
744
- document.querySelector(".editor-highlight[data-block-id]") ??
745
- document.querySelector("[data-block-id]") ??
746
- document.querySelector("main.editor-mode, main");
747
- if (!scopedBlock)
748
- return;
749
- // Always paint shimmer on block/root so it's clearly visible in preview.
750
- // Field-level nodes are often inline/small and can make shimmer imperceptible.
751
- const target = scopedBlock;
752
- const overlay = document.createElement("div");
753
- overlay.className = "aifx-shimmer-overlay";
754
- overlay.setAttribute("data-aifx-block-id", blockId ?? "");
755
- overlay.setAttribute("data-aifx-editable-path", editablePath ?? "");
756
- const sparkle = document.createElement("div");
757
- sparkle.className = "aifx-shimmer-sparkle";
758
- sparkle.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>';
759
- if (!target.hasAttribute("data-aifx-prev-position")) {
760
- target.setAttribute("data-aifx-prev-position", target.style.position);
761
- }
762
- target.style.position = target.style.position || "relative";
763
- target.appendChild(overlay);
764
- if (blockId) {
765
- target.appendChild(sparkle);
766
- }
753
+ }
754
+ /*
755
+ * Paint "AI is working on this" into the preview.
756
+ *
757
+ * `scope: "page"` frames the viewport, because the request is about the page
758
+ * rather than any one block — "review this page" writes no ops, so there is no
759
+ * block to point at, and pointing at the selected one says the wrong thing.
760
+ * A `blockId` alongside it adds the sweep to the block currently being
761
+ * written, so a page-wide rewrite shows both the scope and the progress.
762
+ *
763
+ * There is deliberately no fallback. This used to paint the selected block, or
764
+ * failing that the first block on the page, whenever it was not told which
765
+ * block — which is how a page review came to shimmer the site header. A block
766
+ * that is not in the DOM yet paints nothing; the editor re-sends while the
767
+ * turn runs, and `smoothRefresh` re-applies after a re-render.
768
+ */
769
+ export function applyAiFieldLoading(blockId, editablePath, active, scope = "block") {
770
+ blockId = blockId ?? "";
771
+ editablePath = editablePath ?? "";
772
+ if (active && isAiFieldLoadingApplied(blockId, editablePath, scope))
773
+ return;
774
+ clearAiFieldLoading();
775
+ if (!active)
776
+ return;
777
+ const key = aiLoadingKey(scope, blockId, editablePath);
778
+ if (scope === "page" && document.body) {
779
+ const frame = document.createElement("div");
780
+ frame.className = "aifx-page-shimmer";
781
+ frame.setAttribute(AIFX_KEY_ATTR, key);
782
+ frame.appendChild(createSparkle());
783
+ document.body.appendChild(frame);
784
+ }
785
+ const block = blockId ? findBlockNode(blockId) : null;
786
+ if (!block)
787
+ return;
788
+ // Paint on the block, not the field: field nodes are often inline and small
789
+ // enough to make the sweep imperceptible.
790
+ const overlay = document.createElement("div");
791
+ overlay.className = "aifx-shimmer-overlay";
792
+ overlay.setAttribute(AIFX_KEY_ATTR, key);
793
+ // The overlay is absolutely positioned, so the block must be a containing
794
+ // block — but only a static one needs help. Forcing `relative` on a block
795
+ // whose stylesheet makes it sticky or fixed un-sticks it for the whole turn.
796
+ if (getComputedStyle(block).position === "static") {
797
+ if (!block.hasAttribute(AIFX_PREV_POSITION_ATTR))
798
+ block.setAttribute(AIFX_PREV_POSITION_ATTR, block.style.position);
799
+ block.style.position = "relative";
767
800
  }
801
+ block.appendChild(overlay);
802
+ // One sparkle per shimmer: the page frame carries it when there is one.
803
+ if (scope === "block")
804
+ block.appendChild(createSparkle());
768
805
  }
769
806
  export function cleanupOverlayElements() {
770
807
  document
771
- .querySelectorAll(".aifx-shimmer-overlay, .aifx-shimmer-sparkle, .editor-image-change-btn, .editor-integration-warning")
808
+ .querySelectorAll(".aifx-shimmer-overlay, .aifx-shimmer-sparkle, .aifx-page-shimmer, .editor-image-change-btn, .editor-integration-warning")
772
809
  .forEach((el) => el.remove());
773
810
  document.documentElement.removeAttribute("data-editor-active");
774
811
  document.documentElement.removeAttribute("data-editor-selection-mode");
@@ -1575,17 +1612,14 @@ export function createBridgeFunctions(state, callbacks, config) {
1575
1612
  // Re-apply shimmer after DOM settles — poll briefly since React
1576
1613
  // reconciliation after router.refresh() is async and rAF isn't enough.
1577
1614
  if (state.activeShimmer) {
1578
- const { blockId, editablePath } = state.activeShimmer;
1615
+ const { blockId, editablePath, scope } = state.activeShimmer;
1579
1616
  let attempts = 0;
1580
1617
  const tryReapply = () => {
1581
- if (!state.activeShimmer || state.activeShimmer.blockId !== blockId)
1618
+ const current = state.activeShimmer;
1619
+ if (!current || current.blockId !== blockId || current.scope !== scope)
1582
1620
  return;
1583
- applyAiFieldLoading(blockId, editablePath, true);
1584
- const node = blockId ? findBlockNode(blockId) : null;
1585
- const hasOverlay = node
1586
- ? Boolean(node.querySelector(".aifx-shimmer-overlay"))
1587
- : Boolean(document.querySelector(".aifx-shimmer-overlay"));
1588
- if (!hasOverlay && attempts < 8) {
1621
+ applyAiFieldLoading(blockId, editablePath, true, scope);
1622
+ if (!isAiFieldLoadingApplied(blockId, editablePath, scope) && attempts < 8) {
1589
1623
  attempts++;
1590
1624
  setTimeout(tryReapply, 60);
1591
1625
  }
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, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, previewIntegrationWarnings, } from "./bridge-functions.ts";
6
- export type { BridgeCallbacks, BridgeState, BridgeFunctions } 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, clearAiFieldLoading, isAiFieldLoadingApplied, cleanupOverlayElements, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, previewIntegrationWarnings, } from "./bridge-functions.ts";
6
+ export type { AiLoadingScope, 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, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, previewIntegrationWarnings, } 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, clearAiFieldLoading, isAiFieldLoadingApplied, cleanupOverlayElements, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, previewIntegrationWarnings, } from "./bridge-functions.js";
5
5
  export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/preview-adapter",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  "src/styles.css"
38
38
  ],
39
39
  "dependencies": {
40
- "@avocadostudio-ai/shared": "^0.20.0"
40
+ "@avocadostudio-ai/shared": "^0.21.1"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "next": ">=15.0.0",
package/src/styles.css CHANGED
@@ -140,8 +140,11 @@
140
140
  /* Scope the typing shimmer to the fields actually being edited (.editor-live-typing
141
141
  is only added to text targets, never images), so it doesn't wash the whole block —
142
142
  including the hero image, which looked like it was being regenerated. */
143
- .editor-live-draft-active .editor-live-typing:not([class*="absolute"]):not([class*="fixed"]) {
143
+ :where(.editor-live-draft-active .editor-live-typing:not([class*="absolute"]):not([class*="fixed"])) {
144
144
  position: relative;
145
+ }
146
+
147
+ .editor-live-draft-active .editor-live-typing:not([class*="absolute"]):not([class*="fixed"]) {
145
148
  overflow: hidden;
146
149
  }
147
150
 
@@ -265,34 +268,50 @@
265
268
  background: rgba(148, 163, 184, 0.06);
266
269
  }
267
270
 
268
- /* AI field loading shimmer */
271
+ /*
272
+ * "AI is working on this" — the sweep over one block (.aifx-shimmer-overlay)
273
+ * or the frame round the whole page (.aifx-page-shimmer), which the bridge
274
+ * paints when the request is about the page rather than a block.
275
+ *
276
+ * The sweep is tinted, not white. It used to be a white band, which on a white
277
+ * section paints nothing at all; the accent flanks show on light backgrounds
278
+ * and the white core on dark ones, so one gradient serves both. The old
279
+ * `prefers-color-scheme: light` variant keyed on the OS, not on the site's own
280
+ * theme, and was all but identical anyway.
281
+ *
282
+ * The page frame lives on <body>, outside every `.editor-selectable`, where
283
+ * the editor tokens are not defined — hence the fallbacks.
284
+ */
285
+ .aifx-shimmer-overlay,
286
+ .aifx-page-shimmer {
287
+ --aifx-rgb: var(--editor-accent-rgb, 37, 99, 235);
288
+ --aifx-sweep: linear-gradient(
289
+ 110deg,
290
+ transparent 0%,
291
+ transparent 34%,
292
+ rgba(var(--aifx-rgb), 0.10) 42%,
293
+ rgba(255, 255, 255, 0.42) 50%,
294
+ rgba(var(--aifx-rgb), 0.14) 57%,
295
+ transparent 66%,
296
+ transparent 100%
297
+ );
298
+ pointer-events: none;
299
+ overflow: hidden;
300
+ }
301
+
269
302
  .aifx-shimmer-overlay {
270
303
  position: absolute;
271
304
  inset: 0;
272
305
  z-index: 21;
273
- pointer-events: none;
274
306
  border-radius: inherit;
275
- overflow: hidden;
276
- background: transparent;
277
- mix-blend-mode: normal;
278
- box-shadow: none;
279
307
  }
280
308
 
281
- .aifx-shimmer-overlay::before {
309
+ .aifx-shimmer-overlay::before,
310
+ .aifx-page-shimmer::before {
282
311
  content: "";
283
312
  position: absolute;
284
313
  inset: 0;
285
- background: linear-gradient(
286
- 110deg,
287
- transparent 0%,
288
- transparent 38%,
289
- rgba(255, 255, 255, 0.0) 42%,
290
- rgba(255, 255, 255, 0.42) 50%,
291
- rgba(219, 234, 254, 0.30) 54%,
292
- rgba(255, 255, 255, 0.0) 60%,
293
- transparent 66%,
294
- transparent 100%
295
- );
314
+ background: var(--aifx-sweep);
296
315
  transform: translateX(120%);
297
316
  animation: aifx-shimmer-slide 2.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
298
317
  }
@@ -302,6 +321,34 @@
302
321
  100% { transform: translateX(-120%); }
303
322
  }
304
323
 
324
+ /* Fixed, so it frames what the user is looking at wherever they scroll. */
325
+ .aifx-page-shimmer {
326
+ position: fixed;
327
+ inset: 0;
328
+ z-index: 2147483000;
329
+ box-shadow:
330
+ inset 0 0 0 2px rgba(var(--aifx-rgb), 0.5),
331
+ inset 0 0 32px rgba(var(--aifx-rgb), 0.16);
332
+ animation: aifx-page-pulse 2.4s ease-in-out infinite;
333
+ }
334
+
335
+ .aifx-page-shimmer::before {
336
+ opacity: 0.7;
337
+ animation-duration: 3.6s;
338
+ transition: opacity 240ms ease;
339
+ }
340
+
341
+ /* Once a block is being written, the frame says "page" and the block's sweep
342
+ says "here, now". Two sweeps at two speeds just read as noise. */
343
+ body:has(.aifx-shimmer-overlay) > .aifx-page-shimmer::before {
344
+ opacity: 0;
345
+ }
346
+
347
+ @keyframes aifx-page-pulse {
348
+ 0%, 100% { opacity: 1; }
349
+ 50% { opacity: 0.6; }
350
+ }
351
+
305
352
  /* Force chrome visible while AI shimmer is active */
306
353
  .editor-block-badge:has(.aifx-shimmer-sparkle) {
307
354
  opacity: 1 !important;
@@ -326,6 +373,14 @@
326
373
  animation: aifx-sparkle-blink 1.6s ease-in-out infinite;
327
374
  }
328
375
 
376
+ /* Bottom corner: the top-right of a page is where the site's header puts its
377
+ own buttons. */
378
+ .aifx-page-shimmer > .aifx-shimmer-sparkle {
379
+ top: auto;
380
+ right: 14px;
381
+ bottom: 14px;
382
+ }
383
+
329
384
  .aifx-shimmer-sparkle svg {
330
385
  display: block;
331
386
  width: 13px;
@@ -338,34 +393,27 @@
338
393
  50% { transform: scale(0.9); opacity: 0.65; }
339
394
  }
340
395
 
341
- @media (prefers-color-scheme: light) {
342
- .aifx-shimmer-overlay {
343
- background: transparent;
344
- box-shadow: none;
345
- }
346
-
347
- .aifx-shimmer-overlay::before {
348
- background: linear-gradient(
349
- 110deg,
350
- transparent 0%,
351
- transparent 36%,
352
- rgba(255, 255, 255, 0.0) 41%,
353
- rgba(255, 255, 255, 0.46) 50%,
354
- rgba(191, 219, 254, 0.34) 54%,
355
- rgba(255, 255, 255, 0.0) 60%,
356
- transparent 66%,
357
- transparent 100%
358
- );
359
- }
360
- }
361
-
362
396
  @media (prefers-reduced-motion: reduce) {
363
397
  .aifx-shimmer-overlay,
364
398
  .aifx-shimmer-overlay::before,
399
+ .aifx-page-shimmer,
400
+ .aifx-page-shimmer::before,
365
401
  .aifx-shimmer-sparkle,
366
402
  .editor-skeleton-block::before {
367
403
  animation: none !important;
368
404
  }
405
+
406
+ /* A stopped sweep rests off-canvas at translateX(120%), which left nothing
407
+ on screen at all. Hold a still tint instead; the page frame's border
408
+ already reads without motion. */
409
+ .aifx-shimmer-overlay::before {
410
+ transform: none;
411
+ background: rgba(var(--aifx-rgb), 0.08);
412
+ }
413
+
414
+ .aifx-page-shimmer::before {
415
+ display: none;
416
+ }
369
417
  }
370
418
 
371
419
  [data-editor-active] .editor-highlight::before {
@@ -816,7 +864,18 @@
816
864
  transform: none;
817
865
  }
818
866
 
819
- [data-editor-active] .editor-selectable [data-editable-label]:not([class*="absolute"]):not([class*="fixed"]) {
867
+ /*
868
+ * `position: relative` anchors the field pills, and it has to lose to anything
869
+ * the site says. Written plainly this selector was (0,5,0), which beats almost
870
+ * any site rule, and the only guard was a Tailwind class-name match. So the
871
+ * full-width hero's background image — `.hero--full-width .hero__bg-image
872
+ * { position: absolute }` in the blocks package, no Tailwind class on it —
873
+ * went back into the section's flex row. It pushed the headline off the right
874
+ * edge of every editor preview while the public page rendered correctly.
875
+ * Inside `:where()` the rule has no specificity, so it only applies to elements
876
+ * the site left static, which are the only ones that need it.
877
+ */
878
+ :where([data-editor-active] .editor-selectable [data-editable-label]:not([class*="absolute"]):not([class*="fixed"])) {
820
879
  position: relative;
821
880
  }
822
881
 
@@ -825,7 +884,7 @@
825
884
  display: none;
826
885
  }
827
886
 
828
- .editor-highlight [data-editable-target]:not([class*="absolute"]):not([class*="fixed"]) {
887
+ :where(.editor-highlight [data-editable-target]:not([class*="absolute"]):not([class*="fixed"])) {
829
888
  position: relative;
830
889
  }
831
890
 
@@ -1234,6 +1293,8 @@
1234
1293
  animation: none !important;
1235
1294
  }
1236
1295
  .aifx-shimmer-overlay,
1296
+ .aifx-page-shimmer,
1297
+ .aifx-page-shimmer::before,
1237
1298
  .aifx-shimmer-sparkle {
1238
1299
  animation: none !important;
1239
1300
  }