global tooltip system, migrate all title attrs to data-tip/data-help
New tooltip.js module with styled tooltips (data-tip) and multi-line inline help (data-help). Migrates all ~70 native title attributes across index.php, JS modules, and all plugins. Adds inline help on 9 strategic UI elements (split, superview, zen, extract, assets, etc). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
527305355c
commit
f465306c1d
21 files changed
+229
-109
No files matched your search
@@ -13,6 +13,7 @@ import { openModal, closeModal, saveCard, saveOnly, openFindBar } from './module
|
||||
import { openSearch, closeSearch } from './modules/search.js';
|
||||
import { showContextMenu } from './modules/context-menu.js';
|
||||
import { undo, canUndo } from './modules/undo.js';
|
||||
import './modules/tooltip.js';
|
||||
import { registerShortcut, guards, renderShortcutHelp } from './modules/shortcuts.js';
|
||||
import { openGallery, closeGallery } from './modules/gallery.js';
|
||||
import { refreshTasksView, openTasks, closeTasks } from './modules/tasks.js';
|
||||
|
||||
@@ -294,11 +294,11 @@ function renderBar() {
|
||||
// render (e.g. bucket reorder, plain renderBar() call).
|
||||
const allUnread = state.activeBucket !== 'all' && state.unseenCardIds.size > 0;
|
||||
bar.innerHTML = `
|
||||
<button class="${tab(state.activeBucket === 'all')}${allUnread ? ' has-unread' : ''}" data-bucket="all" title="all"><i class="ph ph-squares-four text-sm"></i>${allUnread ? '<span class="bar-unread-dot"></span>' : ''}</button>
|
||||
<button class="${tab(state.activeBucket === 'current')}" data-bucket="current" title="current"><i class="ph ph-lightning text-sm"></i></button>
|
||||
<button class="${tab(state.activeBucket === 'favorites')}" data-bucket="favorites" title="favorites"><i class="ph ph-star text-sm"></i></button>
|
||||
<button class="${tab(state.activeBucket === 'journal')}" data-bucket="journal" title="journal"><i class="ph ph-notebook text-sm"></i></button>
|
||||
<button class="${tab(state.activeBucket === 'none')}" data-bucket="none" title="unsorted"><i class="ph ph-tray text-sm"></i></button>
|
||||
<button class="${tab(state.activeBucket === 'all')}${allUnread ? ' has-unread' : ''}" data-bucket="all" data-tip="all"><i class="ph ph-squares-four text-sm"></i>${allUnread ? '<span class="bar-unread-dot"></span>' : ''}</button>
|
||||
<button class="${tab(state.activeBucket === 'current')}" data-bucket="current" data-tip="current"><i class="ph ph-lightning text-sm"></i></button>
|
||||
<button class="${tab(state.activeBucket === 'favorites')}" data-bucket="favorites" data-tip="favorites"><i class="ph ph-star text-sm"></i></button>
|
||||
<button class="${tab(state.activeBucket === 'journal')}" data-bucket="journal" data-tip="journal"><i class="ph ph-notebook text-sm"></i></button>
|
||||
<button class="${tab(state.activeBucket === 'none')}" data-bucket="none" data-tip="unsorted"><i class="ph ph-tray text-sm"></i></button>
|
||||
${topLevel.map(b => {
|
||||
const kids = childrenOf(b.id);
|
||||
const isSuper = kids.length > 0;
|
||||
@@ -400,7 +400,7 @@ function renderBar() {
|
||||
const c = m.color || '#64748b';
|
||||
const active = state.activeBucket == m.id || (!members.some(x => x.id == state.activeBucket) && lastId == m.id);
|
||||
const isParent = m.id === parentId;
|
||||
return `<button class="bucket-tab ${active ? 'active' : ''} flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors w-full" data-bucket="${m.id}" draggable="true" data-bucket-drag="${m.id}" style="border-left:3px solid ${c};${active ? `background:${c};color:var(--t-bg);border-color:${c}` : ''}"><i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px] flex-1 text-left">${esc(m.name)}</span>${isParent ? '<i class="ph ph-house text-[10px] opacity-60" title="parent"></i>' : ''}</button>`;
|
||||
return `<button class="bucket-tab ${active ? 'active' : ''} flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors w-full" data-bucket="${m.id}" draggable="true" data-bucket-drag="${m.id}" style="border-left:3px solid ${c};${active ? `background:${c};color:var(--t-bg);border-color:${c}` : ''}"><i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px] flex-1 text-left">${esc(m.name)}</span>${isParent ? '<i class="ph ph-house text-[10px] opacity-60" data-tip="parent"></i>' : ''}</button>`;
|
||||
};
|
||||
pop.innerHTML = members.map(renderEntry).join('');
|
||||
// Append inside #bucket-bar (not document.body) so the collab
|
||||
|
||||
@@ -76,7 +76,7 @@ function renderCalList(calendars) {
|
||||
${CAL_COLORS.map(c => `<button class="cal-cfg-color" data-color="${c}" style="width:14px;height:14px;border-radius:2px;background:${c};border:2px solid ${c === activeColor ? 'var(--t-bright)' : 'transparent'};cursor:pointer;"></button>`).join('')}
|
||||
</div>
|
||||
<div class="flex gap-1 ml-1">
|
||||
${CAL_ICONS.slice(0, 6).map(ic => `<button class="cal-cfg-icon" data-icon="${ic.id}" title="${ic.label}" style="font-size:12px;cursor:pointer;padding:1px;border-radius:2px;${ic.id === activeIcon ? 'color:var(--t-accent);' : 'color:var(--t-dim);'}"><i class="ph ph-${ic.id}"></i></button>`).join('')}
|
||||
${CAL_ICONS.slice(0, 6).map(ic => `<button class="cal-cfg-icon" data-icon="${ic.id}" data-tip="${ic.label}" style="font-size:12px;cursor:pointer;padding:1px;border-radius:2px;${ic.id === activeIcon ? 'color:var(--t-accent);' : 'color:var(--t-dim);'}"><i class="ph ph-${ic.id}"></i></button>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
@@ -433,7 +433,7 @@ function renderSidebar(card) {
|
||||
<span class="w-2 h-2 rounded-full shrink-0" style="background:${gc.color}"></span>
|
||||
${img ? `<span class="w-6 h-6 rounded-full shrink-0 bg-cover bg-center border border-t-border" style="background-image:url('${img}')"></span>` : ''}
|
||||
<span class="font-pixel text-[10px] ${active ? 'text-t-bright' : 'text-t-muted'} truncate flex-1">${esc(gc.title)}</span>
|
||||
<button onclick="event.stopPropagation();window._ungroupCard(${gc.id})" class="text-t-dim hover:text-t-danger transition-colors shrink-0 ${gc.id == groupCards[0]?.id ? 'invisible' : ''}" title="ungroup">
|
||||
<button onclick="event.stopPropagation();window._ungroupCard(${gc.id})" class="text-t-dim hover:text-t-danger transition-colors shrink-0 ${gc.id == groupCards[0]?.id ? 'invisible' : ''}" data-tip="ungroup">
|
||||
<i class="ph ph-x text-[10px]"></i>
|
||||
</button>
|
||||
</div>`;
|
||||
@@ -554,7 +554,7 @@ function applyReadingResizeHandles(root) {
|
||||
const line = document.createElement('button');
|
||||
line.type = 'button';
|
||||
line.className = 'detail-font-dragline';
|
||||
line.title = 'drag to resize text';
|
||||
line.dataset.tip = 'drag to resize text';
|
||||
line.setAttribute('aria-label', 'resize text');
|
||||
el.appendChild(line);
|
||||
|
||||
@@ -615,7 +615,7 @@ export function bindDetailImageFullscreen(root) {
|
||||
// Regular images: double-click, no gallery nav
|
||||
root.querySelectorAll('img').forEach(img => {
|
||||
if (img.closest('.md-gallery')) return;
|
||||
img.title = 'double-click to open fullscreen';
|
||||
img.dataset.tip = 'double-click to fullscreen';
|
||||
img.addEventListener('dblclick', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -898,7 +898,7 @@ function loadSplitCard(id) {
|
||||
<button id="split-back" class="flex items-center gap-1.5 flex-1 px-3 py-2 text-xs text-t-muted hover:text-t-accent hover:bg-t-raised transition-colors">
|
||||
<i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">${splitLastQuery ? 'results' : 'related cards'}</span>
|
||||
</button>
|
||||
<button id="split-promote" class="flex items-center gap-1.5 px-3 py-2 text-xs text-t-muted hover:text-t-accent hover:bg-t-raised transition-colors" title="open as main card">
|
||||
<button id="split-promote" class="flex items-center gap-1.5 px-3 py-2 text-xs text-t-muted hover:text-t-accent hover:bg-t-raised transition-colors" data-tip="open as main card">
|
||||
<i class="ph ph-arrow-line-left text-sm"></i><span class="font-pixel text-[10px]">Focus</span>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
@@ -102,7 +102,7 @@ function startCloseMorph() {
|
||||
|
||||
CARD_COLORS.forEach(c => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button'; btn.title = c.name;
|
||||
btn.type = 'button'; btn.dataset.tip = c.name;
|
||||
btn.dataset.color = c.hex;
|
||||
btn.className = 'w-5 h-5 rounded-sm border-2 border-transparent transition-all hover:scale-125 color-swatch';
|
||||
btn.style.background = c.hex;
|
||||
@@ -1243,13 +1243,13 @@ $('md-asset-picker')?.addEventListener('click', async () => {
|
||||
const type = assetType(f.name);
|
||||
const label = f.name.replace(/\.[^.]+$/, '');
|
||||
if (type === 'image') {
|
||||
return `<div class="asset-pick-tile" data-url="${esc(f.url)}" data-name="${esc(f.name)}" data-type="image" title="${esc(f.name)}" style="cursor:pointer;border-radius:4px;overflow:hidden;aspect-ratio:1;background:var(--t-bg);border:1px solid var(--t-border);">
|
||||
return `<div class="asset-pick-tile" data-url="${esc(f.url)}" data-name="${esc(f.name)}" data-type="image" data-tip="${esc(f.name)}" style="cursor:pointer;border-radius:4px;overflow:hidden;aspect-ratio:1;background:var(--t-bg);border:1px solid var(--t-border);">
|
||||
<img src="${esc(f.url)}" style="width:100%;height:100%;object-fit:cover;" loading="lazy">
|
||||
</div>`;
|
||||
}
|
||||
const icon = type === 'audio' ? 'ph-music-note' : type === 'pdf' ? 'ph-file-pdf' : 'ph-file';
|
||||
const iconColor = type === 'pdf' ? 'color:var(--t-danger)' : 'color:var(--t-dim)';
|
||||
return `<div class="asset-pick-tile" data-url="${esc(f.url)}" data-name="${esc(f.name)}" data-type="${type}" title="${esc(f.name)}" style="cursor:pointer;border-radius:4px;overflow:hidden;aspect-ratio:1;background:var(--t-bg);border:1px solid var(--t-border);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:4px;">
|
||||
return `<div class="asset-pick-tile" data-url="${esc(f.url)}" data-name="${esc(f.name)}" data-type="${type}" data-tip="${esc(f.name)}" style="cursor:pointer;border-radius:4px;overflow:hidden;aspect-ratio:1;background:var(--t-bg);border:1px solid var(--t-border);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:4px;">
|
||||
<i class="ph ${icon}" style="font-size:20px;${iconColor}"></i>
|
||||
<span style="font-size:7px;color:var(--t-muted);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--t-font-pixel,'Silkscreen',monospace);">${esc(label)}</span>
|
||||
</div>`;
|
||||
|
||||
@@ -170,9 +170,9 @@ export async function openGallery() {
|
||||
<div class="relative">
|
||||
${mediaTile(f.url, 'border-t-danger/30 opacity-60')}
|
||||
<div style="position:absolute;top:4px;right:4px;display:flex;gap:4px;" class="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-accent rounded border-0 cursor-pointer hover:bg-t-accent hover:text-t-bg transition-colors" data-new-orphan="${f.name}" title="new card"><i class="ph ph-plus text-sm"></i></button>
|
||||
<button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-accent rounded border-0 cursor-pointer hover:bg-t-accent hover:text-t-bg transition-colors" data-new-slide="${f.name}" title="new slide (background)"><i class="ph ph-image-square text-sm"></i></button>
|
||||
<button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-danger rounded border-0 cursor-pointer hover:bg-t-danger hover:text-t-bg transition-colors" data-del-orphan="${f.name}" title="delete"><i class="ph ph-trash text-sm"></i></button>
|
||||
<button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-accent rounded border-0 cursor-pointer hover:bg-t-accent hover:text-t-bg transition-colors" data-new-orphan="${f.name}" data-tip="new card"><i class="ph ph-plus text-sm"></i></button>
|
||||
<button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-accent rounded border-0 cursor-pointer hover:bg-t-accent hover:text-t-bg transition-colors" data-new-slide="${f.name}" data-tip="new slide"><i class="ph ph-image-square text-sm"></i></button>
|
||||
<button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-danger rounded border-0 cursor-pointer hover:bg-t-danger hover:text-t-bg transition-colors" data-del-orphan="${f.name}" data-tip="delete"><i class="ph ph-trash text-sm"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 font-pixel text-[8px] text-t-dim truncate">${esc(f.name)}</div>
|
||||
|
||||
@@ -146,7 +146,7 @@ export function renderMd(src) {
|
||||
const linked = processed.replace(/\[\[([^\]]+)\]\]/g, (_, title) => {
|
||||
const card = state.cards.find(c => c.title.toLowerCase() === title.toLowerCase());
|
||||
if (card) return `<span class="card-link" data-card-id="${card.id}">${esc(title)}</span>`;
|
||||
return `<span class="card-link broken" title="card not found">${esc(title)}</span>`;
|
||||
return `<span class="card-link broken" data-tip="card not found">${esc(title)}</span>`;
|
||||
});
|
||||
|
||||
let html = marked.parse(linked);
|
||||
|
||||
@@ -73,7 +73,7 @@ export function render() {
|
||||
<span>${esc(i.label)}</span>
|
||||
${i.dismissable !== false ? `<button class="upcoming-dismiss" data-dismiss="${i.id}"><i class="ph ph-x text-xs"></i></button>` : ''}
|
||||
</div>`).join('')}
|
||||
<button id="dismiss-all" class="shrink-0 ml-auto text-t-dim hover:text-t-danger transition-colors" title="dismiss all"><i class="ph ph-x-circle text-sm"></i></button>
|
||||
<button id="dismiss-all" class="shrink-0 ml-auto text-t-dim hover:text-t-danger transition-colors" data-tip="dismiss all"><i class="ph ph-x-circle text-sm"></i></button>
|
||||
</div>`;
|
||||
|
||||
banner.querySelectorAll('[data-notif-id]').forEach(el => {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// ── 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 target = e.target.closest('[data-tip],[data-help]');
|
||||
if (!target) return;
|
||||
clearTimeout(hideTimer);
|
||||
showTimer = setTimeout(() => show(target), SHOW_DELAY);
|
||||
}
|
||||
|
||||
function onLeave(e) {
|
||||
const target = e.target.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);
|
||||
Reference in new issue
Block a user