search: right-click a hit to move it to a bucket

Drag was the only way to move a search hit into a bucket, which meant
opening the main board to find a drop target first. Right-click on a
search-hit now opens a small bucket picker anchored at the cursor:
clicking a bucket PATCHes the card's bucket_id and re-runs the
current search so the row updates in place. Includes an "(unsorted)"
entry that clears the bucket.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-15 22:02:51 +02:00
1 parent d5ebbab6a5
commit 29f92dbf9e
3 files changed
+72 -2

No files matched your search

+70
View File
@@ -226,6 +226,66 @@ function hitHtml(card, idx, words) {
${htags.length ? `<div class="flex flex-wrap gap-1 mt-1">${htags.slice(0, 3).map(t => `<span class="tag-chip" style="font-size:8px;padding:0 4px;">#${esc(t)}</span>`).join('')}</div>` : ''}</div></div>`;
}
function openBucketPicker(cardId, x, y) {
document.querySelector('.search-bucket-picker')?.remove();
const picker = document.createElement('div');
picker.className = 'search-bucket-picker';
// Clamp to viewport so a click near the bottom-right doesn't flow
// off-screen — rendered invisibly first to measure, then placed.
picker.style.cssText = `position:fixed;left:0;top:0;visibility:hidden;z-index:300;background:var(--t-surface);border:1px solid var(--t-border);border-radius:4px;box-shadow:0 4px 20px rgba(0,0,0,0.4);min-width:180px;max-height:280px;display:flex;flex-direction:column;`;
const list = document.createElement('div');
list.style.cssText = 'overflow-y:auto;flex:1;';
const addRow = (label, color, onClick) => {
const item = document.createElement('button');
item.style.cssText = 'display:flex;align-items:center;gap:8px;width:100%;padding:6px 12px;background:none;border:none;color:var(--t-muted);font-size:12px;cursor:pointer;text-align:left;';
item.innerHTML = `<span style="width:8px;height:8px;border-radius:2px;background:${color};flex-shrink:0;"></span><span style="font-family:var(--t-font-pixel);font-size:10px;">${esc(label)}</span>`;
item.addEventListener('mouseenter', () => { item.style.color = 'var(--t-bright)'; });
item.addEventListener('mouseleave', () => { item.style.color = 'var(--t-muted)'; });
item.addEventListener('click', onClick);
list.appendChild(item);
};
addRow('(unsorted)', 'transparent', async () => {
picker.remove();
await api(`api/cards/${cardId}`, { method: 'PATCH', body: { bucket_id: null } });
await window._reloadAll();
search(input.value);
});
state.buckets.forEach(b => {
addRow(b.name, b.color || '#64748b', async () => {
picker.remove();
await api(`api/cards/${cardId}`, { method: 'PATCH', body: { bucket_id: b.id } });
await window._reloadAll();
search(input.value);
});
});
picker.appendChild(list);
document.body.appendChild(picker);
// Place, then flip if we'd overflow the viewport.
const r = picker.getBoundingClientRect();
let left = x, top = y;
if (left + r.width > window.innerWidth - 8) left = Math.max(8, window.innerWidth - r.width - 8);
if (top + r.height > window.innerHeight - 8) top = Math.max(8, window.innerHeight - r.height - 8);
picker.style.left = left + 'px';
picker.style.top = top + 'px';
picker.style.visibility = '';
const onDown = (e) => { if (!picker.contains(e.target)) { picker.remove(); cleanup(); } };
const onEsc = (e) => { if (e.key === 'Escape') { e.stopPropagation(); picker.remove(); cleanup(); } };
const cleanup = () => {
document.removeEventListener('mousedown', onDown, true);
document.removeEventListener('keydown', onEsc, true);
};
setTimeout(() => {
document.addEventListener('mousedown', onDown, true);
document.addEventListener('keydown', onEsc, true);
}, 0);
}
function bindHits() {
results.querySelectorAll('.search-hit').forEach(el => {
el.addEventListener('click', () => {
@@ -234,6 +294,16 @@ function bindHits() {
else { active = i; updateActive(); }
});
el.addEventListener('dblclick', () => { const i = parseInt(el.dataset.idx); if (hits[i]) window._openDetail(hits[i].id); });
el.addEventListener('contextmenu', (e) => {
e.preventDefault();
const id = el.dataset.cardId;
if (!id) return;
// Highlight the row so the user sees which card the picker
// will move — mirrors the active-on-click behaviour.
const i = parseInt(el.dataset.idx);
if (!isNaN(i)) { active = i; updateActive(); }
openBucketPicker(id, e.clientX, e.clientY);
});
// Drag: only non-group, non-child cards
if (el.draggable) {