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 ( +
+ + {open && ( +
+ {item.children.map((child) => ( + + + {t(child.labelKey)} + + ))} +
+ )} +
+ ); +} + +function DisplayNavItem({ item }: { item: NavItem }) { if (item.role === "divider") { - return
; + return ( +
+ ); + } + + if (item.role === "group") { + return ; } - // item must be a link if not divider + const { t } = useTranslation("settings"); const Icon = item.icon; return ( - + {t(item.labelKey)} @@ -73,7 +164,7 @@ function SettingsLayout() { const { t } = useTranslation("settings"); return ( -
+

{t("settings-title")}

@@ -84,19 +175,22 @@ function SettingsLayout() {
-
+
diff --git a/js/web/src/styles.css b/js/web/src/styles.css index 4132f906..05cc3c00 100644 --- a/js/web/src/styles.css +++ b/js/web/src/styles.css @@ -130,9 +130,11 @@ code { --color-accent: var(--color-accent); --color-accent-hover: var(--color-accent); --color-accent-fg: var(--color-accent-foreground); - --color-danger: var(--color-destructive); + --color-danger: var(--color-chart-1); --color-success: var(--color-primary); + --color-info: var(--color-chart-3); + --font-display: "Valley Sans", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", system-ui, sans-serif; --font-sans: "Atkinson Hyperlegible Next", ui-sans-serif, system-ui,