calender stuff

This commit is contained in:
th committed 2026-04-08 14:04:23 +02:00
1 parent 8e78468743
commit e0fca064ae
15 files changed
+1716 -67

No files matched your search

+180 -36
View File
@@ -4,6 +4,7 @@ import { $, esc, api } from './util.js';
import { state, sortedCards, parseTags, tagChipHtml, getChildren, getRecent, getFavorites, isFavorite, SORT_MODES, SORT_ICONS, CARD_COLORS } from './state.js';
import { stripMd, getTodos, firstImage, leadingImage } from './markdown.js';
import { pushReorder, pushCardEdit, pushBatch, snapshotCard } from './undo.js';
import { getCalConfig } from './caldav.js';
const board = $('board');
let activeDragEl = null;
@@ -90,6 +91,39 @@ export function updateSortUI() {
if (icon) icon.className = `ph ${SORT_ICONS[state.sortMode]} text-xs`;
}
$('superview-toggle')?.addEventListener('click', () => {
state.superView = !state.superView;
localStorage.setItem('hyprcrd-superview', state.superView ? '1' : '0');
const btn = $('superview-toggle');
const icon = btn?.querySelector('i');
if (icon) icon.className = `ph ${state.superView ? 'ph-squares-four' : 'ph-list'} text-xs`;
if (btn) btn.classList.toggle('text-t-accent', state.superView);
renderBoard();
});
if (state.superView) {
const btn = $('superview-toggle');
const icon = btn?.querySelector('i');
if (icon) icon.className = 'ph ph-squares-four text-xs';
if (btn) btn.classList.add('text-t-accent');
}
$('hide-termine')?.addEventListener('click', () => {
state.hideTermine = !state.hideTermine;
localStorage.setItem('hyprcrd-hide-termine', state.hideTermine ? '1' : '0');
const btn = $('hide-termine');
const icon = btn?.querySelector('i');
if (icon) icon.className = `ph ${state.hideTermine ? 'ph-calendar-check' : 'ph-calendar-x'} text-xs`;
if (btn) btn.classList.toggle('text-t-accent', state.hideTermine);
renderBoard();
});
// Init toggle state
if (state.hideTermine) {
const btn = $('hide-termine');
const icon = btn?.querySelector('i');
if (icon) icon.className = 'ph ph-calendar-check text-xs';
if (btn) btn.classList.add('text-t-accent');
}
$('sort-toggle')?.addEventListener('click', () => {
state.sortMode = SORT_MODES[(SORT_MODES.indexOf(state.sortMode) + 1) % SORT_MODES.length];
localStorage.setItem('hyprcrd-sort', state.sortMode);
@@ -97,6 +131,49 @@ $('sort-toggle')?.addEventListener('click', () => {
renderBoard();
});
function renderCardHtml(c, inGroup = false, recent = []) {
const coverImg = leadingImage(c.body);
const thumbImg = coverImg ? null : firstImage(c.body);
const children = getChildren(c.id);
const isGroup = !inGroup && children.length > 0;
const todos = getTodos(c.body);
const todoPct = todos ? Math.round(todos.done / todos.total * 100) : 0;
const hasCover = !!coverImg;
const tags = parseTags(c);
const isTermin = tags.includes('termin') || tags.includes('geburtstag');
let calCfg = null;
if (isTermin) { for (const t of tags) { if (t && t !== 'termin' && t !== 'geburtstag') { const cfg = getCalConfig(t); if (cfg) { calCfg = cfg; break; } } } }
const cardColor = calCfg?.color || c.color;
const calIcon = isTermin ? (calCfg?.icon || 'calendar-dots') : null;
return `
<div class="board-card ${isGroup ? 'card-group ' : ''}card-glow ${hasCover ? 'card-has-cover' : 'bg-t-surface'} border border-t-border cursor-pointer hover:border-t-muted transition-colors relative group"
style="--glow-color:${cardColor}22;${hasCover ? `background-image:url('${coverImg}')` : ''};${recent.includes(String(c.id)) ? 'border-bottom:3px solid ' + cardColor : ''}" draggable="true" data-id="${c.id}">
${hasCover ? '<div class="card-cover-overlay"></div>' : ''}
<div class="card-bar w-full card-layer" style="background:${cardColor}"></div>
${isGroup ? `<div class="card-group-badge cursor-pointer hover:scale-110 transition-transform" style="background:${cardColor};color:#000" data-group="${c.id}">${children.length + 1}</div>` : ''}
${thumbImg ? `<div class="card-thumb" style="background-image:url('${thumbImg}')"></div>` : ''}
<div class="p-4 card-layer flex flex-col h-full ${thumbImg ? 'pr-16' : ''}">
<div class="flex items-start gap-2 mb-2">
${calIcon ? `<i class="ph ph-${calIcon} text-xs mt-0.5 shrink-0" style="color:${cardColor}"></i>` : ''}
${isFavorite(c.id) ? `<i class="ph-fill ph-star text-amber-400 text-xs mt-0.5 shrink-0"></i>` : ''}
${todos ? `<i class="ph ${todos.done === todos.total ? 'ph-check-square text-t-accent' : 'ph-squares-four text-t-muted'} text-sm mt-0.5 shrink-0"></i>` : ''}
<h3 class="font-pixel text-xs text-t-bright leading-relaxed line-clamp-2">${esc(c.title)}</h3>
</div>
${todos ? `<div class="mb-2"><div class="flex items-center gap-2 mb-1">
<div class="flex-1 h-1.5 bg-t-border rounded-full overflow-hidden"><div class="h-full rounded-full transition-all" style="width:${todoPct}%;background:${cardColor}"></div></div>
<span class="font-pixel text-[9px] ${todos.done === todos.total ? 'text-t-accent' : 'text-t-dim'}">${todos.done}/${todos.total}</span>
</div></div>` : ''}
<p class="text-xs ${hasCover ? 'text-t-text' : 'text-t-muted'} leading-relaxed line-clamp-5 flex-1">${esc(stripMd(c.body))}</p>
${tags.length ? `<div class="flex flex-wrap gap-1 mt-1.5 card-layer">${tags.slice(0, 5).map(t => tagChipHtml(t)).join('')}${tags.length > 5 ? `<span class="tag-chip tag-chip-more">+${tags.length - 5}</span>` : ''}</div>` : ''}
<div class="mt-auto pt-2 flex items-center gap-2 text-[10px] ${hasCover ? 'text-t-text' : 'text-t-dim'} font-pixel">
<span class="flex items-center gap-1"><i class="ph ph-clock text-xs"></i>${(c.updated_at || c.created_at || '').substring(0, 10)}</span>
${c.bucket_name ? `<span class="flex items-center gap-1"><i class="ph ph-folder text-xs"></i>${esc(c.bucket_name)}</span>` : ''}
</div>
</div>
</div>`;
}
// ── Render ──
export function renderBoard() {
@@ -122,7 +199,8 @@ export function renderBoard() {
const favs = getFavorites();
cards = cards.filter(c => favs.includes(String(c.id)));
} else if (state.activeBucket === 'journal') {
cards = cards.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title));
const today = new Date().toISOString().substring(0, 10);
cards = cards.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title) && c.title.substring(0, 10) <= today);
cards.sort((a, b) => b.title.localeCompare(a.title));
} else if (state.activeBucket === 'none') {
cards = cards.filter(c => !c.bucket_id && !/^\d{4}-\d{2}-\d{2}/.test(c.title));
@@ -131,6 +209,10 @@ export function renderBoard() {
}
}
}
// Hide appointments
if (state.hideTermine) {
cards = cards.filter(c => !parseTags(c).includes('termin'));
}
// Tag filter (intersects with bucket filter) — hierarchical: #nase matches #nase/popel
if (state.activeTags.length) {
cards = cards.filter(c => {
@@ -149,6 +231,7 @@ export function renderBoard() {
renderGroupBar(cards);
if (!cards.length) {
board.style.display = '';
board.innerHTML = `<div class="col-span-full text-center py-20">
<i class="ph ph-cards text-4xl text-t-dim mb-4 block"></i>
<p class="font-pixel text-sm text-t-dim cursor-blink">no cards found</p></div>`;
@@ -160,42 +243,103 @@ export function renderBoard() {
const recent = getRecent();
const inGroup = !!state.activeGroup;
board.innerHTML = cards.map(c => {
const coverImg = leadingImage(c.body);
const thumbImg = coverImg ? null : firstImage(c.body);
const children = getChildren(c.id);
const isGroup = !inGroup && children.length > 0;
const todos = getTodos(c.body);
const todoPct = todos ? Math.round(todos.done / todos.total * 100) : 0;
const hasCover = !!coverImg;
const tags = parseTags(c);
if (state.superView) {
// Compact multi-column list view — override board grid
board.style.display = 'block';
const cols = cards.length <= 20 ? 2 : cards.length <= 50 ? 3 : cards.length <= 100 ? 4 : 5;
board.innerHTML = `<div class="superview-grid" style="column-count:${cols};">` + cards.map(c => {
const tags = parseTags(c);
const isTermin = tags.includes('termin') || tags.includes('geburtstag');
let calCfg = null;
if (isTermin) { for (const t of tags) { if (t !== 'termin' && t !== 'geburtstag') { const cfg = getCalConfig(t); if (cfg) { calCfg = cfg; break; } } } }
const dotColor = calCfg?.color || c.color;
const icon = isTermin ? (calCfg?.icon || 'calendar-dots') : (isFavorite(c.id) ? 'star' : null);
const iconColor = isTermin ? dotColor : (isFavorite(c.id) ? '#fbbf24' : 'var(--t-dim)');
return `<div class="superview-item" data-id="${c.id}">
<span class="superview-dot" style="background:${dotColor}"></span>
${icon ? `<i class="ph ph-${icon} superview-icon" style="color:${iconColor}"></i>` : ''}
<span class="superview-title">${esc(c.title)}</span>
</div>`;
}).join('') + '</div>';
return `
<div class="board-card ${isGroup ? 'card-group ' : ''}card-glow ${hasCover ? 'card-has-cover' : 'bg-t-surface'} border border-t-border cursor-pointer hover:border-t-muted transition-colors relative group"
style="--glow-color:${c.color}22;${hasCover ? `background-image:url('${coverImg}')` : ''};${recent.includes(String(c.id)) ? 'border-bottom:3px solid ' + c.color : ''}" draggable="true" data-id="${c.id}">
${hasCover ? '<div class="card-cover-overlay"></div>' : ''}
<div class="card-bar w-full card-layer" style="background:${c.color}"></div>
${isGroup ? `<div class="card-group-badge cursor-pointer hover:scale-110 transition-transform" style="background:${c.color};color:#000" data-group="${c.id}">${children.length + 1}</div>` : ''}
${thumbImg ? `<div class="card-thumb" style="background-image:url('${thumbImg}')"></div>` : ''}
<div class="p-4 card-layer flex flex-col h-full ${thumbImg ? 'pr-16' : ''}">
<div class="flex items-start gap-2 mb-2">
${isFavorite(c.id) ? `<i class="ph-fill ph-star text-amber-400 text-xs mt-0.5 shrink-0"></i>` : ''}
${todos ? `<i class="ph ${todos.done === todos.total ? 'ph-check-square text-t-accent' : 'ph-squares-four text-t-muted'} text-sm mt-0.5 shrink-0"></i>` : ''}
<h3 class="font-pixel text-xs text-t-bright leading-relaxed line-clamp-2">${esc(c.title)}</h3>
</div>
${todos ? `<div class="mb-2"><div class="flex items-center gap-2 mb-1">
<div class="flex-1 h-1.5 bg-t-border rounded-full overflow-hidden"><div class="h-full rounded-full transition-all" style="width:${todoPct}%;background:${c.color}"></div></div>
<span class="font-pixel text-[9px] ${todos.done === todos.total ? 'text-t-accent' : 'text-t-dim'}">${todos.done}/${todos.total}</span>
</div></div>` : ''}
<p class="text-xs ${hasCover ? 'text-t-text' : 'text-t-muted'} leading-relaxed line-clamp-5 flex-1">${esc(stripMd(c.body))}</p>
${tags.length ? `<div class="flex flex-wrap gap-1 mt-1.5 card-layer">${tags.slice(0, 5).map(t => tagChipHtml(t)).join('')}${tags.length > 5 ? `<span class="tag-chip tag-chip-more">+${tags.length - 5}</span>` : ''}</div>` : ''}
<div class="mt-auto pt-2 flex items-center gap-2 text-[10px] ${hasCover ? 'text-t-text' : 'text-t-dim'} font-pixel">
<span class="flex items-center gap-1"><i class="ph ph-clock text-xs"></i>${(c.updated_at || c.created_at || '').substring(0, 10)}</span>
${c.bucket_name ? `<span class="flex items-center gap-1"><i class="ph ph-folder text-xs"></i>${esc(c.bucket_name)}</span>` : ''}
</div>
</div>
</div>`;
}).join('');
let hoverPopup = null, hoverTimer = null, hoverCloseTimer = null;
function clearHover() {
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; }
if (hoverCloseTimer) { clearTimeout(hoverCloseTimer); hoverCloseTimer = null; }
if (hoverPopup) { hoverPopup.remove(); hoverPopup = null; }
}
function delayedClose() {
hoverCloseTimer = setTimeout(clearHover, 150);
}
function cancelClose() {
if (hoverCloseTimer) { clearTimeout(hoverCloseTimer); hoverCloseTimer = null; }
}
board.querySelectorAll('.superview-item[data-id]').forEach(el => {
el.addEventListener('click', () => { clearHover(); window._openDetail(el.dataset.id); });
el.addEventListener('dblclick', () => { clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._openModal(card); });
el.addEventListener('contextmenu', e => { e.preventDefault(); clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._showContextMenu(e.clientX, e.clientY, card); });
el.addEventListener('mouseenter', () => {
cancelClose();
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; }
hoverTimer = setTimeout(() => {
if (hoverPopup) { hoverPopup.remove(); hoverPopup = null; }
const card = state.cards.find(c => c.id == el.dataset.id);
if (!card) return;
const rect = el.getBoundingClientRect();
hoverPopup = document.createElement('div');
Object.assign(hoverPopup.style, {
position: 'fixed', zIndex: '100',
width: '280px',
filter: 'drop-shadow(0 8px 24px rgba(0,0,0,0.4))',
});
const textEnd = el.querySelector('.superview-title');
const textRect = textEnd ? textEnd.getBoundingClientRect() : rect;
const left = textRect.right + 8;
const useLeft = left + 290 < window.innerWidth;
hoverPopup.style.left = (useLeft ? left : textRect.left - 288) + 'px';
hoverPopup.style.top = Math.min(rect.top - 20, window.innerHeight - 320) + 'px';
hoverPopup.innerHTML = renderCardHtml(card);
hoverPopup.addEventListener('mouseenter', e => { e.stopPropagation(); cancelClose(); });
hoverPopup.addEventListener('mouseleave', delayedClose);
hoverPopup.addEventListener('mouseover', e => e.stopPropagation());
// Tag clicks inside hover card
hoverPopup.querySelectorAll('.tag-seg[data-tag]').forEach(seg => {
seg.addEventListener('click', e => {
e.stopPropagation();
const tag = seg.dataset.tag;
if (e.ctrlKey || e.metaKey) {
const idx = state.activeTags.indexOf(tag);
if (idx >= 0) state.activeTags.splice(idx, 1);
else state.activeTags.push(tag);
} else {
state.activeTags = state.activeTags.length === 1 && state.activeTags[0] === tag ? [] : [tag];
}
clearHover();
window._renderTagBar?.();
renderBoard();
});
});
// Click on hover card opens detail
hoverPopup.querySelector('.board-card')?.addEventListener('click', () => {
clearHover();
window._openDetail(card.id);
});
document.body.appendChild(hoverPopup);
}, 400);
});
el.addEventListener('mouseleave', delayedClose);
});
const fc = $('footer-count');
if (fc) fc.textContent = `${cards.length} cards · ${state.cards.length} total`;
return;
}
board.style.display = '';
board.innerHTML = cards.map(c => renderCardHtml(c, inGroup, recent)).join('');
bindCardEvents();