Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ebbab6a5 | ||
|
|
c9749352ec | ||
|
|
412dc527a3 | ||
|
|
27cc3f98ac |
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('APP_NAME', 'hyprcrd');
|
||||
define('APP_VERSION', '1.9.12');
|
||||
define('APP_VERSION', '1.9.15');
|
||||
|
||||
// Database path — writable directory, outside webroot
|
||||
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hyprcrd",
|
||||
"version": "1.9.12",
|
||||
"version": "1.9.15",
|
||||
"description": "hyprcrd — self-hosted card manager",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -113,7 +113,6 @@ select[class*="border-t-accent"] {
|
||||
#modal > div,
|
||||
#detail > div,
|
||||
#settings-overlay > div,
|
||||
#help > div,
|
||||
#search > div,
|
||||
#tasks > div,
|
||||
#calendar > div,
|
||||
@@ -126,6 +125,14 @@ select[class*="border-t-accent"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* The help dialog scrolls its own body — keep the border-radius and
|
||||
shadow but drop overflow:hidden so the Tailwind overflow-y-auto on
|
||||
the same element actually takes effect. */
|
||||
#help > div {
|
||||
border-radius: var(--ui-radius-modal);
|
||||
box-shadow: var(--ui-shadow-modal);
|
||||
}
|
||||
|
||||
/* Overlay backdrop blur (applies to anything that's already an .overlay) */
|
||||
.overlay { backdrop-filter: blur(var(--ui-blur-overlay)); -webkit-backdrop-filter: blur(var(--ui-blur-overlay)); }
|
||||
|
||||
|
||||
@@ -768,9 +768,24 @@ export function renderBoard() {
|
||||
// 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.
|
||||
// Journal entries are exempt from the pulse entirely — they're daily
|
||||
// log jottings, not "here's a new thing" moments, so the flash
|
||||
// reads as noise. Detection mirrors calendar.js isJournal: title
|
||||
// begins with YYYY-MM-DD, no termin/geburtstag tag, no caldav_uid,
|
||||
// and no " — <summary>" suffix (which denotes an appointment).
|
||||
if (newLocalIds.size) {
|
||||
for (const id of newLocalIds) state.recentlyCreatedIds.delete(id);
|
||||
pulseCards(newLocalIds, 'card-pulse-local');
|
||||
const toPulse = new Set();
|
||||
for (const id of newLocalIds) {
|
||||
const card = state.cards.find(c => String(c.id) === id);
|
||||
if (!card) continue;
|
||||
const isJournal = /^\d{4}-\d{2}-\d{2}$/.test((card.title || '').trim())
|
||||
&& !/^\d{4}-\d{2}-\d{2}\s*[—-]\s*.+/.test(card.title || '')
|
||||
&& !card.caldav_uid
|
||||
&& !(card.tags || '').split(',').some(t => t === 'termin' || t === 'geburtstag');
|
||||
if (!isJournal) toPulse.add(id);
|
||||
}
|
||||
if (toPulse.size) pulseCards(toPulse, '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
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
|
||||
const SHOW_DELAY = 400;
|
||||
const HIDE_DELAY = 100;
|
||||
const MAX_LIFE = 5000; // watchdog: tooltip never outlives this
|
||||
const OFFSET = 8;
|
||||
|
||||
let tipEl = null;
|
||||
let showTimer = null;
|
||||
let hideTimer = null;
|
||||
let lifeTimer = null;
|
||||
let currentTarget = null;
|
||||
|
||||
function create() {
|
||||
@@ -63,10 +65,18 @@ function show(target) {
|
||||
tipEl.style.top = top + 'px';
|
||||
tipEl.style.left = left + 'px';
|
||||
tipEl.style.visibility = '';
|
||||
|
||||
// Watchdog: if pointerleave never fires (fast flick off the
|
||||
// element, target removed from the DOM mid-hover, focus taken by
|
||||
// another window), the tooltip would sit forever. Force-hide
|
||||
// after MAX_LIFE regardless.
|
||||
clearTimeout(lifeTimer);
|
||||
lifeTimer = setTimeout(hide, MAX_LIFE);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
currentTarget = null;
|
||||
clearTimeout(lifeTimer);
|
||||
if (tipEl) tipEl.style.display = 'none';
|
||||
}
|
||||
|
||||
@@ -91,3 +101,20 @@ document.addEventListener('pointerleave', onLeave, true);
|
||||
document.addEventListener('pointerdown', () => { clearTimeout(showTimer); hide(); }, true);
|
||||
document.addEventListener('keydown', () => { clearTimeout(showTimer); hide(); }, true);
|
||||
document.addEventListener('scroll', () => { clearTimeout(showTimer); hide(); }, true);
|
||||
|
||||
// Belt-and-suspenders sanity check on every move: if the pointer is
|
||||
// no longer over the tip-owning target (or the target has been
|
||||
// detached from the DOM since show time), hide. Catches fast flicks
|
||||
// that skip the expected pointerleave and DOM churn that yanks the
|
||||
// element out from under us.
|
||||
document.addEventListener('pointermove', (e) => {
|
||||
if (!currentTarget) return;
|
||||
if (!currentTarget.isConnected) { hide(); return; }
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el || !currentTarget.contains(el)) hide();
|
||||
}, true);
|
||||
|
||||
// Window losing focus / tab hidden — nothing to hover over while
|
||||
// we're not there, kill any lingering tip.
|
||||
window.addEventListener('blur', () => { clearTimeout(showTimer); hide(); });
|
||||
document.addEventListener('visibilitychange', () => { if (document.hidden) { clearTimeout(showTimer); hide(); } });
|
||||
@@ -184,3 +184,50 @@ describe('Board: Focus restoration after overlay close', () => {
|
||||
expect(document.querySelector('#board .card-focused')).toExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Board: new-card pulse', () => {
|
||||
// Journal entries shouldn't get the 1× card-pulse-local flash —
|
||||
// they're daily-log jottings, not "new thing" moments. We use
|
||||
// title "9999-12-31" for the journal probe to ensure we can't
|
||||
// collide with a real user entry; a control card with a
|
||||
// __prefixed__ title verifies the pulse still fires for normal
|
||||
// local creations.
|
||||
|
||||
it('locally-created journal-format cards skip the pulse', async () => {
|
||||
const token = csrf();
|
||||
const hJson = { 'Content-Type': 'application/json', 'X-CSRF-Token': token };
|
||||
const hCsrf = { 'X-CSRF-Token': token };
|
||||
let journal, control;
|
||||
try {
|
||||
journal = await fetch('api/cards', { method: 'POST', headers: hJson, body: JSON.stringify({ title: '9999-12-31', body: '' }) }).then(r => r.json());
|
||||
control = await fetch('api/cards', { method: 'POST', headers: hJson, body: JSON.stringify({ title: '__test_pulse_ctrl__', body: '' }) }).then(r => r.json());
|
||||
expect(journal?.id).toExist();
|
||||
expect(control?.id).toExist();
|
||||
|
||||
// Flag both as "just created locally" so renderBoard routes
|
||||
// them through the local-pulse branch. The journal
|
||||
// exemption then has to filter out the journal one.
|
||||
window.state.recentlyCreatedIds.add(String(journal.id));
|
||||
window.state.recentlyCreatedIds.add(String(control.id));
|
||||
await window._reloadAll();
|
||||
// Wait past the rAF tick so pulse classes have been
|
||||
// applied, but well within the 900ms lifetime so we
|
||||
// can still observe them.
|
||||
await sleep(150);
|
||||
|
||||
const journalEl = document.querySelector(`.board-card[data-id="${journal.id}"]`);
|
||||
const controlEl = document.querySelector(`.board-card[data-id="${control.id}"]`);
|
||||
expect(journalEl).toExist();
|
||||
expect(controlEl).toExist();
|
||||
expect(journalEl.classList.contains('card-pulse-local')).toBe(false);
|
||||
expect(controlEl.classList.contains('card-pulse-local')).toBe(true);
|
||||
} finally {
|
||||
// Clean up even on failure. Backup/restore would catch it
|
||||
// eventually, but between tests we don't want a stray
|
||||
// 9999-12-31 card floating around upsetting other suites.
|
||||
if (journal?.id) await fetch(`api/cards/${journal.id}`, { method: 'DELETE', headers: hCsrf });
|
||||
if (control?.id) await fetch(`api/cards/${control.id}`, { method: 'DELETE', headers: hCsrf });
|
||||
await window._reloadAll?.();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user