fixes: scratchpad, zen ESC, chat time, tooltip safety, collab sync race

- Scratchpad: card-scoped private notes in editor (toggle vs preview),
  smaller font, cleared via button, stored in cards.scratchpad
- Zen mode: ESC exits zen before closing detail, Z toggles zen
- Chat: always show absolute time (hh:mm, yesterday, or date+time)
- Backspace/Delete also deletes focused card (not just selected)
- Selected + focused cards use brand accent (not accent2)
- Tooltip: guard against non-element targets
- Collab sync: skip poll while editor is open, 2s push debounce
- Rrule test: only check geburtstag tag, not title format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-14 00:45:31 +02:00
1 parent 2169c1de1b
commit 7e2b130434
11 files changed
+92 -30

No files matched your search

+12 -5
View File
@@ -214,7 +214,8 @@ const { board: gBoard } = guards;
// — Escape chain (highest priority first) —
registerShortcut({ key: 'Escape', priority: 100, guard: () => !isDialogOpen() && !document.getElementById('quick-capture').classList.contains('hidden'), section: 'global', label: 'close quick capture', hidden: true, handler: () => { const qc = document.getElementById('quick-capture'); qc.classList.add('hidden'); document.getElementById('quick-capture-input').value = ''; } });
registerShortcut({ key: 'Escape', priority: 90, guard: () => !isDialogOpen() && guards.isOpen('detail'), section: 'global', label: 'close detail', hidden: true, handler: () => { const escAction = localStorage.getItem('hyprcrd-esc-action') || 'close'; if (escAction === 'exitZen' && document.getElementById('detail')?.classList.contains('zen')) { document.getElementById('detail-zen')?.click(); return; } closeDetail(); } });
registerShortcut({ key: 'Escape', priority: 90, guard: () => !isDialogOpen() && guards.isOpen('detail'), section: 'global', label: 'close detail', hidden: true, handler: () => { if (document.getElementById('detail')?.classList.contains('zen')) { document.getElementById('detail-zen')?.click(); return; } closeDetail(); } });
registerShortcut({ key: 'z', guard: () => !isDialogOpen() && guards.isOpen('detail'), section: 'detail', keys: '<kbd>Z</kbd>', label: 'toggle zen mode', handler: () => document.getElementById('detail-zen')?.click() });
registerShortcut({ key: 'Escape', priority: 80, guard: () => !isDialogOpen() && guards.isOpen('modal'), section: 'global', label: 'close editor', hidden: true, handler: () => closeModal() });
registerShortcut({ key: 'Escape', priority: 70, guard: () => !isDialogOpen() && guards.isOpen('search'), section: 'global', label: 'close search', hidden: true, handler: () => closeSearch() });
registerShortcut({ key: 'Escape', priority: 60, guard: () => !isDialogOpen() && guards.isOpen('tasks'), section: 'global', label: 'close tasks', hidden: true, handler: () => closeTasks() });
@@ -316,17 +317,23 @@ registerShortcut({ key: 'Enter', guard: () => gBoard() && !!document.querySelect
const focused = document.querySelector('#board .card-focused');
if (focused) { window._openDetail(focused.dataset.id); clearCardFocus(); }
}});
// Delete selected cards
registerShortcut({ match: (e) => (e.key === 'Delete' || e.key === 'Backspace') && !e.ctrlKey && gBoard() && getSelectedIds().length > 0, section: 'global', keys: '<kbd>Del</kbd>', label: 'delete selected', hidden: true, handler: async () => {
const ids = getSelectedIds();
// Delete selected or focused card
registerShortcut({ match: (e) => (e.key === 'Delete' || e.key === 'Backspace') && !e.ctrlKey && gBoard() && (getSelectedIds().length > 0 || !!document.querySelector('#board .card-focused')), section: 'global', keys: '<kbd>⌫</kbd>', label: 'delete selected / focused', hidden: true, handler: async () => {
let ids = getSelectedIds();
if (!ids.length) {
const focused = document.querySelector('#board .card-focused');
if (focused) ids = [focused.dataset.id];
}
if (!ids.length) return;
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') {
const ok = await showDialog({ title: 'delete cards', message: `Delete ${ids.length} card${ids.length > 1 ? 's' : ''}?`, danger: true, okLabel: 'Delete' });
if (!ok) return;
}
const { animateCardRemoval } = await import('./modules/board.js');
const { animateCardRemoval, clearCardFocus } = await import('./modules/board.js');
await animateCardRemoval(ids);
for (const id of ids) await api(`api/cards/${id}`, { method: 'DELETE' });
clearSelection();
clearCardFocus();
await window._reloadAll();
}});
+36 -14
View File
@@ -189,12 +189,8 @@ export function openModal(card = null) {
selectColor(card ? card.color : '#38bdf8');
setDueDate(card?.due_date || '');
updateDoneToggle();
state.previewVisible = true;
preview.classList.remove('hidden');
editorDivider?.classList.remove('hidden');
$('toggle-preview').classList.add('text-t-accent');
applyEditorSplit();
updatePreview();
if (scratchpadBody) scratchpadBody.value = card?.scratchpad || '';
showSidePanel('preview');
modal.classList.add('open');
if (linesVisible) updateGutter();
currentLine = -1;
@@ -396,7 +392,8 @@ function updatePreview() {
}
async function doSave(close, { animate = true } = {}) {
const data = { title: inpTitle.value, body: body.value, color: inpColor.value, bucket_id: inpBucket.value || null, due_date: inpDueValue?.value || null };
const scratch = scratchpadBody?.value || null;
const data = { title: inpTitle.value, body: body.value, color: inpColor.value, bucket_id: inpBucket.value || null, due_date: inpDueValue?.value || null, scratchpad: scratch };
if (!data.title.trim()) return;
const returnToGallery = state.returnToGalleryAfterEdit;
if (state.editingId) {
@@ -536,18 +533,43 @@ function applyEditorSplit() {
preview.style.flex = `${100 - editorSplit} 1 0`;
}
$('toggle-preview')?.addEventListener('click', () => {
state.previewVisible = !state.previewVisible;
preview.classList.toggle('hidden', !state.previewVisible);
editorDivider?.classList.toggle('hidden', !state.previewVisible);
$('toggle-preview').classList.toggle('text-t-accent', state.previewVisible);
if (state.previewVisible) {
const scratchpadPanel = $('editor-scratchpad');
const scratchpadBody = $('scratchpad-body');
function showSidePanel(which) {
const showPreview = which === 'preview';
const showScratch = which === 'scratchpad';
state.previewVisible = showPreview;
preview.classList.toggle('hidden', !showPreview);
scratchpadPanel?.classList.toggle('hidden', !showScratch);
editorDivider?.classList.toggle('hidden', !showPreview && !showScratch);
$('toggle-preview')?.classList.toggle('text-t-accent', showPreview);
$('toggle-scratchpad')?.classList.toggle('text-t-accent', showScratch);
if (showPreview || showScratch) {
applyEditorSplit();
if (showScratch) scratchpadPanel.style.flex = preview.style.flex || '1 1 0%';
} else {
if (editorPane) editorPane.style.flex = '1 1 0%';
preview.style.flex = '';
if (scratchpadPanel) scratchpadPanel.style.flex = '';
}
updatePreview();
if (showPreview) updatePreview();
}
$('toggle-preview')?.addEventListener('click', () => {
showSidePanel(state.previewVisible ? null : 'preview');
});
$('toggle-scratchpad')?.addEventListener('click', () => {
const isOpen = !scratchpadPanel?.classList.contains('hidden');
showSidePanel(isOpen ? null : 'scratchpad');
if (!isOpen) scratchpadBody?.focus();
});
$('scratchpad-clear')?.addEventListener('click', () => {
if (scratchpadBody) scratchpadBody.value = '';
});
if (editorDivider) {
+4 -2
View File
@@ -71,14 +71,16 @@ function hide() {
}
function onEnter(e) {
const target = e.target.closest('[data-tip],[data-help]');
const el = e.target?.nodeType === 1 ? e.target : e.target?.parentElement;
const target = el?.closest?.('[data-tip],[data-help]');
if (!target) return;
clearTimeout(hideTimer);
showTimer = setTimeout(() => show(target), SHOW_DELAY);
}
function onLeave(e) {
const target = e.target.closest('[data-tip],[data-help]');
const el = e.target?.nodeType === 1 ? e.target : e.target?.parentElement;
const target = el?.closest?.('[data-tip],[data-help]');
if (!target) return;
clearTimeout(showTimer);
hideTimer = setTimeout(hide, HIDE_DELAY);