bucket-bar: hide scrollbar, soft edge fades, wheel-to-scroll

The horizontal scrollbar on the bucket row was ugly and
inconsistent with the rest of the app's scrollbar-less panels. Hide
it, redirect vertical wheel over the bar to horizontal scroll, and
surface overflow with soft bg-coloured gradients on whichever side
has more content off-screen: right only while anchored at the
start, both in the middle, left only at the end.

Fade overlays live on #bucket-row (parent) as pseudo-elements with
pointer-events:none, rather than mask-image on the bar itself — a
mask would also clip super-bucket popovers that are intentionally
rendered inside #bucket-bar for the collab MutationObserver.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-15 21:20:27 +02:00
1 parent 41272bd9ce
commit f2a33a9164
4 files changed
+83 -2

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
<?php
define('APP_NAME', 'hyprcrd');
define('APP_VERSION', '1.9.6');
define('APP_VERSION', '1.9.7');
// Database path — writable directory, outside webroot
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hyprcrd",
"version": "1.9.6",
"version": "1.9.7",
"description": "hyprcrd — self-hosted card manager",
"main": "main.js",
"scripts": {
+44
View File
@@ -780,6 +780,50 @@ body.detail-image-fs-open { overflow: hidden; }
background: color-mix(in srgb, var(--t-accent) 12%, transparent) !important;
}
/* Bucket bar: no visible scrollbar, soft edge fades when the row
overflows so the user knows there's more off-screen. Fade is
implemented via absolutely-positioned overlay pseudo-elements on
#bucket-row (parent) rather than mask-image on the bar itself,
because mask-image would also clip super-bucket popovers rendered
inside the bar. Overlays use pointer-events:none so clicks pass
through to the tabs underneath. */
#bucket-bar {
scrollbar-width: none;
-ms-overflow-style: none;
scroll-behavior: smooth;
}
#bucket-bar::-webkit-scrollbar { display: none; }
#bucket-row::before,
#bucket-row::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
width: 32px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 2;
}
#bucket-row::before {
left: 24px; /* matches .px-6 = 24px on #bucket-row */
background: linear-gradient(to right, var(--t-bg) 0%, transparent 100%);
}
#bucket-row::after {
/* Stop at the right edge of #bucket-bar (before the toolbar
toggles). The toolbar is flex-0, bar is flex-1 — the easiest
way is to inset the overlay by the total width of the toolbar
via a right offset that the JS writes. Defaults to 24px (the
.px-6 pad) and the JS overrides with the real value. */
right: var(--bucket-row-right-inset, 24px);
background: linear-gradient(to left, var(--t-bg) 0%, transparent 100%);
}
#bucket-row.fade-left::before,
#bucket-row.fade-both::before { opacity: 1; }
#bucket-row.fade-right::after,
#bucket-row.fade-both::after { opacity: 1; }
/* Unread dot on the fixed `all` bucket-tab — appears when new cards
arrive while the user is viewing a different bucket. Cleared the
moment the user navigates back to all (renderBoard drains the
+37
View File
@@ -186,6 +186,43 @@ const inpBucket = $('inp-bucket');
const searchFilter = $('search-bucket-filter');
const tasksFilter = $('tasks-bucket-filter');
// ── Overflow handling for the bucket bar ─────────────────────────
// Scrollbar is hidden (CSS). Mouse-wheel over the bar translates to
// horizontal scroll so long bucket rows stay usable without a bar.
// Edge fades kick in dynamically based on scroll position so the
// user sees there's more off-screen — both sides at once in the
// middle, one side at either extreme. Fade overlays live on the
// parent #bucket-row (see CSS) so they don't clip super-bucket
// popovers that render inside #bucket-bar itself.
const barRow = $('bucket-row');
function updateBucketBarFades() {
if (!bar || !barRow) return;
const canLeft = bar.scrollLeft > 2;
const canRight = bar.scrollLeft + bar.clientWidth < bar.scrollWidth - 2;
barRow.classList.toggle('fade-left', canLeft && !canRight);
barRow.classList.toggle('fade-right', !canLeft && canRight);
barRow.classList.toggle('fade-both', canLeft && canRight);
// Right-side fade overlay is anchored to the parent's right edge;
// the actual bar ends before the toolbar toggles. Compute the
// distance so the gradient lines up with the bar's right edge.
const rowRect = barRow.getBoundingClientRect();
const barRect = bar.getBoundingClientRect();
const rightInset = Math.max(0, rowRect.right - barRect.right);
barRow.style.setProperty('--bucket-row-right-inset', rightInset + 'px');
}
if (bar) {
bar.addEventListener('wheel', (e) => {
if (e.deltaY === 0) return;
if (bar.scrollWidth <= bar.clientWidth) return;
e.preventDefault();
bar.scrollLeft += e.deltaY;
}, { passive: false });
bar.addEventListener('scroll', updateBucketBarFades, { passive: true });
window.addEventListener('resize', updateBucketBarFades);
new MutationObserver(updateBucketBarFades).observe(bar, { childList: true, subtree: false });
updateBucketBarFades();
}
/** Switch the active bucket without going through the click path —
* used by the number-key hotkeys (and any other code that wants to
* activate a bucket programmatically without triggering the super-