Skip to content

Commit cb59aa2

Browse files
committed
Make the sponsor tooltip magical: charmed bubble, sparkle rim, falling hearts
The sponsor tooltip becomes the one enchanted moment in the shell: the bubble springs open wearing a pink-to-gold gradient keyline and a breathing glow, eight sparkles twinkle around its rim on their own phases, and small hearts tumble out of the button and drift down across the card while a mouse hovers. The sparkle layer is built by script to the bubble's real measured box; the hearts carry per-heart drift, spin, size, and duration. Keyboard focus gets the bubble and sparkles, touch keeps its plain tap-through, reduced motion gets a calm fade with none of the moving parts, and a hidden tab spawns nothing.
1 parent af05d45 commit cb59aa2

7 files changed

Lines changed: 155 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
All notable changes to this project are documented here.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

6+
## [1.4.0] - 2026-07-19
7+
8+
### Added
9+
10+
- The sponsor tooltip turned magical: a charmed bubble with a pink-to-gold keyline that springs open and breathes a soft glow, sparkles twinkling around its rim, and little hearts tumbling out of the button while it is hovered. Keyboard focus gets the bubble and sparkles, touch stays plain, and reduced motion gets a calm fade.
11+
612
## [1.3.38] - 2026-07-19
713

814
### Changed

docs/404.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230d0c0a'/%3E%3Ctext x='32' y='44' text-anchor='middle' font-size='30' font-family='monospace' fill='%23abcf37'%3E s:%3C/text%3E%3C/svg%3E">
2424
<link rel="icon" type="image/png" sizes="32x32" href="/wp-serial-fix/assets/favicon-32.png">
2525
<link rel="apple-touch-icon" href="/wp-serial-fix/assets/apple-touch-icon.png">
26-
<link rel="stylesheet" href="/wp-serial-fix/styles.css?v=1.3.38">
26+
<link rel="stylesheet" href="/wp-serial-fix/styles.css?v=1.4.0">
2727
</head>
2828
<body>
2929
<a class="skip-link" href="#main">Skip to main content</a>
@@ -181,6 +181,6 @@ <h1>404, page not found.</h1>
181181
<span>Copyright © <a href="https://www.JaydenYoonZK.com"><strong>Jayden Yoon ZK</strong></a> <span id="copyright-year">2026</span>. All Rights Reserved.</span>
182182
</div>
183183
</footer>
184-
<script src="/wp-serial-fix/notfound.js?v=1.3.38"></script>
184+
<script src="/wp-serial-fix/notfound.js?v=1.4.0"></script>
185185
</body>
186186
</html>

docs/app.js

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*! WP Serial Fix | Copyright (c) 2026 Jayden Yoon ZK | MIT License | https://github.com/JaydenYoonZK/wp-serial-fix */
2-
import { process, isSerialized, byteLength, serialize } from "./serial.js?v=1.3.38";
2+
import { process, isSerialized, byteLength, serialize } from "./serial.js?v=1.4.0";
33

44
const $ = (id) => document.getElementById(id);
55
const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -190,6 +190,73 @@ syncControls();
190190
syncMode();
191191
if (new URLSearchParams(location.search).has("demo")) loadSample();
192192

