diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index fbce204352..10880603d1 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { Button, Card, Input, Select, Toggle } from "@/shared/components"; +import { Button, Card, Collapsible, Input, Select, Toggle } from "@/shared/components"; import { useTranslations } from "next-intl"; import FallbackChainsEditor from "./FallbackChainsEditor"; import { @@ -973,7 +973,42 @@ export default function RoutingTab() { -
{t("systemTransformsNoProviders")}
+ )} + +
- {providerId}
-
- {display.name}
- {display.description}
-- {opCount} op{opCount === 1 ? "" : "s"} -
+ defaultOpen={false} + title={ +
+ {providerId}
+
+ {display.name}
{display.description}
- {/* Pipeline op list with per-op editor */} + {/* Pipeline op list โ each op is itself collapsible. */} {opCount > 0 && ({t("systemTransformsNoProviders")}
- )} - -
All transform ops are idempotent on re-run. Changes take effect immediately on the next
request.
diff --git a/src/shared/components/Collapsible.tsx b/src/shared/components/Collapsible.tsx
new file mode 100644
index 0000000000..3a3a63b21d
--- /dev/null
+++ b/src/shared/components/Collapsible.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { useState, type ReactNode } from "react";
+import { cn } from "@/shared/utils/cn";
+
+interface CollapsibleProps {
+ /** Header content (always visible, click-to-toggle). */
+ title: ReactNode;
+ /** Optional secondary line under the title. */
+ subtitle?: ReactNode;
+ /** Material symbol name shown left of the title. */
+ icon?: string;
+ /** Trailing element rendered next to the chevron (badges, op counts, etc). */
+ trailing?: ReactNode;
+ /** Whether the section is open on first render. Defaults to false. */
+ defaultOpen?: boolean;
+ /** Visual variant. `default` for top-level sections; `inline` for nested rows. */
+ variant?: "default" | "inline";
+ /** Custom class for the wrapper. */
+ className?: string;
+ /** Content rendered when expanded. */
+ children: ReactNode;
+}
+
+/**
+ * Minimal click-to-expand section. Stateless from the caller's perspective
+ * (open/closed lives in local state โ does NOT survive page refresh, per the
+ * UX brief). Uses material-symbols-outlined chevrons to match the rest of
+ * the OmniRoute UI.
+ */
+export default function Collapsible({
+ title,
+ subtitle,
+ icon,
+ trailing,
+ defaultOpen = false,
+ variant = "default",
+ className,
+ children,
+}: CollapsibleProps) {
+ const [open, setOpen] = useState(defaultOpen);
+
+ const wrapperClasses = cn(
+ variant === "default"
+ ? "rounded-lg border border-black/5 dark:border-white/5 bg-surface"
+ : "rounded-md border border-black/5 dark:border-white/5 bg-black/[0.02] dark:bg-white/[0.02]",
+ className
+ );
+
+ const headerRowClasses = cn(
+ "flex items-center gap-3",
+ variant === "default" ? "p-4" : "p-3",
+ "hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors",
+ open && "border-b border-black/5 dark:border-white/5"
+ );
+
+ // The chevron + title region is the click target. Trailing interactive
+ // controls (Toggle, Button) live OUTSIDE the toggle button so we never nest
+ //