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>
Drops the multi-column compact list in favour of a one-row-per-card
layout. Each row: color dot · optional cal/fav icon · title (flex-grows
+ ellipsis) · up-to-4 tag chips with overflow indicator · bucket name +
folder icon · right-aligned date column.
Date column picks (in order): due_date → leading YYYY-MM-DD in the
title → updated_at/created_at. Termin/geburtstag tags are filtered out
of the chip list since they're already represented by the icon + dot.
Title font goes from 11px → 14px so the list reads more like a real
list than a label cloud. Rows have 8px vertical padding, hover
background, and a thin bottom-border. Whole grid is capped at 1100px
and centered.
chore: bump version to 0.9.4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Super-buckets: a bucket can act as a dropdown of nested children, 1-level
deep, created by dragging one bucket onto the center of another. The
parent shows up as a single tile in the bar with a stack icon and the
active member's name; clicking opens a popover that lists parent + all
children. Selecting an entry sets state.activeBucket. Implementation:
* src/Database.php — ALTER TABLE buckets ADD COLUMN parent_id + index.
* public/index.php — PATCH /api/buckets accepts parent_id with depth /
cycle / collab guards. DELETE on a parent un-nests its children
rather than cascade-deleting. The /api/import handler does a second
pass to remap parent_id through the new bucket id map so backup +
restore round-trips the hierarchy.
* public/js/modules/buckets.js renderBar() — top-level buckets only;
buckets with ≥1 child render as a `.bucket-super` tile with
data-bucket-super=<parent.id> and data-super-shown=<active member>.
Three drop zones per tile: left/right 25% → reorder via insert
cursor, center 50% → nest (PATCH parent_id) with .bucket-nest-target
outline. canNestInto() mirrors the backend rules client-side.
openSuperDropdown() renders a popover under the tile with parent +
children as real .bucket-tab[data-bucket=…] entries (so the collab
plugin's MutationObserver still picks them up); appended inside
#bucket-bar but `position:fixed` so the bar's overflow:auto can't
clip it. showBucketCtx() now scopes move-left/right to siblings
(top-level peers OR same parent_id) so the action works in both
the bar and the dropdown.
Drag-and-drop insertion cursor for cards (matches the bucket fix):
* public/js/modules/board.js bindCardEvents() — single
#card-drop-cursor element inside #board, repositioned by per-card
dragover. Three zones (left/right 25% → cursor in gap, center 50% →
card-drop-group highlight). dragleave does NOT hide the cursor (was
flickering as the mouse moved between adjacent cards); only dragend
clears it. handleCardDrop recomputes the drop zone from e.clientX
since the cursor approach no longer tags the target with
.card-drop-before / .card-drop-after.
* "Vor der ersten Card" works automatically: the first card's left
25% positions the cursor at offsetLeft - 6, which sits inside
#board's px-6 padding and stays visible.
* public/css/style.css — old .card-drop-before::before /
.card-drop-after::after pseudo-elements removed in favour of the
#card-drop-cursor rule.
Drag ghost survives background polls:
* board.js draggingCardIds: Set<string> at module scope. dragstart
fills it; dragend clears. renderBoard() re-applies the
.card-dragging class to every member of the set after every render
so caldav / collab polls calling _reloadAll mid-drag don't
visually pop the source card back to its normal state.
Tests (public/plugins/tests/super-buckets.test.js):
* API parent_id: nest, un-nest, self-parent rejected, level-2
rejected (bucket with own children, target is itself a child),
DELETE un-nests, export → import preserves parent_id.
* Bar rendering: super-tile DOM markers, label = parent name when
no child active, label = active child name when selected, popover
opens with parent + children entries, click child sets activeBucket,
click super-tile twice toggles popover closed, plain bucket stays
plain.
* Drop cursor element exists.
* Both describe blocks reset state.activeBucket = 'all' in afterEach
— the rendering tests set it to a child id and the leak was breaking
downstream Card: Due Date & Done / Card: Grouping tests by filtering
every newly-created card with bucket_id=null off the board.
chore: bump version to 0.9.3
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>