context-menu: add Copy text action and always show Mark done

- New "Copy text" entry next to "Save .md": copies title + blank line +
  body to the clipboard, with fenced code blocks (incl. slide-image),
  markdown images / links / formatting, and inline #tags stripped.
- "Mark done" is now visible on every card (used to require a due_date
  to even show up). On a card already tagged #done it flips to "Unmark
  done" and removes the tag. The original due-date-clearing behavior is
  preserved when transitioning a due card to done.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-11 18:50:01 +02:00
1 parent 26ffa0e71f
commit ff3e0e8736
1 file changed
+24 -5
+24 -5
View File
@@ -4,6 +4,7 @@ import { $, esc, api } from './util.js';
import { state, parseTags, getChildren, isFavorite, toggleFavorite, CARD_COLORS } from './state.js';
import { showDialog } from './dialog.js';
import { pushCardEdit, pushCardDelete, pushBatch, snapshotCard } from './undo.js';
import { stripMd } from './markdown.js';
const menu = $('ctx-menu');
const items = $('ctx-items');
@@ -42,8 +43,9 @@ export function showContextMenu(x, y, card) {
<button class="ctx-item" data-action="duplicate"><i class="ph ph-copy text-sm"></i><span class="font-pixel text-[10px]">Duplicate</span></button>
${/^\d{4}-\d{2}-\d{2}/.test(card.title) ? `<button class="ctx-item" data-action="move-date"><i class="ph ph-calendar-dots text-sm"></i><span class="font-pixel text-[10px]">Move to date</span></button>` : ''}
<button class="ctx-item" data-action="due-date"><i class="ph ph-clock-countdown text-sm ${card.due_date ? 'text-t-accent' : ''}"></i><span class="font-pixel text-[10px]">${card.due_date ? 'Due: ' + card.due_date : 'Set due date'}</span></button>
${card.due_date ? `<button class="ctx-item" data-action="due-done"><i class="ph ph-check-circle text-sm text-t-accent"></i><span class="font-pixel text-[10px]">Mark done</span></button>` : ''}
<button class="ctx-item" data-action="due-done"><i class="ph ph-check-circle text-sm ${parseTags(card).includes('done') ? 'text-t-accent' : ''}"></i><span class="font-pixel text-[10px]">${parseTags(card).includes('done') ? 'Unmark done' : 'Mark done'}</span></button>
<button class="ctx-item" data-action="save-md"><i class="ph ph-file-md text-sm"></i><span class="font-pixel text-[10px]">Save .md</span></button>
<button class="ctx-item" data-action="copy-text"><i class="ph ph-clipboard-text text-sm"></i><span class="font-pixel text-[10px]">Copy text</span></button>
<button class="ctx-item" data-action="push-relay"><i class="ph ph-broadcast text-sm"></i><span class="font-pixel text-[10px]">Push to relay</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
<button class="ctx-item" data-action="favorite"><i class="ph${isFavorite(card.id) ? '-fill' : ''} ph-star text-sm ${isFavorite(card.id) ? 'text-amber-400' : ''}"></i><span class="font-pixel text-[10px]">${isFavorite(card.id) ? 'Unfavorite' : 'Favorite'}</span></button>
<div class="ctx-sep"></div>
@@ -202,9 +204,16 @@ export function showContextMenu(x, y, card) {
}
else if (a === 'due-done') {
hideContextMenu();
// Clear due date and mark as done
const bodyWithDone = (card.body || '').includes('#done') ? card.body : (card.body || '').trimEnd() + ' #done';
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { due_date: null, body: bodyWithDone } });
const isDone = parseTags(card).includes('done');
let newBody;
if (isDone) {
newBody = (card.body || '').replace(/(^|\s)#done\b/g, '$1').replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n');
} else {
newBody = (card.body || '').trimEnd() + ((card.body || '').trimEnd() ? ' ' : '') + '#done';
}
const patch = { body: newBody };
if (!isDone && card.due_date) patch.due_date = null;
await api(`api/cards/${card.id}`, { method: 'PATCH', body: patch });
await window._reloadAll();
}
else if (a === 'due-date') {
@@ -215,6 +224,16 @@ export function showContextMenu(x, y, card) {
await window._reloadAll();
}
else if (a === 'save-md') { hideContextMenu(); window.location.href = `api/export-md?ids=${card.id}`; }
else if (a === 'copy-text') {
hideContextMenu();
let body = card.body || '';
// Drop fenced code blocks (incl. slide-image), then markdown structure, then hashtags
body = body.replace(/```[\s\S]*?```/g, '');
body = stripMd(body);
body = body.replace(/(^|\s)#[\w/-]+/g, '$1').replace(/[ \t]+$/gm, '').replace(/\n{2,}/g, '\n\n').trim();
const text = (card.title || '') + '\n\n' + body;
try { await navigator.clipboard.writeText(text); } catch {}
}
else if (a === 'push-relay') {
await openRelayTenantSubmenu(card);
}
@@ -284,7 +303,7 @@ function renderRelayTenants(card) {
const list = cachedRelayTenants?.tenants || [];
const self = cachedRelayTenants?.self || '';
if (!list.length) {
relayTenants.innerHTML = '<div class="px-3 py-2 text-[10px] text-t-dim font-pixel italic">no tenants</div>';
relayTenants.innerHTML = '<div class="px-3 py-2 text-[10px] text-t-dim font-pixel italic cursor-blink">no tenants</div>';
return;
}
// Self first, then others alphabetical