Tooltip kustom premium via Alpine directive x-tooltip

- Directive global di app.js: satu tooltip singleton di body, posisi dari
  getBoundingClientRect (fixed, tidak terpotong overflow-hidden), animasi
  fade + slide halus, delay 120ms anti-kedip, fallback ke bawah jika di
  tepi atas viewport, clamp ke viewport
- Tooltip gelap glass: bg-slate-900/95 + ring + shadow + backdrop-blur
- Badge kalender pelanggaran ('X pelanggaran') & presensi ('X siswa, Y
  alpha') beralih dari :title native ke x-tooltip
- Sembunyi otomatis saat scroll/resize/klik
This commit is contained in:
Novy San87
2026-08-03 18:08:57 +07:00
parent 349ebb2b30
commit 3169c636c5
3 changed files with 85 additions and 2 deletions
+83
View File
@@ -4,4 +4,87 @@ import Alpine from 'alpinejs';
window.Alpine = Alpine;
/**
* x-tooltip — tooltip kustom premium (Alpine directive)
*
* Pemakaian: <span x-tooltip="'teks' atau ekspresi">...</span>
* Satu elemen tooltip global di <body>, posisi dihitung dari bounding rect
* (fixed), animasi fade + slide halus. Tidak terpotong elemen overflow-hidden.
*/
document.addEventListener('alpine:init', () => {
let sharedTip = null;
let showTimer = null;
let activeEl = null;
const getTip = () => {
if (sharedTip) return sharedTip;
sharedTip = document.createElement('div');
sharedTip.className =
'pointer-events-none fixed z-[70] px-2.5 py-1.5 rounded-lg bg-slate-900/95 text-white text-[11px] font-semibold shadow-xl ring-1 ring-white/10 backdrop-blur-sm whitespace-nowrap transition-all duration-150 opacity-0 translate-y-1';
sharedTip.style.display = 'none';
document.body.appendChild(sharedTip);
return sharedTip;
};
const hide = () => {
window.clearTimeout(showTimer);
if (!sharedTip) return;
sharedTip.classList.add('opacity-0', 'translate-y-1');
window.setTimeout(() => {
if (sharedTip) sharedTip.style.display = 'none';
}, 150);
activeEl = null;
};
const show = (el, text) => {
if (!text) return;
const tip = getTip();
tip.textContent = text;
tip.style.display = 'block';
tip.classList.add('opacity-0', 'translate-y-1');
const r = el.getBoundingClientRect();
const tw = tip.offsetWidth;
const th = tip.offsetHeight;
let x = Math.round(r.left + r.width / 2 - tw / 2);
let y = r.top - th - 8;
if (y < 8) y = r.bottom + 8; // fallback: tampil di bawah elemen
x = Math.max(8, Math.min(x, window.innerWidth - tw - 8));
tip.style.left = x + 'px';
tip.style.top = y + 'px';
requestAnimationFrame(() => {
tip.classList.remove('opacity-0', 'translate-y-1');
});
};
Alpine.directive('tooltip', (el, { expression }) => {
const getText = () => {
try {
return Alpine.evaluate(el, expression);
} catch {
return expression;
}
};
el.addEventListener('mouseenter', () => {
window.clearTimeout(showTimer);
const text = getText();
activeEl = el;
// delay kecil agar tidak berkedip saat hover lintas elemen
showTimer = window.setTimeout(() => {
if (activeEl === el) show(el, text);
}, 120);
});
el.addEventListener('mouseleave', hide);
el.addEventListener('click', hide);
});
// Sembunyikan saat halaman digulir/di-klik (posisi berubah / sel kalender re-render)
window.addEventListener('scroll', hide, true);
window.addEventListener('resize', hide);
document.addEventListener('click', hide);
});
Alpine.start();
+1 -1
View File
@@ -132,7 +132,7 @@
<template x-if="day.total > 0 && day.isCurrentMonth">
<span class="absolute bottom-1.5 right-1.5 inline-flex items-center justify-center min-w-[28px] h-[28px] text-xs font-extrabold text-white rounded-full shadow-lg transition-transform hover:scale-110"
:class="day.alpha > 0 ? 'bg-gradient-to-br from-red-500 to-rose-600 shadow-red-500/30' : 'bg-gradient-to-br from-emerald-500 to-emerald-600 shadow-emerald-500/30'"
:title="day.total + ' siswa, ' + day.alpha + ' alpha'"
x-tooltip="day.total + ' siswa, ' + day.alpha + ' alpha'"
x-text="day.total">
</span>
</template>
+1 -1
View File
@@ -196,7 +196,7 @@
<a :href="'{{ route('violations.index') }}?date_from=' + day.dateStr + '&date_to=' + day.dateStr"
class="absolute bottom-1.5 right-1.5 inline-flex items-center justify-center min-w-[28px] h-[28px] text-xs font-extrabold text-white rounded-full shadow-lg transition-transform hover:scale-110"
:class="day.count >= 3 ? 'bg-gradient-to-br from-red-500 to-rose-600 shadow-red-500/30' : (day.count >= 2 ? 'bg-gradient-to-br from-orange-400 to-orange-500 shadow-orange-400/30' : 'bg-gradient-to-br from-blue-400 to-blue-500 shadow-blue-400/30')"
:title="day.count + ' pelanggaran'"
x-tooltip="day.count + ' pelanggaran'"
x-text="day.count">
</a>
</template>