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 (
-
+ {tabs.map((t) => (
+ {tab === "users" && } {tab === "agents" && } {tab === "settings" && } diff --git a/UI/src/pages/Requests.tsx b/UI/src/pages/Requests.tsx index ebb75ec..18c3f38 100644 --- a/UI/src/pages/Requests.tsx +++ b/UI/src/pages/Requests.tsx @@ -7,6 +7,7 @@ import { Card, EmptyState, PageHeader, Spinner, Button } from "../components/ui" import { StateBadge } from "../components/StateBadge"; import { AgentAvatar } from "../components/AgentAvatar"; import { ChevronRightIcon } from "../components/icons"; +import { ScrollRow } from "../components/ScrollRow"; import { relativeTime } from "../utils"; const STATES: RequestState[] = [ @@ -61,7 +62,7 @@ export function RequestsPage() { const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const chip = (active: boolean) => - `tap-sm min-h-9 shrink-0 snap-start rounded-full border px-3.5 text-sm font-medium transition ${ + `tap-sm min-h-9 shrink-0 rounded-full border px-3.5 text-sm font-medium transition ${ active ? "border-primary/40 bg-primary/15 text-primary" : "border-border-strong bg-surface text-muted hover:text-text" @@ -71,10 +72,11 @@ export function RequestsPage() {
-
- {/* 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. */} + ))} -
+