fix(ui): un-cramp the requests filter row
The state pills and the agent select sat 10px apart, and the pill row was sliced mid-chip at the viewport edge with nothing to say it scrolled. - New `ScrollRow`: fades whichever edge still has content behind it, so a clipped chip reads as "scroll for more" instead of as a broken layout. Reused for the admin and connect-modal tab strips. - Drop scroll-snap from the pill row. It clamped the resting scrollLeft to the container's 12px padding, so the first chip sat flush against the screen edge with no gutter; flick-snapping through short chips felt wrong regardless. - Breathing room: pills to select 10px -> 14px, filters to list 16px -> 20px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
const FADE = 28; // px of taper at each overflowing edge
|
||||
|
||||
/**
|
||||
* Horizontally scrollable row of chips/tabs. Fades whichever edge still has
|
||||
* content behind it, so a clipped item reads as "scroll for more" rather than
|
||||
* as a layout bug — and contains its overscroll so a sideways swipe never
|
||||
* triggers the browser's back gesture.
|
||||
*/
|
||||
export function ScrollRow({
|
||||
children,
|
||||
className = "",
|
||||
wrapperClassName = "",
|
||||
bleed = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
/** Applied to the scrolling element (layout of the items). */
|
||||
className?: string;
|
||||
/** Applied to the positioned wrapper (borders, margins). */
|
||||
wrapperClassName?: string;
|
||||
/** Extend to the screen edges on phones so the row reads as scrollable. */
|
||||
bleed?: boolean;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [edges, setEdges] = useState({ start: false, end: false });
|
||||
|
||||
const update = useCallback(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const max = el.scrollWidth - el.clientWidth;
|
||||
setEdges({ start: el.scrollLeft > 4, end: max > 4 && el.scrollLeft < max - 4 });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
const el = ref.current;
|
||||
if (!el || typeof ResizeObserver === "undefined") return;
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(el);
|
||||
for (const child of Array.from(el.children)) ro.observe(child);
|
||||
return () => ro.disconnect();
|
||||
}, [update]);
|
||||
|
||||
const mask = `linear-gradient(to right, ${
|
||||
edges.start ? `transparent 0, black ${FADE}px` : "black 0"
|
||||
}, ${edges.end ? `black calc(100% - ${FADE}px), transparent 100%` : "black 100%"})`;
|
||||
|
||||
return (
|
||||
<div className={`relative ${bleed ? "-mx-3 sm:mx-0" : ""} ${wrapperClassName}`}>
|
||||
<div
|
||||
ref={ref}
|
||||
onScroll={update}
|
||||
style={{ maskImage: mask, WebkitMaskImage: mask }}
|
||||
className={`no-scrollbar flex overflow-x-auto overscroll-x-contain ${
|
||||
bleed ? "px-3 sm:px-0" : ""
|
||||
} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user