- Scratchpad: card-scoped private notes in editor (toggle vs preview), smaller font, cleared via button, stored in cards.scratchpad - Zen mode: ESC exits zen before closing detail, Z toggles zen - Chat: always show absolute time (hh:mm, yesterday, or date+time) - Backspace/Delete also deletes focused card (not just selected) - Selected + focused cards use brand accent (not accent2) - Tooltip: guard against non-element targets - Collab sync: skip poll while editor is open, 2s push debounce - Rrule test: only check geburtstag tag, not title format Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
2.9 KiB
JavaScript
94 lines
2.9 KiB
JavaScript
// ── Global tooltip system ──
|
|
// Replaces native title="" with styled tooltips. Supports:
|
|
// data-tip="short text" — single-line tooltip (replaces title)
|
|
// data-help="line1\nline2" — multi-line inline help (shown on hover)
|
|
|
|
const SHOW_DELAY = 400;
|
|
const HIDE_DELAY = 100;
|
|
const OFFSET = 8;
|
|
|
|
let tipEl = null;
|
|
let showTimer = null;
|
|
let hideTimer = null;
|
|
let currentTarget = null;
|
|
|
|
function create() {
|
|
if (tipEl) return;
|
|
tipEl = document.createElement('div');
|
|
tipEl.id = 'tooltip';
|
|
tipEl.setAttribute('role', 'tooltip');
|
|
document.body.appendChild(tipEl);
|
|
}
|
|
|
|
function show(target) {
|
|
clearTimeout(hideTimer);
|
|
if (currentTarget === target) return;
|
|
currentTarget = target;
|
|
create();
|
|
|
|
const tip = target.dataset.tip || '';
|
|
const help = target.dataset.help || '';
|
|
if (!tip && !help) { hide(); return; }
|
|
|
|
if (help) {
|
|
tipEl.classList.add('tooltip-help');
|
|
tipEl.innerHTML = help.replace(/\\n/g, '<br>');
|
|
} else {
|
|
tipEl.classList.remove('tooltip-help');
|
|
tipEl.textContent = tip;
|
|
}
|
|
|
|
tipEl.style.visibility = 'hidden';
|
|
tipEl.style.display = 'block';
|
|
|
|
const rect = target.getBoundingClientRect();
|
|
const tipRect = tipEl.getBoundingClientRect();
|
|
|
|
let top, left;
|
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
const spaceAbove = rect.top;
|
|
|
|
if (spaceBelow >= tipRect.height + OFFSET || spaceBelow >= spaceAbove) {
|
|
top = rect.bottom + OFFSET;
|
|
tipEl.classList.remove('tooltip-above');
|
|
} else {
|
|
top = rect.top - tipRect.height - OFFSET;
|
|
tipEl.classList.add('tooltip-above');
|
|
}
|
|
|
|
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
|
left = Math.max(8, Math.min(left, window.innerWidth - tipRect.width - 8));
|
|
top = Math.max(4, top);
|
|
|
|
tipEl.style.top = top + 'px';
|
|
tipEl.style.left = left + 'px';
|
|
tipEl.style.visibility = '';
|
|
}
|
|
|
|
function hide() {
|
|
currentTarget = null;
|
|
if (tipEl) tipEl.style.display = 'none';
|
|
}
|
|
|
|
function onEnter(e) {
|
|
const el = e.target?.nodeType === 1 ? e.target : e.target?.parentElement;
|
|
const target = el?.closest?.('[data-tip],[data-help]');
|
|
if (!target) return;
|
|
clearTimeout(hideTimer);
|
|
showTimer = setTimeout(() => show(target), SHOW_DELAY);
|
|
}
|
|
|
|
function onLeave(e) {
|
|
const el = e.target?.nodeType === 1 ? e.target : e.target?.parentElement;
|
|
const target = el?.closest?.('[data-tip],[data-help]');
|
|
if (!target) return;
|
|
clearTimeout(showTimer);
|
|
hideTimer = setTimeout(hide, HIDE_DELAY);
|
|
}
|
|
|
|
document.addEventListener('pointerenter', onEnter, true);
|
|
document.addEventListener('pointerleave', onLeave, true);
|
|
document.addEventListener('pointerdown', () => { clearTimeout(showTimer); hide(); }, true);
|
|
document.addEventListener('keydown', () => { clearTimeout(showTimer); hide(); }, true);
|
|
document.addEventListener('scroll', () => { clearTimeout(showTimer); hide(); }, true);
|