@avocadostudio-ai/preview-adapter 0.19.0 → 0.21.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.
@@ -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.19.0",
3
+ "version": "0.21.0",
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.19.0"
40
+ "@avocadostudio-ai/shared": "^0.21.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "next": ">=15.0.0",
package/src/styles.css CHANGED
@@ -265,34 +265,50 @@
265
265
  background: rgba(148, 163, 184, 0.06);
266
266
  }
267
267
 
268
- /* AI field loading shimmer */
268
+ /*
269
+ * "AI is working on this" — the sweep over one block (.aifx-shimmer-overlay)
270
+ * or the frame round the whole page (.aifx-page-shimmer), which the bridge
271
+ * paints when the request is about the page rather than a block.
272
+ *
273
+ * The sweep is tinted, not white. It used to be a white band, which on a white
274
+ * section paints nothing at all; the accent flanks show on light backgrounds
275
+ * and the white core on dark ones, so one gradient serves both. The old
276
+ * `prefers-color-scheme: light` variant keyed on the OS, not on the site's own
277
+ * theme, and was all but identical anyway.
278
+ *
279
+ * The page frame lives on <body>, outside every `.editor-selectable`, where
280
+ * the editor tokens are not defined — hence the fallbacks.
281
+ */
282
+ .aifx-shimmer-overlay,
283
+ .aifx-page-shimmer {
284
+ --aifx-rgb: var(--editor-accent-rgb, 37, 99, 235);
285
+ --aifx-sweep: linear-gradient(
286
+ 110deg,
287
+ transparent 0%,
288
+ transparent 34%,
289
+ rgba(var(--aifx-rgb), 0.10) 42%,
290
+ rgba(255, 255, 255, 0.42) 50%,
291
+ rgba(var(--aifx-rgb), 0.14) 57%,
292
+ transparent 66%,
293
+ transparent 100%
294
+ );
295
+ pointer-events: none;
296
+ overflow: hidden;
297
+ }
298
+
269
299
  .aifx-shimmer-overlay {
270
300
  position: absolute;
271
301
  inset: 0;
272
302
  z-index: 21;
273
- pointer-events: none;
274
303
  border-radius: inherit;
275
- overflow: hidden;
276
- background: transparent;
277
- mix-blend-mode: normal;
278
- box-shadow: none;
279
304
  }
280
305
 
281
- .aifx-shimmer-overlay::before {
306
+ .aifx-shimmer-overlay::before,
307
+ .aifx-page-shimmer::before {
282
308
  content: "";
283
309
  position: absolute;
284
310
  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
- );
311
+ background: var(--aifx-sweep);
296
312
  transform: translateX(120%);
297
313
  animation: aifx-shimmer-slide 2.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
298
314
  }
@@ -302,6 +318,34 @@
302
318
  100% { transform: translateX(-120%); }
303
319
  }
304
320
 
321
+ /* Fixed, so it frames what the user is looking at wherever they scroll. */
322
+ .aifx-page-shimmer {
323
+ position: fixed;
324
+ inset: 0;
325
+ z-index: 2147483000;
326
+ box-shadow:
327
+ inset 0 0 0 2px rgba(var(--aifx-rgb), 0.5),
328
+ inset 0 0 32px rgba(var(--aifx-rgb), 0.16);
329
+ animation: aifx-page-pulse 2.4s ease-in-out infinite;
330
+ }
331
+
332
+ .aifx-page-shimmer::before {
333
+ opacity: 0.7;
334
+ animation-duration: 3.6s;
335
+ transition: opacity 240ms ease;
336
+ }
337
+
338
+ /* Once a block is being written, the frame says "page" and the block's sweep
339
+ says "here, now". Two sweeps at two speeds just read as noise. */
340
+ body:has(.aifx-shimmer-overlay) > .aifx-page-shimmer::before {
341
+ opacity: 0;
342
+ }
343
+
344
+ @keyframes aifx-page-pulse {
345
+ 0%, 100% { opacity: 1; }
346
+ 50% { opacity: 0.6; }
347
+ }
348
+
305
349
  /* Force chrome visible while AI shimmer is active */
306
350
  .editor-block-badge:has(.aifx-shimmer-sparkle) {
307
351
  opacity: 1 !important;
@@ -326,6 +370,14 @@
326
370
  animation: aifx-sparkle-blink 1.6s ease-in-out infinite;
327
371
  }
328
372
 
373
+ /* Bottom corner: the top-right of a page is where the site's header puts its
374
+ own buttons. */
375
+ .aifx-page-shimmer > .aifx-shimmer-sparkle {
376
+ top: auto;
377
+ right: 14px;
378
+ bottom: 14px;
379
+ }
380
+
329
381
  .aifx-shimmer-sparkle svg {
330
382
  display: block;
331
383
  width: 13px;
@@ -338,34 +390,27 @@
338
390
  50% { transform: scale(0.9); opacity: 0.65; }
339
391
  }
340
392
 
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
393
  @media (prefers-reduced-motion: reduce) {
363
394
  .aifx-shimmer-overlay,
364
395
  .aifx-shimmer-overlay::before,
396
+ .aifx-page-shimmer,
397
+ .aifx-page-shimmer::before,
365
398
  .aifx-shimmer-sparkle,
366
399
  .editor-skeleton-block::before {
367
400
  animation: none !important;
368
401
  }
402
+
403
+ /* A stopped sweep rests off-canvas at translateX(120%), which left nothing
404
+ on screen at all. Hold a still tint instead; the page frame's border
405
+ already reads without motion. */
406
+ .aifx-shimmer-overlay::before {
407
+ transform: none;
408
+ background: rgba(var(--aifx-rgb), 0.08);
409
+ }
410
+
411
+ .aifx-page-shimmer::before {
412
+ display: none;
413
+ }
369
414
  }
370
415
 
371
416
  [data-editor-active] .editor-highlight::before {
@@ -1234,6 +1279,8 @@
1234
1279
  animation: none !important;
1235
1280
  }
1236
1281
  .aifx-shimmer-overlay,
1282
+ .aifx-page-shimmer,
1283
+ .aifx-page-shimmer::before,
1237
1284
  .aifx-shimmer-sparkle {
1238
1285
  animation: none !important;
1239
1286
  }