Self-hosted card manager with markdown, tags, calendar, CalDAV sync, encrypted WebDAV notes, and Obsidian-compatible export.
diff --git a/public/js/app.js b/public/js/app.js
index 8de42ad..db6284d 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -158,6 +158,13 @@ async function checkAuth() {
const res = await fetch('api/auth/status');
const data = await res.json();
if (data.csrf) { setCsrf(data.csrf); window.hyprcrd?.util?.setCsrf?.(data.csrf); }
+ // Refresh version-displaying spans from the live API response so the
+ // GUI doesn't show a stale APP_VERSION when the PHP page is cached.
+ if (data.version) {
+ for (const el of document.querySelectorAll('[data-app-version]')) {
+ el.textContent = 'v' + data.version;
+ }
+ }
if (data.setup) {
let pw = null;
@@ -223,6 +230,7 @@ document.addEventListener('keydown', e => {
return;
}
if (e.key === 's' && (e.ctrlKey || e.metaKey) && modal.classList.contains('open')) { e.preventDefault(); e.shiftKey ? saveCard() : saveOnly(); return; }
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && modal.classList.contains('open')) { e.preventDefault(); saveCard(); return; }
if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !modal.classList.contains('open') && canUndo()) { e.preventDefault(); undo(); return; }
if (e.key === '/' && ![modal, detail, search, tasks].some(el => el?.classList.contains('open')) && document.activeElement === document.body) { e.preventDefault(); openSearch(); }
diff --git a/public/js/modules/undo.js b/public/js/modules/undo.js
index e3641c4..3f0710f 100644
--- a/public/js/modules/undo.js
+++ b/public/js/modules/undo.js
@@ -66,21 +66,29 @@ export function pushBatch(label, patches) {
}
// Toast hint
+//
+// Centering note: tailwind v4 compiles `-translate-x-1/2` to the *new*
+// CSS `translate` property, which then stacks with any inline `transform`
+// we set — the box ended up shifted ~ -100% (utility translate + JS
+// transform translate both applied) and looked left-anchored. Fix: drop
+// the tailwind translate utility and centre purely via the modern
+// `translate` property in inline styles, which is one source of truth.
let hintTimer = null;
function showHint(text) {
let el = document.getElementById('undo-hint');
if (!el) {
el = document.createElement('div');
el.id = 'undo-hint';
- el.className = 'fixed bottom-16 left-1/2 -translate-x-1/2 z-[95] bg-t-surface border border-t-border px-4 py-2 text-xs font-pixel text-t-muted shadow-lg transition-all';
+ el.className = 'fixed bottom-16 left-1/2 z-[95] bg-t-surface border border-t-border px-4 py-2 text-xs font-pixel text-t-muted shadow-lg transition-all';
+ el.style.translate = '-50% 0';
document.body.appendChild(el);
}
el.textContent = text + ' (ctrl+z undo)';
el.style.opacity = '1';
- el.style.transform = 'translateX(-50%) translateY(0)';
+ el.style.translate = '-50% 0';
clearTimeout(hintTimer);
hintTimer = setTimeout(() => {
el.style.opacity = '0';
- el.style.transform = 'translateX(-50%) translateY(8px)';
+ el.style.translate = '-50% 8px';
}, 2500);
}
diff --git a/public/plugins/collab-buckets/collab-buckets.css b/public/plugins/collab-buckets/collab-buckets.css
index a4c8908..4dc2cb9 100644
--- a/public/plugins/collab-buckets/collab-buckets.css
+++ b/public/plugins/collab-buckets/collab-buckets.css
@@ -8,8 +8,6 @@
.bucket-tab.bucket-tab-collab {
position: relative;
- /* Subtle left emphasis so collab tabs read differently at a glance */
- box-shadow: inset 2px 0 0 0 color-mix(in srgb, var(--t-accent2) 70%, transparent);
}
.bucket-tab.bucket-tab-collab .collab-tab-dot {
@@ -35,14 +33,8 @@ body.anim-slow .bucket-tab.bucket-tab-collab.pulsing {
animation-duration: 1.2s !important;
}
@keyframes collab-bucket-pulse {
- 0%, 100% {
- box-shadow: inset 2px 0 0 0 color-mix(in srgb, var(--t-accent2) 70%, transparent),
- 0 0 0 0 color-mix(in srgb, var(--t-danger) 60%, transparent);
- }
- 50% {
- box-shadow: inset 2px 0 0 0 color-mix(in srgb, var(--t-accent2) 70%, transparent),
- 0 0 0 4px color-mix(in srgb, var(--t-danger) 0%, transparent);
- }
+ 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--t-danger) 60%, transparent); }
+ 50% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--t-danger) 0%, transparent); }
}
/* ── Create / edit modal (used by the + Bucket button when the plugin
@@ -136,26 +128,20 @@ body.anim-slow .bucket-tab.bucket-tab-collab.pulsing {
display: flex !important;
align-items: center;
gap: 8px;
- padding: 8px 10px;
- background: var(--t-bg);
- border: 1px solid var(--t-border);
- border-radius: var(--ui-radius-input);
+ padding: 6px 0;
cursor: pointer;
font-size: 12px;
color: var(--t-text);
user-select: none;
}
.collab-toggle-row input { accent-color: var(--t-accent); cursor: pointer; }
-.collab-toggle-row:hover { border-color: var(--t-accent); color: var(--t-bright); }
+.collab-toggle-row:hover { color: var(--t-bright); }
.collab-members-wrap { margin-top: 10px; }
.collab-members {
- border: 1px solid var(--t-border);
- border-radius: var(--ui-radius-input);
- background: var(--t-bg);
max-height: 200px;
overflow-y: auto;
- padding: 4px;
+ padding: 0;
}
.collab-members-empty {
font-size: 11px;
diff --git a/public/plugins/tests/loader.js b/public/plugins/tests/loader.js
index d283f29..94c0977 100644
--- a/public/plugins/tests/loader.js
+++ b/public/plugins/tests/loader.js
@@ -43,6 +43,12 @@ const RUNNER_V = '?v=2';
function ensureProgressBar() {
if (document.getElementById('test-progress')) return;
+ // Push the whole document down by 30px AND shrink the body by the
+ // same amount so the footer doesn't get clipped (body is h-screen +
+ // overflow-hidden, so without the height shrink the bottom 30px get
+ // pushed off-viewport). hideProgress() reverts both.
+ document.body.style.marginTop = '30px';
+ document.body.style.height = 'calc(100vh - 30px)';
const bar = document.createElement('div');
bar.id = 'test-progress';
const s = bar.style;
diff --git a/public/plugins/tests/runner.js b/public/plugins/tests/runner.js
index 2ca7aed..0ae934e 100644
--- a/public/plugins/tests/runner.js
+++ b/public/plugins/tests/runner.js
@@ -234,6 +234,11 @@ function updateProgress(current, total, name, failed) {
}
function hideProgress() {
getProgressBar()?.remove();
+ // ensureProgressBar() in loader.js pushed the document down by 30px and
+ // shrunk it by the same amount so the footer wouldn't get clipped.
+ // Restore both inline overrides here.
+ document.body.style.marginTop = '';
+ document.body.style.height = '';
}
async function runAll() {