Files
patchpass/UI/src/components/ScrollRow.tsx
T
space ce2d9d9f77
Deploy / Build (pull_request) Successful in 1m15s
Deploy / Test & Lint (pull_request) Successful in 53s
Deploy / Build and Push Docker Image (pull_request) Has been skipped
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>
2026-08-01 01:19:30 +02:00

64 lines
1.9 KiB
TypeScript

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>
);
}