bak
This commit is contained in:
1 parent
03f57cbaee
commit
a9603aa0e8
11 files changed
+1010
-93
No files matched your search
+141
-23
@@ -1,7 +1,7 @@
|
||||
// ── Board rendering & card interactions ──
|
||||
|
||||
import { $, esc, api } from './util.js';
|
||||
import { state, sortedCards, getChildren, getRecent, SORT_MODES, SORT_ICONS, CARD_COLORS } from './state.js';
|
||||
import { state, sortedCards, 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';
|
||||
|
||||
@@ -9,6 +9,58 @@ const board = $('board');
|
||||
let activeDragEl = null;
|
||||
let dragGhost = null;
|
||||
|
||||
// ── Multi-select ──
|
||||
const selectedIds = new Set();
|
||||
const selectBar = $('select-bar');
|
||||
const selectCount = $('select-count');
|
||||
|
||||
function updateSelectBar() {
|
||||
if (!selectBar) return;
|
||||
if (selectedIds.size) {
|
||||
selectBar.classList.remove('hidden');
|
||||
selectCount.textContent = `${selectedIds.size} selected`;
|
||||
} else {
|
||||
selectBar.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelect(id) {
|
||||
const sid = String(id);
|
||||
const el = board.querySelector(`[data-id="${id}"]`);
|
||||
if (selectedIds.has(sid)) {
|
||||
selectedIds.delete(sid);
|
||||
el?.classList.remove('card-selected');
|
||||
el?.querySelector('.card-select-check')?.remove();
|
||||
} else {
|
||||
selectedIds.add(sid);
|
||||
el?.classList.add('card-selected');
|
||||
if (el && !el.querySelector('.card-select-check')) {
|
||||
const check = document.createElement('div');
|
||||
check.className = 'card-select-check';
|
||||
el.appendChild(check);
|
||||
}
|
||||
}
|
||||
updateSelectBar();
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedIds.clear();
|
||||
board.querySelectorAll('.card-selected').forEach(el => {
|
||||
el.classList.remove('card-selected');
|
||||
el.querySelector('.card-select-check')?.remove();
|
||||
});
|
||||
updateSelectBar();
|
||||
}
|
||||
|
||||
export function getSelectedIds() { return [...selectedIds]; }
|
||||
export { clearSelection };
|
||||
|
||||
$('select-export')?.addEventListener('click', () => {
|
||||
if (!selectedIds.size) return;
|
||||
window.location.href = 'api/export?ids=' + [...selectedIds].join(',');
|
||||
});
|
||||
$('select-clear')?.addEventListener('click', clearSelection);
|
||||
|
||||
// Custom drag ghost that fades near header
|
||||
document.addEventListener('dragover', e => {
|
||||
if (!dragGhost) return;
|
||||
@@ -29,8 +81,11 @@ document.addEventListener('dragover', e => {
|
||||
// ── Sort toggle ──
|
||||
|
||||
export function updateSortUI() {
|
||||
const btn = $('sort-toggle');
|
||||
const label = $('sort-label');
|
||||
const icon = document.querySelector('#sort-toggle i');
|
||||
const fixedSort = state.activeBucket === 'current' || state.activeBucket === 'journal';
|
||||
if (btn) btn.style.display = fixedSort ? 'none' : '';
|
||||
if (label) label.textContent = state.sortMode;
|
||||
if (icon) icon.className = `ph ${SORT_ICONS[state.sortMode]} text-xs`;
|
||||
}
|
||||
@@ -50,20 +105,37 @@ export function renderBoard() {
|
||||
const parent = state.cards.find(c => c.id == state.activeGroup);
|
||||
cards = parent ? [parent, ...getChildren(parent.id)] : [];
|
||||
} else {
|
||||
cards = state.cards.filter(c => !c.parent_id);
|
||||
if (state.activeBucket === 'current') {
|
||||
// Current shows ALL recently touched cards, including group children
|
||||
const recent = getRecent();
|
||||
const now = Date.now();
|
||||
const freshMs = 48 * 60 * 60 * 1000; // 48h
|
||||
cards = cards.filter(c => {
|
||||
const freshMs = 48 * 60 * 60 * 1000;
|
||||
cards = state.cards.filter(c => {
|
||||
if (recent.includes(String(c.id))) return true;
|
||||
const updated = new Date(c.updated_at || c.created_at || 0).getTime();
|
||||
return (now - updated) < freshMs;
|
||||
});
|
||||
} else if (state.activeBucket === 'none') cards = cards.filter(c => !c.bucket_id);
|
||||
else if (state.activeBucket !== 'all') cards = cards.filter(c => c.bucket_id == state.activeBucket);
|
||||
cards.sort((a, b) => (b.updated_at || b.created_at || '').localeCompare(a.updated_at || a.created_at || ''));
|
||||
} else {
|
||||
cards = state.cards.filter(c => !c.parent_id);
|
||||
if (state.activeBucket === 'favorites') {
|
||||
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));
|
||||
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));
|
||||
} else if (state.activeBucket !== 'all') {
|
||||
cards = cards.filter(c => c.bucket_id == state.activeBucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
cards = sortedCards(cards);
|
||||
// Current and journal have fixed sort order, skip global sort
|
||||
if (state.activeBucket !== 'current' && state.activeBucket !== 'journal') {
|
||||
cards = sortedCards(cards);
|
||||
}
|
||||
updateSortUI();
|
||||
window._refreshTasks?.();
|
||||
|
||||
hideFan();
|
||||
@@ -97,17 +169,18 @@ export function renderBoard() {
|
||||
<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 ${thumbImg ? 'pr-16' : ''}">
|
||||
<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">${esc(c.title)}</h3>
|
||||
<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-3">${esc(stripMd(c.body))}</p>
|
||||
<div class="mt-2 flex items-center gap-2 text-[10px] ${hasCover ? 'text-t-text' : 'text-t-dim'} font-pixel">
|
||||
<p class="text-xs ${hasCover ? 'text-t-text' : 'text-t-muted'} leading-relaxed line-clamp-5 flex-1">${esc(stripMd(c.body))}</p>
|
||||
<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>
|
||||
@@ -117,6 +190,20 @@ export function renderBoard() {
|
||||
|
||||
bindCardEvents();
|
||||
|
||||
// Restore selection state
|
||||
selectedIds.forEach(id => {
|
||||
const el = board.querySelector(`[data-id="${id}"]`);
|
||||
if (el) {
|
||||
el.classList.add('card-selected');
|
||||
if (!el.querySelector('.card-select-check')) {
|
||||
const check = document.createElement('div');
|
||||
check.className = 'card-select-check';
|
||||
el.appendChild(check);
|
||||
}
|
||||
}
|
||||
});
|
||||
updateSelectBar();
|
||||
|
||||
// Footer count
|
||||
const fc = $('footer-count');
|
||||
if (fc) fc.textContent = `${cards.length} card${cards.length !== 1 ? 's' : ''} · ${state.cards.length} total`;
|
||||
@@ -226,7 +313,8 @@ function bindCardEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
el.addEventListener('click', (e) => {
|
||||
if (e.ctrlKey || e.metaKey) { e.preventDefault(); toggleSelect(id); return; }
|
||||
if (clickTimer) return;
|
||||
clickTimer = setTimeout(() => { clickTimer = null; window._openDetail(id); }, 200);
|
||||
});
|
||||
@@ -246,9 +334,17 @@ function bindCardEvents() {
|
||||
el.addEventListener('dragstart', e => {
|
||||
e.dataTransfer.setData('text/plain', id);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
el.classList.add('card-dragging');
|
||||
activeDragEl = el;
|
||||
|
||||
// Determine all dragged cards (selected or just this one)
|
||||
const isMulti = selectedIds.has(String(id)) && selectedIds.size > 1;
|
||||
const dragIds = isMulti ? [...selectedIds] : [String(id)];
|
||||
|
||||
// Ghost all dragged cards
|
||||
dragIds.forEach(did => {
|
||||
board.querySelector(`[data-id="${did}"]`)?.classList.add('card-dragging');
|
||||
});
|
||||
|
||||
// Hide native drag image
|
||||
const empty = document.createElement('div');
|
||||
empty.style.width = '1px'; empty.style.height = '1px'; empty.style.opacity = '0.01';
|
||||
@@ -256,19 +352,41 @@ function bindCardEvents() {
|
||||
e.dataTransfer.setDragImage(empty, 0, 0);
|
||||
setTimeout(() => empty.remove(), 0);
|
||||
|
||||
// Create custom ghost
|
||||
// Create stacked ghost
|
||||
const rect = el.getBoundingClientRect();
|
||||
const ghost = el.cloneNode(true);
|
||||
ghost.style.cssText = `position:fixed;z-index:9998;width:${rect.width}px;pointer-events:none;opacity:0.5;transition:opacity 0.12s;`;
|
||||
ghost._offsetX = e.clientX - rect.left;
|
||||
ghost._offsetY = e.clientY - rect.top;
|
||||
ghost.style.left = (e.clientX - ghost._offsetX) + 'px';
|
||||
ghost.style.top = (e.clientY - ghost._offsetY) + 'px';
|
||||
document.body.appendChild(ghost);
|
||||
dragGhost = ghost;
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = `position:fixed;z-index:9998;pointer-events:none;transition:opacity 0.12s;opacity:0.5;`;
|
||||
container._offsetX = e.clientX - rect.left;
|
||||
container._offsetY = e.clientY - rect.top;
|
||||
|
||||
if (isMulti) {
|
||||
// Stack effect: offset layers behind
|
||||
const count = Math.min(dragIds.length, 4);
|
||||
for (let i = count - 1; i >= 1; i--) {
|
||||
const layer = document.createElement('div');
|
||||
layer.style.cssText = `position:absolute;top:${i * 4}px;left:${i * 4}px;width:${rect.width}px;height:${rect.height}px;background:var(--t-surface);border:1px solid var(--t-border);border-radius:inherit;opacity:${0.3 + (count - i) * 0.15};`;
|
||||
container.appendChild(layer);
|
||||
}
|
||||
// Badge
|
||||
const badge = document.createElement('div');
|
||||
badge.textContent = dragIds.length;
|
||||
badge.style.cssText = `position:absolute;top:-6px;right:-6px;min-width:20px;height:20px;border-radius:10px;background:var(--t-accent);color:var(--t-bg);font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 6px;z-index:5;font-family:var(--t-font-pixel,'Silkscreen',cursive);`;
|
||||
container.appendChild(badge);
|
||||
}
|
||||
|
||||
const clone = el.cloneNode(true);
|
||||
clone.style.cssText = `position:relative;width:${rect.width}px;`;
|
||||
clone.querySelector('.card-select-check')?.remove();
|
||||
container.appendChild(clone);
|
||||
|
||||
container.style.left = (e.clientX - container._offsetX) + 'px';
|
||||
container.style.top = (e.clientY - container._offsetY) + 'px';
|
||||
document.body.appendChild(container);
|
||||
dragGhost = container;
|
||||
});
|
||||
el.addEventListener('dragend', () => {
|
||||
el.classList.remove('card-dragging');
|
||||
// Remove ghost class from all dragged cards
|
||||
board.querySelectorAll('.card-dragging').forEach(c => c.classList.remove('card-dragging'));
|
||||
activeDragEl = null;
|
||||
if (dragGhost) { dragGhost.remove(); dragGhost = null; }
|
||||
board.querySelectorAll('.card-drop-before,.card-drop-after,.card-drop-group').forEach(x =>
|
||||
|
||||
Reference in new issue
Block a user