Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 78 additions & 15 deletions src/extensionsIntegrated/Phoenix/phoenix-tour.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@

let $overlay = null;
let _rafId = null;
let _resizeObserver = null;
let _positionHandler = null;
let _timers = [];

// Per-step: tracks a click on the highlighted target while the
Expand Down Expand Up @@ -97,10 +99,22 @@
clearTimeout(_timers[i]);
}
_timers = [];
}

function _stopTracking() {
if (_rafId) {
cancelAnimationFrame(_rafId);
_rafId = null;
}
if (_resizeObserver) {
_resizeObserver.disconnect();
_resizeObserver = null;
}
if (_positionHandler) {
window.removeEventListener("scroll", _positionHandler, true);
window.removeEventListener("resize", _positionHandler);
_positionHandler = null;
}
}

/**
Expand Down Expand Up @@ -169,6 +183,7 @@

function _teardown() {
_clearTimers();
_stopTracking();
_detachStepClickMetric();
_cancelStep2AIPeek();
if ($overlay) {
Expand All @@ -185,8 +200,12 @@
}
$overlay = $(
'<div class="phoenix-tour-overlay" data-tip-placement="right">' +
'<div class="phoenix-tour-ring"></div>' +
'<div class="phoenix-tour-ring phoenix-tour-ring-2"></div>' +
'<div class="phoenix-tour-halo-outer"></div>' +
'<div class="phoenix-tour-halo-mid"></div>' +
'<div class="phoenix-tour-focus-ring"></div>' +
'<div class="phoenix-tour-pulse-ring"></div>' +
'<div class="phoenix-tour-pulse-ring phoenix-tour-pulse-ring-2"></div>' +
'<div class="phoenix-tour-pulse-ring phoenix-tour-pulse-ring-3"></div>' +
'<div class="phoenix-tour-tooltip">' +
'<div class="phoenix-tour-step"></div>' +
'<div class="phoenix-tour-text"></div>' +
Expand All @@ -197,6 +216,22 @@
$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;

Check failure on line 231 in src/extensionsIntegrated/Phoenix/phoenix-tour.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of the "void" operator.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ5DXs5rFW163agrRtC9&open=AZ5DXs5rFW163agrRtC9&pullRequest=2929
$overlay.addClass("phoenix-tour-visible phoenix-tour-animating");
}

function _setText(text) {
if ($overlay) {
// `text` may include `\n` for multi-line steps; CSS uses
Expand Down Expand Up @@ -241,28 +276,53 @@
}

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();
}

/**
Expand Down Expand Up @@ -297,7 +357,7 @@
// 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 () {
Expand Down Expand Up @@ -352,6 +412,7 @@
}
]);
_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();
Expand Down Expand Up @@ -384,6 +445,7 @@
}
}
]);
_enterStep();
// Intentionally do NOT advance on a real click of the target — only
// the Next button advances.
}
Expand Down Expand Up @@ -477,6 +539,7 @@
}
}
]);
_enterStep();
}

function _shouldRun() {
Expand Down
99 changes: 94 additions & 5 deletions src/styles/Extn-PhoenixTour.less
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand All @@ -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 {
Expand All @@ -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 {

Check warning on line 144 in src/styles/Extn-PhoenixTour.less

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Duplicate selector ".phoenix-tour-overlay.phoenix-tour-animating", first used at line 66

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ5DXs-9FW163agrRtC-&open=AZ5DXs-9FW163agrRtC-&pullRequest=2929
.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
Expand Down
Loading