mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 20:06:57 +02:00
Add a reusable `use:tooltip` Svelte action as a drop-in replacement for native title attributes. Styled to match the retro parchment theme with JetBrains Mono font, dark background, and 400ms hover delay. Replace all title attributes in NoteEditor, NoteCard, and NotesView with the custom tooltip. The action supports top/bottom positioning and auto-clamps to viewport edges. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
/**
|
|
* Custom tooltip action — drop-in replacement for title attributes.
|
|
* Usage: <button use:tooltip={"Label"}>
|
|
* Or with options: <button use:tooltip={{ text: "Label", position: "bottom" }}>
|
|
*/
|
|
|
|
export type TooltipPosition = 'top' | 'bottom';
|
|
|
|
interface TooltipOptions {
|
|
text: string;
|
|
position?: TooltipPosition;
|
|
}
|
|
|
|
function parseOptions(value: string | TooltipOptions): { text: string; position: TooltipPosition } {
|
|
if (typeof value === 'string') return { text: value, position: 'top' };
|
|
return { text: value.text, position: value.position ?? 'top' };
|
|
}
|
|
|
|
export function tooltip(node: HTMLElement, value: string | TooltipOptions) {
|
|
let opts = parseOptions(value);
|
|
let el: HTMLDivElement | null = null;
|
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
function show() {
|
|
timeout = setTimeout(() => {
|
|
if (!opts.text) return;
|
|
el = document.createElement('div');
|
|
el.className = 'tooltip-popup';
|
|
el.textContent = opts.text;
|
|
document.body.appendChild(el);
|
|
position();
|
|
}, 400);
|
|
}
|
|
|
|
function position() {
|
|
if (!el) return;
|
|
const rect = node.getBoundingClientRect();
|
|
const tipRect = el.getBoundingClientRect();
|
|
|
|
let top: number;
|
|
if (opts.position === 'bottom') {
|
|
top = rect.bottom + 6;
|
|
} else {
|
|
top = rect.top - tipRect.height - 6;
|
|
}
|
|
|
|
let left = rect.left + rect.width / 2 - tipRect.width / 2;
|
|
// Clamp to viewport
|
|
left = Math.max(8, Math.min(left, window.innerWidth - tipRect.width - 8));
|
|
|
|
el.style.top = `${top}px`;
|
|
el.style.left = `${left}px`;
|
|
el.classList.add('tooltip-visible');
|
|
}
|
|
|
|
function hide() {
|
|
clearTimeout(timeout);
|
|
if (el) {
|
|
el.remove();
|
|
el = null;
|
|
}
|
|
}
|
|
|
|
node.addEventListener('mouseenter', show);
|
|
node.addEventListener('mouseleave', hide);
|
|
node.addEventListener('pointerdown', hide);
|
|
// Remove native title to prevent double tooltip
|
|
const nativeTitle = node.getAttribute('title');
|
|
if (nativeTitle) node.removeAttribute('title');
|
|
|
|
return {
|
|
update(newValue: string | TooltipOptions) {
|
|
opts = parseOptions(newValue);
|
|
},
|
|
destroy() {
|
|
hide();
|
|
node.removeEventListener('mouseenter', show);
|
|
node.removeEventListener('mouseleave', hide);
|
|
node.removeEventListener('pointerdown', hide);
|
|
if (nativeTitle) node.setAttribute('title', nativeTitle);
|
|
}
|
|
};
|
|
}
|