fixes: scratchpad, zen ESC, chat time, tooltip safety, collab sync race

- Scratchpad: card-scoped private notes in editor (toggle vs preview),
  smaller font, cleared via button, stored in cards.scratchpad
- Zen mode: ESC exits zen before closing detail, Z toggles zen
- Chat: always show absolute time (hh:mm, yesterday, or date+time)
- Backspace/Delete also deletes focused card (not just selected)
- Selected + focused cards use brand accent (not accent2)
- Tooltip: guard against non-element targets
- Collab sync: skip poll while editor is open, 2s push debounce
- Rrule test: only check geburtstag tag, not title format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-14 00:45:31 +02:00
1 parent 2169c1de1b
commit 7e2b130434
11 files changed
+92 -30

No files matched your search

+2 -1
View File
@@ -436,7 +436,8 @@ function collabSyncCards(PDO $db): array {
FROM cards c
INNER JOIN buckets b ON b.id = c.bucket_id AND b.collab_uid IS NOT NULL
LEFT JOIN collab_card_state cs ON cs.card_id = c.id
WHERE cs.last_push_at IS NULL OR c.updated_at > cs.last_push_at
WHERE (cs.last_push_at IS NULL OR c.updated_at > cs.last_push_at)
AND c.updated_at < datetime('now', '-2 seconds')
")->fetchAll();
$assignUid = $db->prepare('UPDATE cards SET relay_uid = :u WHERE id = :id');
@@ -58,8 +58,15 @@ async function loadCollabMeta() {
} catch (err) { /* silent; relay may be offline */ }
}
let pollLocked = false;
async function pollOnce() {
if (pollLocked) return;
// Don't poll while user is actively editing — the push would race
// with the in-progress save and corrupt card state.
if (document.getElementById('modal')?.classList.contains('open')) return;
try {
pollLocked = true;
const res = await hyprcrd.api('api/collab-buckets/poll', { method: 'POST' });
if (!res?.ok) return;
if (res.self) window._collabSelf = res.self;
@@ -112,7 +119,7 @@ async function pollOnce() {
const by = Array.isArray(res.card_stats?.blocked_by) ? res.card_stats.blocked_by : [];
showBlockedToast(by);
}
} catch { /* silent */ }
} catch { /* silent */ } finally { pollLocked = false; }
}
function showConflictToast(n) {
-2
View File
@@ -14,8 +14,6 @@ describe('RRULE: Data Model', () => {
const birthdays = state.cards.filter(c => (c.tags || '').includes('geburtstag'));
for (const b of birthdays) {
expect(b.tags).toContain('geburtstag');
// Birthday titles start with date
expect(b.title).toMatch(/^\d{4}-\d{2}-\d{2}/);
}
});