diff --git a/js/web/src/components/ui/switch.tsx b/js/web/src/components/ui/switch.tsx index 3177c8b8..a92232e6 100644 --- a/js/web/src/components/ui/switch.tsx +++ b/js/web/src/components/ui/switch.tsx @@ -25,7 +25,7 @@ function Switch({ onClick={() => onCheckedChange(!checked)} className={cn( "relative inline-flex shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors disabled:opacity-50 disabled:cursor-not-allowed", - checked ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]", + checked ? "bg-info" : "bg-danger", isSmall ? "h-4 w-7" : "h-5 w-9", className, )} diff --git a/js/web/src/routes/settings.tsx b/js/web/src/routes/settings.tsx index 7fae508c..ba74aafe 100644 --- a/js/web/src/routes/settings.tsx +++ b/js/web/src/routes/settings.tsx @@ -1,24 +1,58 @@ import { cn } from "@/lib/utils"; -import { createFileRoute, Link, Outlet } from "@tanstack/react-router"; -import { Globe, Info, Lock, Shield, User2, Video } from "lucide-react"; +import { + createFileRoute, + Link, + Outlet, + useMatchRoute, +} from "@tanstack/react-router"; +import { + ChevronDown, + Globe, + Heart, + Info, + Key, + Lock, + Shield, + User2, + Video, + Webhook, +} from "lucide-react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; +interface NavLink { + to: string; + icon: React.ComponentType<{ className?: string }>; + labelKey: string; +} + +interface NavGroup { + icon: React.ComponentType<{ className?: string }>; + labelKey: string; + children: NavLink[]; +} + type NavItem = - | { - role: "link"; - to: string; - icon: React.ComponentType<{ className?: string }>; - labelKey: string; - } - | { role: "divider"; labelKey: string }; - -const NAV_ITEMS = [ + | ({ role: "link" } & NavLink) + | ({ role: "group" } & NavGroup) + | { role: "divider" }; + +const NAV_ITEMS: NavItem[] = [ { role: "link", to: "/settings/account", icon: User2, labelKey: "account" }, { - role: "link", - to: "/settings/streaming", + role: "group", icon: Video, labelKey: "streaming", + children: [ + { to: "/settings/keys", icon: Key, labelKey: "key-management" }, + { + to: "/settings/recommendations", + icon: Heart, + labelKey: "recommendations-to-others", + }, + { to: "/settings/webhooks", icon: Webhook, labelKey: "webhooks" }, + { to: "/settings/multistream", icon: Globe, labelKey: "multistream" }, + ], }, { role: "link", @@ -26,7 +60,7 @@ const NAV_ITEMS = [ icon: Shield, labelKey: "privacy-security", }, - { role: "divider", labelKey: "divider1" }, + { role: "divider" }, { role: "link", to: "/settings/languages", @@ -35,34 +69,91 @@ const NAV_ITEMS = [ }, { role: "link", to: "/settings/advanced", icon: Lock, labelKey: "advanced" }, { role: "link", to: "/settings/about", icon: Info, labelKey: "about" }, -] as const; +]; + +const linkClass = cn( + "flex items-center gap-2", + "px-3 py-1.5 md:rounded-md transition-colors -mr-0.5 md:mr-0", + "text-(--color-fg-muted) hover:text-(--color-fg) hover:bg-(--color-bg-elevated)", + "[&.active]:text-(--color-fg) [&.active]:font-medium", + "md:[&.active]:bg-(--color-bg-elevated)", + "border-b [&.active]:border-b-(--color-fg)", + "md:border-b-0 md:[&.active]:border-0", +); + +const childLinkClass = cn( + "flex items-center gap-2", + "px-3 py-1 md:rounded-md transition-colors text-sm", + "text-(--color-fg-muted) hover:text-(--color-fg) hover:bg-(--color-bg-elevated)", + "[&.active]:text-(--color-fg) [&.active]:font-medium", + "md:[&.active]:bg-(--color-bg-elevated)", +); export const Route = createFileRoute("/settings")({ component: SettingsLayout, }); -function DisplayNavItem({ item }: { item: NavItem }) { +function NavGroupItem({ item }: { item: NavGroup }) { const { t } = useTranslation("settings"); + const matchRoute = useMatchRoute(); + const Icon = item.icon; + + // Check if any child route is active + const isChildActive = item.children.some((child) => + matchRoute({ to: child.to, fuzzy: true }), + ); + + const [open, setOpen] = useState(isChildActive); + + return ( +