diff --git a/backend/app/DomainObjects/Enums/HomepageFontFamily.php b/backend/app/DomainObjects/Enums/HomepageFontFamily.php
new file mode 100644
index 0000000000..8c565d6ef2
--- /dev/null
+++ b/backend/app/DomainObjects/Enums/HomepageFontFamily.php
@@ -0,0 +1,32 @@
+ ['nullable', 'string', ...RulesHelper::HEX_COLOR],
'homepage_theme_settings.mode' => ['nullable', 'string', Rule::in(['light', 'dark'])],
'homepage_theme_settings.background_type' => ['nullable', 'string', Rule::in(HomepageBackgroundType::valuesArray())],
+ 'homepage_theme_settings.font_family' => ['nullable', 'string', Rule::in(HomepageFontFamily::valuesArray())],
// Self-service settings
'allow_attendee_self_edit' => ['boolean'],
@@ -147,6 +149,7 @@ public function messages(): array
'homepage_theme_settings.background' => $colorMessage,
'homepage_theme_settings.mode.in' => __('The mode must be light or dark.'),
'homepage_theme_settings.background_type.in' => __('The background type must be COLOR or MIRROR_COVER_IMAGE.'),
+ 'homepage_theme_settings.font_family.in' => __('The selected font is not supported.'),
];
}
}
diff --git a/backend/app/Http/Request/Organizer/Settings/PartialUpdateOrganizerSettingsRequest.php b/backend/app/Http/Request/Organizer/Settings/PartialUpdateOrganizerSettingsRequest.php
index 5441436925..a86e345eb6 100644
--- a/backend/app/Http/Request/Organizer/Settings/PartialUpdateOrganizerSettingsRequest.php
+++ b/backend/app/Http/Request/Organizer/Settings/PartialUpdateOrganizerSettingsRequest.php
@@ -4,6 +4,7 @@
use HiEvents\DomainObjects\Enums\AttendeeDetailsCollectionMethod;
use HiEvents\DomainObjects\Enums\HomepageBackgroundType;
+use HiEvents\DomainObjects\Enums\HomepageFontFamily;
use HiEvents\DomainObjects\Enums\OrganizerHomepageVisibility;
use HiEvents\DomainObjects\Enums\TrackingPixelProvider;
use HiEvents\Http\Request\BaseRequest;
@@ -110,6 +111,7 @@ public static function rules(): array
'homepage_theme_settings.background' => ['nullable', 'string', ...RulesHelper::HEX_COLOR],
'homepage_theme_settings.mode' => ['nullable', 'string', Rule::in(['light', 'dark'])],
'homepage_theme_settings.background_type' => ['nullable', 'string', Rule::in(HomepageBackgroundType::valuesArray())],
+ 'homepage_theme_settings.font_family' => ['nullable', 'string', Rule::in(HomepageFontFamily::valuesArray())],
// SEO
'seo_keywords' => ['sometimes', 'nullable', 'string', 'max:255'],
diff --git a/backend/app/Services/Domain/Event/CreateEventService.php b/backend/app/Services/Domain/Event/CreateEventService.php
index 7126eda943..76e08b974d 100644
--- a/backend/app/Services/Domain/Event/CreateEventService.php
+++ b/backend/app/Services/Domain/Event/CreateEventService.php
@@ -190,6 +190,10 @@ private function createEventSettings(
: ($organizerThemeSettings['background_type'] ?? HomepageBackgroundType::COLOR->name),
];
+ if (!empty($organizerThemeSettings['font_family'])) {
+ $homepageThemeSettings['font_family'] = $organizerThemeSettings['font_family'];
+ }
+
$this->eventSettingsRepository->create([
'event_id' => $event->getId(),
diff --git a/backend/tests/Unit/DomainObjects/Enums/HomepageFontFamilyTest.php b/backend/tests/Unit/DomainObjects/Enums/HomepageFontFamilyTest.php
new file mode 100644
index 0000000000..37ab229d68
--- /dev/null
+++ b/backend/tests/Unit/DomainObjects/Enums/HomepageFontFamilyTest.php
@@ -0,0 +1,33 @@
+assertContains('Outfit', $values);
+ $this->assertContains('Inter', $values);
+ $this->assertContains('Plus Jakarta Sans', $values);
+ $this->assertContains('Playfair Display', $values);
+ $this->assertContains('Bebas Neue', $values);
+ }
+
+ public function test_values_are_unique_non_empty_strings(): void
+ {
+ $values = HomepageFontFamily::valuesArray();
+
+ $this->assertNotEmpty($values);
+ $this->assertSame($values, array_values(array_unique($values)));
+
+ foreach ($values as $value) {
+ $this->assertIsString($value);
+ $this->assertNotSame('', trim($value));
+ }
+ }
+}
diff --git a/backend/tests/Unit/Services/Domain/Event/CreateEventServiceTest.php b/backend/tests/Unit/Services/Domain/Event/CreateEventServiceTest.php
index f05c7ae6f1..b932f77683 100644
--- a/backend/tests/Unit/Services/Domain/Event/CreateEventServiceTest.php
+++ b/backend/tests/Unit/Services/Domain/Event/CreateEventServiceTest.php
@@ -351,6 +351,132 @@ public function testCreateEventWithoutEventCoverDoesNotCreateImageRecord(): void
$this->assertTrue(true);
}
+ public function testCreateEventInheritsOrganizerFontFamily(): void
+ {
+ $eventData = $this->createMockEventDomainObjectWithCategory('MUSIC');
+
+ $organizerSettings = Mockery::mock(OrganizerSettingDomainObject::class);
+ $organizerSettings->shouldReceive('getHomepageThemeSettings')
+ ->andReturn([
+ 'accent' => '#ff0000',
+ 'background' => '#ffffff',
+ 'mode' => 'light',
+ 'background_type' => HomepageBackgroundType::COLOR->name,
+ 'font_family' => 'Inter',
+ ]);
+ $organizerSettings->shouldReceive('getDefaultAttendeeDetailsCollectionMethod')
+ ->andReturn('per_order');
+ $organizerSettings->shouldReceive('getDefaultShowMarketingOptIn')
+ ->andReturn(false);
+ $organizerSettings->shouldReceive('getDefaultPassPlatformFeeToBuyer')
+ ->andReturn(false);
+ $organizerSettings->shouldReceive('getDefaultAllowAttendeeSelfEdit')
+ ->andReturn(false);
+
+ $organizer = $this->createMockOrganizerDomainObject()
+ ->shouldReceive('getOrganizerSettings')
+ ->andReturn($organizerSettings)
+ ->getMock();
+
+ $this->databaseManager->shouldReceive('transaction')->once()->andReturnUsing(function ($callback) {
+ return $callback();
+ });
+
+ $this->organizerRepository
+ ->shouldReceive('loadRelation')
+ ->with(OrganizerSettingDomainObject::class)
+ ->once()
+ ->andReturnSelf()
+ ->getMock()
+ ->shouldReceive('findFirstWhere')
+ ->andReturn($organizer);
+
+ $this->eventRepository->shouldReceive('create')->andReturn($eventData);
+
+ $this->config->shouldReceive('get')
+ ->with('filesystems.public')
+ ->andReturn('public');
+ $this->config->shouldReceive('get')
+ ->with('app.event_categories_cover_images_path')
+ ->andReturn('event-covers');
+
+ $mockDisk = Mockery::mock();
+ $mockDisk->shouldReceive('exists')
+ ->with('event-covers/MUSIC.jpg')
+ ->andReturn(false);
+
+ $this->filesystemManager->shouldReceive('disk')
+ ->with('public')
+ ->andReturn($mockDisk);
+
+ $this->eventSettingsRepository->shouldReceive('create')
+ ->with(Mockery::on(function ($arg) {
+ return isset($arg['homepage_theme_settings']['font_family'])
+ && $arg['homepage_theme_settings']['font_family'] === 'Inter';
+ }));
+
+ $this->eventStatisticsRepository->shouldReceive('create');
+
+ $this->purifier->shouldReceive('purify')->andReturn('Test Description');
+
+ $this->createEventService->createEvent($eventData);
+
+ $this->assertTrue(true);
+ }
+
+ public function testCreateEventOmitsFontFamilyWhenOrganizerHasNone(): void
+ {
+ $eventData = $this->createMockEventDomainObjectWithCategory('MUSIC');
+ $organizer = $this->createMockOrganizerDomainObject()
+ ->shouldReceive('getOrganizerSettings')
+ ->andReturn(new OrganizerSettingDomainObject())
+ ->getMock();
+
+ $this->databaseManager->shouldReceive('transaction')->once()->andReturnUsing(function ($callback) {
+ return $callback();
+ });
+
+ $this->organizerRepository
+ ->shouldReceive('loadRelation')
+ ->with(OrganizerSettingDomainObject::class)
+ ->once()
+ ->andReturnSelf()
+ ->getMock()
+ ->shouldReceive('findFirstWhere')
+ ->andReturn($organizer);
+
+ $this->eventRepository->shouldReceive('create')->andReturn($eventData);
+
+ $this->config->shouldReceive('get')
+ ->with('filesystems.public')
+ ->andReturn('public');
+ $this->config->shouldReceive('get')
+ ->with('app.event_categories_cover_images_path')
+ ->andReturn('event-covers');
+
+ $mockDisk = Mockery::mock();
+ $mockDisk->shouldReceive('exists')
+ ->with('event-covers/MUSIC.jpg')
+ ->andReturn(false);
+
+ $this->filesystemManager->shouldReceive('disk')
+ ->with('public')
+ ->andReturn($mockDisk);
+
+ $this->eventSettingsRepository->shouldReceive('create')
+ ->with(Mockery::on(function ($arg) {
+ return !array_key_exists('font_family', $arg['homepage_theme_settings'] ?? []);
+ }));
+
+ $this->eventStatisticsRepository->shouldReceive('create');
+
+ $this->purifier->shouldReceive('purify')->andReturn('Test Description');
+
+ $this->createEventService->createEvent($eventData);
+
+ $this->assertTrue(true);
+ }
+
private function createMockEventDomainObject(): EventDomainObject
{
return Mockery::mock(EventDomainObject::class, static function ($mock) {
diff --git a/frontend/src/components/common/ThemeFontControl/ThemeFontControl.module.scss b/frontend/src/components/common/ThemeFontControl/ThemeFontControl.module.scss
new file mode 100644
index 0000000000..b6b6dfda58
--- /dev/null
+++ b/frontend/src/components/common/ThemeFontControl/ThemeFontControl.module.scss
@@ -0,0 +1,18 @@
+.option {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ width: 100%;
+ gap: 12px;
+}
+
+.optionLabel {
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.optionSample {
+ font-size: 13px;
+ color: var(--mantine-color-dimmed);
+ letter-spacing: 0.5px;
+}
diff --git a/frontend/src/components/common/ThemeFontControl/index.tsx b/frontend/src/components/common/ThemeFontControl/index.tsx
new file mode 100644
index 0000000000..8fd4c2eee6
--- /dev/null
+++ b/frontend/src/components/common/ThemeFontControl/index.tsx
@@ -0,0 +1,87 @@
+import {Select, Text} from "@mantine/core";
+import {t} from "@lingui/macro";
+import {useEffect, useMemo} from "react";
+import {
+ buildHomepageFontStack,
+ buildHomepageFontUrl,
+ DEFAULT_HOMEPAGE_FONT,
+ HOMEPAGE_FONTS,
+} from "../../../constants/homepageFonts.ts";
+import {ensureHomepageFontLoaded} from "../../../utilites/fontLoader.ts";
+import classes from "./ThemeFontControl.module.scss";
+
+interface ThemeFontControlProps {
+ value: string | null | undefined;
+ onChange: (fontFamily: string) => void;
+ disabled?: boolean;
+}
+
+export const ThemeFontControl = ({value, onChange, disabled = false}: ThemeFontControlProps) => {
+ const selected = value || DEFAULT_HOMEPAGE_FONT;
+
+ const data = useMemo(
+ () => HOMEPAGE_FONTS.map(font => ({value: font.value, label: font.label})),
+ [],
+ );
+
+ useEffect(() => {
+ HOMEPAGE_FONTS.forEach(font => {
+ if (font.value === DEFAULT_HOMEPAGE_FONT || typeof document === 'undefined') {
+ return;
+ }
+ const id = `hi-font-preview-${font.bunnyFamily}`;
+ if (document.getElementById(id)) {
+ return;
+ }
+ const link = document.createElement('link');
+ link.id = id;
+ link.rel = 'stylesheet';
+ link.href = buildHomepageFontUrl(font.value);
+ document.head.appendChild(link);
+ });
+ }, []);
+
+ useEffect(() => {
+ ensureHomepageFontLoaded(selected);
+ }, [selected]);
+
+ const handleChange = (next: string | null) => {
+ if (!next) {
+ return;
+ }
+ onChange(next);
+ };
+
+ const renderOption = ({option}: {option: {value: string; label: string}}) => (
+
+ {option.label}
+ Aa 123
+
+ );
+
+ return (
+
+
+
+ {t`The quick brown fox jumps over the lazy dog.`}
+
+
+ );
+};
+
+export default ThemeFontControl;
diff --git a/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss b/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss
index 1637015eb8..6b7ddffe08 100644
--- a/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss
+++ b/frontend/src/components/layouts/EventHomepage/EventHomepage.module.scss
@@ -1,8 +1,8 @@
@use "../../../styles/mixins.scss";
// Design tokens
-$font-display: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-$font-body: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+$font-display: var(--theme-font-family, 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
+$font-body: var(--theme-font-family, 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
$radius-xl: 28px;
$radius-lg: 20px;
diff --git a/frontend/src/components/layouts/EventHomepage/index.tsx b/frontend/src/components/layouts/EventHomepage/index.tsx
index 223f400a79..7dd831ab80 100644
--- a/frontend/src/components/layouts/EventHomepage/index.tsx
+++ b/frontend/src/components/layouts/EventHomepage/index.tsx
@@ -37,6 +37,7 @@ import {useOrganizerTrackingPixels} from "../../../hooks/useOrganizerTrackingPix
import {trackPixelEvent, hasActivePixels} from "../../../utilites/trackingPixels";
import {CookieConsentBanner} from "../../common/CookieConsentBanner";
import {removeTransparency} from "../../../utilites/colorHelper.ts";
+import {ensureHomepageFontLoaded} from "../../../utilites/fontLoader.ts";
import {ShareComponent} from "../../common/ShareIcon";
import {EventDateRange} from "../../common/EventDateRange";
import {CalendarOptionsPopover} from "../../common/CalendarOptionsPopover";
@@ -116,6 +117,10 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => {
const cssVars = computeThemeVariables(themeSettings);
const backgroundType = themeSettings.background_type;
+ useEffect(() => {
+ ensureHomepageFontLoaded(themeSettings.font_family);
+ }, [themeSettings.font_family]);
+
const themeStyles = {
'--event-bg-color': themeSettings.background,
'--event-content-bg-color': cssVars['--theme-surface'],
@@ -127,6 +132,8 @@ const EventHomepage = ({...loaderData}: EventHomepageProps) => {
'--event-accent-soft': cssVars['--theme-accent-soft'],
'--event-accent-muted': cssVars['--theme-accent-muted'],
'--event-border-color': cssVars['--theme-border'],
+ '--theme-font-family': cssVars['--theme-font-family'],
+ fontFamily: cssVars['--theme-font-family'],
} as React.CSSProperties;
const coverImageData = eventCoverImage(event);
diff --git a/frontend/src/components/layouts/OrganizerHomepage/EventCard/EventCard.module.scss b/frontend/src/components/layouts/OrganizerHomepage/EventCard/EventCard.module.scss
index 2375552ead..b13708d504 100644
--- a/frontend/src/components/layouts/OrganizerHomepage/EventCard/EventCard.module.scss
+++ b/frontend/src/components/layouts/OrganizerHomepage/EventCard/EventCard.module.scss
@@ -1,8 +1,8 @@
@use "../../../../styles/mixins.scss";
// Design tokens - matching OrganizerHomepage
-$font-display: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-$font-body: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+$font-display: var(--theme-font-family, 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
+$font-body: var(--theme-font-family, 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
$radius-lg: 20px;
$radius-md: 14px;
diff --git a/frontend/src/components/layouts/OrganizerHomepage/OrganizerHomepage.module.scss b/frontend/src/components/layouts/OrganizerHomepage/OrganizerHomepage.module.scss
index 46cb458793..51058c29ac 100644
--- a/frontend/src/components/layouts/OrganizerHomepage/OrganizerHomepage.module.scss
+++ b/frontend/src/components/layouts/OrganizerHomepage/OrganizerHomepage.module.scss
@@ -1,8 +1,8 @@
@use "../../../styles/mixins.scss";
// Design tokens - matching EventHomepage
-$font-display: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-$font-body: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+$font-display: var(--theme-font-family, 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
+$font-body: var(--theme-font-family, 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
$radius-xl: 28px;
$radius-lg: 20px;
diff --git a/frontend/src/components/layouts/OrganizerHomepage/index.tsx b/frontend/src/components/layouts/OrganizerHomepage/index.tsx
index 18c1920f32..769e82730b 100644
--- a/frontend/src/components/layouts/OrganizerHomepage/index.tsx
+++ b/frontend/src/components/layouts/OrganizerHomepage/index.tsx
@@ -17,6 +17,7 @@ import {StatusToggle} from "../../common/StatusToggle";
import {getConfig} from "../../../utilites/config.ts";
import {Pagination} from "../../common/Pagination";
import {computeThemeVariables, validateThemeSettings} from "../../../utilites/themeUtils.ts";
+import {ensureHomepageFontLoaded} from "../../../utilites/fontLoader.ts";
import {useOrganizerTrackingPixels} from "../../../hooks/useOrganizerTrackingPixels";
import {CookieConsentBanner} from "../../common/CookieConsentBanner";
@@ -91,6 +92,10 @@ export const OrganizerHomepage = ({
const cssVars = computeThemeVariables(themeSettings);
const backgroundType = themeSettings.background_type;
+ useEffect(() => {
+ ensureHomepageFontLoaded(themeSettings.font_family);
+ }, [themeSettings.font_family]);
+
const themeStyles = {
'--organizer-bg-color': themeSettings.background,
'--organizer-content-bg-color': cssVars['--theme-surface'],
@@ -102,6 +107,8 @@ export const OrganizerHomepage = ({
'--organizer-accent-soft': cssVars['--theme-accent-soft'],
'--organizer-accent-muted': cssVars['--theme-accent-muted'],
'--organizer-border-color': cssVars['--theme-border'],
+ '--theme-font-family': cssVars['--theme-font-family'],
+ fontFamily: cssVars['--theme-font-family'],
} as React.CSSProperties;
return (
diff --git a/frontend/src/components/routes/event/HomepageDesigner/index.tsx b/frontend/src/components/routes/event/HomepageDesigner/index.tsx
index 6332e3e59a..edf7bfbb03 100644
--- a/frontend/src/components/routes/event/HomepageDesigner/index.tsx
+++ b/frontend/src/components/routes/event/HomepageDesigner/index.tsx
@@ -19,7 +19,9 @@ import {ImageUploadDropzone} from "../../../common/ImageUploadDropzone";
import {queryClient} from "../../../../utilites/queryClient.ts";
import {GET_EVENT_PUBLIC_QUERY_KEY} from "../../../../queries/useGetEventPublic.ts";
import {ThemeColorControls} from "../../../common/ThemeColorControls";
+import {ThemeFontControl} from "../../../common/ThemeFontControl";
import {validateThemeSettings} from "../../../../utilites/themeUtils.ts";
+import {DEFAULT_HOMEPAGE_FONT} from "../../../../constants/homepageFonts.ts";
interface FormValues {
homepage_theme_settings: Partial;
@@ -38,7 +40,7 @@ const HomepageDesigner = () => {
const [iframeSrc, setIframeSrc] = useState(null);
const [iframeLoaded, setIframeLoaded] = useState(false);
const [lastCoverId, setLastCoverId] = useState(null);
- const [accordionValue, setAccordionValue] = useState(['images', 'colors', 'button']);
+ const [accordionValue, setAccordionValue] = useState(['images', 'colors', 'typography', 'button']);
const existingCover = eventImagesQuery.data?.find((image) => image.type === 'EVENT_COVER');
@@ -49,6 +51,7 @@ const HomepageDesigner = () => {
background: '#f5f3ff',
mode: 'light',
background_type: 'COLOR',
+ font_family: DEFAULT_HOMEPAGE_FONT,
},
continue_button_text: '',
}
@@ -240,6 +243,24 @@ const HomepageDesigner = () => {
+
+ }>
+ {t`Typography`}
+
+
+
+
+
+
}>
{t`Button Text`}
diff --git a/frontend/src/components/routes/organizer/OrganizerHomepageDesigner/index.tsx b/frontend/src/components/routes/organizer/OrganizerHomepageDesigner/index.tsx
index 7d79fdcd26..d88ba57d51 100644
--- a/frontend/src/components/routes/organizer/OrganizerHomepageDesigner/index.tsx
+++ b/frontend/src/components/routes/organizer/OrganizerHomepageDesigner/index.tsx
@@ -9,7 +9,7 @@ import {showSuccess} from "../../../../utilites/notifications.tsx";
import {t} from "@lingui/macro";
import {useForm} from "@mantine/form";
import {Accordion, Button, Group, Stack, Text} from "@mantine/core";
-import {IconColorPicker, IconHelp, IconPalette, IconPhoto} from "@tabler/icons-react";
+import {IconColorPicker, IconHelp, IconPalette, IconPhoto, IconTypography} from "@tabler/icons-react";
import {Tooltip} from "../../../common/Tooltip";
import {LoadingMask} from "../../../common/LoadingMask";
import {CustomSelect} from "../../../common/CustomSelect";
@@ -19,7 +19,9 @@ import {organizerPreviewPath} from "../../../../utilites/urlHelper.ts";
import {queryClient} from "../../../../utilites/queryClient.ts";
import {GET_ORGANIZER_PUBLIC_QUERY_KEY} from "../../../../queries/useGetOrganizerPublic.ts";
import {ThemeColorControls} from "../../../common/ThemeColorControls";
+import {ThemeFontControl} from "../../../common/ThemeFontControl";
import {computeThemeVariables, validateThemeSettings} from "../../../../utilites/themeUtils.ts";
+import {DEFAULT_HOMEPAGE_FONT} from "../../../../constants/homepageFonts.ts";
interface FormValues {
homepage_theme_settings: Partial;
@@ -38,7 +40,7 @@ const OrganizerHomepageDesigner = () => {
const [iframeSrc, setIframeSrc] = useState(null);
const [iframeLoaded, setIframeLoaded] = useState(false);
- const [accordionValue, setAccordionValue] = useState(['images', 'theme']);
+ const [accordionValue, setAccordionValue] = useState(['images', 'theme', 'typography']);
const [lastCoverId, setLastCoverId] = useState(null);
const [lastLogoId, setLastLogoId] = useState(null);
@@ -52,6 +54,7 @@ const OrganizerHomepageDesigner = () => {
background: '#f5f3ff',
mode: 'light',
background_type: 'COLOR',
+ font_family: DEFAULT_HOMEPAGE_FONT,
},
}
});
@@ -271,6 +274,25 @@ const OrganizerHomepageDesigner = () => {
+
+
+ }>
+ {t`Typography`}
+
+
+
+
+