diff --git a/public/css/style.css b/public/css/style.css index 17f9d13..4b23c5e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -132,6 +132,70 @@ body::after { .card-drop-before::before { left: -6px; } .card-drop-after::after { right: -6px; } +/* Group fan preview */ +.group-fan { + position: fixed; + z-index: 50; + padding: 8px; + background: var(--t-bg); + border: 1px solid var(--t-border); + border-radius: 6px; + box-shadow: 0 8px 30px -6px rgba(0,0,0,0.4); + pointer-events: auto; + animation: fanIn 0.25s ease; + max-width: min(720px, 95vw); +} +@keyframes fanIn { + from { opacity: 0; transform: translateY(6px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +.group-fan-grid { + display: grid; + grid-template-columns: repeat(var(--fan-cols, 6), 104px); + gap: 5px; + position: relative; + overflow: hidden; +} +.group-fan-grid::after { + content: ''; + position: absolute; + top: 0; bottom: 0; right: 0; + width: 0; + background: linear-gradient(to right, transparent, var(--t-bg)); + pointer-events: none; + transition: width 0.2s; +} +.group-fan-grid.has-more::after { width: 40px; } +.group-fan-card { + width: 104px; + background: var(--t-surface); + border: 1px solid var(--t-border); + border-radius: 4px; + cursor: pointer; + overflow: hidden; + transition: border-color 0.12s, transform 0.12s; +} +.group-fan-card:hover { + border-color: var(--t-muted); + box-shadow: 0 2px 8px rgba(0,0,0,0.25); +} +.group-fan-bar { height: 2px; } +.group-fan-title { + padding: 5px 6px; + font-size: 9px; + line-height: 1.3; + color: var(--t-bright); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.group-fan-thumb { + height: 44px; + background-size: cover; + background-position: center; +} + /* Board card animation */ #board > div { transition: transform 0.2s ease, opacity 0.2s ease; diff --git a/public/index.php b/public/index.php index 6486d5e..7d14145 100644 --- a/public/index.php +++ b/public/index.php @@ -421,6 +421,9 @@ if (str_starts_with($route, '/api/')) { v0.1
+ - diff --git a/public/js/modules/board.js b/public/js/modules/board.js index d3677cd..72d663b 100644 --- a/public/js/modules/board.js +++ b/public/js/modules/board.js @@ -57,6 +57,7 @@ export function renderBoard() { cards = sortedCards(cards); window._refreshTasks?.(); + hideFan(); renderGroupBar(cards); if (!cards.length) { @@ -127,6 +128,60 @@ function renderGroupBar(cards) { } } +// ── Group fan preview ── + +let fanEl = null; +let fanTimer = null; +let fanGroupId = null; + +function showFan(groupEl, parentId) { + hideFan(); + const parent = state.cards.find(c => c.id == parentId); + if (!parent) return; + const children = getChildren(parentId); + const all = children; + const maxShow = 12; + const visible = all.slice(0, maxShow); + const hasMore = all.length > maxShow; + + fanGroupId = parentId; + fanEl = document.createElement('div'); + fanEl.className = 'group-fan'; + const cols = Math.min(6, visible.length); + fanEl.innerHTML = `
${visible.map(c => { + const img = firstImage(c.body); + return `
+
+ ${img ? `
` : ''} +
${esc(c.title)}
+
`; + }).join('')}
`; + + document.body.appendChild(fanEl); + + // Position below the group card + const rect = groupEl.getBoundingClientRect(); + const fanW = fanEl.offsetWidth; + let left = rect.left + rect.width / 2 - fanW / 2; + left = Math.max(8, Math.min(left, innerWidth - fanW - 8)); + let top = rect.bottom + 6; + if (top + fanEl.offsetHeight > innerHeight - 40) top = rect.top - fanEl.offsetHeight - 6; + fanEl.style.left = left + 'px'; + fanEl.style.top = top + 'px'; + + fanEl.querySelectorAll('[data-fan-id]').forEach(card => { + card.addEventListener('click', () => { hideFan(); window._openDetail(card.dataset.fanId); }); + }); + + fanEl.addEventListener('mouseleave', hideFan); +} + +function hideFan() { + if (fanTimer) { clearTimeout(fanTimer); fanTimer = null; } + if (fanEl) { fanEl.remove(); fanEl = null; } + fanGroupId = null; +} + // ── Card event binding ── function bindCardEvents() { @@ -136,8 +191,26 @@ function bindCardEvents() { board.querySelectorAll('[data-id]').forEach(el => { const id = el.dataset.id; + const children = getChildren(id); + const isGroup = children.length > 0 && !state.activeGroup; let clickTimer = null; + // Group fan on long hover + if (isGroup) { + el.addEventListener('mouseenter', () => { + fanTimer = setTimeout(() => showFan(el, id), 800); + }); + el.addEventListener('mouseleave', e => { + // Don't hide if moving to the fan itself + if (fanEl && fanEl.contains(e.relatedTarget)) return; + if (fanTimer) { clearTimeout(fanTimer); fanTimer = null; } + // Delay hiding to allow moving to fan + if (fanEl && fanGroupId == id) { + setTimeout(() => { if (fanGroupId == id && fanEl && !fanEl.matches(':hover')) hideFan(); }, 200); + } + }); + } + el.addEventListener('click', () => { if (clickTimer) return; clickTimer = setTimeout(() => { clickTimer = null; window._openDetail(id); }, 200);