Moving a card to another bucket took a visible round-trip (PATCH +
reloadAll) during which the card sat unchanged in the source bucket,
leaving the user wondering if the drop registered. Now every
bucket-move path marks the affected cards `.card-moving` as soon as
the action fires: 50% opacity, dark scrim and a centred accent-ring
spinner. The next renderBoard() rebuilds the DOM so the class comes
off automatically — no explicit cleanup needed.
Call sites:
- drag-drop onto a bucket tile (single + multi-select)
- drag onto an existing bucket to nest it (auto-super-bucket create)
- context-menu → Bucket → <target>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Context menu tag toggle reads fresh body from state.cards — was
overwriting with stale snapshot, dropping existing tags (e.g. #done)
- Board empty-state centered with flex (vertical + horizontal)
- Plugin settings: batch toggles via Apply button, single reload
instead of one reload per toggle
- New bucket creation activates it directly on the board
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Locally-created cards (editor, quick capture, duplicate) now get a
single subtle pulse instead of the triple-pulse reserved for relay-
synced cards. Local cards also never trigger the unread dot on the
fixed "all" bucket tab — the user just created them, there's nothing
"unseen" about their own work.
state.js: new state.recentlyCreatedIds Set. Populated by editor.js
doSave (POST), app.js quick capture (POST), and context-menu.js
duplicate (both date-rename and copy paths).
board.js renderBoard diff: new card ids are checked against
recentlyCreatedIds at diff time. Local ids go into a renderBoard-
local newLocalIds set (never into state.unseenCardIds). Remote ids
go into unseenCardIds as before.
board.js drain block (end of renderBoard): two-phase drain.
Phase 1: newLocalIds → pulseCards(local, 'card-pulse-local') with
900ms timeout, works in any bucket. Consumed recentlyCreatedIds
entries are deleted.
Phase 2: remaining unseenCardIds (remote only) → pulseCards(remote,
'card-pulse-new') with 2400ms timeout, only on activeBucket=all.
decorateAllUnreadDot() reads only state.unseenCardIds which now
contains exclusively remote ids → no dot from local card creation.
style.css: new .card-pulse-local class — same @keyframes but
animation-iteration-count:1. Anim-fast/slow overrides added.
chore: bump version to 0.9.16
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
context-menu: group delete (public/js/modules/context-menu.js):
* When the delete action targets a card that has children (a group
parent), the confirmation dialog is ALWAYS shown regardless of the
hyprcrd-confirm-delete setting — group deletion is destructive
enough that it should never happen accidentally. The dialog lists
every child card by title and the OK button reads
"Delete all N" (parent + children counted).
* Children are deleted first (avoids orphan flicker), then the parent.
* Undo: a single pushUndo entry recreates the parent via POST first,
then all children with parent_id pointing at the new parent id.
due_date is restored via a follow-up PATCH since POST doesn't
accept it. Ctrl+Z fully restores the group hierarchy.
* Non-group cards keep the existing behavior (simple confirm if the
setting is on, single delete, pushCardDelete for undo).
tests (public/plugins/tests/card-interactions.test.js):
* New describe "Card: Group Delete" with two tests:
- "deleting a group removes parent AND all children": creates a
parent + 2 children, right-clicks the parent, clicks delete,
asserts the dialog text contains both child titles, confirms,
verifies all 3 cards are gone from the API.
- "Ctrl+Z after group delete restores parent + children": creates
parent + 1 child, deletes the group, confirms both gone, fires
Ctrl+Z, finds the restored cards by title, asserts
child.parent_id === restoredParent.id.
Both tests save/restore the hyprcrd-confirm-delete localStorage
value and soft-skip if the parent isn't visible on the board.
runner: retry progress bar (public/plugins/tests/runner.js):
* hideProgress() was called inside runAll() at the end of the first
pass, which removed the #test-progress element before the retry
pass started. The retry ran invisibly — "retrying…" never showed.
Moved hideProgress() out of runAll() into run(), after both the
first pass and the retry pass are done. The bar now stays visible
for the whole sequence: first pass fills to 100%, retry resets the
fill to 0 (via updateProgress(0, total, 'retrying…', false)) and
fills again for just the failed tests.
chore: bump version to 0.9.10
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
board hotkeys (public/js/app.js):
* "c" toggles the calendar from the board (no modifier, body focus,
no full-screen overlay in front).
* "t" opens a tag quick-pick popover that lists every unique tag from
state.cards. Filter input + arrow-key/enter pick + ESC/click-outside
close. Picking sets state.activeTags = [tag] and re-renders the
board + tag bar. Toggle: a second "t" press closes the picker.
* "1"-"9" picks the Nth user bucket in the bar. The fixed filters
(all/current/favorites/journal/none) are skipped. Super-buckets
count as one slot and activate via the new activateBucket() helper
in buckets.js — that bypasses the click handler so the dropdown
popover does NOT open; the currently shown member (parent or
active child) is selected directly.
* ESC chain in the existing handler now also closes #calendar (was
missing — calendar.js only handles ESC for the day-popup, not the
whole overlay).
buckets (public/js/modules/buckets.js):
* New exported activateBucket(idOrSpecial) helper: setActiveBucket
+ clear activeGroup + renderBar + window._renderBoard. Used by
the number-key hotkey to select super-buckets without triggering
their dropdown.
board split panel (public/js/modules/board.js):
* closeBoardPreview() now removes the .active class from the
toolbar's #board-split-toggle. The previous code removed
.text-t-accent which was a no-op since toggleBoardSplit() uses
.active — the X button on the panel closed the split but left
the toolbar showing the toggle as still on.
email plugin (public/plugins/email/email.js):
* ESC closes the overlay reliably: the previous handler was on
#email-overlay, which only fires when focus is *inside* the
overlay (rare after open). Moved to a document-level keydown.
* Bare "m" toggles the mail overlay open/closed. Suppressed when
Ctrl/Meta/Alt is held, focus is in an input/textarea/contenteditable,
or any other full-screen overlay is in front.
collab-buckets (public/plugins/collab-buckets/backend.php):
* When a card change is applied in the pull phase from a foreign
author (insert / update / tombstone), bump
collab_buckets.updated_at to NOW for that bucket. has_unread is
computed as updated_at > last_seen_at and was previously never
triggered for incoming cards — the bucket-bar tab now gets the
unread dot + 3-pulse when the user is looking at a different
bucket. Own pushes are skipped (the relay echoes them back too).
gitea plugin: make ticket from card (public/plugins/gitea/gitea.js):
* New window._giteaMakeIssueFromCard(card) helper. Opens the gitea
overlay, switches to the create-issue form, fetches the card body
if it's lazy/null, and pre-fills #gitea-create-title and
#gitea-create-body. Does NOT submit — the user picks the repo and
reviews before clicking submit. Soft-bails on the welcome view
if no gitea account is configured yet.
context-menu (public/js/modules/context-menu.js):
* New "Make ticket" entry in the card right-click menu, only rendered
when window._giteaMakeIssueFromCard is exposed. Action handler
hides the menu and calls the helper. Sits next to "Copy text".
* joinSelectedCards() now dedupes hashtag mentions in the merged body
(first occurrence wins, case-insensitive) so a tag that appeared on
multiple source cards isn't repeated in the resulting body. Followed
by a tab/space/blank-line cleanup pass.
test runner auto-retry (public/plugins/tests/runner.js):
* runAll() takes an optional Set<string> of `${suite}::${test}` keys
and runs only those when set. Progress bar denominator reflects
the filtered count.
* run() collects every failed test after the first pass, restores the
backup so the retry runs against the same baseline state, then
re-runs only those via runAll(failedKeys). Retry results are merged
into the original results array with `retried: true` and the
original error preserved on `originalError`.
* renderResults distinguishes three states: passed first try (green ✓),
pass-on-retry (amber ⚠ "flaky — passed on retry" with the first-run
error shown), failed twice (red ✗ "failed twice"). Summary line
carries a "N flaky" count and goes amber if only flaky tests
appeared in the run.
tests (public/plugins/tests/context-menu.test.js):
* Two new tests for the make-ticket action:
- action exists in the menu when window._giteaMakeIssueFromCard
is exposed (soft-skip otherwise)
- clicking it opens the gitea overlay, surfaces the create-issue
form, and pre-fills title + body without submitting. Soft-skips
the assertion when no gitea account is configured (welcome view).
chore: bump version to 0.9.7
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Right-click on a card that's part of a ≥2-card selection now offers
"Join N cards" at the top of the context menu, and the same action
sits next to "Group" in the bottom select-bar.
Title strategy:
* sequential ctrl-click selection → first selected wins
* bulk gesture (rubber-band) → longest title wins
The bulk vs sequential distinction is tracked via a new
`state.selectionWasBulk` flag set in board.js when the rubber-band
becomes active and cleared in clearSelection() and after a successful
join. ctrl-clicks alone don't touch it, so bulk + ctrl-click hybrid
selections stay bulk.
Body format: list of every original title as `## heading` lines, then
each card's body separated by `---`. The winner keeps its color, tags,
bucket, due_date, parent_id; only the body is rewritten. Losers are
deleted via DELETE /api/cards/{id}.
Undo: a single pushUndo entry restores the winner's original body and
recreates every loser via POST + due_date PATCH (since POST doesn't
take due_date). Ctrl+Z reverts the whole join.
Implementation:
* state.js: new selectionWasBulk boolean in state.
* board.js: rubber-band onMove sets state.selectionWasBulk = true on
first activation; clearSelection clears it. New select-join click
handler dynamic-imports joinSelectedCards from context-menu.js to
keep the dependency lazy.
* context-menu.js: joinSelectedCards is now exported. Inserts
data-action="join" into the menu when the right-clicked card is in
a ≥2-card selection. Action handler dispatches to joinSelectedCards
before falling through to the regular if-chain so it can early-return.
* index.php: new #select-join button in #select-bar with ph-arrows-merge
icon, sits between Group and Bucket.
Tests (public/plugins/tests/card-join.test.js, registered in tests.js):
* visibility: shows for ≥2 selected with right-clicked card in the
selection; hidden for solo selection; hidden when right-click target
isn't selected.
* sequential winner: first ctrl-clicked card survives, others deleted.
* merged body lists every title at the top and contains every body.
* selection cleared after a successful join.
* bulk winner: longest title survives regardless of insertion order.
* undo: Ctrl+Z restores winner body + recreates losers.
undo.test.js race-fix:
* New waitForStateCard(id, predicate, timeout=2000) helper polls
state.cards every 30ms until the in-memory state reflects the
expected change. Replaces fragile fixed `sleep(300)` waits between
chained user actions where the second action reads from state.cards
before the first action's _reloadAll has landed.
* Multi-undo color test: between the two color changes, wait for
state.cards to actually contain the pink color before doing the
second rightClick. Without this the second action's contextmenu
handler reads the stale card and pushes the wrong undo snapshot,
collapsing both undo entries onto the original color.
* Due_date undo test: after PATCHing due_date directly via API, wait
for state.cards to reflect it before opening the editor — the
editor's snapshot for the undo entry reads from state.cards.find()
and would otherwise capture due_date=null.
chore: bump version to 0.9.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- 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>