193+
// -------- sponsor button magic (sparkle rim + floating hearts) --------
194+
// The tooltip bubble itself is pure CSS; this builds the sparkle layer sized
195+
// to the bubble's real box and streams hearts while a mouse hovers. Reduced
196+
// motion skips all of it, touch never sees it, keyboard focus gets sparkles.
197+
const sponsorBtn = document.querySelector(".sponsor-btn");
198+
if (sponsorBtn && !matchMedia("(prefers-reduced-motion: reduce)").matches) {
199+
const HEART_PATH = "M12 21s-6.7-4.35-9.33-8.11C.8 10.2 1.96 6.5 5.14 5.44c1.9-.63 3.98.03 5.36 1.6L12 8.6l1.5-1.56c1.38-1.57 3.46-2.23 5.36-1.6 3.18 1.06 4.34 4.76 2.47 7.45C18.7 16.65 12 21 12 21z";
200+
const SPARKS = ["✦", "✧", "⋆"];
201+
const SPARK_TINTS = ["", "var(--spk-b)", "var(--spk-c)"];
202+
let fx = null, heartTimer = 0, liveHearts = 0;
203+
const buildFx = () => {
204+
if (fx) return;
205+
const tip = getComputedStyle(sponsorBtn, "::after");
206+
// computed width/height are the content box; the visible bubble adds
207+
// padding and the gradient keyline, so include them or the stars hug a
208+
// box smaller than what the eye sees
209+
const pad = (p) => parseFloat(tip[p]) || 0;
210+
const w = (parseFloat(tip.width) || 122) + pad("paddingLeft") + pad("paddingRight") + 2;
211+
const h = (parseFloat(tip.height) || 18) + pad("paddingTop") + pad("paddingBottom") + 2;
212+
fx = document.createElement("span");
213+
fx.className = "sponsor-fx";
214+
fx.setAttribute("aria-hidden", "true");
215+
fx.style.width = w + "px";
216+
fx.style.height = h + "px";
217+
// eight stars parked around the bubble's rim, each on its own phase
218+
const spots = [[-38, 4], [-30, 34], [-42, 68], [10, 102], [62, 96], [108, 74], [116, 30], [96, -5]];
219+
spots.forEach(([top, left], k) => {
220+
const s = document.createElement("span");
221+
s.className = "spk";
222+
s.textContent = SPARKS[k % SPARKS.length];
223+
s.style.top = top + "%";
224+
s.style.left = left + "%";
225+
s.style.fontSize = (9 + ((k * 5) % 6)) + "px";
226+
s.style.animationDelay = (-k * 0.21).toFixed(2) + "s";
227+
s.style.animationDuration = (1.5 + (k % 3) * 0.35).toFixed(2) + "s";
228+
if (SPARK_TINTS[k % 3]) s.style.color = SPARK_TINTS[k % 3];
229+
fx.appendChild(s);
230+
});
231+
sponsorBtn.appendChild(fx);
232+
};
233+
const spawnHeart = () => {
234+
if (liveHearts >= 7 || document.hidden) return;
235+
liveHearts++;
236+
const el = document.createElement("span");
237+
el.className = "sponsor-heart";
238+
el.setAttribute("aria-hidden", "true");
239+
el.style.setProperty("--hx", (Math.random() * 44 - 22).toFixed(0) + "px");
240+
el.style.setProperty("--hd", (1.05 + Math.random() * 0.7).toFixed(2) + "s");
241+
el.style.setProperty("--hs", (0.7 + Math.random() * 0.7).toFixed(2));
242+
el.style.setProperty("--hr", (Math.random() * 40 - 20).toFixed(0) + "deg");
243+
if (Math.random() < 0.33) el.style.color = "#ff9ed2";
244+
el.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${HEART_PATH}"/></svg>`;
245+
el.addEventListener("animationend", () => { el.remove(); liveHearts--; });
246+
sponsorBtn.appendChild(el);
247+
};
248+
sponsorBtn.addEventListener("pointerenter", (e) => {
249+
buildFx();
250+
if (e.pointerType === "mouse") {
251+
spawnHeart();
252+
clearInterval(heartTimer);
253+
heartTimer = setInterval(spawnHeart, 300);
254+
}
255+
});
256+
sponsorBtn.addEventListener("pointerleave", () => { clearInterval(heartTimer); heartTimer = 0; });
257+
sponsorBtn.addEventListener("focus", buildFx);
258+
}
259+
193260
const themeToggle = document.getElementById("theme-toggle");
194261
function syncThemeIcon() {
195262
const label = document.documentElement.dataset.theme === "light" ? "Switch to dark mode" : "Switch to light mode";

docs/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
document.head.appendChild(m);
3838
})();
3939
</script>
40-
<link rel="stylesheet" href="styles.css?v=1.3.38">
40+
<link rel="stylesheet" href="styles.css?v=1.4.0">
4141
<script type="application/ld+json">
4242
{
4343
"@context": "https://schema.org",
@@ -326,6 +326,6 @@ <h2 id="why-built">🌱 Why I built this</h2>
326326
</footer>
327327

328328
<button id="to-top" class="to-top" type="button" aria-label="Back to top">&#8593;</button>
329-
<script type="module" src="app.js?v=1.3.38"></script>
329+
<script type="module" src="app.js?v=1.4.0"></script>
330330
</body>
331331
</html>

docs/styles.css

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -976,33 +976,96 @@ html[data-theme="dark"] .tt-moon { opacity: 1; transform: rotate(0deg) scale(1);
976976
/* Its own tooltip, matching the theme toggle's exactly: the bubble opens
977977
downward hugging the button's right edge, while the arrow wedge stays
978978
centered under the heart, so the pair reads as one consistent family. */
979+
/* The sponsor tooltip is the one place the suite is allowed to be magical: a
980+
charmed bubble with a pink-to-gold gradient keyline that springs open and
981+
breathes a soft aura, JS-built sparkles twinkling around its rim, and a
982+
gentle stream of hearts floating up from the button while it is hovered.
983+
Everything degrades: keyboard focus gets the bubble and sparkles, touch
984+
gets none of it (the tap navigates), and reduced motion gets a calm fade. */
979985
.sponsor-btn[data-tip]::after {
980986
content: attr(data-tip);
981-
position: absolute; top: calc(100% + 10px); right: 0;
987+
position: absolute; top: calc(100% + 12px); right: 0;
982988
width: max-content; max-width: 230px;
983-
padding: 8px 11px; border-radius: 10px;
984-
background: var(--ink); color: var(--bg); border: 1px solid var(--line-strong);
985-
font-size: 0.76rem; font-weight: 600; line-height: 1.35;
986-
opacity: 0; transform: translateY(-6px); pointer-events: none;
987-
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
988-
transition: opacity 0.18s ease, transform 0.18s ease;
989+
padding: 9px 14px; border-radius: 12px;
990+
border: 1px solid transparent;
991+
background:
992+
linear-gradient(var(--ink), var(--ink)) padding-box,
993+
linear-gradient(125deg, var(--sponsor), #c9a6ff 48%, #ffd66b) border-box;
994+
color: var(--bg);
995+
font-size: 0.8rem; font-weight: 700; line-height: 1.35; letter-spacing: 0.01em;
996+
opacity: 0; transform: translateY(-8px) scale(0.86); transform-origin: top right;
997+
pointer-events: none;
998+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35), 0 0 16px var(--sponsor-glow), 0 0 40px var(--sponsor-glow);
999+
transition: opacity 0.2s ease, transform 0.38s cubic-bezier(0.34, 1.56, 0.64, 1);
9891000
z-index: 60;
9901001
}
9911002
.sponsor-btn[data-tip]::before {
9921003
content: "";
993-
position: absolute; left: 50%; top: calc(100% - 1px);
1004+
position: absolute; left: 50%; top: calc(100% + 1px);
9941005
border: 6px solid transparent;
995-
border-bottom-color: var(--ink);
1006+
border-bottom-color: var(--sponsor);
9961007
opacity: 0; transform: translate(-50%, -6px); pointer-events: none;
9971008
transition: opacity 0.18s ease, transform 0.18s ease;
1009+
filter: drop-shadow(0 -1px 4px var(--sponsor-glow));
9981010
z-index: 61; /* paints over the bubble edge so wedge and bubble read as one shape */
9991011
}
1000-
.sponsor-btn[data-tip]:hover::after, .sponsor-btn[data-tip]:focus-visible::after { opacity: 1; transform: translateY(0); }
1012+
.sponsor-btn[data-tip]:hover::after, .sponsor-btn[data-tip]:focus-visible::after {
1013+
opacity: 1; transform: translateY(0) scale(1);
1014+
animation: sponsor-aura 2.6s ease-in-out 0.5s infinite;
1015+
}
10011016
.sponsor-btn[data-tip]:hover::before, .sponsor-btn[data-tip]:focus-visible::before { opacity: 1; transform: translate(-50%, 0); }
1002-
@media (hover: none) { .sponsor-btn[data-tip]::after, .sponsor-btn[data-tip]::before { display: none; } }
1017+
@keyframes sponsor-aura {
1018+
0%, 100% { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35), 0 0 16px var(--sponsor-glow), 0 0 40px var(--sponsor-glow); }
1019+
50% { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35), 0 0 24px var(--sponsor-glow), 0 0 60px var(--sponsor-glow); }
1020+
}
1021+
1022+
/* The sparkle rim: a JS-built layer sized to the bubble, holding little stars
1023+
parked just off its edge, each twinkling on its own phase. */
1024+
.sponsor-fx {
1025+
position: absolute; top: calc(100% + 12px); right: 0;
1026+
pointer-events: none; opacity: 0;
1027+
transition: opacity 0.25s ease 0.08s;
1028+
z-index: 62;
1029+
}
1030+
.sponsor-btn:hover .sponsor-fx, .sponsor-btn:focus-visible .sponsor-fx { opacity: 1; }
1031+
.sponsor-fx { --spk-a: var(--sponsor); --spk-b: #ffd9ec; --spk-c: #ffe2a1; }
1032+
html[data-theme="light"] .sponsor-fx { --spk-b: #d4589f; --spk-c: #d99a2b; }
1033+
.sponsor-fx .spk {
1034+
position: absolute; line-height: 1;
1035+
color: var(--spk-a);
1036+
text-shadow: 0 0 6px var(--sponsor-glow), 0 0 14px var(--sponsor-glow);
1037+
animation: spk-twinkle 1.8s ease-in-out infinite;
1038+
}
1039+
@keyframes spk-twinkle {
1040+
0%, 100% { transform: scale(0.2) rotate(0deg); opacity: 0; }
1041+
55% { transform: scale(1) rotate(120deg); opacity: 1; }
1042+
}
1043+
1044+
/* Hearts that drift up out of the button while it is hovered. Spawned by JS
1045+
with per-heart drift, spin, size, and duration in custom properties. */
1046+
.sponsor-heart {
1047+
position: absolute; left: 50%; top: 50%;
1048+
width: 14px; height: 14px; margin: -7px 0 0 -7px;
1049+
pointer-events: none; opacity: 0; color: var(--sponsor);
1050+
animation: heart-float var(--hd, 1.3s) cubic-bezier(0.2, 0.6, 0.3, 1) forwards;
1051+
z-index: 63; /* above the bubble: the hearts tumble across it, not behind it */
1052+
}
1053+
.sponsor-heart svg { display: block; width: 100%; height: 100%; fill: currentColor; filter: drop-shadow(0 0 6px var(--sponsor-glow)); }
1054+
/* The button lives at the top of the page, so the hearts fall like petals,
1055+
outward and down past the tooltip, instead of rising straight off-screen. */
1056+
@keyframes heart-float {
1057+
0% { transform: translate(0, 0) scale(0.35) rotate(0deg); opacity: 0; }
1058+
16% { opacity: 0.95; }
1059+
100% { transform: translate(var(--hx, 0px), 84px) scale(var(--hs, 1)) rotate(var(--hr, 0deg)); opacity: 0; }
1060+
}
1061+
1062+
@media (hover: none) { .sponsor-btn[data-tip]::after, .sponsor-btn[data-tip]::before, .sponsor-fx { display: none; } }
10031063
@media (prefers-reduced-motion: reduce) {
10041064
.sponsor-btn { transition: color 0.2s, background 0.2s, border-color 0.2s; }
1005-
.sponsor-btn[data-tip]::after, .sponsor-btn[data-tip]::before { transition: opacity 0.12s ease; }
1065+
.sponsor-btn[data-tip]::after, .sponsor-btn[data-tip]::before { transition: opacity 0.12s ease; transform: none; animation: none; }
1066+
.sponsor-btn[data-tip]:hover::after, .sponsor-btn[data-tip]:focus-visible::after { transform: none; animation: none; }
1067+
.sponsor-fx .spk { animation: none; opacity: 0.75; }
1068+
.sponsor-heart { display: none; }
10061069
}
10071070

10081071
[id] { scroll-margin-top: calc(var(--nav-h, 104px) + 16px); }

docs/sw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
requests pass through untouched so live lookups stay live. The cache name
55
carries the release version and old caches are dropped on activate. */
66

7-
const VERSION = "?v=1.3.38";
7+
const VERSION = "?v=1.4.0";
88
const CACHE = "wp-serial-fix-" + VERSION;
99
const SHELL = [
1010
"./",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wp-serial-fix",
3-
"version": "1.3.38",
3+
"version": "1.4.0",
44
"description": "Serialization-safe search and replace for WordPress data, plus repair for already-broken serialized values. Browser-based, no dependencies.",
55
"type": "module",
66
"private": true,

0 commit comments

Comments
 (0)