From ce2d9d9f77b3b8f1b649eb26fee9bd0c06861c6d Mon Sep 17 00:00:00 2001
From: space
Date: Sat, 1 Aug 2026 01:19:30 +0200
Subject: [PATCH] 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
---
UI/src/components/AgentModals.tsx | 5 ++-
UI/src/components/ScrollRow.tsx | 63 +++++++++++++++++++++++++++++++
UI/src/pages/Admin.tsx | 5 ++-
UI/src/pages/Requests.tsx | 14 ++++---
4 files changed, 77 insertions(+), 10 deletions(-)
create mode 100644 UI/src/components/ScrollRow.tsx
diff --git a/UI/src/components/AgentModals.tsx b/UI/src/components/AgentModals.tsx
index d7acc26..e5632fb 100644
--- a/UI/src/components/AgentModals.tsx
+++ b/UI/src/components/AgentModals.tsx
@@ -5,6 +5,7 @@ import type { Agent } from "../api/types";
import { Modal } from "./Modal";
import { Button, Input, Textarea } from "./ui";
import { CodeBlock } from "./CodeBlock";
+import { ScrollRow } from "./ScrollRow";
// ── Create / edit form ───────────────────────────────────────────────────────────
@@ -229,7 +230,7 @@ curl -X POST ${origin}/v1/change-requests/{request_id}/consume -H "x-api-key: ${
)}
-
+
{tabs.map((t) => (
+
{tab === "openclaw" && (
diff --git a/UI/src/components/ScrollRow.tsx b/UI/src/components/ScrollRow.tsx
new file mode 100644
index 0000000..7bbeb49
--- /dev/null
+++ b/UI/src/components/ScrollRow.tsx
@@ -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(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 (
+
+
+ {children}
+
+
+ );
+}
diff --git a/UI/src/pages/Admin.tsx b/UI/src/pages/Admin.tsx
index 0e66198..ebedd91 100644
--- a/UI/src/pages/Admin.tsx
+++ b/UI/src/pages/Admin.tsx
@@ -5,6 +5,7 @@ import type { AdminUser, AuditLog, Agent, GlobalSettings } from "../api/types";
import { useAuth } from "../context/AuthContext";
import { Button, Card, PageHeader, Spinner, Toggle } from "../components/ui";
import { ConfirmSheet } from "../components/ConfirmSheet";
+import { ScrollRow } from "../components/ScrollRow";
import { relativeTime } from "../utils";
type Tab = "users" | "agents" | "settings" | "audit";
@@ -21,7 +22,7 @@ export function AdminPage() {
return (
- {/* Swipeable filter pills — bleeds to the screen edges on phones so the
- row reads as scrollable. */}
-
+
+ {/* No scroll-snap: it clamps the resting scrollLeft to the container's
+ padding, which eats the left gutter, and flick-snapping through
+ short chips feels wrong anyway. */}
+
))}
-