diff --git a/config.php b/config.php index d5ba2ee..54de1f5 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ Search ctrl+k - @@ -1256,19 +1256,19 @@ if (str_starts_with($route, '/api/')) {
- - - - - +
@@ -1693,7 +1693,7 @@ if (str_starts_with($route, '/api/')) {
effects
-
+
0
0
@@ -1935,7 +1935,7 @@ if (str_starts_with($route, '/api/')) { - - -
-
- - @@ -2182,33 +2182,33 @@ if (str_starts_with($route, '/api/')) {
- - - - + + + +
- - + +
- - - - - + + + + +
- - - + + +
- - + + - +
- + slot('md-toolbar') ?>
- +
@@ -2220,9 +2220,9 @@ if (str_starts_with($route, '/api/')) { - - - + + +
diff --git a/public/js/app.js b/public/js/app.js index 9c4dd68..d0cdcdc 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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'; diff --git a/public/js/modules/buckets.js b/public/js/modules/buckets.js index 7fb2a26..596bda0 100644 --- a/public/js/modules/buckets.js +++ b/public/js/modules/buckets.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 = ` - - - - - + + + + + ${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 ``; + return ``; }; pop.innerHTML = members.map(renderEntry).join(''); // Append inside #bucket-bar (not document.body) so the collab diff --git a/public/js/modules/caldav.js b/public/js/modules/caldav.js index 8239f51..d5b541e 100644 --- a/public/js/modules/caldav.js +++ b/public/js/modules/caldav.js @@ -76,7 +76,7 @@ function renderCalList(calendars) { ${CAL_COLORS.map(c => ``).join('')}
- ${CAL_ICONS.slice(0, 6).map(ic => ``).join('')} + ${CAL_ICONS.slice(0, 6).map(ic => ``).join('')}
`; }).join(''); diff --git a/public/js/modules/detail.js b/public/js/modules/detail.js index 672ee40..0711e7e 100644 --- a/public/js/modules/detail.js +++ b/public/js/modules/detail.js @@ -433,7 +433,7 @@ function renderSidebar(card) { ${img ? `` : ''} ${esc(gc.title)} - `; @@ -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) { - `; diff --git a/public/js/modules/editor.js b/public/js/modules/editor.js index 9d91d57..87d776b 100644 --- a/public/js/modules/editor.js +++ b/public/js/modules/editor.js @@ -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 `
+ return `
`; } 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 `
+ return `
${esc(label)}
`; diff --git a/public/js/modules/gallery.js b/public/js/modules/gallery.js index abec05f..7727b8a 100644 --- a/public/js/modules/gallery.js +++ b/public/js/modules/gallery.js @@ -170,9 +170,9 @@ export async function openGallery() {
${mediaTile(f.url, 'border-t-danger/30 opacity-60')}
- - - + + +
${esc(f.name)}
diff --git a/public/js/modules/markdown.js b/public/js/modules/markdown.js index 6b24cb6..1897ab1 100644 --- a/public/js/modules/markdown.js +++ b/public/js/modules/markdown.js @@ -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 `${esc(title)}`; - return `${esc(title)}`; + return `${esc(title)}`; }); let html = marked.parse(linked); diff --git a/public/js/modules/notifications.js b/public/js/modules/notifications.js index c02e068..35e971c 100644 --- a/public/js/modules/notifications.js +++ b/public/js/modules/notifications.js @@ -73,7 +73,7 @@ export function render() { ${esc(i.label)} ${i.dismissable !== false ? `` : ''}
`).join('')} - +
`; banner.querySelectorAll('[data-notif-id]').forEach(el => { diff --git a/public/js/modules/tooltip.js b/public/js/modules/tooltip.js new file mode 100644 index 0000000..a103782 --- /dev/null +++ b/public/js/modules/tooltip.js @@ -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, '
'); + } 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); diff --git a/public/plugins/canvas/canvas.html b/public/plugins/canvas/canvas.html index f676eb4..ff90b2b 100644 --- a/public/plugins/canvas/canvas.html +++ b/public/plugins/canvas/canvas.html @@ -3,13 +3,13 @@
- - - - - - - + + + + + + +
- +
- +
1:1
diff --git a/public/plugins/chat/chat.html b/public/plugins/chat/chat.html index 9b96c0c..03c253c 100644 --- a/public/plugins/chat/chat.html +++ b/public/plugins/chat/chat.html @@ -4,8 +4,8 @@
chat - - + +
@@ -29,7 +29,7 @@
- +
diff --git a/public/plugins/email/email.html b/public/plugins/email/email.html index 2db8fd6..5a35409 100644 --- a/public/plugins/email/email.html +++ b/public/plugins/email/email.html @@ -7,10 +7,10 @@

mail

- +
- + @@ -41,11 +41,11 @@
- - - + + +
diff --git a/public/plugins/email/email.js b/public/plugins/email/email.js index 2ff040c..dc21120 100644 --- a/public/plugins/email/email.js +++ b/public/plugins/email/email.js @@ -218,9 +218,9 @@ function renderAccountsList() {
${escapeHtml(a.username || '')} @ ${escapeHtml(a.host)}:${a.port} · ${escapeHtml(a.encryption)}
- - - + + +
`).join(''); @@ -452,13 +452,13 @@ function renderMessageList() { const spamScore = m.spam?.score ?? 0; const spamReasons = (m.spam?.reasons || []).join(' · '); const spamBadge = spamLevel === 'spam' - ? `⚠ spam` + ? `⚠ spam` : spamLevel === 'suspicious' - ? `? suspicious` + ? `? suspicious` : ''; const rowTitle = spamReasons ? `spam score ${spamScore}: ${spamReasons}` : ''; return ` -
+
- +
diff --git a/public/plugins/slideshow/slideshow.js b/public/plugins/slideshow/slideshow.js index cbb065d..4d1f44d 100644 --- a/public/plugins/slideshow/slideshow.js +++ b/public/plugins/slideshow/slideshow.js @@ -187,7 +187,7 @@ function renderImageSlide(slide) { const rects = slide.hotspots.map((h, i) => { const style = `left:${h.x}%;top:${h.y}%;width:${h.w}%;height:${h.h}%`; const label = resolveTargetLabel(h.target) || h.target || ''; - return `
`; + return `
`; }).join(''); viewerSlide.innerHTML = `