From 195b0e343f243844cde98efbdb434b861fb64471 Mon Sep 17 00:00:00 2001
From: th
Date: Sun, 12 Apr 2026 11:25:57 +0200
Subject: [PATCH] delete animation, no #inbox tag, preset delete lock on
builtins
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Card delete animation (public/css/style.css, public/js/modules/board.js):
* @keyframes card-remove: opacity 1→0 + scale(1)→0.85 + translateY(8px),
250ms ease-in forwards. .card-removing class with pointer-events:none.
* New exported animateCardRemoval(ids) in board.js: adds .card-removing
to each matching board-card element, resolves after 260ms so the
caller can await before the API DELETE + _reloadAll.
* Wired into all three delete paths:
- context-menu.js group delete: animates parent + all children
- context-menu.js single delete: same path (allCards has one entry)
- app.js Del/Backspace shortcut: animates all selected cards
Quick capture no #inbox (public/js/app.js):
* Removed the line that auto-injected #inbox into the body of every
quick-captured card. Cards now land tag-free in the active bucket
(or unsorted).
Settings: preset delete lock (public/js/modules/theme.js):
* refreshPresetSelect() now disables the delete button (disabled attr
+ opacity 0.35 + cursor not-allowed) when a builtin: preset is
selected. Re-enabled when a user: preset is selected. The click
handler already had a startsWith('user:') guard but the button
wasn't visually locked.
chore: bump version to 0.9.17
Co-Authored-By: Claude Opus 4.6 (1M context)
---
config.php | 2 +-
public/css/style.css | 8 ++++++++
public/js/app.js | 6 +++---
public/js/modules/board.js | 12 ++++++++++++
public/js/modules/context-menu.js | 4 +++-
public/js/modules/theme.js | 8 ++++++++
6 files changed, 35 insertions(+), 5 deletions(-)
diff --git a/config.php b/config.php
index a9650f6..4995867 100644
--- a/config.php
+++ b/config.php
@@ -1,7 +1,7 @@
gBoard() && !!document.querySelect
}});
// Delete selected cards
registerShortcut({ match: (e) => (e.key === 'Delete' || e.key === 'Backspace') && !e.ctrlKey && gBoard() && getSelectedIds().length > 0, section: 'global', keys: 'Del', label: 'delete selected', hidden: true, handler: async () => {
- // Delegated to board.js's existing handler via a synthetic event is
- // impractical; re-implement the small logic here to avoid coupling.
const ids = getSelectedIds();
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');
+ await animateCardRemoval(ids);
for (const id of ids) await api(`api/cards/${id}`, { method: 'DELETE' });
clearSelection();
await window._reloadAll();
@@ -394,7 +394,7 @@ document.getElementById('quick-capture-input')?.addEventListener('keydown', asyn
body = lines.slice(1).join('\n').trim();
}
if (!cardTitle) return;
- if (!body.includes('#inbox')) body = (body ? body + '\n\n' : '') + '#inbox';
+ // No auto-tag — the card goes into the active bucket (or unsorted).
// Place the card in the currently active bucket (if it's a real
// user bucket). Special filters (all/current/favorites/journal/none)
diff --git a/public/js/modules/board.js b/public/js/modules/board.js
index 57864a3..739a8e7 100644
--- a/public/js/modules/board.js
+++ b/public/js/modules/board.js
@@ -38,6 +38,18 @@ const draggingCardIds = new Set();
// the all view) or wait for the next visit to the all tab to pulse.
let knownCardIds = null;
+/** Animate card removal: adds .card-removing to each matching card
+ * element and waits for the CSS animation to finish before resolving.
+ * Call this BEFORE the API delete + _reloadAll so the user sees the
+ * card shrink/fade out instead of just vanishing. */
+export function animateCardRemoval(ids) {
+ if (!ids || !ids.length) return Promise.resolve();
+ const els = ids.map(id => board.querySelector(`[data-id="${id}"]`)).filter(Boolean);
+ if (!els.length) return Promise.resolve();
+ for (const el of els) el.classList.add('card-removing');
+ return new Promise(r => setTimeout(r, 260));
+}
+
/** Add a pulse class to matching board cards and strip it after the
* animation. `cls` controls the variant: 'card-pulse-new' (3×, for
* relay-synced cards) or 'card-pulse-local' (1×, for cards the user
diff --git a/public/js/modules/context-menu.js b/public/js/modules/context-menu.js
index 16c43e0..93f06c7 100644
--- a/public/js/modules/context-menu.js
+++ b/public/js/modules/context-menu.js
@@ -397,7 +397,9 @@ export function showContextMenu(x, y, card) {
}
}
});
- // Delete children first, then parent (avoids orphan flicker)
+ // Animate out, then delete children first, then parent
+ const { animateCardRemoval } = await import('./board.js');
+ await animateCardRemoval(allCards.map(c => c.id));
for (const c of children) await api(`api/cards/${c.id}`, { method: 'DELETE' });
await api(`api/cards/${card.id}`, { method: 'DELETE' });
await window._reloadAll();
diff --git a/public/js/modules/theme.js b/public/js/modules/theme.js
index 78ea6b2..a1cbfef 100644
--- a/public/js/modules/theme.js
+++ b/public/js/modules/theme.js
@@ -258,6 +258,14 @@ function refreshPresetSelect() {
sel.appendChild(userGroup);
}
if (cur && sel.querySelector(`option[value="${CSS.escape(cur)}"]`)) sel.value = cur;
+ // Disable delete button when a built-in preset is selected
+ const delBtn = $('design-preset-delete');
+ if (delBtn) {
+ const isUser = sel.value?.startsWith('user:');
+ delBtn.disabled = !isUser;
+ delBtn.style.opacity = isUser ? '' : '0.35';
+ delBtn.style.cursor = isUser ? '' : 'not-allowed';
+ }
}
function getSelectedPreset() {
|