feat(onboarding): enhance target highlighting with mask and pulsing effect

This commit is contained in:
Will Miao
2025-09-03 21:44:23 +08:00
parent 5520aecbba
commit ea727aad2e
2 changed files with 125 additions and 11 deletions

View File

@@ -8,6 +8,9 @@
background: rgba(0, 0, 0, 0.8);
z-index: var(--z-overlay);
display: none;
/* Use mask to create cutout for highlighted element */
mask-composite: subtract;
-webkit-mask-composite: subtract;
}
.onboarding-overlay.active {
@@ -19,10 +22,26 @@
background: transparent;
border: 3px solid var(--lora-accent);
border-radius: var(--border-radius-base);
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.8);
z-index: calc(var(--z-overlay) + 1);
pointer-events: none;
transition: all 0.3s ease;
/* Add glow effect */
box-shadow:
0 0 0 2px rgba(24, 144, 255, 0.3),
0 0 20px rgba(24, 144, 255, 0.2),
inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
/* Target element highlighting */
.onboarding-target-highlight {
position: relative;
z-index: calc(var(--z-overlay) + 2) !important;
pointer-events: auto !important;
}
/* Ensure highlighted elements are interactive */
.onboarding-target-highlight * {
pointer-events: auto !important;
}
.onboarding-popup {
@@ -33,7 +52,7 @@
padding: var(--space-3);
min-width: 320px;
max-width: 400px;
z-index: calc(var(--z-overlay) + 2);
z-index: calc(var(--z-overlay) + 3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
@@ -201,10 +220,16 @@
@keyframes onboarding-pulse {
0%, 100% {
box-shadow: 0 0 0 0 var(--lora-accent);
box-shadow:
0 0 0 2px rgba(24, 144, 255, 0.4),
0 0 20px rgba(24, 144, 255, 0.3),
inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
50% {
box-shadow: 0 0 0 8px transparent;
box-shadow:
0 0 0 4px rgba(24, 144, 255, 0.6),
0 0 30px rgba(24, 144, 255, 0.4),
inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
}

View File

@@ -9,6 +9,7 @@ export class OnboardingManager {
this.overlay = null;
this.spotlight = null;
this.popup = null;
this.currentTarget = null; // Track current highlighted element
// Available languages with SVG flags (using flag-icons)
this.languages = [
@@ -252,16 +253,15 @@ export class OnboardingManager {
return;
}
// Position spotlight
// Clear previous target highlighting
this.clearTargetHighlight();
// Position spotlight and create mask
if (target && step.target !== 'body') {
const rect = target.getBoundingClientRect();
this.spotlight.style.left = `${rect.left - 5}px`;
this.spotlight.style.top = `${rect.top - 5}px`;
this.spotlight.style.width = `${rect.width + 10}px`;
this.spotlight.style.height = `${rect.height + 10}px`;
this.spotlight.style.display = 'block';
this.highlightTarget(target);
} else {
this.spotlight.style.display = 'none';
this.clearOverlayMask();
}
// Update popup content
@@ -344,6 +344,92 @@ export class OnboardingManager {
popup.style.transform = 'none';
}
// Highlight target element with mask approach
highlightTarget(target) {
const rect = target.getBoundingClientRect();
const padding = 4; // Padding around the target element
const offset = 3; // Shift spotlight up and left by 3px
// Position spotlight
this.spotlight.style.left = `${rect.left - padding - offset}px`;
this.spotlight.style.top = `${rect.top - padding - offset}px`;
this.spotlight.style.width = `${rect.width + padding * 2}px`;
this.spotlight.style.height = `${rect.height + padding * 2}px`;
this.spotlight.style.display = 'block';
// Create mask for overlay to cut out the highlighted area
this.createOverlayMask(rect, padding, offset);
// Add highlight class to target and ensure it's interactive
target.classList.add('onboarding-target-highlight');
this.currentTarget = target;
// Add pulsing animation
this.spotlight.classList.add('onboarding-highlight');
}
// Create mask for overlay to cut out highlighted area
createOverlayMask(rect, padding, offset = 0) {
const x = rect.left - padding - offset;
const y = rect.top - padding - offset;
const width = rect.width + padding * 2;
const height = rect.height + padding * 2;
// Create SVG mask
const maskId = 'onboarding-mask';
let maskSvg = document.getElementById(maskId);
if (!maskSvg) {
maskSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
maskSvg.id = maskId;
maskSvg.style.position = 'absolute';
maskSvg.style.top = '0';
maskSvg.style.left = '0';
maskSvg.style.width = '100%';
maskSvg.style.height = '100%';
maskSvg.style.pointerEvents = 'none';
document.body.appendChild(maskSvg);
}
// Clear existing mask content
maskSvg.innerHTML = `
<defs>
<mask id="overlay-mask">
<rect width="100%" height="100%" fill="white"/>
<rect x="${x}" y="${y}" width="${width}" height="${height}"
rx="8" ry="8" fill="black"/>
</mask>
</defs>
`;
// Apply mask to overlay
this.overlay.style.mask = 'url(#overlay-mask)';
this.overlay.style.webkitMask = 'url(#overlay-mask)';
}
// Clear overlay mask
clearOverlayMask() {
this.overlay.style.mask = 'none';
this.overlay.style.webkitMask = 'none';
const maskSvg = document.getElementById('onboarding-mask');
if (maskSvg) {
maskSvg.remove();
}
}
// Clear target highlighting
clearTargetHighlight() {
if (this.currentTarget) {
this.currentTarget.classList.remove('onboarding-target-highlight');
this.currentTarget = null;
}
if (this.spotlight) {
this.spotlight.classList.remove('onboarding-highlight');
}
}
// Navigate to next step
nextStep() {
this.showStep(this.currentStep + 1);
@@ -370,6 +456,9 @@ export class OnboardingManager {
// Clean up overlay elements
cleanup() {
this.clearTargetHighlight();
this.clearOverlayMask();
if (this.overlay) {
document.body.removeChild(this.overlay);
this.overlay = null;