' +
'
' +
'
' +
@@ -197,6 +216,22 @@ define(function (require, exports, module) {
$overlay.appendTo(document.body);
}
+ /**
+ * Restart the ring animations at the new target so every step gets a
+ * fresh ripple → settle sequence, not the leftover settled rings from
+ * the previous step. The reflow read between class toggles is what
+ * makes the CSS animations restart; without it the browser coalesces
+ * the remove+add and nothing replays.
+ */
+ function _enterStep() {
+ if (!$overlay) {
+ return;
+ }
+ $overlay.removeClass("phoenix-tour-animating");
+ void $overlay[0].offsetWidth;
+ $overlay.addClass("phoenix-tour-visible phoenix-tour-animating");
+ }
+
function _setText(text) {
if ($overlay) {
// `text` may include `\n` for multi-line steps; CSS uses
@@ -241,28 +276,53 @@ define(function (require, exports, module) {
}
function _trackTarget($target, placement) {
- function update() {
- if (!$overlay || !$target.length || !$target[0].isConnected) {
- _rafId = null;
- return;
+ // Tour targets are stationary toolbar buttons, so we position once
+ // and then react to scroll/resize/target-size changes via observers
+ // — avoids the compositor cost of a per-frame rAF loop on low-end
+ // hardware. The rAF below is only a short poll for targets that
+ // aren't laid out yet (zero-size on first call).
+ _stopTracking();
+ if (!$overlay || !$target.length) {
+ return;
+ }
+ $overlay.attr("data-tip-placement", placement || "right");
+ const targetEl = $target[0];
+
+ function position() {
+ if (!$overlay || !targetEl.isConnected) {
+ return false;
}
- const r = $target[0].getBoundingClientRect();
+ const r = targetEl.getBoundingClientRect();
if (r.width === 0 && r.height === 0) {
- _rafId = requestAnimationFrame(update);
- return;
+ return false;
}
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const el = $overlay[0];
el.style.left = cx + "px";
el.style.top = cy + "px";
- _rafId = requestAnimationFrame(update);
+ return true;
}
- if (_rafId) {
- cancelAnimationFrame(_rafId);
+
+ function attachObservers() {
+ _positionHandler = position;
+ if (typeof ResizeObserver !== "undefined") {
+ _resizeObserver = new ResizeObserver(position);
+ _resizeObserver.observe(targetEl);
+ }
+ window.addEventListener("scroll", _positionHandler, true);
+ window.addEventListener("resize", _positionHandler);
}
- $overlay.attr("data-tip-placement", placement || "right");
- update();
+
+ function pollUntilVisible() {
+ if (position()) {
+ _rafId = null;
+ attachObservers();
+ return;
+ }
+ _rafId = requestAnimationFrame(pollUntilVisible);
+ }
+ pollUntilVisible();
}
/**
@@ -297,7 +357,7 @@ define(function (require, exports, module) {
// demo is too quick to read.
_setText(Strings.PHOENIX_TOUR_DESIGN_MODE);
_setActions([]); // hidden during the auto-demo
- $overlay.addClass("phoenix-tour-visible");
+ _enterStep();
// Auto-demo: enter design mode, hold, exit, then show "Next".
_timers.push(setTimeout(function () {
@@ -352,6 +412,7 @@ define(function (require, exports, module) {
}
]);
_attachStepClickMetric(2, $tab);
+ _enterStep();
// Auto-peek the AI panel for a couple of seconds so the user gets
// a glance at its contents, then revert.
_runStep2AIPeek();
@@ -384,6 +445,7 @@ define(function (require, exports, module) {
}
}
]);
+ _enterStep();
// Intentionally do NOT advance on a real click of the target — only
// the Next button advances.
}
@@ -477,6 +539,7 @@ define(function (require, exports, module) {
}
}
]);
+ _enterStep();
}
function _shouldRun() {
diff --git a/src/styles/Extn-PhoenixTour.less b/src/styles/Extn-PhoenixTour.less
index 88ce05e8ff..50adfbdfd6 100644
--- a/src/styles/Extn-PhoenixTour.less
+++ b/src/styles/Extn-PhoenixTour.less
@@ -38,7 +38,18 @@
}
}
-.phoenix-tour-ring {
+/* Two-phase ring system:
+ 1. ~3.2s of animated pulse (three staggered ripples) — the initial
+ attention-grab.
+ 2. Pulse fades out and a settled focus halo appears: two soft radial
+ gradient annuli (transparent centers, feathered edges) plus a crisp
+ ring snug around the target icon. Reads as a deliberate spotlight,
+ not a stalled ripple. Pulse is bounded (not infinite) so weak GPUs
+ aren't stuck rasterizing scaling box-shadowed elements forever. */
+
+/* Phase 1: 3 staggered pulse rings, 1.6s each at 0.8s intervals → ripple
+ stays continuously visible from t=0 to t=3.2s. */
+.phoenix-tour-pulse-ring {
position: absolute;
left: 50%;
top: 50%;
@@ -48,12 +59,20 @@
border: 2px solid #4aa3ff;
border-radius: 50%;
box-shadow: 0 0 12px rgba(74, 163, 255, 0.45);
- opacity: 0.9;
- animation: phoenix-tour-pulse 1.6s ease-out infinite;
+ opacity: 0;
+ pointer-events: none;
}
-.phoenix-tour-ring-2 {
- animation-delay: 0.8s;
+.phoenix-tour-overlay.phoenix-tour-animating {
+ .phoenix-tour-pulse-ring {
+ animation: phoenix-tour-pulse 1.6s ease-out forwards;
+ }
+ .phoenix-tour-pulse-ring-2 {
+ animation-delay: 0.8s;
+ }
+ .phoenix-tour-pulse-ring-3 {
+ animation-delay: 1.6s;
+ }
}
@keyframes phoenix-tour-pulse {
@@ -70,6 +89,76 @@
}
}
+/* Phase 2: settled focus halo. All three layers share a common animation
+ that fades them in (with a slight inward-settle stagger) once the pulse
+ phase ends, then idles. */
+.phoenix-tour-halo-outer,
+.phoenix-tour-halo-mid,
+.phoenix-tour-focus-ring {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ border-radius: 50%;
+ pointer-events: none;
+ opacity: 0;
+}
+
+/* Wide soft outer glow — gentle "spotlight" reaching the surrounding UI.
+ Transparent at the very center so the inner layers and icon show
+ through, transparent again at the disc edge so it feathers into the
+ background instead of looking like a hard circle. */
+.phoenix-tour-halo-outer {
+ width: 180px;
+ height: 180px;
+ margin: -90px 0 0 -90px;
+ background: radial-gradient(circle closest-side,
+ transparent 38%,
+ rgba(74, 163, 255, 0.09) 55%,
+ rgba(74, 163, 255, 0.05) 75%,
+ transparent 100%);
+}
+
+/* Mid annulus — denser ring of fill that gives the halo visible structure
+ without resorting to a hard outline. */
+.phoenix-tour-halo-mid {
+ width: 96px;
+ height: 96px;
+ margin: -48px 0 0 -48px;
+ background: radial-gradient(circle closest-side,
+ transparent 28%,
+ rgba(74, 163, 255, 0.20) 50%,
+ rgba(74, 163, 255, 0.10) 75%,
+ transparent 100%);
+}
+
+/* Crisp focus ring at the icon — the unambiguous "this one" marker. */
+.phoenix-tour-focus-ring {
+ width: 38px;
+ height: 38px;
+ margin: -19px 0 0 -19px;
+ border: 2px solid rgba(74, 163, 255, 0.95);
+ background: rgba(74, 163, 255, 0.08);
+ box-shadow: 0 0 14px rgba(74, 163, 255, 0.55);
+}
+
+.phoenix-tour-overlay.phoenix-tour-animating {
+ .phoenix-tour-halo-outer,
+ .phoenix-tour-halo-mid,
+ .phoenix-tour-focus-ring {
+ animation: phoenix-tour-settle 600ms ease-out forwards;
+ }
+ /* Settle inward: outer halo first, then mid, then the crisp ring snaps
+ in last — feels like the ripples collapsing into a focus indicator. */
+ .phoenix-tour-halo-outer { animation-delay: 3.2s; }
+ .phoenix-tour-halo-mid { animation-delay: 3.35s; }
+ .phoenix-tour-focus-ring { animation-delay: 3.5s; }
+}
+
+@keyframes phoenix-tour-settle {
+ 0% { opacity: 0; transform: scale(0.9); }
+ 100% { opacity: 1; transform: scale(1); }
+}
+
/* Tooltip — sits below-right of the ring so it stays within the viewport
even when the target is near the top of the window (both tour targets,
the design-mode button and the AI sidebar tab, sit at the top). Solid