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:
1 parent
6855381d5c
commit
9018f09351
5 files changed
+90
-41
No files matched your search
@@ -720,12 +720,6 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
<button id="detail-delete" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-danger hover:text-t-danger transition-colors">
|
<button id="detail-delete" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-danger hover:text-t-danger transition-colors">
|
||||||
<i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">del</span>
|
<i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">del</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="detail-extract" 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" title="extract selection to new card">
|
|
||||||
<i class="ph ph-arrow-square-out text-sm"></i>
|
|
||||||
</button>
|
|
||||||
<button id="detail-tree" 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" title="tree view">
|
|
||||||
<i class="ph ph-tree-structure text-sm"></i>
|
|
||||||
</button>
|
|
||||||
<button id="detail-zen" 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" title="zen mode">
|
<button id="detail-zen" 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" title="zen mode">
|
||||||
<i class="ph ph-corners-out text-sm"></i>
|
<i class="ph ph-corners-out text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -448,18 +448,26 @@ async function handleCardDrop(e, el, targetId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute new order
|
// Compute new order — must mirror renderBoard's filtering
|
||||||
let filtered;
|
let filtered;
|
||||||
if (state.activeGroup) {
|
if (state.activeGroup) {
|
||||||
const parent = state.cards.find(c => c.id == state.activeGroup);
|
const parent = state.cards.find(c => c.id == state.activeGroup);
|
||||||
filtered = parent ? sortedCards([parent, ...getChildren(parent.id)]) : [];
|
filtered = parent ? sortedCards([parent, ...getChildren(parent.id)]) : [];
|
||||||
} else {
|
} else {
|
||||||
filtered = sortedCards(state.cards.filter(c => {
|
const root = state.cards.filter(c => !c.parent_id);
|
||||||
if (c.parent_id) return false;
|
if (state.activeBucket === 'favorites') {
|
||||||
if (state.activeBucket === 'none') return !c.bucket_id;
|
const favs = getFavorites();
|
||||||
if (state.activeBucket !== 'all') return c.bucket_id == state.activeBucket;
|
filtered = sortedCards(root.filter(c => favs.includes(String(c.id))));
|
||||||
return true;
|
} else if (state.activeBucket === 'journal') {
|
||||||
}));
|
filtered = root.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title));
|
||||||
|
filtered.sort((a, b) => b.title.localeCompare(a.title));
|
||||||
|
} else if (state.activeBucket === 'none') {
|
||||||
|
filtered = sortedCards(root.filter(c => !c.bucket_id && !/^\d{4}-\d{2}-\d{2}/.test(c.title)));
|
||||||
|
} else if (state.activeBucket !== 'all' && state.activeBucket !== 'current') {
|
||||||
|
filtered = sortedCards(root.filter(c => c.bucket_id == state.activeBucket));
|
||||||
|
} else {
|
||||||
|
filtered = sortedCards(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const ids = filtered.map(c => String(c.id)).filter(i => i !== String(dragId));
|
const ids = filtered.map(c => String(c.id)).filter(i => i !== String(dragId));
|
||||||
ids.splice(wasBefore ? ids.indexOf(String(targetId)) : ids.indexOf(String(targetId)) + 1, 0, String(dragId));
|
ids.splice(wasBefore ? ids.indexOf(String(targetId)) : ids.indexOf(String(targetId)) + 1, 0, String(dragId));
|
||||||
|
|||||||
@@ -122,6 +122,25 @@ export function showContextMenu(x, y, card) {
|
|||||||
renderCtxTags();
|
renderCtxTags();
|
||||||
tagInput.value = '';
|
tagInput.value = '';
|
||||||
tagInput.addEventListener('input', () => renderCtxTags(tagInput.value));
|
tagInput.addEventListener('input', () => renderCtxTags(tagInput.value));
|
||||||
|
tagInput.addEventListener('keydown', async e => {
|
||||||
|
if (e.key !== 'Enter') return;
|
||||||
|
e.preventDefault();
|
||||||
|
const raw = tagInput.value.replace(/^#/, '').trim().toLowerCase();
|
||||||
|
if (!raw) return;
|
||||||
|
const tag = raw;
|
||||||
|
if (!cardTags.includes(tag)) {
|
||||||
|
const newBody = (card.body || '').trimEnd() + ' #' + tag;
|
||||||
|
card.body = newBody;
|
||||||
|
cardTags.push(tag);
|
||||||
|
if (!allTags.includes(tag)) { allTags.push(tag); allTags.sort(); }
|
||||||
|
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { body: newBody } });
|
||||||
|
const c = state.cards.find(x => x.id == card.id);
|
||||||
|
if (c) { c.body = newBody; c.tags = cardTags.join(','); }
|
||||||
|
window._renderBoard();
|
||||||
|
}
|
||||||
|
tagInput.value = '';
|
||||||
|
renderCtxTags('');
|
||||||
|
});
|
||||||
tags.classList.add('hidden');
|
tags.classList.add('hidden');
|
||||||
|
|
||||||
menu.style.left = Math.min(x, innerWidth - 188) + 'px';
|
menu.style.left = Math.min(x, innerWidth - 188) + 'px';
|
||||||
|
|||||||
+47
-28
@@ -613,20 +613,6 @@ document.addEventListener('keydown', e => {
|
|||||||
// Zen mode
|
// Zen mode
|
||||||
// Tree view toggle
|
// Tree view toggle
|
||||||
// Extract selection to new card
|
// 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-zen')?.addEventListener('click', () => {
|
||||||
detail.classList.toggle('zen');
|
detail.classList.toggle('zen');
|
||||||
@@ -644,13 +630,29 @@ const splitSearch = $('split-search');
|
|||||||
const splitPicks = $('split-picks');
|
const splitPicks = $('split-picks');
|
||||||
const splitContent = $('split-content');
|
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) {
|
function findRelatedCards(card) {
|
||||||
if (!card) return [];
|
if (!card) return [];
|
||||||
const id = card.id;
|
const id = card.id;
|
||||||
const title = (card.title || '').toLowerCase();
|
const title = (card.title || '').toLowerCase();
|
||||||
const body = (card.body || '').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 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()
|
const words = `${card.title} ${stripMd(card.body)}`.toLowerCase()
|
||||||
.split(/[^a-zA-Z0-9äöüß]+/)
|
.split(/[^a-zA-Z0-9äöüß]+/)
|
||||||
@@ -664,16 +666,11 @@ function findRelatedCards(card) {
|
|||||||
const ct = (c.title || '').toLowerCase();
|
const ct = (c.title || '').toLowerCase();
|
||||||
const cb = stripMd(c.body || '').toLowerCase();
|
const cb = stripMd(c.body || '').toLowerCase();
|
||||||
|
|
||||||
// Backlink: this card references the other
|
|
||||||
if (body.includes(`[[${c.title.toLowerCase()}]]`)) score += 80;
|
if (body.includes(`[[${c.title.toLowerCase()}]]`)) score += 80;
|
||||||
// Backlink: the other references this card
|
|
||||||
if ((c.body || '').toLowerCase().includes(`[[${title}]]`)) score += 80;
|
if ((c.body || '').toLowerCase().includes(`[[${title}]]`)) score += 80;
|
||||||
// Same bucket
|
|
||||||
if (card.bucket_id && c.bucket_id == card.bucket_id) score += 15;
|
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;
|
if (card.parent_id && c.parent_id == card.parent_id) score += 20;
|
||||||
|
|
||||||
// Word overlap
|
|
||||||
for (const w of wordSet) {
|
for (const w of wordSet) {
|
||||||
if (ct.includes(w)) score += 6;
|
if (ct.includes(w)) score += 6;
|
||||||
if (cb.includes(w)) score += 2;
|
if (cb.includes(w)) score += 2;
|
||||||
@@ -687,19 +684,34 @@ function findRelatedCards(card) {
|
|||||||
.map(s => s.card);
|
.map(s => s.card);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSplitPicks(cards, label) {
|
function renderSplitGroup(cards, label) {
|
||||||
if (!cards.length) {
|
if (!cards.length) return '';
|
||||||
splitPicks.innerHTML = `<div class="px-3 py-4 text-center text-t-dim font-pixel text-[9px]">no matches</div>`;
|
return `<div class="px-3 pt-2 pb-1"><span class="font-pixel text-[9px] text-t-dim uppercase tracking-wider">${label}</span></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>` +
|
|
||||||
cards.map(c => `<div class="split-pick" data-split-card="${c.id}">
|
cards.map(c => `<div class="split-pick" data-split-card="${c.id}">
|
||||||
<span class="sp-dot" style="background:${c.color}"></span>
|
<span class="sp-dot" style="background:${c.color}"></span>
|
||||||
<span class="font-pixel text-[10px] truncate flex-1">${esc(c.title)}</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>` : ''}
|
${c.bucket_name ? `<span class="text-[8px] text-t-dim font-pixel">${esc(c.bucket_name)}</span>` : ''}
|
||||||
</div>`).join('');
|
</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 => {
|
splitPicks.querySelectorAll('[data-split-card]').forEach(el => {
|
||||||
el.addEventListener('click', () => loadSplitCard(el.dataset.splitCard));
|
el.addEventListener('click', () => loadSplitCard(el.dataset.splitCard));
|
||||||
});
|
});
|
||||||
@@ -768,8 +780,15 @@ function openSplitPane() {
|
|||||||
splitPane.classList.remove('hidden');
|
splitPane.classList.remove('hidden');
|
||||||
splitSearch.value = '';
|
splitSearch.value = '';
|
||||||
splitContent.innerHTML = '';
|
splitContent.innerHTML = '';
|
||||||
|
const tagged = findTaggedCards(state.viewingCard);
|
||||||
const related = findRelatedCards(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();
|
splitSearch.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ window.addEventListener('resize', () => { if (linesVisible) updateGutter(); });
|
|||||||
export function closeModal({ animate = true } = {}) {
|
export function closeModal({ animate = true } = {}) {
|
||||||
if (animate) startCloseMorph();
|
if (animate) startCloseMorph();
|
||||||
else clearMorphGhost();
|
else clearMorphGhost();
|
||||||
|
hideLinkAc();
|
||||||
|
hideTagAc();
|
||||||
modal.classList.remove('open');
|
modal.classList.remove('open');
|
||||||
state.editingId = null;
|
state.editingId = null;
|
||||||
state.returnToGalleryAfterEdit = false;
|
state.returnToGalleryAfterEdit = false;
|
||||||
@@ -507,6 +509,13 @@ function showTagAc(query) {
|
|||||||
|
|
||||||
function hideTagAc() { tagAc.classList.add('hidden'); tagAcVisible = false; }
|
function hideTagAc() { tagAc.classList.add('hidden'); tagAcVisible = false; }
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if (tagAcVisible && !tagAc.contains(e.target) && e.target !== body) hideTagAc();
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (tagAcVisible && e.key === 'Escape') { hideTagAc(); }
|
||||||
|
});
|
||||||
|
|
||||||
function completeTagAc(idx) {
|
function completeTagAc(idx) {
|
||||||
const tag = tagAcItems[idx];
|
const tag = tagAcItems[idx];
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
|
|||||||
Reference in new issue
Block a user