bak
This commit is contained in:
1 parent
9c71e65041
commit
69a3a27348
3 files changed
+140
-3
No files matched your search
@@ -132,6 +132,70 @@ body::after {
|
|||||||
.card-drop-before::before { left: -6px; }
|
.card-drop-before::before { left: -6px; }
|
||||||
.card-drop-after::after { right: -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 card animation */
|
||||||
#board > div {
|
#board > div {
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
|
|||||||
+3
-3
@@ -421,6 +421,9 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
<span class="text-[10px] text-t-dim font-pixel ml-1">v0.1</span>
|
<span class="text-[10px] text-t-dim font-pixel ml-1">v0.1</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
<button id="gallery-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
|
||||||
|
<i class="ph ph-image-square text-sm"></i><span class="font-pixel text-[10px]">images</span>
|
||||||
|
</button>
|
||||||
<button id="search-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
|
<button id="search-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
|
||||||
<i class="ph ph-magnifying-glass text-sm"></i><span class="font-pixel text-[10px]">search</span>
|
<i class="ph ph-magnifying-glass text-sm"></i><span class="font-pixel text-[10px]">search</span>
|
||||||
<kbd class="text-[9px] text-t-dim border border-t-border rounded px-1 py-0.5 ml-1 font-pixel">ctrl+k</kbd>
|
<kbd class="text-[9px] text-t-dim border border-t-border rounded px-1 py-0.5 ml-1 font-pixel">ctrl+k</kbd>
|
||||||
@@ -451,9 +454,6 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
<button id="import-btn" class="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">
|
<button id="import-btn" class="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">
|
||||||
<i class="ph ph-upload-simple text-sm"></i><span class="font-pixel text-[10px]">import</span>
|
<i class="ph ph-upload-simple text-sm"></i><span class="font-pixel text-[10px]">import</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="gallery-btn" class="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">
|
|
||||||
<i class="ph ph-images text-sm"></i><span class="font-pixel text-[10px]">images</span>
|
|
||||||
</button>
|
|
||||||
<button id="export-btn" class="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">
|
<button id="export-btn" class="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">
|
||||||
<i class="ph ph-download-simple text-sm"></i><span class="font-pixel text-[10px]">export</span>
|
<i class="ph ph-download-simple text-sm"></i><span class="font-pixel text-[10px]">export</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export function renderBoard() {
|
|||||||
cards = sortedCards(cards);
|
cards = sortedCards(cards);
|
||||||
window._refreshTasks?.();
|
window._refreshTasks?.();
|
||||||
|
|
||||||
|
hideFan();
|
||||||
renderGroupBar(cards);
|
renderGroupBar(cards);
|
||||||
|
|
||||||
if (!cards.length) {
|
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 = `<div class="group-fan-grid ${hasMore ? 'has-more' : ''}" style="--fan-cols:${cols}">${visible.map(c => {
|
||||||
|
const img = firstImage(c.body);
|
||||||
|
return `<div class="group-fan-card" data-fan-id="${c.id}">
|
||||||
|
<div class="group-fan-bar" style="background:${c.color}"></div>
|
||||||
|
${img ? `<div class="group-fan-thumb" style="background-image:url('${img}')"></div>` : ''}
|
||||||
|
<div class="group-fan-title font-pixel">${esc(c.title)}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('')}</div>`;
|
||||||
|
|
||||||
|
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 ──
|
// ── Card event binding ──
|
||||||
|
|
||||||
function bindCardEvents() {
|
function bindCardEvents() {
|
||||||
@@ -136,8 +191,26 @@ function bindCardEvents() {
|
|||||||
|
|
||||||
board.querySelectorAll('[data-id]').forEach(el => {
|
board.querySelectorAll('[data-id]').forEach(el => {
|
||||||
const id = el.dataset.id;
|
const id = el.dataset.id;
|
||||||
|
const children = getChildren(id);
|
||||||
|
const isGroup = children.length > 0 && !state.activeGroup;
|
||||||
let clickTimer = null;
|
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', () => {
|
el.addEventListener('click', () => {
|
||||||
if (clickTimer) return;
|
if (clickTimer) return;
|
||||||
clickTimer = setTimeout(() => { clickTimer = null; window._openDetail(id); }, 200);
|
clickTimer = setTimeout(() => { clickTimer = null; window._openDetail(id); }, 200);
|
||||||
|
|||||||
Reference in new issue
Block a user