PDF upload support:
* index.php: application/pdf added to $mimeMap + $extMap in the
/api/upload handler. PDFs are stored in uploads/ like images/audio.
* editor.js uploadFile(): detects PDF via MIME or .pdf extension.
Inserts a markdown link [📄 filename](uploads/...) instead of an
image embed. Paste + drop handlers updated to accept PDFs.
* index.php: new toolbar button #md-upload-pdf with ph-file-pdf icon
next to the extract button. Triggers a hidden file input
(accept=".pdf"). Wired in editor.js.
Gallery → Assets rename:
* index.php footer button: "Images" → "Assets", icon ph-folder-open.
* index.php gallery overlay header: "images" → "assets", icon
ph-folder-open, upload input accept extended to include .pdf.
* Empty state text: "no images" → "no assets".
File-type tabs in the assets overlay:
* index.php: new #gallery-tabs div with 4 buttons (All / Images /
Audio / PDF), each with data-filter attribute.
* gallery.js: fileType(url) helper returns 'image'|'audio'|'pdf'|
'other'. Tab clicks toggle .active class and filter gallery-items
by data-url + fileType. activeFilter persists across re-opens.
* CSS: .gallery-tab styling with hover + .active accent fill.
PDF rendering in the assets grid:
* gallery.js mediaTile(): PDFs render as ph-file-pdf icon + filename
instead of a thumbnail. Orphaned PDFs show the same icon tile.
* Click on an orphaned PDF opens it in a new tab (window.open).
* "New card" from an orphaned PDF inserts a markdown link (not
image embed).
Orphan detection fix (gallery.js):
* Root cause: loadCards() in app.js uses two-phase lazy loading —
first /api/cards/meta (body=null on all cards), then batches of 25
with full bodies. Opening the gallery before the last batch
completes meant most cards had body=null, getReferencedUrls()
found no URLs, and every file appeared orphaned.
* Fix: openGallery() now fetches its own card list with full bodies
via api('api/cards') in parallel with api('api/uploads'), stored in
a module-local galleryCards array. Both getReferencedUrls() and
findCardForUrl() read from galleryCards instead of state.cards.
* Bonus: added a fallback regex uploads/[hex]{32}.[ext] that catches
references in any format the specific patterns might miss (HTML
attributes, custom blocks, inline text).
chore: bump version to 0.9.18
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>
Focus restoration:
* editor.js closeModal(): blur activeElement after the close so board
shortcuts (arrows, bare keys) work immediately without clicking.
* detail.js closeDetail(): same blur, skipped when openedFromSearch
(search input should keep focus in that case).
* search.js closeSearch(): blur after removing .open.
* calendar.js closeCalendar(): blur after closeDayPopup.
All four check activeElement !== body before blurring to avoid no-ops.
Test shield (loader.js):
* ensureProgressBar() now also creates #test-shield — a transparent
fixed overlay (z-index one below the progress bar) covering the
viewport below the bar. Blocks all real mouse interaction while
tests run; synthetic events from the test suite still work because
they bypass pointer-events. cursor:not-allowed as visual feedback.
Removed by hideProgress() alongside the progress bar.
ESC closes test results (runner.js):
* The existing keydown listener now also handles Escape when the
test-panel is visible. stopPropagation prevents the app's Escape
chain from firing a second close underneath.
Tests (board.test.js):
* New describe "Board: Focus restoration after overlay close":
- closing the editor returns focus to body
- closing the detail view returns focus to body
- closing search returns focus to body
- arrow keys work after editor round-trip (enter → enter → escape)
— the exact flow the user reported as broken. Dispatches
ArrowDown → Enter (detail) → Enter (editor) → Escape (close) →
asserts body focus → ArrowDown → asserts .card-focused exists.
* Fixed .not.toBe() → (x !== y).toBe(true) since the custom runner
doesn't have a .not chain.
chore: bump version to 0.9.14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
End-to-end shared buckets between relay tenants. Bucket metadata + members
sync through new relay routes (collab-buckets, collab-cards, collab-locks);
card content rides the existing inter-tenant encryption hop. Edit locks,
ownership transfer, LWW conflict stashing, undo for local-bucket destructive
actions, "Make collaborative…" upgrade path, and a resilient outbox for
relay-down create attempts. See plugins/collab-buckets/README.md and
TODO.md for the full picture.
Core hooks added so the plugin can integrate without monkey-patching:
- buckets.js context menu: collab-aware rename/color/delete + Members &
owner / Make collaborative entries
- editor.js dispatches hyprcrd:editor-open / hyprcrd:editor-close for the
per-card edit-lock heartbeat
- app.js exposes window.state for the plugin's MutationObserver
- index.php POST/PATCH/DELETE on /api/buckets refuse to mutate collab
buckets directly (must go through the relay)
- Database.php adds buckets.collab_uid column + unique partial index
- import handler now restores color and collab_uid (was silently dropping
color, which caused the test runner's auto-restore to wipe custom
bucket colors)
- relay bootstrap creates collab_buckets, collab_bucket_members,
collab_cards, collab_card_locks tables and prunes tombstones / expired
locks on every request
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>