refactor(frontend): convert settings to vertical sidebar layout (#10971)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
sp.wack
2025-09-26 17:57:37 +04:00
committed by GitHub
parent ef12adc107
commit fb6f688049
20 changed files with 373 additions and 169 deletions

View File

@@ -133,7 +133,7 @@ describe("Settings Screen", () => {
"user",
"integrations",
"application",
"billing", // The nav item shows "billing" text and routes to /billing
"billing", // The nav item shows "Billing" text and routes to /billing
"secrets",
"api keys",
];

View File

@@ -10779,18 +10779,6 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
"license": "MIT",
"optional": true,
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",

View File

@@ -1,3 +1,4 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { ContextMenu } from "#/ui/context-menu";
@@ -6,91 +7,14 @@ import { Divider } from "#/ui/divider";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
import CreditCardIcon from "#/icons/credit-card.svg?react";
import KeyIcon from "#/icons/key.svg?react";
import LogOutIcon from "#/icons/log-out.svg?react";
import ServerProcessIcon from "#/icons/server-process.svg?react";
import SettingsGearIcon from "#/icons/settings-gear.svg?react";
import CircuitIcon from "#/icons/u-circuit.svg?react";
import PuzzlePieceIcon from "#/icons/u-puzzle-piece.svg?react";
import UserIcon from "#/icons/user.svg?react";
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
interface AccountSettingsContextMenuProps {
onLogout: () => void;
onClose: () => void;
}
const SAAS_NAV_ITEMS = [
{
icon: <UserIcon width={16} height={16} />,
to: "/settings/user",
text: "COMMON$USER_SETTINGS",
},
{
icon: <PuzzlePieceIcon width={16} height={16} />,
to: "/settings/integrations",
text: "SETTINGS$NAV_INTEGRATIONS",
},
{
icon: <SettingsGearIcon width={16} height={16} />,
to: "/settings/app",
text: "COMMON$APPLICATION_SETTINGS",
},
{
icon: <CircuitIcon width={16} height={16} />,
to: "/settings",
text: "COMMON$LANGUAGE_MODEL_LLM",
},
{
icon: <CreditCardIcon width={16} height={16} />,
to: "/settings/billing",
text: "SETTINGS$NAV_BILLING",
},
{
icon: <KeyIcon width={16} height={16} />,
to: "/settings/secrets",
text: "SETTINGS$NAV_SECRETS",
},
{
icon: <KeyIcon width={16} height={16} />,
to: "/settings/api-keys",
text: "SETTINGS$NAV_API_KEYS",
},
{
icon: <ServerProcessIcon width={16} height={16} />,
to: "/settings/mcp",
text: "SETTINGS$NAV_MCP",
},
];
const OSS_NAV_ITEMS = [
{
icon: <CircuitIcon width={16} height={16} />,
to: "/settings",
text: "COMMON$LANGUAGE_MODEL_LLM",
},
{
icon: <ServerProcessIcon width={16} height={16} />,
to: "/settings/mcp",
text: "COMMON$MODEL_CONTEXT_PROTOCOL_MCP",
},
{
icon: <PuzzlePieceIcon width={16} height={16} />,
to: "/settings/integrations",
text: "SETTINGS$NAV_INTEGRATIONS",
},
{
icon: <SettingsGearIcon width={16} height={16} />,
to: "/settings/app",
text: "COMMON$APPLICATION_SETTINGS",
},
{
icon: <KeyIcon width={16} height={16} />,
to: "/settings/secrets",
text: "SETTINGS$NAV_SECRETS",
},
];
export function AccountSettingsContextMenu({
onLogout,
onClose,
@@ -100,7 +24,13 @@ export function AccountSettingsContextMenu({
const { data: config } = useConfig();
const isSaas = config?.APP_MODE === "saas";
const navItems = isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS;
const navItems = (isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS).map((item) => ({
...item,
icon: React.cloneElement(item.icon, {
width: 16,
height: 16,
} as React.SVGProps<SVGSVGElement>),
}));
const handleNavigationClick = () => {
onClose();
@@ -112,7 +42,7 @@ export function AccountSettingsContextMenu({
testId="account-settings-context-menu"
ref={ref}
alignment="right"
className="mt-0 md:right-full md:left-full md:bottom-0 ml-0 z-10 w-fit z-[9999]"
className="mt-0 md:right-full md:left-full md:bottom-0 ml-0 w-fit z-[9999]"
>
{navItems.map(({ to, text, icon }) => (
<Link key={to} to={to} className="text-decoration-none">

View File

@@ -51,7 +51,7 @@ export function PaymentForm() {
<form
action={billingFormAction}
data-testid="billing-settings"
className="flex flex-col gap-6 px-11 py-9"
className="flex flex-col gap-6"
>
<div
className={cn(

View File

@@ -1,3 +1,6 @@
export { UpgradeBanner } from "./upgrade-banner";
export { UpgradeButton } from "./upgrade-button";
export { BannerMessage } from "./banner-message";
export { MobileHeader } from "./mobile-header";
export { SettingsNavigation } from "./settings-navigation";
export { SettingsLayout } from "./settings-layout";

View File

@@ -0,0 +1,55 @@
import { useTranslation } from "react-i18next";
import SettingsIcon from "#/icons/settings-gear.svg?react";
import { Typography } from "#/ui/typography";
import { I18nKey } from "#/i18n/declaration";
interface MobileHeaderProps {
isMobileMenuOpen: boolean;
onToggleMenu: () => void;
}
export function MobileHeader({
isMobileMenuOpen,
onToggleMenu,
}: MobileHeaderProps) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-between mb-4 md:hidden">
<div className="flex items-center gap-2">
<SettingsIcon width={16} height={16} />
<Typography.H2>{t(I18nKey.SETTINGS$TITLE)}</Typography.H2>
</div>
<button
type="button"
onClick={onToggleMenu}
className="p-2 rounded-md bg-tertiary hover:bg-[#454545] transition-colors"
aria-label="Toggle settings menu"
>
<svg
width={20}
height={20}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
{isMobileMenuOpen ? (
<>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</>
) : (
<>
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</>
)}
</svg>
</button>
</div>
);
}

View File

@@ -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 (
<div className="flex flex-col h-full px-[14px] pt-8">
{/* Mobile header */}
<MobileHeader
isMobileMenuOpen={isMobileMenuOpen}
onToggleMenu={toggleMobileMenu}
/>
{/* Desktop layout with navigation and main content */}
<div className="flex flex-1 overflow-hidden gap-10">
{/* Navigation */}
<SettingsNavigation
isMobileMenuOpen={isMobileMenuOpen}
onCloseMobileMenu={closeMobileMenu}
navigationItems={navigationItems}
isSaas={isSaas}
/>
{/* Main content */}
<main className="flex-1 overflow-auto">{children}</main>
</div>
</div>
);
}

View File

@@ -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 && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
onClick={onCloseMobileMenu}
/>
)}
{/* Navigation sidebar */}
<nav
data-testid="settings-navbar"
className={cn(
"flex flex-col gap-6 transition-transform duration-300 ease-in-out",
// Mobile: full screen overlay
"fixed inset-0 z-50 w-full bg-base-secondary p-4 transform md:transform-none",
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full",
// Desktop: static sidebar
"md:relative md:translate-x-0 md:w-64 md:p-0 md:bg-transparent",
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 ml-1 sm:ml-4.5">
<SettingsIcon width={16} height={16} />
<Typography.H2>{t(I18nKey.SETTINGS$TITLE)}</Typography.H2>
</div>
{/* Close button - only visible on mobile */}
<button
type="button"
onClick={onCloseMobileMenu}
className="md:hidden p-0.5 hover:bg-[#454545] rounded-md transition-colors"
aria-label="Close navigation menu"
>
<CloseIcon width={32} height={32} />
</button>
</div>
<div className="flex flex-col gap-2">
{navigationItems.map(({ to, icon, text }) => (
<NavLink
end
key={to}
to={to}
onClick={onCloseMobileMenu}
className={({ isActive }) =>
cn(
"flex items-center gap-3 p-1 sm:px-[14px] sm:py-2 rounded-md transition-colors",
isActive ? "bg-[#454545]" : "hover:bg-[#454545]",
)
}
>
{icon}
<div className="flex items-center gap-1.5 min-w-0 flex-1">
<Typography.Text className="text-[#A3A3A3] whitespace-nowrap">
{t(text as I18nKey)}
</Typography.Text>
{isSaas && to === "/settings" && <ProPill />}
</div>
</NavLink>
))}
</div>
</nav>
</>
);
}

View File

@@ -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({
<UpgradeBanner
message={t("SETTINGS$UPGRADE_BANNER_MESSAGE")}
onUpgradeClick={onUpgradeClick}
className="sticky top-0 z-30"
className="sticky top-0 z-30 mb-6"
isDisabled={isDisabled}
/>
<div

View File

@@ -18,7 +18,7 @@ export function UpgradeBanner({
return (
<div
className={cn(
"bg-primary text-base flex items-center justify-center gap-3 p-2 w-full",
"bg-primary text-base flex items-center justify-center gap-3 p-2 w-full rounded",
className,
)}
data-testid="upgrade-banner"

View File

@@ -0,0 +1,84 @@
import CreditCardIcon from "#/icons/credit-card.svg?react";
import KeyIcon from "#/icons/key.svg?react";
import ServerProcessIcon from "#/icons/server-process.svg?react";
import SettingsGearIcon from "#/icons/settings-gear.svg?react";
import CircuitIcon from "#/icons/u-circuit.svg?react";
import PuzzlePieceIcon from "#/icons/u-puzzle-piece.svg?react";
import UserIcon from "#/icons/user.svg?react";
export interface SettingsNavItem {
icon: React.ReactElement;
to: string;
text: string;
}
export const SAAS_NAV_ITEMS: SettingsNavItem[] = [
{
icon: <UserIcon width={22} height={22} />,
to: "/settings/user",
text: "SETTINGS$NAV_USER",
},
{
icon: <PuzzlePieceIcon width={22} height={22} />,
to: "/settings/integrations",
text: "SETTINGS$NAV_INTEGRATIONS",
},
{
icon: <SettingsGearIcon width={22} height={22} />,
to: "/settings/app",
text: "SETTINGS$NAV_APPLICATION",
},
{
icon: <CircuitIcon width={22} height={22} />,
to: "/settings",
text: "COMMON$LANGUAGE_MODEL_LLM",
},
{
icon: <CreditCardIcon width={22} height={22} />,
to: "/settings/billing",
text: "SETTINGS$NAV_BILLING",
},
{
icon: <KeyIcon width={22} height={22} />,
to: "/settings/secrets",
text: "SETTINGS$NAV_SECRETS",
},
{
icon: <KeyIcon width={22} height={22} />,
to: "/settings/api-keys",
text: "SETTINGS$NAV_API_KEYS",
},
{
icon: <ServerProcessIcon width={22} height={22} />,
to: "/settings/mcp",
text: "SETTINGS$NAV_MCP",
},
];
export const OSS_NAV_ITEMS: SettingsNavItem[] = [
{
icon: <CircuitIcon width={22} height={22} />,
to: "/settings",
text: "SETTINGS$NAV_LLM",
},
{
icon: <ServerProcessIcon width={22} height={22} />,
to: "/settings/mcp",
text: "SETTINGS$NAV_MCP",
},
{
icon: <PuzzlePieceIcon width={22} height={22} />,
to: "/settings/integrations",
text: "SETTINGS$NAV_INTEGRATIONS",
},
{
icon: <SettingsGearIcon width={22} height={22} />,
to: "/settings/app",
text: "SETTINGS$NAV_APPLICATION",
},
{
icon: <KeyIcon width={22} height={22} />,
to: "/settings/secrets",
text: "SETTINGS$NAV_SECRETS",
},
];

View File

@@ -3,7 +3,7 @@ import { ApiKeysManager } from "#/components/features/settings/api-keys-manager"
function ApiKeysScreen() {
return (
<div className="flex flex-col grow overflow-auto p-9">
<div className="flex flex-col grow overflow-auto">
<ApiKeysManager />
</div>
);

View File

@@ -187,7 +187,7 @@ function AppSettingsScreen() {
>
{shouldBeLoading && <AppSettingsInputsSkeleton />}
{!shouldBeLoading && (
<div className="p-9 flex flex-col gap-6">
<div className="flex flex-col gap-6">
<LanguageInput
name="language-input"
defaultKey={settings.LANGUAGE}
@@ -282,7 +282,7 @@ function AppSettingsScreen() {
</div>
)}
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
<div className="flex gap-6 p-6 justify-end">
<BrandButton
testId="submit-button"
variant="primary"

View File

@@ -123,7 +123,7 @@ function GitSettingsScreen() {
className="flex flex-col h-full justify-between"
>
{!isLoading && (
<div className="p-9 flex flex-col">
<div className="flex flex-col">
{shouldRenderExternalConfigureButtons && !isLoading && (
<>
<div className="pb-1 flex flex-col">
@@ -202,7 +202,7 @@ function GitSettingsScreen() {
{isLoading && <GitSettingInputsSkeleton />}
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
<div className="flex gap-6 p-6 justify-end">
{!shouldRenderExternalConfigureButtons && (
<>
<BrandButton

View File

@@ -476,7 +476,7 @@ function LlmSettingsScreen() {
)}
inert={shouldShowUpgradeBanner}
>
<div className="p-9 flex flex-col gap-6">
<div className="flex flex-col gap-6">
<SettingsSwitch
testId="advanced-settings-switch"
defaultIsToggled={view === "advanced"}
@@ -751,7 +751,7 @@ function LlmSettingsScreen() {
)}
</div>
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
<div className="flex gap-6 p-6 justify-end">
<BrandButton
testId="submit-button"
type="submit"

View File

@@ -126,7 +126,7 @@ function MCPSettingsScreen() {
if (isLoading) {
return (
<div className="px-11 py-9 flex flex-col gap-5">
<div className="flex flex-col gap-5">
<div className="animate-pulse">
<div className="h-6 bg-gray-300 rounded w-1/4 mb-4" />
<div className="h-4 bg-gray-300 rounded w-1/2 mb-8" />
@@ -137,7 +137,7 @@ function MCPSettingsScreen() {
}
return (
<div className="px-11 py-9 flex flex-col gap-5">
<div className="flex flex-col gap-5">
{view === "list" && (
<>
<BrandButton

View File

@@ -62,10 +62,7 @@ function SecretsSettingsScreen() {
};
return (
<div
data-testid="secrets-settings-screen"
className="px-11 py-9 flex flex-col gap-5"
>
<div data-testid="secrets-settings-screen" className="flex flex-col gap-5">
{isLoadingSecrets && view === "list" && (
<ul>
<SecretListItemSkeleton />

View File

@@ -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: <CircuitIcon width={22} height={22} />,
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 (
<main
data-testid="settings-screen"
className="bg-base-secondary border border-tertiary h-full rounded-xl flex flex-col"
>
<header className="px-3 py-1.5 border-b border-b-tertiary flex items-center gap-2">
<SettingsIcon width={16} height={16} />
<h1 className="text-sm leading-6">{t(I18nKey.SETTINGS$TITLE)}</h1>
</header>
<nav
data-testid="settings-navbar"
className="flex items-end gap-6 px-3 md:px-9 border-b border-tertiary"
>
{navItems.map(({ to, text }) => (
<NavLink
end
key={to}
to={to}
className={({ isActive }) =>
cn(
"border-b-2 border-transparent py-2.5 px-4 min-w-[40px] flex items-center justify-center relative",
isActive && "border-primary",
)
}
>
<span className="text-[#F9FBFE] text-sm">{t(text)}</span>
{isSaas && to === "/settings" && <ProPill className="ml-2" />}
</NavLink>
))}
</nav>
<div className="flex flex-col grow overflow-auto">
<Outlet />
</div>
<main data-testid="settings-screen" className="h-full">
<SettingsLayout navigationItems={navItems} isSaas={isSaas}>
<div className="flex flex-col gap-6 h-full">
<Typography.H2>{t(currentSectionTitle)}</Typography.H2>
<div className="flex-1 overflow-auto">
<Outlet />
</div>
</div>
</SettingsLayout>
</main>
);
}

View File

@@ -203,7 +203,7 @@ function UserSettingsScreen() {
return (
<div data-testid="user-settings-screen" className="flex flex-col h-full">
<div className="p-9 flex flex-col gap-6">
<div className="flex flex-col gap-6">
{isLoading ? (
<div className="animate-pulse h-8 w-64 bg-tertiary rounded-sm" />
) : (

View File

@@ -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<TypographyProps, "variant">) {
return (
<Typography variant="h2" className={className} testId={testId}>
{children}
</Typography>
);
}
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;