feat(ui): desktop scrolls inside, under a fixed header

On the desktop the window no longer scrolls: the area between header
and footer does, with room for its scrollbar kept on both sides so the
content stays centred and nothing shifts between pages. This replaces
the scrollbar-gutter tricks (header colour in the gutter, Chrome's
always-on scrollbar, which also let the sticky header scroll away in
Chrome). Triage fills the height: card and buttons stay, the queue takes
the rest and scrolls inside. The Kanban height fit and the Quick Dump
button use the scroll area. Phones keep scrolling the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-02 19:15:43 +01:00
1 parent c31eed5a3a
commit c7cff441a2
4 files changed
+48 -80

No files matched your search

+8 -4
View File
@@ -554,11 +554,15 @@ export const TasksView: React.FC<TasksViewProps> = ({
setKanbanColHeight(null);
return;
}
// From the board's top to the window's bottom; then take back what still overflows (footer, paddings)
const top = el.getBoundingClientRect().top + window.scrollY;
setKanbanColHeight(Math.max(320, window.innerHeight - top - 16));
// From the board's top to the bottom of the app's scroll area (the desktop window itself does not
// scroll); then take back what still overflows (paddings)
const area = el.closest<HTMLElement>('[data-app-scroll]');
const areaTop = area ? area.getBoundingClientRect().top - area.scrollTop : -window.scrollY;
const areaHeight = area ? area.clientHeight : window.innerHeight;
const top = el.getBoundingClientRect().top - areaTop;
setKanbanColHeight(Math.max(320, areaHeight - top - 16));
requestAnimationFrame(() => {
const over = document.documentElement.scrollHeight - window.innerHeight;
const over = area ? area.scrollHeight - area.clientHeight : document.documentElement.scrollHeight - window.innerHeight;
if (over > 0) setKanbanColHeight((h) => (h ? Math.max(320, h - over) : h));
});
};
+7 -5
View File
@@ -488,7 +488,9 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
}
return (
<div className="max-w-2xl mx-auto select-none space-y-4">
// Desktop: a column filling the height under the header; card and buttons keep their size, the queue
// takes the rest and scrolls inside (the page only scrolls when the window is too low even for that)
<div className="w-full max-w-2xl mx-auto select-none space-y-4 sm:flex-1 sm:min-h-0 sm:flex sm:flex-col">
{/* Top Deck Status & Undo Bar */}
<div className="flex items-center justify-between px-1">
<div className="flex items-center gap-2">
@@ -931,7 +933,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
{/* Waiting Quick Dump Queue List (Touch-sized & Swipeable Left/Right) */}
{cards.length > 1 && (
<div className="pt-2" ref={queueRef}>
<div className="pt-2 sm:flex-1 sm:min-h-[9rem] sm:flex sm:flex-col" ref={queueRef}>
{/* Phones: the queue stays folded so the card and its buttons fit on the screen */}
<button
type="button"
@@ -955,11 +957,11 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</button>
{/* Folds open/closed on phones; always open from sm upwards */}
<div
className={`grid transition-[grid-template-rows,opacity,visibility] duration-300 ease-out sm:grid-rows-[1fr] sm:opacity-100 sm:visible ${
className={`grid transition-[grid-template-rows,opacity,visibility] duration-300 ease-out sm:flex-1 sm:min-h-0 sm:grid-rows-[1fr] sm:opacity-100 sm:visible ${
showQueueMobile ? 'grid-rows-[1fr] opacity-100 visible' : 'grid-rows-[0fr] opacity-0 invisible'
}`}
>
<div className="overflow-hidden min-h-0">
<div className="overflow-hidden min-h-0 sm:flex sm:flex-col">
<div className="flex items-center justify-between mb-2 px-1 pt-2 sm:pt-0">
<span className="text-xs font-semibold uppercase tracking-wider text-muted flex items-center gap-1.5">
<Layers className="w-3.5 h-3.5 text-accent" />
@@ -970,7 +972,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</span>
</div>
<div className="panel-surface divide-y divide-border max-h-80 overflow-y-auto overflow-x-hidden">
<div className="panel-surface divide-y divide-border max-h-80 sm:max-h-none sm:flex-1 sm:min-h-0 overflow-y-auto overflow-x-hidden">
{cards.slice(1).map((item) => (
<SwipeableQueueItem
key={item.id}