fix reorder in virtual buckets, split view tags, editor/preview cleanup

- Fix card drag reorder in favorites/journal/current (filter logic
  now mirrors renderBoard)
- Split view shows tag-matched cards as own group alongside related
- Remove extract/tree-view buttons from detail preview (edit-only)
- Context menu tag input: Enter assigns tag directly
- Tag autocomplete closes on ESC/click-outside globally
- Close modal cleans up both autocomplete popups

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-07 22:42:08 +02:00
1 parent 6855381d5c
commit 9018f09351
5 files changed
+90 -41

No files matched your search

+47 -28
View File
@@ -613,20 +613,6 @@ document.addEventListener('keydown', e => {
// Zen mode
// Tree view toggle
// Extract selection to new card
$('detail-extract')?.addEventListener('click', async () => {
const sel = window.getSelection();
const text = sel?.toString();
if (!text?.trim()) return;
await window._extractToCard(text);
});
$('detail-tree')?.addEventListener('click', () => {
const body = $('detail-body');
if (!body) return;
body.classList.toggle('tree');
$('detail-tree')?.classList.toggle('text-t-accent', body.classList.contains('tree'));
$('detail-tree')?.classList.toggle('border-t-accent', body.classList.contains('tree'));
});
$('detail-zen')?.addEventListener('click', () => {
detail.classList.toggle('zen');
@@ -644,13 +630,29 @@ const splitSearch = $('split-search');
const splitPicks = $('split-picks');
const splitContent = $('split-content');
function findTaggedCards(card) {
if (!card) return [];
const myTags = parseTags(card);
if (!myTags.length) return [];
return state.cards
.filter(c => c.id != card.id)
.map(c => {
const ct = parseTags(c);
const shared = myTags.filter(t => ct.some(ct2 => ct2 === t || ct2.startsWith(t + '/') || t.startsWith(ct2 + '/')));
return shared.length ? { card: c, score: shared.length } : null;
})
.filter(Boolean)
.sort((a, b) => b.score - a.score)
.slice(0, 12)
.map(s => s.card);
}
function findRelatedCards(card) {
if (!card) return [];
const id = card.id;
const title = (card.title || '').toLowerCase();
const body = (card.body || '').toLowerCase();
// Extract significant words (3+ chars) from title and body
const stopwords = new Set(['the','and','for','that','this','with','from','are','was','were','been','have','has','had','not','but','what','all','can','her','his','how','its','may','new','now','old','see','way','who','did','get','let','say','she','too','use','eine','ein','der','die','das','und','ist','von','mit','den','dem','des','auf','fuer','auch','sich','noch','wie','man','aber','nach','bei','nur','dann','wenn','oder','als','bis','zum','zur','kann','doch','ich','wir','sie','aus','vor']);
const words = `${card.title} ${stripMd(card.body)}`.toLowerCase()
.split(/[^a-zA-Z0-9äöüß]+/)
@@ -664,16 +666,11 @@ function findRelatedCards(card) {
const ct = (c.title || '').toLowerCase();
const cb = stripMd(c.body || '').toLowerCase();
// Backlink: this card references the other
if (body.includes(`[[${c.title.toLowerCase()}]]`)) score += 80;
// Backlink: the other references this card
if ((c.body || '').toLowerCase().includes(`[[${title}]]`)) score += 80;
// Same bucket
if (card.bucket_id && c.bucket_id == card.bucket_id) score += 15;
// Same group
if (card.parent_id && c.parent_id == card.parent_id) score += 20;
// Word overlap
for (const w of wordSet) {
if (ct.includes(w)) score += 6;
if (cb.includes(w)) score += 2;
@@ -687,19 +684,34 @@ function findRelatedCards(card) {
.map(s => s.card);
}
function renderSplitPicks(cards, label) {
if (!cards.length) {
splitPicks.innerHTML = `<div class="px-3 py-4 text-center text-t-dim font-pixel text-[9px]">no matches</div>`;
return;
}
splitPicks.innerHTML =
`<div class="px-3 pt-2 pb-1"><span class="font-pixel text-[9px] text-t-dim uppercase tracking-wider">${label}</span></div>` +
function renderSplitGroup(cards, label) {
if (!cards.length) return '';
return `<div class="px-3 pt-2 pb-1"><span class="font-pixel text-[9px] text-t-dim uppercase tracking-wider">${label}</span></div>` +
cards.map(c => `<div class="split-pick" data-split-card="${c.id}">
<span class="sp-dot" style="background:${c.color}"></span>
<span class="font-pixel text-[10px] truncate flex-1">${esc(c.title)}</span>
${c.bucket_name ? `<span class="text-[8px] text-t-dim font-pixel">${esc(c.bucket_name)}</span>` : ''}
</div>`).join('');
}
function renderSplitPicks(cards, label) {
if (!cards.length) {
splitPicks.innerHTML = `<div class="px-3 py-4 text-center text-t-dim font-pixel text-[9px]">no matches</div>`;
return;
}
splitPicks.innerHTML = renderSplitGroup(cards, label);
splitPicks.querySelectorAll('[data-split-card]').forEach(el => {
el.addEventListener('click', () => loadSplitCard(el.dataset.splitCard));
});
}
function renderSplitGroups(groups) {
const html = groups.filter(g => g.cards.length).map(g => renderSplitGroup(g.cards, g.label)).join('');
if (!html) {
splitPicks.innerHTML = `<div class="px-3 py-4 text-center text-t-dim font-pixel text-[9px]">no matches</div>`;
return;
}
splitPicks.innerHTML = html;
splitPicks.querySelectorAll('[data-split-card]').forEach(el => {
el.addEventListener('click', () => loadSplitCard(el.dataset.splitCard));
});
@@ -768,8 +780,15 @@ function openSplitPane() {
splitPane.classList.remove('hidden');
splitSearch.value = '';
splitContent.innerHTML = '';
const tagged = findTaggedCards(state.viewingCard);
const related = findRelatedCards(state.viewingCard);
renderSplitPicks(related, related.length ? 'related' : 'no related cards');
// Dedupe: remove tagged cards from related
const taggedIds = new Set(tagged.map(c => c.id));
const relatedOnly = related.filter(c => !taggedIds.has(c.id));
renderSplitGroups([
{ label: 'tags', cards: tagged },
{ label: 'related', cards: relatedOnly },
]);
splitSearch.focus();
}