From ce6ca73ea017125a1f86ab915d7524d4813975ed Mon Sep 17 00:00:00 2001
From: th
Date: Sun, 12 Apr 2026 11:15:53 +0200
Subject: [PATCH] =?UTF-8?q?pulse:=201=C3=97=20local=20vs=203=C3=97=20remot?=
=?UTF-8?q?e,=20local=20cards=20don't=20trigger=20all-tab=20dot?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
config.php | 2 +-
public/css/style.css | 12 ++++++++++
public/js/app.js | 3 ++-
public/js/modules/board.js | 39 ++++++++++++++++++++-----------
public/js/modules/context-menu.js | 6 +++--
public/js/modules/editor.js | 1 +
public/js/modules/state.js | 5 ++++
7 files changed, 51 insertions(+), 17 deletions(-)
diff --git a/config.php b/config.php
index 5065451..a9650f6 100644
--- a/config.php
+++ b/config.php
@@ -1,7 +1,7 @@
{
for (const id of ids) {
const el = board.querySelector(`[data-id="${id}"]`);
if (!el) continue;
- el.classList.add('card-pulse-new');
- setTimeout(() => el.classList.remove('card-pulse-new'), 2400);
+ el.classList.add(cls);
+ setTimeout(() => el.classList.remove(cls), durationMs);
}
});
}
@@ -487,9 +489,14 @@ export function renderBoard() {
// the first real load looks like "every card just arrived" and the
// entire board pulses on page open.
const currentIds = new Set(state.cards.map(c => String(c.id)));
+ const newLocalIds = new Set();
if (knownCardIds && knownCardIds.size > 0) {
for (const id of currentIds) {
- if (!knownCardIds.has(id)) state.unseenCardIds.add(id);
+ if (knownCardIds.has(id)) continue;
+ // Locally-created cards go into a separate set — they get a
+ // 1× pulse but must NOT trigger the unread dot on the all-tab.
+ if (state.recentlyCreatedIds.has(id)) newLocalIds.add(id);
+ else state.unseenCardIds.add(id);
}
}
knownCardIds = currentIds;
@@ -733,14 +740,20 @@ export function renderBoard() {
// Notification banner
renderNotifs();
- // Unread tracking: if we're on the all view, drain the unseen set
- // by pulsing every matching card on the board (the "look at me"
- // accent flash). If we're on a different view, the unseen ids stay
- // accumulated and the all-tab gets a dot until the user opens it.
+ // Pulse locally-created cards in ANY bucket (1× subtle pulse).
+ // These never enter unseenCardIds so they can't trigger the all-tab
+ // dot — the user just made them, there's nothing "unseen" about it.
+ if (newLocalIds.size) {
+ for (const id of newLocalIds) state.recentlyCreatedIds.delete(id);
+ pulseCards(newLocalIds, 'card-pulse-local');
+ }
+ // Remote (relay-synced) cards: pulse 3× only when the user is on
+ // the all view. On other views, the unseen ids accumulate and
+ // the all-tab gets a dot until the user opens it.
if (state.activeBucket === 'all' && state.unseenCardIds.size) {
- const toPulse = new Set(state.unseenCardIds);
+ const remote = new Set(state.unseenCardIds);
state.unseenCardIds.clear();
- pulseCardsNew(toPulse);
+ pulseCards(remote, 'card-pulse-new');
}
decorateAllUnreadDot();
}
diff --git a/public/js/modules/context-menu.js b/public/js/modules/context-menu.js
index 3b224a5..16c43e0 100644
--- a/public/js/modules/context-menu.js
+++ b/public/js/modules/context-menu.js
@@ -283,9 +283,11 @@ export function showContextMenu(x, y, card) {
const newDate = await showDialog({ title: 'duplicate_to_date', input: 'date', defaultValue: curDate, okLabel: 'Duplicate' });
if (!newDate) return;
const newTitle = card.title.replace(/^\d{4}-\d{2}-\d{2}/, newDate.trim());
- await api('api/cards', { method: 'POST', body: { title: newTitle, body: card.body, color: card.color, bucket_id: card.bucket_id, position: -1 } });
+ const dup1 = await api('api/cards', { method: 'POST', body: { title: newTitle, body: card.body, color: card.color, bucket_id: card.bucket_id, position: -1 } });
+ if (dup1?.id) state.recentlyCreatedIds?.add(String(dup1.id));
} else {
- await api('api/cards', { method: 'POST', body: { title: card.title + ' (copy)', body: card.body, color: card.color, bucket_id: card.bucket_id, position: -1 } });
+ const dup2 = await api('api/cards', { method: 'POST', body: { title: card.title + ' (copy)', body: card.body, color: card.color, bucket_id: card.bucket_id, position: -1 } });
+ if (dup2?.id) state.recentlyCreatedIds?.add(String(dup2.id));
}
await window._reloadAll();
}
diff --git a/public/js/modules/editor.js b/public/js/modules/editor.js
index c687df6..334323e 100644
--- a/public/js/modules/editor.js
+++ b/public/js/modules/editor.js
@@ -415,6 +415,7 @@ async function doSave(close, { animate = true } = {}) {
if (created?.id) {
if (!close) state.editingId = created.id;
state._lastSavedId = created.id;
+ state.recentlyCreatedIds?.add(String(created.id));
}
}
await window._reloadAll();
diff --git a/public/js/modules/state.js b/public/js/modules/state.js
index a8ef035..cb60024 100644
--- a/public/js/modules/state.js
+++ b/public/js/modules/state.js
@@ -37,6 +37,11 @@ export const state = {
// accent pulse on the cards themselves once the user looks at them.
// Updated by board.js renderBoard() — see the diff there.
unseenCardIds: new Set(),
+ // Ids of cards the user just created locally (editor save, quick
+ // capture, duplicate, undo-recreate). These get a single-pulse (1x)
+ // instead of the triple-pulse (3x) that relay-synced cards get.
+ // Consumed by renderBoard's diff — once used, the id is removed.
+ recentlyCreatedIds: new Set(),
// Detail / editor
editingId: null,
|