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>
165 lines
6.2 KiB
JavaScript
165 lines
6.2 KiB
JavaScript
// ── Notification banner system ──
|
|
// Providers register via addProvider(fn). Each fn(settings) returns an array of items.
|
|
|
|
import { $, esc } from './util.js';
|
|
|
|
const banner = $('upcoming-banner');
|
|
const providers = [];
|
|
const DISMISS_KEY = 'hyprcrd-dismissed-notifs';
|
|
const DISMISS_DATE_KEY = 'hyprcrd-dismissed-date';
|
|
const SETTINGS_KEY = 'hyprcrd-notif-settings';
|
|
|
|
// Clear dismissed list daily
|
|
if (localStorage.getItem(DISMISS_DATE_KEY) !== new Date().toISOString().substring(0, 10)) {
|
|
localStorage.removeItem(DISMISS_KEY);
|
|
localStorage.setItem(DISMISS_DATE_KEY, new Date().toISOString().substring(0, 10));
|
|
}
|
|
|
|
function getDismissed() {
|
|
try { return JSON.parse(localStorage.getItem(DISMISS_KEY) || '[]'); } catch { return []; }
|
|
}
|
|
|
|
function dismiss(id) {
|
|
const d = getDismissed();
|
|
if (!d.includes(id)) d.push(id);
|
|
localStorage.setItem(DISMISS_KEY, JSON.stringify(d));
|
|
render();
|
|
}
|
|
|
|
export function getSettings() {
|
|
const defaults = { lookahead: 3, time: 0, birthdays: true, termine: true, overdue: true };
|
|
try { return { ...defaults, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}') }; } catch { return defaults; }
|
|
}
|
|
|
|
export function saveSettings(s) {
|
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify(s));
|
|
render();
|
|
}
|
|
|
|
/**
|
|
* Register a notification provider.
|
|
* @param {function} fn - Receives settings, returns array of { id, icon, iconColor, label, when, sort, onClick?, dismissable?, type? }
|
|
*/
|
|
export function addProvider(fn) {
|
|
providers.push(fn);
|
|
}
|
|
|
|
function getAllItems() {
|
|
const settings = getSettings();
|
|
const items = [];
|
|
for (const fn of providers) {
|
|
try {
|
|
const result = fn(settings);
|
|
if (Array.isArray(result)) items.push(...result);
|
|
} catch {}
|
|
}
|
|
items.sort((a, b) => (a.sort || '').localeCompare(b.sort || ''));
|
|
return items;
|
|
}
|
|
|
|
export function render() {
|
|
if (!banner) return;
|
|
const dismissed = getDismissed();
|
|
const visible = getAllItems().filter(i => !dismissed.includes(String(i.id)));
|
|
|
|
if (!visible.length) { banner.classList.add('hidden'); banner.innerHTML = ''; return; }
|
|
|
|
banner.classList.remove('hidden');
|
|
banner.innerHTML = `<div class="flex items-center gap-2 px-6 py-2 bg-t-raised border-b border-t-border overflow-x-auto">
|
|
<i class="ph ph-bell-ringing text-t-accent text-sm shrink-0"></i>
|
|
${visible.map(i => `<div class="upcoming-item" data-notif-id="${i.id}" style="color:${i.iconColor || 'var(--t-text)'}">
|
|
<i class="ph ph-${i.icon || 'info'}" style="font-size:11px;"></i>
|
|
${i.when ? `<span class="font-pixel text-[10px]">${esc(i.when)}</span>` : ''}
|
|
<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" data-tip="dismiss all"><i class="ph ph-x-circle text-sm"></i></button>
|
|
</div>`;
|
|
|
|
banner.querySelectorAll('[data-notif-id]').forEach(el => {
|
|
el.addEventListener('click', e => {
|
|
if (e.target.closest('.upcoming-dismiss')) return;
|
|
const item = visible.find(i => String(i.id) === el.dataset.notifId);
|
|
if (item?.onClick) item.onClick();
|
|
});
|
|
});
|
|
banner.querySelectorAll('.upcoming-dismiss').forEach(btn => {
|
|
btn.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
dismiss(btn.dataset.dismiss);
|
|
});
|
|
});
|
|
banner.querySelector('#dismiss-all')?.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
for (const i of visible) dismiss(String(i.id));
|
|
});
|
|
}
|
|
|
|
// ── Settings panel integration ──
|
|
|
|
export function renderSettingsPanel() {
|
|
const s = getSettings();
|
|
|
|
// Populate controls
|
|
const lookahead = $('notif-lookahead');
|
|
if (lookahead) lookahead.value = String(s.lookahead);
|
|
const time = $('notif-time');
|
|
if (time) time.value = String(s.time);
|
|
const cbBday = $('notif-birthdays');
|
|
if (cbBday) cbBday.checked = s.birthdays;
|
|
const cbTermin = $('notif-termine');
|
|
if (cbTermin) cbTermin.checked = s.termine;
|
|
const cbOverdue = $('notif-overdue');
|
|
if (cbOverdue) cbOverdue.checked = s.overdue;
|
|
|
|
// Auto-save on change (use onchange to avoid stacking listeners)
|
|
const save = () => saveSettings({
|
|
lookahead: parseInt(lookahead?.value || '3'),
|
|
time: parseInt(time?.value || '0'),
|
|
birthdays: cbBday?.checked ?? true,
|
|
termine: cbTermin?.checked ?? true,
|
|
overdue: cbOverdue?.checked ?? true,
|
|
});
|
|
if (lookahead) lookahead.onchange = save;
|
|
if (time) time.onchange = save;
|
|
if (cbBday) cbBday.onchange = save;
|
|
if (cbTermin) cbTermin.onchange = save;
|
|
if (cbOverdue) cbOverdue.onchange = save;
|
|
|
|
// Restore dismissed — replace element to avoid duplicate listeners
|
|
const restoreBtn = $('notif-clear-dismissed');
|
|
if (restoreBtn) {
|
|
const fresh = restoreBtn.cloneNode(true);
|
|
restoreBtn.parentNode.replaceChild(fresh, restoreBtn);
|
|
fresh.addEventListener('click', () => {
|
|
localStorage.removeItem(DISMISS_KEY);
|
|
render();
|
|
renderStream();
|
|
});
|
|
}
|
|
|
|
renderStream();
|
|
}
|
|
|
|
function renderStream() {
|
|
const stream = $('notif-stream');
|
|
if (!stream) return;
|
|
const dismissed = getDismissed();
|
|
const all = getAllItems();
|
|
|
|
if (!all.length) {
|
|
stream.innerHTML = '<div class="text-xs text-t-dim font-pixel text-center py-4 cursor-blink">no notifications</div>';
|
|
return;
|
|
}
|
|
|
|
stream.innerHTML = all.map(i => {
|
|
const isDismissed = dismissed.includes(String(i.id));
|
|
return `<div class="flex items-center gap-2 px-2 py-1.5 rounded text-xs ${isDismissed ? 'opacity-40' : ''}" style="color:${i.iconColor || 'var(--t-text)'}">
|
|
<i class="ph ph-${i.icon || 'info'}" style="font-size:11px;flex-shrink:0;"></i>
|
|
${i.when ? `<span class="font-pixel text-[9px] text-t-dim" style="min-width:55px;">${esc(i.when)}</span>` : ''}
|
|
<span class="flex-1 truncate">${esc(i.label)}</span>
|
|
${isDismissed ? '<span class="font-pixel text-[8px] text-t-dim">dismissed</span>' : ''}
|
|
</div>`;
|
|
}).join('');
|
|
}
|