void;
+}
+
+export function MobileHeader({
+ isMobileMenuOpen,
+ onToggleMenu,
+}: MobileHeaderProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t(I18nKey.SETTINGS$TITLE)}
+
+
+
+ );
+}
diff --git a/frontend/src/components/features/settings/settings-layout.tsx b/frontend/src/components/features/settings/settings-layout.tsx
new file mode 100644
index 0000000000..73b47f7768
--- /dev/null
+++ b/frontend/src/components/features/settings/settings-layout.tsx
@@ -0,0 +1,55 @@
+import { useState } from "react";
+import { MobileHeader } from "./mobile-header";
+import { SettingsNavigation } from "./settings-navigation";
+
+interface NavigationItem {
+ to: string;
+ icon: React.ReactNode;
+ text: string;
+}
+
+interface SettingsLayoutProps {
+ children: React.ReactNode;
+ navigationItems: NavigationItem[];
+ isSaas: boolean;
+}
+
+export function SettingsLayout({
+ children,
+ navigationItems,
+ isSaas,
+}: SettingsLayoutProps) {
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+
+ const toggleMobileMenu = () => {
+ setIsMobileMenuOpen(!isMobileMenuOpen);
+ };
+
+ const closeMobileMenu = () => {
+ setIsMobileMenuOpen(false);
+ };
+
+ return (
+
+ {/* Mobile header */}
+
+
+ {/* Desktop layout with navigation and main content */}
+
+ {/* Navigation */}
+
+
+ {/* Main content */}
+ {children}
+
+
+ );
+}
diff --git a/frontend/src/components/features/settings/settings-navigation.tsx b/frontend/src/components/features/settings/settings-navigation.tsx
new file mode 100644
index 0000000000..7eb5be0c71
--- /dev/null
+++ b/frontend/src/components/features/settings/settings-navigation.tsx
@@ -0,0 +1,96 @@
+import { useTranslation } from "react-i18next";
+import { NavLink } from "react-router";
+import { cn } from "#/utils/utils";
+import { Typography } from "#/ui/typography";
+import { I18nKey } from "#/i18n/declaration";
+import SettingsIcon from "#/icons/settings-gear.svg?react";
+import CloseIcon from "#/icons/close.svg?react";
+import { ProPill } from "./pro-pill";
+
+interface NavigationItem {
+ to: string;
+ icon: React.ReactNode;
+ text: string;
+}
+
+interface SettingsNavigationProps {
+ isMobileMenuOpen: boolean;
+ onCloseMobileMenu: () => void;
+ navigationItems: NavigationItem[];
+ isSaas: boolean;
+}
+
+export function SettingsNavigation({
+ isMobileMenuOpen,
+ onCloseMobileMenu,
+ navigationItems,
+ isSaas,
+}: SettingsNavigationProps) {
+ const { t } = useTranslation();
+
+ return (
+ <>
+ {/* Mobile backdrop */}
+ {isMobileMenuOpen && (
+
+ )}
+
+ {/* Navigation sidebar */}
+
+ >
+ );
+}
diff --git a/frontend/src/components/features/settings/upgrade-banner-with-backdrop.tsx b/frontend/src/components/features/settings/upgrade-banner-with-backdrop.tsx
index 369869aa92..506bb71ec2 100644
--- a/frontend/src/components/features/settings/upgrade-banner-with-backdrop.tsx
+++ b/frontend/src/components/features/settings/upgrade-banner-with-backdrop.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { useTranslation } from "react-i18next";
import { UpgradeBanner } from "#/components/features/settings";
@@ -18,7 +17,7 @@ export function UpgradeBannerWithBackdrop({
,
+ to: "/settings/user",
+ text: "SETTINGS$NAV_USER",
+ },
+ {
+ icon:
,
+ to: "/settings/integrations",
+ text: "SETTINGS$NAV_INTEGRATIONS",
+ },
+ {
+ icon:
,
+ to: "/settings/app",
+ text: "SETTINGS$NAV_APPLICATION",
+ },
+ {
+ icon:
,
+ to: "/settings",
+ text: "COMMON$LANGUAGE_MODEL_LLM",
+ },
+ {
+ icon:
,
+ to: "/settings/billing",
+ text: "SETTINGS$NAV_BILLING",
+ },
+ {
+ icon:
,
+ to: "/settings/secrets",
+ text: "SETTINGS$NAV_SECRETS",
+ },
+ {
+ icon:
,
+ to: "/settings/api-keys",
+ text: "SETTINGS$NAV_API_KEYS",
+ },
+ {
+ icon:
,
+ to: "/settings/mcp",
+ text: "SETTINGS$NAV_MCP",
+ },
+];
+
+export const OSS_NAV_ITEMS: SettingsNavItem[] = [
+ {
+ icon:
,
+ to: "/settings",
+ text: "SETTINGS$NAV_LLM",
+ },
+ {
+ icon:
,
+ to: "/settings/mcp",
+ text: "SETTINGS$NAV_MCP",
+ },
+ {
+ icon:
,
+ to: "/settings/integrations",
+ text: "SETTINGS$NAV_INTEGRATIONS",
+ },
+ {
+ icon:
,
+ to: "/settings/app",
+ text: "SETTINGS$NAV_APPLICATION",
+ },
+ {
+ icon:
,
+ to: "/settings/secrets",
+ text: "SETTINGS$NAV_SECRETS",
+ },
+];
diff --git a/frontend/src/routes/api-keys.tsx b/frontend/src/routes/api-keys.tsx
index 96669b37e6..e5d733ecb7 100644
--- a/frontend/src/routes/api-keys.tsx
+++ b/frontend/src/routes/api-keys.tsx
@@ -3,7 +3,7 @@ import { ApiKeysManager } from "#/components/features/settings/api-keys-manager"
function ApiKeysScreen() {
return (
-
+
);
diff --git a/frontend/src/routes/app-settings.tsx b/frontend/src/routes/app-settings.tsx
index f6b64c0b01..71d0230d5d 100644
--- a/frontend/src/routes/app-settings.tsx
+++ b/frontend/src/routes/app-settings.tsx
@@ -187,7 +187,7 @@ function AppSettingsScreen() {
>
{shouldBeLoading &&
}
{!shouldBeLoading && (
-
+
)}
-
+
{!isLoading && (
-
+
{shouldRenderExternalConfigureButtons && !isLoading && (
<>
@@ -202,7 +202,7 @@ function GitSettingsScreen() {
{isLoading &&
}
-
+
{!shouldRenderExternalConfigureButtons && (
<>
-
+
-
+
+
@@ -137,7 +137,7 @@ function MCPSettingsScreen() {
}
return (
-
+
{view === "list" && (
<>
+
{isLoadingSecrets && view === "list" && (
diff --git a/frontend/src/routes/settings.tsx b/frontend/src/routes/settings.tsx
index 6c88ab7526..88398acd66 100644
--- a/frontend/src/routes/settings.tsx
+++ b/frontend/src/routes/settings.tsx
@@ -1,14 +1,17 @@
-import { NavLink, Outlet, redirect } from "react-router";
+import { useMemo } from "react";
+import { Outlet, redirect, useLocation } from "react-router";
import { useTranslation } from "react-i18next";
-import SettingsIcon from "#/icons/settings.svg?react";
-import { cn } from "#/utils/utils";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
import { Route } from "./+types/settings";
import OptionService from "#/api/option-service/option-service.api";
import { queryClient } from "#/query-client-config";
-import { ProPill } from "#/components/features/settings/pro-pill";
import { GetConfigResponse } from "#/api/option-service/option.types";
+import { useSubscriptionAccess } from "#/hooks/query/use-subscription-access";
+import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
+import CircuitIcon from "#/icons/u-circuit.svg?react";
+import { Typography } from "#/ui/typography";
+import { SettingsLayout } from "#/components/features/settings/settings-layout";
const SAAS_ONLY_PATHS = [
"/settings/user",
@@ -17,25 +20,6 @@ const SAAS_ONLY_PATHS = [
"/settings/api-keys",
];
-const SAAS_NAV_ITEMS = [
- { to: "/settings/user", text: "SETTINGS$NAV_USER" },
- { to: "/settings/integrations", text: "SETTINGS$NAV_INTEGRATIONS" },
- { to: "/settings/app", text: "SETTINGS$NAV_APPLICATION" },
- { to: "/settings", text: "SETTINGS$NAV_LLM" },
- { to: "/settings/billing", text: "SETTINGS$NAV_BILLING" },
- { to: "/settings/secrets", text: "SETTINGS$NAV_SECRETS" },
- { to: "/settings/api-keys", text: "SETTINGS$NAV_API_KEYS" },
- { to: "/settings/mcp", text: "SETTINGS$NAV_MCP" },
-];
-
-const OSS_NAV_ITEMS = [
- { to: "/settings", text: "SETTINGS$NAV_LLM" },
- { to: "/settings/mcp", text: "SETTINGS$NAV_MCP" },
- { to: "/settings/integrations", text: "SETTINGS$NAV_INTEGRATIONS" },
- { to: "/settings/app", text: "SETTINGS$NAV_APPLICATION" },
- { to: "/settings/secrets", text: "SETTINGS$NAV_SECRETS" },
-];
-
export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
const url = new URL(request.url);
const { pathname } = url;
@@ -59,46 +43,45 @@ export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
function SettingsScreen() {
const { t } = useTranslation();
const { data: config } = useConfig();
+ const { data: subscriptionAccess } = useSubscriptionAccess();
+ const location = useLocation();
const isSaas = config?.APP_MODE === "saas";
- const navItems = isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS;
+ // Navigation items configuration
+ const navItems = useMemo(() => {
+ const items = [];
+ if (isSaas) {
+ if (subscriptionAccess) {
+ items.push({
+ icon: ,
+ to: "/settings",
+ text: "SETTINGS$NAV_LLM" as I18nKey,
+ });
+ }
+ items.push(...SAAS_NAV_ITEMS);
+ } else {
+ items.push(...OSS_NAV_ITEMS);
+ }
+ return items;
+ }, [isSaas, !!subscriptionAccess]);
+
+ // Current section title for the main content area
+ const currentSectionTitle = useMemo(() => {
+ const currentItem = navItems.find((item) => item.to === location.pathname);
+ return currentItem ? currentItem.text : "SETTINGS$NAV_LLM";
+ }, [navItems, location.pathname]);
return (
-
-
-
- {t(I18nKey.SETTINGS$TITLE)}
-
-
-
-
-
-
-
+
+
+
+
{t(currentSectionTitle)}
+
+
+
+
+
);
}
diff --git a/frontend/src/routes/user-settings.tsx b/frontend/src/routes/user-settings.tsx
index a5fb51bbf6..93366574b0 100644
--- a/frontend/src/routes/user-settings.tsx
+++ b/frontend/src/routes/user-settings.tsx
@@ -203,7 +203,7 @@ function UserSettingsScreen() {
return (
-
+
{isLoading ? (
) : (
diff --git a/frontend/src/ui/typography.tsx b/frontend/src/ui/typography.tsx
index 86668efa07..fc574d6f51 100644
--- a/frontend/src/ui/typography.tsx
+++ b/frontend/src/ui/typography.tsx
@@ -5,6 +5,7 @@ const typographyVariants = cva("", {
variants: {
variant: {
h1: "text-[32px] text-white font-bold leading-5",
+ h2: "text-xl font-semibold leading-6 -tracking-[0.02em] text-white",
h3: "text-sm font-semibold text-gray-300",
span: "text-sm font-normal text-white leading-5.5",
codeBlock:
@@ -53,6 +54,18 @@ export function H1({
);
}
+export function H2({
+ className,
+ testId,
+ children,
+}: Omit
) {
+ return (
+
+ {children}
+
+ );
+}
+
export function H3({
className,
testId,
@@ -91,6 +104,7 @@ export function CodeBlock({
// Attach components to Typography for the expected API
Typography.H1 = H1;
+Typography.H2 = H2;
Typography.H3 = H3;
Typography.Text = Text;
Typography.CodeBlock = CodeBlock;