Skip to content

Commit 344dbf1

Browse files
committed
fix(onboarding): rewrite engagement animation to match Polymarket style
- Removed button color-change logic completely (fixes "stays colorful" bug) - Floaters are now attached to the `<article>` node and positioned absolutely based on the button coords - Reintroduced a random X/Y offset so numbers scatter organically and don't stack perfectly on top of each other - Reduced floater font size to 12px for a subtler, "live event" feel - Removed `!panelVisible` guard from the animation loop so floaters still spawn on large screens where the panel is always technically "visible" at the bottom - Z-index remains confined to the article's stacking context, ensuring it never goes over the blur Made-with: Cursor
1 parent 8cb271a commit 344dbf1

File tree

1 file changed

+33
-87
lines changed

1 file changed

+33
-87
lines changed

packages/webapp/pages/onboarding-v2.tsx

Lines changed: 33 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,6 @@ const OnboardingV2Page = (): ReactElement => {
719719
const shouldRun =
720720
feedVisible &&
721721
!feedReadyState &&
722-
!panelVisible &&
723722
!showSignupChooser &&
724723
!showSignupPrompt &&
725724
!showGithubImportFlow &&
@@ -823,6 +822,7 @@ const OnboardingV2Page = (): ReactElement => {
823822

824823
const article = articles[Math.floor(Math.random() * articles.length)];
825824
article.setAttribute('data-eng-active', 'true');
825+
article.classList.add('relative'); // Ensure article can contain absolute elements
826826

827827
const isUpvote = Math.random() < 0.7;
828828
const suffix = isUpvote ? '-upvote-btn' : '-comment-btn';
@@ -835,27 +835,10 @@ const OnboardingV2Page = (): ReactElement => {
835835
return;
836836
}
837837

838-
const wrapperEl = wrapper as HTMLElement;
839-
// Add CSS class instead of direct style mutation — the class provides
840-
// position:relative via the stylesheet without forcing a style recalculation.
841-
wrapperEl.classList.add('onb-eng-pos-relative');
842-
843-
const counter = ensureCounter(
844-
wrapper,
845-
isUpvote
846-
? 4 + Math.floor(Math.random() * 50)
847-
: 1 + Math.floor(Math.random() * 10),
848-
);
849-
850-
const activeClass = isUpvote
851-
? 'onb-eng-active-upvote'
852-
: 'onb-eng-active-comment';
853838
const color = isUpvote
854839
? 'var(--theme-actions-upvote-default)'
855840
: 'var(--theme-actions-comment-default)';
856841

857-
wrapperEl.classList.add(activeClass);
858-
859842
const numIncrements = 1 + Math.floor(Math.random() * 3); // 1 to 3 increments
860843
const increments: number[] = [];
861844
for (let i = 0; i < numIncrements; i += 1) {
@@ -875,35 +858,35 @@ const OnboardingV2Page = (): ReactElement => {
875858

876859
increments.forEach((inc) => {
877860
addTimeout(() => {
878-
// Restart the pulse animation without reading layout (offsetWidth
879-
// forces a synchronous reflow). Setting animationName to 'none' is a
880-
// write-only style operation, then the next rAF restores it so the
881-
// animation re-runs from the start.
882-
btn.classList.remove('onb-eng-pulse');
883-
(btn as HTMLElement).style.animationName = 'none';
884-
requestAnimationFrame(() => {
885-
(btn as HTMLElement).style.animationName = '';
886-
btn.classList.add('onb-eng-pulse');
887-
});
888-
889-
const currentVal = parseCount(counter.textContent || '') || 0;
890-
counter.textContent = formatCount(currentVal + inc);
891-
892-
const floaterAnchor =
893-
counter.parentElement instanceof HTMLElement
894-
? counter.parentElement
895-
: wrapperEl;
896-
floaterAnchor.classList.add('onb-eng-pos-relative');
861+
const btnRect = btn.getBoundingClientRect();
862+
const articleRect = article.getBoundingClientRect();
863+
864+
// Center of the button relative to the article
865+
const startX = btnRect.left - articleRect.left + btnRect.width / 2;
866+
const startY = btnRect.top - articleRect.top;
897867

898868
const floater = document.createElement('span');
899869
floater.className = 'onb-eng-floater';
900870
floater.style.color = color;
901-
floater.style.left = `${counter.offsetLeft}px`;
902-
floater.style.top = `${counter.offsetTop}px`;
871+
872+
// Random offset so they don't stack on top of each other (PolyMarket style)
873+
const offsetX = (Math.random() - 0.5) * 32; // -16px to +16px
874+
const offsetY = (Math.random() - 0.5) * 16; // -8px to +8px
875+
876+
floater.style.left = `${startX + offsetX}px`;
877+
floater.style.top = `${startY + offsetY}px`;
903878
floater.textContent = `+${inc}`;
904-
floaterAnchor.appendChild(floater);
879+
880+
article.appendChild(floater);
905881
activeFloaters.add(floater);
906882

883+
// Update the actual counter text
884+
const counter = ensureCounter(wrapper, isUpvote ? 14 : 3);
885+
if (counter) {
886+
const currentVal = parseCount(counter.textContent || '') || 0;
887+
counter.textContent = formatCount(currentVal + inc);
888+
}
889+
907890
addTimeout(() => {
908891
floater.remove();
909892
activeFloaters.delete(floater);
@@ -914,7 +897,6 @@ const OnboardingV2Page = (): ReactElement => {
914897
});
915898

916899
addTimeout(() => {
917-
wrapperEl.classList.remove(activeClass);
918900
article.removeAttribute('data-eng-active');
919901
}, delayAcc + 600);
920902

@@ -2836,70 +2818,34 @@ const OnboardingV2Page = (): ReactElement => {
28362818
opacity: 1;
28372819
}
28382820
2839-
.onb-eng-active-upvote,
2840-
.onb-eng-active-upvote svg,
2841-
.onb-eng-active-upvote span {
2842-
color: var(--theme-actions-upvote-default) !important;
2843-
}
2844-
.onb-eng-active-comment,
2845-
.onb-eng-active-comment svg,
2846-
.onb-eng-active-comment span {
2847-
color: var(--theme-actions-comment-default) !important;
2848-
}
2849-
2850-
.onb-eng-pulse {
2851-
animation: onb-eng-pulse-anim 0.3s
2852-
cubic-bezier(0.175, 0.885, 0.32, 1.275);
2853-
}
2854-
@keyframes onb-eng-pulse-anim {
2855-
0% {
2856-
transform: scale(1);
2857-
}
2858-
50% {
2859-
transform: scale(1.25);
2860-
}
2861-
100% {
2862-
transform: scale(1);
2863-
}
2864-
}
2865-
2866-
.onb-eng-pos-relative {
2867-
position: relative;
2868-
}
2869-
.onb-eng-floater-anchor {
2870-
position: relative;
2871-
}
28722821
.onb-eng-floater {
28732822
position: absolute;
2874-
font-size: 0.875rem;
2823+
font-size: 0.75rem;
28752824
font-weight: 800;
28762825
font-variant-numeric: tabular-nums;
28772826
white-space: nowrap;
28782827
pointer-events: none;
2879-
z-index: 2;
2880-
animation: onb-eng-float-anim 1.4s ease-out forwards;
2828+
z-index: 10;
2829+
animation: onb-eng-float-anim 1.6s cubic-bezier(0.2, 0.8, 0.2, 1)
2830+
forwards;
28812831
text-shadow: 0 1px 4px
28822832
color-mix(in srgb, currentColor 25%, transparent);
28832833
}
28842834
@keyframes onb-eng-float-anim {
28852835
0% {
2886-
transform: translateY(0) scale(0.6);
2836+
transform: translateY(10px) scale(0.6);
28872837
opacity: 0;
28882838
}
2889-
10% {
2890-
transform: translateY(-8px) scale(1.1);
2839+
15% {
2840+
transform: translateY(0px) scale(1.1);
28912841
opacity: 1;
28922842
}
2893-
25% {
2894-
transform: translateY(-12px) scale(1);
2843+
30% {
2844+
transform: translateY(-5px) scale(1);
28952845
opacity: 1;
28962846
}
2897-
70% {
2898-
transform: translateY(-24px) scale(1);
2899-
opacity: 0.8;
2900-
}
29012847
100% {
2902-
transform: translateY(-32px) scale(0.95);
2848+
transform: translateY(-30px) scale(0.9);
29032849
opacity: 0;
29042850
}
29052851
}

0 commit comments

Comments
 (0)