{showSubtitle && subtitle &&
{subtitle}
}
+ {showReadMore && (
+
+ {readMoreLabel}
+
+ )}
{showDate && publishedAt && (
diff --git a/src/components/StoryImage/StoryImage.module.scss b/src/components/StoryImage/StoryImage.module.scss
index 3e4b3da72..ddf54056f 100644
--- a/src/components/StoryImage/StoryImage.module.scss
+++ b/src/components/StoryImage/StoryImage.module.scss
@@ -15,6 +15,7 @@
@include branding;
object-fit: cover;
+object-position: 50% 75%;
&.static {
position: static !important;
diff --git a/src/modules/Boilerplate/ui/Boilerplate.module.scss b/src/modules/Boilerplate/ui/Boilerplate.module.scss
index a9feec43f..1d99e4e21 100644
--- a/src/modules/Boilerplate/ui/Boilerplate.module.scss
+++ b/src/modules/Boilerplate/ui/Boilerplate.module.scss
@@ -24,9 +24,7 @@
}
.container {
- padding: $spacing-9 0;
color: var(--prezly-footer-text-color);
- background: var(--prezly-footer-background-color);
}
.columns {
@@ -37,6 +35,16 @@
}
}
+// Neumann: the boilerplate shows only the centered "About" block. The
+// contacts column and the social-media row are hidden.
+.contacts {
+ display: none !important;
+}
+
+.socialMedia {
+ display: none !important;
+}
+
.heading {
@include heading-3;
@@ -50,25 +58,13 @@
.aboutUs {
flex-grow: 2;
- margin-bottom: $spacing-6;
-
- @include desktop-up {
- margin-bottom: 0;
-
- + .contacts {
- margin-left: $spacing-6;
- }
- }
-}
-
-.contacts {
- @include tablet-up {
- min-width: 350px;
- }
+ margin: 0 auto;
+ max-width: 900px;
+ text-align: center;
}
.about {
- margin-bottom: $spacing-5;
+ margin-bottom: 0;
p {
@include text-label;
@@ -107,10 +103,6 @@
}
}
-.socialMedia {
- margin-bottom: $spacing-3;
-}
-
.link,
.about a {
color: var(--prezly-footer-text-color);
diff --git a/src/modules/Boilerplate/ui/Boilerplate.tsx b/src/modules/Boilerplate/ui/Boilerplate.tsx
index 57e45abb7..894473044 100644
--- a/src/modules/Boilerplate/ui/Boilerplate.tsx
+++ b/src/modules/Boilerplate/ui/Boilerplate.tsx
@@ -7,6 +7,7 @@ import classNames from 'classnames';
import { SocialMedia } from '@/components/SocialMedia';
import { PREVIEW } from '@/events';
import { IconBuilding, IconEmail, IconExternalLink, IconGlobe, IconPhone } from '@/icons';
+import { useBroadcastedPageTypeCheck } from '@/modules/Broadcast';
import { analytics, isPreviewActive } from '@/utils';
import { getWebsiteHostname } from '../utils';
@@ -21,6 +22,7 @@ interface Props {
export function Boilerplate({ newsroom, companyInformation }: Props) {
const { formatMessage } = useIntl();
const isPreview = isPreviewActive();
+ const isSearchPage = useBroadcastedPageTypeCheck('search');
const siteInfoSettingsUrl = `https://rock.prezly.com/sites/${newsroom.uuid}/settings/information`;
const hasAboutInformation = Helper.hasAnyAboutInformation(companyInformation);
@@ -30,6 +32,11 @@ export function Boilerplate({ newsroom, companyInformation }: Props) {
const hasPhone = Boolean(companyInformation.phone);
const hasEmail = Boolean(companyInformation.email);
+ // Neumann: the boilerplate is not shown on the search results page.
+ if (isSearchPage) {
+ return null;
+ }
+
if (!hasAboutInformation && !hasContactInformation && !isPreview) {
return null;
}
diff --git a/src/modules/Category/Category.tsx b/src/modules/Category/Category.tsx
index e4f78d149..92d671ee2 100644
--- a/src/modules/Category/Category.tsx
+++ b/src/modules/Category/Category.tsx
@@ -1,9 +1,7 @@
import type { Category as CategoryType, TranslatedCategory } from '@prezly/sdk';
-
import { app } from '@/adapters/server';
import { PageTitle } from '@/components/PageTitle';
import type { ThemeSettings } from '@/theme-settings';
-
import { InfiniteStories } from '../InfiniteStories';
interface Props {
@@ -40,7 +38,7 @@ export async function Category({
>
);
-}
+}
\ No newline at end of file
diff --git a/src/modules/Contacts/ui/Contacts.module.scss b/src/modules/Contacts/ui/Contacts.module.scss
index b39127333..18e0732d7 100644
--- a/src/modules/Contacts/ui/Contacts.module.scss
+++ b/src/modules/Contacts/ui/Contacts.module.scss
@@ -1,23 +1,15 @@
.contacts {
- margin-top: $spacing-8;
- padding-top: $spacing-8;
-
- &::before {
- content: '';
- display: block;
- border-top: 1px solid var(--prezly-border-color);
- position: absolute;
- left: 0;
- right: 0;
- translate: 0 (-$spacing-8);
- }
+ margin-bottom: $spacing-8;
}
.title {
- @include heading-1;
-
margin: 0 auto $spacing-6;
- text-align: left;
+ text-align: center;
+ font-family: var(--prezly-font-family);
+ font-weight: 300;
+ font-size: 2.5rem;
+ line-height: 1.2;
+ color: var(--neumann-text-strong);
}
.grid {
diff --git a/src/modules/ContactsAndBoilerplate/ContactsAndBoilerplate.module.scss b/src/modules/ContactsAndBoilerplate/ContactsAndBoilerplate.module.scss
new file mode 100644
index 000000000..24de13412
--- /dev/null
+++ b/src/modules/ContactsAndBoilerplate/ContactsAndBoilerplate.module.scss
@@ -0,0 +1,32 @@
+// Unified Contacts + Boilerplate band.
+//
+// On the live newsroom both the "Contact us" grid and the "About" boilerplate
+// share ONE continuous full-bleed grey band (#1c1c1c) that spans the entire
+// viewport width, while their content stays within the centered content rail.
+// We reproduce that with a transparent wrapper + a full-bleed `::before`
+// (left/right -50vw), exactly like the source.
+//
+// This block only renders on the home and category pages (matching the live
+// site); story / media / search pages do not show it.
+
+.section {
+ position: relative;
+ z-index: 1;
+ padding: $spacing-9 0;
+
+ &::before {
+ content: '';
+ display: block;
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ bottom: 0;
+ left: -50vw;
+ right: -50vw;
+ background-color: var(--prezly-background-color-intermediate);
+ }
+
+ @include mobile-only {
+ padding: $spacing-7 0;
+ }
+}
diff --git a/src/modules/ContactsAndBoilerplate/ContactsAndBoilerplate.tsx b/src/modules/ContactsAndBoilerplate/ContactsAndBoilerplate.tsx
new file mode 100644
index 000000000..0cd08ac36
--- /dev/null
+++ b/src/modules/ContactsAndBoilerplate/ContactsAndBoilerplate.tsx
@@ -0,0 +1,24 @@
+import type { Locale } from '@prezly/theme-kit-nextjs';
+
+import { Boilerplate } from '@/modules/Boilerplate';
+import { Contacts } from '@/modules/Contacts';
+
+import styles from './ContactsAndBoilerplate.module.scss';
+
+interface Props {
+ localeCode: Locale.Code;
+}
+
+/**
+ * Renders the "Contact us" grid and the "About" boilerplate together inside a
+ * single full-bleed grey band, matching the live newsroom. Used on the home
+ * and category pages only.
+ */
+export function ContactsAndBoilerplate({ localeCode }: Props) {
+ return (
+
+ );
+}
diff --git a/src/modules/ContactsAndBoilerplate/index.ts b/src/modules/ContactsAndBoilerplate/index.ts
new file mode 100644
index 000000000..76de0b954
--- /dev/null
+++ b/src/modules/ContactsAndBoilerplate/index.ts
@@ -0,0 +1 @@
+export { ContactsAndBoilerplate } from './ContactsAndBoilerplate';
diff --git a/src/modules/CookieConsent/components/OneTrustCookie/OneTrustManager.ts b/src/modules/CookieConsent/components/OneTrustCookie/OneTrustManager.ts
index 997dc5fb7..6729ef90c 100644
--- a/src/modules/CookieConsent/components/OneTrustCookie/OneTrustManager.ts
+++ b/src/modules/CookieConsent/components/OneTrustCookie/OneTrustManager.ts
@@ -45,6 +45,19 @@ export function OneTrustManager({ category, isPreview }: Props) {
return;
}
+ if (isPreview) {
+ oneTrust.AllowAll();
+ setConsent({
+ categories: [
+ ConsentCategory.NECESSARY,
+ ConsentCategory.FIRST_PARTY_ANALYTICS,
+ ConsentCategory.THIRD_PARTY_COOKIES,
+ ],
+ });
+ document.body.removeEventListener(ONETRUST_INTEGRATION_EVENT, onOneTrustLoaded);
+ return;
+ }
+
oneTrust.OnConsentChanged(handleConsentChange);
oneTrust.Init();
if (isPreview) {
diff --git a/src/modules/Head/components/BrandingSettings.tsx b/src/modules/Head/components/BrandingSettings.tsx
index 8d3f8c5f4..0e43a28dc 100644
--- a/src/modules/Head/components/BrandingSettings.tsx
+++ b/src/modules/Head/components/BrandingSettings.tsx
@@ -108,19 +108,31 @@ function renderCustomFontElements(customFont: CustomFont) {
}
function renderPresetFontElements(font: Font) {
- const primaryGoogleFontName = getGoogleFontName(font).replace(' ', '+');
+ const primaryGoogleFontName = getGoogleFontName(font);
+
+ // Self-hosted fonts return null from getGoogleFontName (e.g. FF Unit Pro
+ // is loaded via @font-face in src/styles/_fonts.scss). Skip the Google
+ // Fonts link entirely in that case.
+ if (!primaryGoogleFontName) {
+ return null;
+ }
+
+ const primaryFamilyParam = primaryGoogleFontName.replace(' ', '+');
const relatedFont = getRelatedFont(font);
- let families = [];
+ let families: string[] = [];
if (relatedFont) {
- const relatedGoogleFontName = getGoogleFontName(relatedFont).replace(' ', '+');
-
- families = [
- `${primaryGoogleFontName}:wght@600`,
- `${relatedGoogleFontName}:wght@400;500;600;700;900`,
- ];
+ const relatedGoogleFontName = getGoogleFontName(relatedFont);
+ if (relatedGoogleFontName) {
+ families = [
+ `${primaryFamilyParam}:wght@600`,
+ `${relatedGoogleFontName.replace(' ', '+')}:wght@400;500;600;700;900`,
+ ];
+ } else {
+ families = [`${primaryFamilyParam}:wght@400;500;600;700;900`];
+ }
} else {
- families = [`${primaryGoogleFontName}:wght@400;500;600;700;900`];
+ families = [`${primaryFamilyParam}:wght@400;500;600;700;900`];
}
return
;
diff --git a/src/modules/Head/components/getCssVariables.ts b/src/modules/Head/components/getCssVariables.ts
index 05a677f94..7d71ab5d4 100644
--- a/src/modules/Head/components/getCssVariables.ts
+++ b/src/modules/Head/components/getCssVariables.ts
@@ -8,6 +8,36 @@ import {
type ThemeSettings,
} from '@/theme-settings';
+/**
+ * Neumann brand tokens.
+ *
+ * These hardcoded values override anything coming from the Prezly API's live
+ * `themeSettings` so the customized Neumann look is guaranteed regardless of
+ * what an admin configures in Prezly's UI. Source of truth for visual parity:
+ * https://newsroom.neumann.com/
+ */
+const NEUMANN_BRAND = {
+ accentColor: '#ef7b0b',
+ accentColorActive: '#ef7b0a', // slightly darker variant from live CSS
+ accentColorHover: '#f18823', // lighter "button hover" variant from live CSS
+ backgroundColor: '#000000', // page background
+ backgroundColorSecondary: '#2d2d2d', // card / dropdown background
+ backgroundColorIntermediate: '#1c1c1c', // mobile drawer / mid layer
+ backgroundColorTertiary: '#141414', // CTA button background
+ textColor: '#c1c1c1', // body text
+ textColorSecondary: '#cccccc', // dates, secondary labels
+ textColorTertiary: '#a7a7a7', // tertiary / disabled
+ textColorStrong: '#ffffff', // headings, strong emphasis
+ headerBackground: '#000000',
+ headerLinkColor: '#c1c1c1',
+ headerLinkHoverColor: '#ef7b0b',
+ footerBackground: '#000000',
+ footerTextColor: '#ffffff',
+ borderColor: 'rgba(193, 193, 193, 0.2)',
+ borderColorSecondary: 'rgba(193, 193, 193, 0.3)',
+ fontFamily: FONT_FAMILY[Font.FF_UNIT_PRO],
+};
+
function getFontFamily(font: Font, customFont: CustomFont | null): string {
if (font === Font.CUSTOM) {
if (customFont) {
@@ -34,46 +64,34 @@ function getParagraphFontFamily(font: Font, customFont: CustomFont | null): stri
}
export function getCssVariables(settings: ThemeSettings): Record
{
- const {
- accent_color,
- background_color,
- custom_font,
- font,
- footer_background_color,
- footer_text_color,
- header_background_color,
- header_link_color,
- text_color,
- } = settings;
+ const { custom_font, font } = settings;
- const placeholderBackgroundColor = tinycolor(header_background_color);
+ // Start from the Neumann brand palette. Live API settings cannot override
+ // these — they are part of the theme's identity, not user-configurable.
+ const accent_color = NEUMANN_BRAND.accentColor;
+ const background_color = NEUMANN_BRAND.backgroundColor;
+ const text_color = NEUMANN_BRAND.textColor;
+ const header_background_color = NEUMANN_BRAND.headerBackground;
+ const header_link_color = NEUMANN_BRAND.headerLinkColor;
+ const footer_background_color = NEUMANN_BRAND.footerBackground;
+ const footer_text_color = NEUMANN_BRAND.footerTextColor;
- const accentColorActive = tinycolor(accent_color).darken(10);
- const accentColorHover = tinycolor(accent_color).lighten(5);
+ const placeholderBackgroundColor = tinycolor(header_background_color);
+ // Auto-derived button text color — stays correct if the accent ever
+ // changes. For Neumann's orange this resolves to white.
const accentColorButtonText = tinycolor(accent_color).isLight() ? '#000000' : '#ffffff';
- const backgroundColorSecondary = tinycolor(background_color).isLight()
- ? tinycolor(background_color).darken(2)
- : tinycolor(background_color).lighten(2);
-
- const backgroundColorIntermidiate = tinycolor(background_color).isLight()
- ? tinycolor(background_color).darken(3)
- : tinycolor(background_color).lighten(3);
-
- const backgroundColorTertiary = tinycolor(background_color).isLight()
- ? tinycolor(background_color).darken(5)
- : tinycolor(background_color).lighten(5);
-
- const textColorSecondary = tinycolor(text_color).setAlpha(0.8);
- const textColorTertiary = tinycolor(text_color).setAlpha(0.6);
-
- const borderColor = tinycolor(text_color).setAlpha(0.2);
- const borderColorSecondary = tinycolor(text_color).setAlpha(0.3);
+ const fontFamily =
+ font === Font.FF_UNIT_PRO ? NEUMANN_BRAND.fontFamily : getFontFamily(font, custom_font);
+ const fontFamilySecondary =
+ font === Font.FF_UNIT_PRO
+ ? NEUMANN_BRAND.fontFamily
+ : getParagraphFontFamily(font, custom_font);
const fontVariables: Record = {
- '--prezly-font-family': getFontFamily(font, custom_font),
- '--prezly-font-family-secondary': getParagraphFontFamily(font, custom_font),
+ '--prezly-font-family': fontFamily,
+ '--prezly-font-family-secondary': fontFamilySecondary,
};
if (font === Font.CUSTOM && custom_font) {
@@ -83,23 +101,23 @@ export function getCssVariables(settings: ThemeSettings): Record
return {
...fontVariables,
- '--prezly-border-color': borderColor.toHex8String(),
- '--prezly-border-color-secondary': borderColorSecondary.toHex8String(),
+ '--prezly-border-color': NEUMANN_BRAND.borderColor,
+ '--prezly-border-color-secondary': NEUMANN_BRAND.borderColorSecondary,
'--prezly-text-color': text_color,
- '--prezly-text-color-secondary': textColorSecondary.toHex8String(),
- '--prezly-text-color-tertiary': textColorTertiary.toHex8String(),
- '--prezly-text-color-hover': text_color,
+ '--prezly-text-color-secondary': NEUMANN_BRAND.textColorSecondary,
+ '--prezly-text-color-tertiary': NEUMANN_BRAND.textColorTertiary,
+ '--prezly-text-color-hover': NEUMANN_BRAND.textColorStrong,
'--prezly-background-color': background_color,
- '--prezly-background-color-secondary': backgroundColorSecondary.toHex8String(),
- '--prezly-background-color-intermediate': backgroundColorIntermidiate.toHex8String(),
- '--prezly-background-color-tertiary': backgroundColorTertiary.toHex8String(),
+ '--prezly-background-color-secondary': NEUMANN_BRAND.backgroundColorSecondary,
+ '--prezly-background-color-intermediate': NEUMANN_BRAND.backgroundColorIntermediate,
+ '--prezly-background-color-tertiary': NEUMANN_BRAND.backgroundColorTertiary,
'--prezly-accent-color': accent_color,
- '--prezly-accent-color-active': accentColorActive.toHex8String(),
- '--prezly-accent-color-hover': accentColorHover.toHex8String(),
+ '--prezly-accent-color-active': NEUMANN_BRAND.accentColorActive,
+ '--prezly-accent-color-hover': NEUMANN_BRAND.accentColorHover,
'--prezly-accent-color-button-text': accentColorButtonText,
'--prezly-header-background-color': header_background_color,
'--prezly-header-link-color': header_link_color,
- '--prezly-header-link-hover-color': header_link_color,
+ '--prezly-header-link-hover-color': NEUMANN_BRAND.headerLinkHoverColor,
'--prezly-footer-background-color': footer_background_color,
'--prezly-footer-text-color': footer_text_color,
'--prezly-footer-text-color-variation': footer_text_color,
@@ -107,5 +125,7 @@ export function getCssVariables(settings: ThemeSettings): Record
'--prezly-white': '#ffffff',
'--prezly-black': '#000000',
'--prezly-grey': '#757575',
+ // Neumann-specific tokens for components that need them directly.
+ '--neumann-text-strong': NEUMANN_BRAND.textColorStrong,
};
}
diff --git a/src/modules/Header/Header.tsx b/src/modules/Header/Header.tsx
index 51a435c31..6a29b4a7a 100644
--- a/src/modules/Header/Header.tsx
+++ b/src/modules/Header/Header.tsx
@@ -1,4 +1,3 @@
-import type { Newsroom } from '@prezly/sdk';
import type { Locale } from '@prezly/theme-kit-nextjs';
import { app, getSearchSettings } from '@/adapters/server';
@@ -23,8 +22,6 @@ export async function Header({ localeCode }: Props) {
categories.filter((category) => category.i18n[localeCode]?.public_stories_number > 0),
);
- const memberNewsrooms = await getMemberNewsrooms(newsroom);
-
return (
);
}
-
-async function getMemberNewsrooms(newsroom: Newsroom) {
- if (newsroom.is_hub) {
- const members = await app().client.newsroomHub.list(newsroom.uuid);
- return members.map((member) => member.newsroom);
- }
-
- return [];
-}
diff --git a/src/modules/Header/ui/Header.module.scss b/src/modules/Header/ui/Header.module.scss
index 9d859b736..c3296f842 100644
--- a/src/modules/Header/ui/Header.module.scss
+++ b/src/modules/Header/ui/Header.module.scss
@@ -5,6 +5,50 @@ $header-height: 88px;
background: var(--prezly-header-background-color);
min-height: $header-height;
z-index: 3;
+
+ // Header spans a wider 1600px rail than the rest of the page content
+ // (which is capped at 1280px). Override the inner global `.container`.
+ :global(.container) {
+ max-width: 1600px;
+ }
+}
+
+// Sticky header: once the user scrolls past the full-size header, it is fixed
+// to the top of the viewport and re-enters with a fade-in-down slide (matching
+// the live newsroom's `sticky animated fadeInDown` effect).
+.sticky {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ min-height: 0;
+ z-index: 9999;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
+ animation: fadeInDown 0.5s both;
+
+ // Slightly tighter vertical padding when stuck, like the source.
+ .header {
+ padding: $spacing-2 0;
+ }
+}
+
+// Occupies the space vacated by the now-fixed header so page content does not
+// jump upward when the header becomes sticky.
+.stickySpacer {
+ min-height: $header-height;
+}
+
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ transform: translate3d(0, -100%, 0);
+ }
+
+ to {
+ opacity: 1;
+ transform: none;
+ }
}
.header {
@@ -28,6 +72,9 @@ $header-height: 88px;
.navigationButton {
color: var(--prezly-header-link-color);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ font-weight: 300;
@include mobile-only {
@include text-label;
@@ -43,6 +90,7 @@ $header-height: 88px;
&:hover,
&:active,
&:focus {
+ color: var(--prezly-accent-color);
opacity: 1 !important;
}
@@ -52,7 +100,7 @@ $header-height: 88px;
}
.searchToggle {
- color: var(--prezly-header-link-color);
+ color: var(--prezly-accent-color);
@include mobile-only {
font-size: $font-size-m;
@@ -62,6 +110,12 @@ $header-height: 88px;
@include tablet-up {
order: 10;
+ margin-right: $spacing-3;
+ }
+
+ &:hover,
+ &:focus {
+ color: var(--prezly-accent-color-hover);
}
&:active {
@@ -69,8 +123,8 @@ $header-height: 88px;
}
svg {
- width: 1.125rem;
- height: 1.125rem;
+ width: 1.25rem;
+ height: 1.25rem;
}
}
@@ -128,18 +182,13 @@ $header-height: 88px;
margin-left: auto;
}
-/* Since search widget is in a portal, there is no better way of increasing the specificity other than adding global selectors */
-.navigation,
-div.mobileSearchWrapper {
+/* Mobile menu drawer */
+.navigation {
@include mobile-only {
position: fixed;
inset: $header-height 0 0 0;
overflow: hidden;
- }
-}
-.navigation {
- @include mobile-only {
&:not(.open) {
pointer-events: none;
}
diff --git a/src/modules/Header/ui/Header.tsx b/src/modules/Header/ui/Header.tsx
index c70eb5391..582cb581d 100644
--- a/src/modules/Header/ui/Header.tsx
+++ b/src/modules/Header/ui/Header.tsx
@@ -18,24 +18,22 @@ import { useEffect, useMemo, useState } from 'react';
import { FormattedMessage, useIntl } from '@/adapters/client';
import { Button, ButtonLink } from '@/components/Button';
-import { CategoriesBar } from '@/components/CategoriesBar';
import { Link } from '@/components/Link';
+import { NeumannCategoriesNav } from '@/custom/NeumannCategoriesNav';
import { useDevice, usePreviewSettings } from '@/hooks';
import { IconClose, IconExternalLink, IconMenu, IconSearch } from '@/icons';
-import { useBroadcastedPageTypeCheck } from '@/modules/Broadcast';
import type { ThemeSettings } from '@/theme-settings';
import type { SearchSettings } from '@/types';
import { isPreviewActive } from '@/utils';
-import { Categories } from './Categories';
import { Logo, LogoPlaceholder } from './Logo';
import styles from './Header.module.scss';
-const SearchWidget = dynamic(
+const NeumannSearchDrawer = dynamic(
async () => {
- const component = await import('./SearchWidget');
- return { default: component.SearchWidget };
+ const mod = await import('@/custom/NeumannSearchDrawer');
+ return { default: mod.NeumannSearchDrawer };
},
{ ssr: false },
);
@@ -54,7 +52,6 @@ interface Props {
logoSize: ThemeSettings['logo_size'];
mainSiteUrl: string | null;
mainSiteLabel: string | null;
- newsrooms: Newsroom[];
}
export function Header({
@@ -67,7 +64,6 @@ export function Header({
displayedGalleries,
displayedLanguages,
children,
- newsrooms,
...props
}: Props) {
const { formatMessage } = useIntl();
@@ -76,8 +72,8 @@ export function Header({
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isSearchOpen, setSearchOpen] = useState(false);
- const [measurement, headerRef] = useMeasure();
- const isSearchPage = useBroadcastedPageTypeCheck('search');
+ const [isSticky, setIsSticky] = useState(false);
+ const [, headerRef] = useMeasure();
const isPreviewMode = process.env.PREZLY_MODE === 'preview';
const isPreview = isPreviewActive();
@@ -131,6 +127,24 @@ export function Header({
};
}, [isMenuOpen]);
+ // Make the header sticky once the user scrolls past the full-size header
+ // (matches the live newsroom, which flips at ~110px). The sticky header
+ // re-enters with a fade-in-down animation defined in Header.module.scss.
+ useEffect(() => {
+ const STICKY_THRESHOLD = 110;
+
+ function onScroll() {
+ setIsSticky(window.scrollY > STICKY_THRESHOLD);
+ }
+
+ onScroll();
+ window.addEventListener('scroll', onScroll, { passive: true });
+
+ return () => {
+ window.removeEventListener('scroll', onScroll);
+ };
+ }, []);
+
const newsroomName = information.name || newsroom.display_name;
const logo = useMemo(() => {
@@ -194,7 +208,6 @@ export function Header({
return props.categoriesLayout;
}, [isPreviewMode, previewSettings, props.categoriesLayout, searchParams]);
- const isCategoriesLayoutBar = categoriesLayout === 'bar';
const isCategoriesLayoutDropdown = categoriesLayout === 'dropdown' || isMobile;
const numberOfPublicGalleries = newsroom.public_galleries_number;
const shouldShowSearchText =
@@ -207,7 +220,12 @@ export function Header({
return (
<>
-
- {isCategoriesLayoutBar &&
{restStories.map((story, index) => {
@@ -248,10 +248,13 @@ export function StoriesList({
text: newsroom?.name ?? '',
}}
forceAspectRatio
+ isCategoryList={isCategoryList}
layout="vertical"
placeholder={getNewsroomPlaceholderColors(newsroom)}
publishedAt={story.published_at}
showDate={showDate}
+ showReadMore
+ readMoreLabel={formatMessage(translations.actions.readMore)}
showSubtitle={showSubtitle}
size={getStoryCardSize(index)}
slug={story.slug}
@@ -307,6 +310,9 @@ export function StoriesList({
placeholder={getNewsroomPlaceholderColors(newsroom)}
publishedAt={story.published_at}
showDate={showDate}
+ isCategoryList={isCategoryList}
+ showReadMore
+ readMoreLabel={formatMessage(translations.actions.readMore)}
showSubtitle={showSubtitle}
size="medium"
slug={story.slug}
diff --git a/src/modules/InfiniteStories/lib/useStoryCardLayout.ts b/src/modules/InfiniteStories/lib/useStoryCardLayout.ts
index 1b7393be5..545077428 100644
--- a/src/modules/InfiniteStories/lib/useStoryCardLayout.ts
+++ b/src/modules/InfiniteStories/lib/useStoryCardLayout.ts
@@ -2,21 +2,12 @@
import { useCallback } from 'react';
-export function useStoryCardLayout(isFlatList: boolean) {
- const getStoryCardSize = useCallback(
- (index: number): 'small' | 'medium' | 'big' => {
- if (isFlatList) {
- return 'medium';
- }
-
- if (index < 2) {
- return 'big';
- }
-
- return 'medium';
- },
- [isFlatList],
- );
+export function useStoryCardLayout(_isFlatList: boolean) {
+ const getStoryCardSize = useCallback((_index: number): 'small' | 'medium' | 'big' => {
+ // Neumann renders a uniform 3-column grid of equally-sized cards
+ // (no "big" hero-style first cards), regardless of list type.
+ return 'medium';
+ }, []);
return getStoryCardSize;
}
diff --git a/src/modules/Search/Search.module.scss b/src/modules/Search/Search.module.scss
new file mode 100644
index 000000000..1a5d52cc0
--- /dev/null
+++ b/src/modules/Search/Search.module.scss
@@ -0,0 +1,40 @@
+.page {
+ position: relative;
+ background-color: var(--prezly-background-color);
+ padding-bottom: $spacing-9;
+
+ // Top decorative gradient: fades from #2d2d2d -> #000 over the title area.
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 360px;
+ background: linear-gradient(
+ 180deg,
+ var(--prezly-background-color-secondary, #2d2d2d) 0%,
+ var(--prezly-background-color, #000) 100%
+ );
+ pointer-events: none;
+ z-index: 0;
+ }
+
+ // Make sure the actual content sits above the decoration.
+ > * {
+ position: relative;
+ z-index: 1;
+ }
+}
+
+.body {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: $spacing-6;
+ align-items: start;
+
+ @include desktop-up {
+ grid-template-columns: minmax(0, 7fr) minmax(0, 3fr);
+ gap: $spacing-7;
+ }
+}
diff --git a/src/modules/Search/Search.tsx b/src/modules/Search/Search.tsx
index efab10f41..2f0a36f08 100644
--- a/src/modules/Search/Search.tsx
+++ b/src/modules/Search/Search.tsx
@@ -12,12 +12,15 @@ import { getSearchClient } from '@/utils';
import { Results } from './components/Results';
import { SearchBar } from './components/SearchBar';
+import { SearchInput } from './components/SearchInput';
import SearchStateContextProvider from './components/SearchStateContext';
import { Subtitle } from './components/Subtitle';
import { Title } from './components/Title';
import type { SearchState } from './types';
import { createUrl, queryToSearchState } from './utils';
+import styles from './Search.module.scss';
+
// const DEBOUNCE_TIME_MS = 300;
interface Props {
@@ -82,16 +85,21 @@ export function Search({
snippetEllipsisText="…"
/>
-
-
-
-
+
);
diff --git a/src/modules/Search/components/Facet.module.scss b/src/modules/Search/components/Facet.module.scss
index f5076e71f..b9ca7ef08 100644
--- a/src/modules/Search/components/Facet.module.scss
+++ b/src/modules/Search/components/Facet.module.scss
@@ -1,103 +1,132 @@
.container {
- @include mobile-only {
- width: 100%;
- }
-
- @include tablet-up {
- &:not(:last-child) {
- margin-right: $spacing-3;
- }
- }
+ width: 100%;
- @include mobile-only {
- &:not(:last-child) {
- margin-bottom: $spacing-1;
- }
- }
-}
-
-.menu {
- z-index: 1;
- width: max-content;
- min-width: 100%;
- max-width: 100%;
-
- @include mobile-only {
- max-width: none !important;
- width: 100%;
+ &:not(:last-child) {
+ margin-bottom: $spacing-5;
}
}
.title {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
+ margin: 0 0 $spacing-3;
+ padding-bottom: $spacing-2;
+ border-bottom: 1px solid var(--prezly-border-color);
-.caret {
- margin-left: $spacing-2;
- flex-shrink: 0;
- width: $spacing-3;
- height: $spacing-3;
-}
-
-.caretOpen {
- transform: rotate(180deg);
-}
-
-.panel {
- @include shadow-l;
- @include border-radius-m;
-
- position: absolute;
- z-index: 10;
- margin-top: $spacing-3;
- background-color: $color-base-white;
- padding: $spacing-3;
+ color: var(--neumann-text-strong, #fff);
+ font-size: 14px;
+ font-weight: $font-weight-semi-bold;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ line-height: 1.2;
}
.list {
list-style-type: none;
margin: 0;
padding: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.listItem {
+ margin: 0;
+ padding: 0;
}
.listItemInner {
- @include text-label;
-
display: flex;
align-items: center;
- padding: $spacing-2;
+ gap: $spacing-2;
+ padding: $spacing-1 0;
+ cursor: pointer;
+ color: var(--prezly-text-color);
+ font-size: $font-size-s;
+ line-height: 1.4;
+ transition: color 0.15s ease;
&:hover {
- cursor: pointer;
+ color: var(--prezly-accent-color);
+
+ .checkbox {
+ border-color: var(--prezly-accent-color);
+ }
+ }
+}
+
+// Hide the native checkbox but keep it accessible.
+.input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+
+ &:focus-visible + .checkbox {
+ outline: 2px solid var(--prezly-accent-color);
+ outline-offset: 2px;
+ }
+
+ &:checked + .checkbox {
+ background-color: var(--prezly-accent-color);
+ border-color: var(--prezly-accent-color);
+
+ .checkmark {
+ opacity: 1;
+ }
}
}
+.checkbox {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border: 1px solid var(--prezly-border-color-secondary);
+ background: transparent;
+ transition: background-color 0.15s ease, border-color 0.15s ease;
+}
+
+.checkmark {
+ width: 12px;
+ height: 12px;
+ color: $color-base-white;
+ opacity: 0;
+ transition: opacity 0.15s ease;
+}
+
.label {
- margin-left: $spacing-1;
+ flex-grow: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.count {
- white-space: nowrap;
- margin-left: $spacing-half;
- opacity: 0.75;
+ flex-shrink: 0;
+ color: var(--prezly-text-color-tertiary);
+ font-size: $font-size-xs;
}
.viewMore {
- @include text-label;
-
border: 0;
- padding: $spacing-1 0;
- margin-top: $spacing-1;
- margin-bottom: -1 * $spacing-1;
+ padding: $spacing-2 0 0;
+ margin: 0;
color: var(--prezly-accent-color);
font-weight: $font-weight-semi-bold;
+ font-size: $font-size-s;
text-decoration: none;
cursor: pointer;
+ background: transparent;
+ align-self: flex-start;
&:hover,
&:focus {
text-decoration: underline;
+ color: var(--prezly-accent-color);
}
}
diff --git a/src/modules/Search/components/Facet.tsx b/src/modules/Search/components/Facet.tsx
index dd43b2f75..7c54318ff 100644
--- a/src/modules/Search/components/Facet.tsx
+++ b/src/modules/Search/components/Facet.tsx
@@ -7,7 +7,6 @@ import { connectRefinementList } from 'react-instantsearch-dom';
import { FormattedMessage, useLocale } from '@/adapters/client';
import { Button } from '@/components/Button';
-import { Dropdown } from '@/components/Dropdown';
import { type ArrayElement, FacetAttribute } from '../types';
@@ -81,26 +80,40 @@ export const Facet = connectRefinementList(
}
return (
-
{facetTitle}}
- className={styles.container}
- menuClassName={styles.menu}
- variation="secondary"
- >
- {visibleItems.map((item) => (
-
-
-
- ))}
+
+ {facetTitle}
+
{items.length > DEFAULT_FACETS_LIMIT && (
)}
-
+
);
},
);
diff --git a/src/modules/Search/components/Hit.tsx b/src/modules/Search/components/Hit.tsx
index d89789d06..ee7c6b844 100644
--- a/src/modules/Search/components/Hit.tsx
+++ b/src/modules/Search/components/Hit.tsx
@@ -1,12 +1,12 @@
'use client';
import type { Newsroom, TranslatedCategory } from '@prezly/sdk';
-import type { Search } from '@prezly/theme-kit-nextjs';
+import { type Search, translations } from '@prezly/theme-kit-nextjs';
import { useMemo } from 'react';
import type { Hit as HitType } from 'react-instantsearch-core';
import { Highlight, Snippet } from 'react-instantsearch-dom';
-import { useLocale } from '@/adapters/client';
+import { useIntl, useLocale } from '@/adapters/client';
import { StoryCard } from '@/components/StoryCards';
import type { ThemeSettings } from '@/theme-settings';
import type { ExternalStoryUrl } from '@/types';
@@ -29,6 +29,7 @@ export function Hit({ external, hit, newsroom, showDate, showSubtitle, storyCard
const { attributes: story } = hit;
const { categories } = story;
const localeCode = useLocale();
+ const { formatMessage } = useIntl();
const { searchState } = useSearchState();
const displayedCategories: TranslatedCategory[] = useMemo(
@@ -84,12 +85,15 @@ export function Hit({ external, hit, newsroom, showDate, showSubtitle, storyCard
image: newsroom.newsroom_logo,
text: newsroom.name,
}}
+ forceAspectRatio
layout="horizontal"
placeholder={getNewsroomPlaceholderColors(newsroom)}
publishedAt={new Date(story.published_at * 1000).toISOString()}
showDate={showDate}
+ showReadMore
+ readMoreLabel={formatMessage(translations.actions.readMore)}
showSubtitle={showSubtitleEffective}
- size="small"
+ size="medium"
slug={story.slug}
subtitle={subtitle}
thumbnailImage={story.thumbnail_image}
@@ -97,6 +101,7 @@ export function Hit({ external, hit, newsroom, showDate, showSubtitle, storyCard
titleAsString={hit.attributes.title}
translatedCategories={displayedCategories}
variant={storyCardVariant}
+ withStaticImage
/>
);
}
diff --git a/src/modules/Search/components/Results.module.scss b/src/modules/Search/components/Results.module.scss
index 949a7c512..b4203caf4 100644
--- a/src/modules/Search/components/Results.module.scss
+++ b/src/modules/Search/components/Results.module.scss
@@ -1,20 +1,16 @@
+// Search results use single-column, full-width horizontal story cards
+// (image-left / content-right), matching the live newsroom search page.
.list {
grid-template-columns: 1fr !important;
+ gap: $spacing-5;
}
.fallbackText {
- @include heading-2;
-
- padding-top: $spacing-8;
+ margin: 0;
+ padding: 80px 24px;
text-align: center;
- font-weight: 400;
- color: $color-base-500;
-
- @include mobile-only {
- padding-top: 0;
- }
-
- @include desktop-up {
- grid-column: span 3;
- }
+ font-weight: $font-weight-regular;
+ font-size: $font-size-l;
+ line-height: 1.5;
+ color: var(--prezly-text-color);
}
diff --git a/src/modules/Search/components/SearchBar.module.scss b/src/modules/Search/components/SearchBar.module.scss
index 32fdfd48e..84dd5517a 100644
--- a/src/modules/Search/components/SearchBar.module.scss
+++ b/src/modules/Search/components/SearchBar.module.scss
@@ -1,27 +1,25 @@
.container {
display: flex;
- justify-content: space-between;
- margin-bottom: $spacing-6;
+ flex-direction: column;
+ width: 100%;
- @include not-desktop {
- flex-direction: column;
+ @include desktop-up {
+ position: sticky;
+ top: $spacing-6;
+ }
+
+ &:empty {
+ display: none;
}
}
.facets {
- @include border-radius-m;
-
- padding: 0;
display: flex;
- align-items: center;
-
- @include not-desktop {
- margin-top: $spacing-3;
- width: 100%;
- }
+ flex-direction: column;
+ gap: $spacing-5;
+ width: 100%;
@include mobile-only {
- flex-direction: column;
display: none;
&.facetsOpen {
@@ -34,42 +32,13 @@
}
}
-.header {
- display: flex;
- align-items: flex-start;
-
- @include tablet-up {
- min-width: 300px;
- }
-
- @include desktop-up {
- min-width: 450px;
- }
-}
-
-.button {
- margin-left: $spacing-3;
- padding: calc(#{$spacing-3} - 1px) $spacing-3;
+.toggleFacets {
+ justify-content: flex-start;
+ padding-left: 0;
+ margin-bottom: $spacing-3;
+ color: var(--neumann-text-strong, #fff);
@include desktop-up {
display: none;
}
}
-
-.filters {
- @include text-label;
-
- margin-right: $spacing-3;
- font-weight: $font-weight-semi-bold;
- color: var(--prezly-text-color-secondary);
-
- @include not-desktop {
- display: none;
- }
-}
-
-.toggleFacets {
- justify-content: flex-start;
- padding-left: 0;
- margin-top: $spacing-3;
-}
diff --git a/src/modules/Search/components/SearchBar.tsx b/src/modules/Search/components/SearchBar.tsx
index b5880fd45..571c21059 100644
--- a/src/modules/Search/components/SearchBar.tsx
+++ b/src/modules/Search/components/SearchBar.tsx
@@ -12,7 +12,6 @@ import { IconMenu } from '@/icons';
import { AVAILABLE_FACET_ATTRIBUTES } from '../utils';
import { Facet } from './Facet';
-import { SearchInput } from './SearchInput';
import { useSearchState } from './SearchStateContext';
import styles from './SearchBar.module.scss';
@@ -32,10 +31,7 @@ export function SearchBar() {
}
return (
-
-
-
-
+
+
);
}
diff --git a/src/modules/Search/components/SearchInput.module.scss b/src/modules/Search/components/SearchInput.module.scss
index 80eacbf67..a4f63fe05 100644
--- a/src/modules/Search/components/SearchInput.module.scss
+++ b/src/modules/Search/components/SearchInput.module.scss
@@ -1,15 +1,39 @@
.container {
position: relative;
- margin: 0;
- flex-grow: 1;
+ margin: 0 auto $spacing-6;
+ width: 100%;
+ max-width: 720px;
}
.input {
margin: 0;
input {
- padding: $spacing-2 $spacing-3;
- border-color: $color-base-200;
+ width: 100%;
+ padding: $spacing-3 calc(#{$spacing-7} + #{$spacing-2}) $spacing-3 $spacing-4;
+ border: 1px solid var(--prezly-border-color);
+ border-radius: 0;
+ background-color: var(--prezly-background-color-intermediate, #1c1c1c);
+ color: var(--neumann-text-strong, #fff);
+ font-size: $font-size-m;
+ font-weight: $font-weight-regular;
+ transition: border-color 0.15s ease, background-color 0.15s ease;
+
+ &::placeholder {
+ color: var(--prezly-text-color-tertiary);
+ opacity: 1;
+ }
+
+ &:hover {
+ border-color: var(--prezly-border-color-secondary);
+ }
+
+ &:focus,
+ &:focus-visible {
+ outline: none;
+ border-color: var(--prezly-accent-color);
+ background-color: var(--prezly-background-color-intermediate, #1c1c1c);
+ }
}
/* clears the 'X' from Internet Explorer */
@@ -31,11 +55,19 @@
.button {
border-color: transparent !important;
- border-width: 2px;
+ border-width: 0 !important;
background-color: transparent !important;
padding: $spacing-2 !important;
position: absolute;
- right: 0;
- top: 0;
+ right: $spacing-2;
+ top: 50%;
+ transform: translateY(-50%);
font-size: $font-size-m;
+ color: var(--prezly-accent-color);
+
+ &:hover,
+ &:focus {
+ color: var(--prezly-accent-color);
+ background-color: transparent !important;
+ }
}
diff --git a/src/modules/Search/components/Subtitle.module.scss b/src/modules/Search/components/Subtitle.module.scss
index c6ebab283..03d4acf59 100644
--- a/src/modules/Search/components/Subtitle.module.scss
+++ b/src/modules/Search/components/Subtitle.module.scss
@@ -1,3 +1,12 @@
.subtitle {
min-height: $spacing-5;
+ margin: 0 0 $spacing-5;
+ text-align: center;
+ color: var(--prezly-text-color-secondary);
+ font-size: $font-size-s;
+
+ b {
+ color: var(--neumann-text-strong, #fff);
+ font-weight: $font-weight-semi-bold;
+ }
}
diff --git a/src/modules/Search/components/Title.module.scss b/src/modules/Search/components/Title.module.scss
new file mode 100644
index 000000000..6b0f471fd
--- /dev/null
+++ b/src/modules/Search/components/Title.module.scss
@@ -0,0 +1,12 @@
+.title {
+ margin: 50px 0;
+ text-align: center;
+
+ h1 {
+ margin: 0;
+ font-size: 2rem;
+ line-height: 3rem;
+ font-weight: $font-weight-regular;
+ color: var(--neumann-text-strong, #fff);
+ }
+}
diff --git a/src/modules/Search/components/Title.tsx b/src/modules/Search/components/Title.tsx
index 8f66d22b2..bc728465a 100644
--- a/src/modules/Search/components/Title.tsx
+++ b/src/modules/Search/components/Title.tsx
@@ -7,6 +7,8 @@ import { PageTitle } from '@/components/PageTitle';
import { useSearchState } from './SearchStateContext';
+import styles from './Title.module.scss';
+
export function Title() {
const { formatMessage } = useIntl();
const { searchState } = useSearchState();
@@ -15,6 +17,7 @@ export function Title() {
return (
/, so force regular weight on the element
+ // and all its descendants.
+ font-family: 'FF Unit Pro', 'Roboto', Arial, sans-serif;
+
+ &,
+ & * {
+ font-weight: $font-weight-regular !important;
+ }
}
.alignLeft {
diff --git a/src/modules/Story/Story.module.scss b/src/modules/Story/Story.module.scss
index 6f7379c5c..7b88e311a 100644
--- a/src/modules/Story/Story.module.scss
+++ b/src/modules/Story/Story.module.scss
@@ -1,13 +1,33 @@
-$story-container-max-width: 720px;
+$story-container-max-width: 900px;
.container {
margin: 0 auto;
- background: var(--prezly-background-color);
+ background: transparent;
+ padding-top: $spacing-4;
+ padding-bottom: $spacing-7;
@include desktop-up {
max-width: $story-container-max-width;
}
+ // Grey backdrop (#202020) spanning the full content width behind the
+ // narrow article column, matching the live story page.
+ &::before {
+ content: '';
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ translate: -50% 0;
+ width: 100vw;
+ background: #202020;
+ }
+
+ & {
+ position: relative;
+ }
+
:global {
.prezly-slate-bookmark-card-component {
&__container {
@@ -27,14 +47,186 @@ $story-container-max-width: 720px;
color: var(--prezly-text-color-secondary);
}
}
+
+ // Neumann press-contact card: dark-grey card, square corners, orange
+ // icons, white text, no avatar.
+ .prezly-slate-contact__wrapper {
+ background: var(--prezly-background-color-secondary);
+ border-radius: 0;
+ padding: $spacing-6;
+ }
+
+ .prezly-slate-contact__avatar {
+ display: none !important;
+ }
+
+ .prezly-slate-contact__name,
+ .prezly-slate-contact__job-description,
+ .prezly-slate-social-field {
+ color: var(--neumann-text-strong) !important;
+ }
+
+ .prezly-slate-social-field__icon {
+ color: var(--prezly-accent-color);
+ }
+ }
+}
+
+// =============================================================================
+// Story hero — header image with the title + subtitle overlaid in a centered
+// translucent well. Rebuilt to mirror https://newsroom.neumann.com/ story
+// pages exactly. Measured source values (desktop):
+// .story__header position:relative; display:flex; center/center;
+// width:1600px (capped, centered — NOT 100vw);
+// height:50vh; max-height:660px; overflow:hidden
+// .story__header-image position:absolute; inset:0; object-fit:cover;
+// object-position:50% 50%
+// .story__header__well rgba(0,0,0,.5); max-width:40vw (~640px); text-center;
+// padding:80px 40px; margin:60px
+// The source caps the band at 1600px to match its .main content container; we
+// break out of this fork's narrow 900px article column up to that same 1600px.
+// =============================================================================
+.hero {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+
+ // Break out of the 900px article column, capped & centered at 1600px to
+ // match the source (which is 1600px wide, NOT full viewport width).
+ width: min(100vw, 1600px);
+ margin-left: 50%;
+ translate: -50% 0;
+ margin-bottom: $spacing-6;
+
+ // Height tracks the viewport (50vh) with a cap, exactly like the source.
+ height: 50vh;
+ max-height: 660px;
+
+ @include not-desktop {
+ height: 500px;
+ max-height: 500px;
+ }
+
+ @include mobile-only {
+ height: 50vh;
+ max-height: 500px;
}
}
-.date {
- @include text-label;
+.heroImage {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ overflow: hidden;
+
+ // The slate image renderer nests the picture inside several wrappers
+ // (figure → rollover anchor → content span → img). Those wrappers take
+ // their natural/auto height, so forcing the img to `height:100%` made it
+ // resolve against a 1139px-tall wrapper and overflow the band (showing only
+ // the top slice, hiding the road). Instead, absolutely position the img to
+ // fill THIS container exactly (1600×band height) so `object-fit:cover`
+ // performs the crop — matching the source's `.story__header-image`.
+ :global(.prezly-slate-document),
+ :global(.prezly-slate-image),
+ :global(.prezly-slate-image-rollover),
+ :global(.prezly-slate-image-rollover__content) {
+ display: block;
+ position: static;
+ width: 100% !important;
+ max-width: 100% !important;
+ height: 100% !important;
+ margin: 0 !important;
+ }
+
+ img {
+ position: absolute;
+ inset: 0;
+ width: 100% !important;
+ height: 100% !important;
+ max-width: 100% !important;
+ margin: 0 !important;
+ object-fit: cover;
+ object-position: 50% 50%;
+ }
+}
+
+.heroWell {
+ position: relative;
+ z-index: 1;
+ background: rgba(0, 0, 0, 0.5);
+ text-align: center;
+ // Source: max-width 40vw (~640px at 1600px), padding 80px 40px, margin 60px.
+ max-width: 640px;
+ padding: 80px 40px;
+ margin: 60px;
+
+ @include not-desktop {
+ max-width: 60vw;
+ margin: 40px;
+ padding: 56px 36px;
+ }
+
+ @include mobile-only {
+ max-width: calc(100% - 32px);
+ margin: 16px;
+ padding: 32px 20px;
+ }
+
+ // Title + subtitle rendered by HeaderRenderer inside the well — force white
+ // title and light-grey subtitle, centered, regardless of the slate defaults.
+ // Source typography: title 29px/32px, subtitle 18px/20px, both weight 400.
+ // Matching these keeps the title at two lines so the well stays compact
+ // (~300px tall) and centered like the source, revealing the road below.
+ :global(.prezly-slate-heading--heading-1) {
+ color: var(--neumann-text-strong) !important;
+ font-size: 29px;
+ line-height: 32px;
+ font-weight: $font-weight-regular;
+ margin: 0 0 20px;
+ }
+
+ :global(.prezly-slate-heading--subtitle),
+ :global(.prezly-slate-heading--heading-2),
+ :global([class*='Subtitle_subtitle']) {
+ color: var(--prezly-text-color-secondary) !important;
+ font-size: 18px;
+ line-height: 20px;
+ font-weight: $font-weight-regular;
+ margin: 0;
+ }
+}
+
+.backLink {
+ display: inline-flex;
+ align-items: center;
+ gap: $spacing-1;
+ margin-bottom: $spacing-4;
+ color: var(--neumann-text-strong);
+ text-decoration: none;
+ text-transform: uppercase;
+ font-weight: $font-weight-medium;
+ letter-spacing: 0.02em;
+
+ // The article column is capped at 900px and centered within the content
+ // rail. Pull the back link out to hug the rail's left padding edge. The
+ // gap is (railInnerWidth − 900px) / 2, clamped to 0 so it never overshoots
+ // on narrower desktop widths where the rail is below its 1280px max.
+ @include desktop-up {
+ margin-left: calc(-1 * max(0px, (min(100vw, 1280px) - 80px - 900px) / 2));
+ }
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
- color: var(--prezly-text-color-secondary);
- margin: 0;
+.backIcon {
+ width: 16px;
+ height: 16px;
+ // The shared icon points right; flip it to point left for "Back".
+ transform: rotate(180deg);
}
.linksAndDateWrapper {
diff --git a/src/modules/Story/Story.tsx b/src/modules/Story/Story.tsx
index c734df185..9be49a2a8 100644
--- a/src/modules/Story/Story.tsx
+++ b/src/modules/Story/Story.tsx
@@ -3,11 +3,11 @@ import type { DocumentNode } from '@prezly/story-content-format';
import { ImageNode, TextAlignment } from '@prezly/story-content-format';
import classNames from 'classnames';
-import { FormattedDate } from '@/adapters/client';
-import { app } from '@/adapters/server';
+import { app, routing } from '@/adapters/server';
import { CategoriesList } from '@/components/CategoriesList';
import { ContentRenderer } from '@/components/ContentRenderer';
import { getRenderableSocialSharingNetworks, SocialShare } from '@/components/SocialShare';
+import { IconArrowRight } from '@/icons';
import type { StoryActions, ThemeSettings } from '@/theme-settings';
import { Embargo } from './Embargo';
@@ -36,7 +36,6 @@ export async function Story({
actions,
relatedStories,
sharingOptions,
- showDate,
story,
withBadges,
withHeaderImage,
@@ -64,14 +63,37 @@ export async function Story({
const categories = await app().translatedCategories(story.culture.code, story.categories);
const newsroom = await app().newsroom();
+ const { generateUrl } = await routing();
+ const backUrl = generateUrl('index', { localeCode: story.culture.code });
return (
-
- {withHeaderImage === 'above' && headerImageDocument && (
-
+ {withHeaderImage === 'above' && headerImageDocument ? (
+ <>
+
+
+
+ Back
+
+ >
+ ) : (
+ <>
+
+
+ Back
+
+
+ >
)}
+
{categories.length > 0 && (
)}
-
- {showDate && story.published_at && (
-
-
-
- )}
;
export const DEFAULT_THEME_SETTINGS: ThemeSettings = {
- accent_color: '#3b82f6',
- background_color: '#ffffff',
+ accent_color: '#ef7b0b',
+ background_color: '#000000',
categories_layout: 'dropdown',
custom_font: null,
- font: Font.INTER,
- footer_background_color: '#111827',
+ font: Font.FF_UNIT_PRO,
+ footer_background_color: '#000000',
footer_text_color: '#ffffff',
- full_width_featured_story: false,
- header_background_color: '#ffffff',
- header_image_placement: 'below',
- header_link_color: '#4b5563',
+ full_width_featured_story: true,
+ header_background_color: '#000000',
+ header_image_placement: 'above',
+ header_link_color: '#c1c1c1',
layout: 'grid',
logo_size: 'medium',
main_logo: null,
- main_site_label: null,
- main_site_url: null,
+ main_site_label: 'Sennheiser',
+ main_site_url: 'https://newsroom.sennheiser.com/',
show_date: true,
show_featured_categories: true,
sharing_actions: [
@@ -118,17 +119,18 @@ export const DEFAULT_THEME_SETTINGS: ThemeSettings = {
sharing_placement: ['bottom'],
show_copy_content: true,
show_copy_url: true,
- show_subtitle: false,
+ show_subtitle: true,
show_read_more: true,
show_download_pdf: true,
show_download_assets: true,
story_card_variant: 'default',
- text_color: '#374151',
+ text_color: '#c1c1c1',
};
export const FONT_FAMILY = {
[Font.ALEGREYA]: "'Alegreya', serif",
[Font.ALEGREYA_SANS]: "'Alegreya Sans', sans-serif",
+ [Font.FF_UNIT_PRO]: "'FF Unit Pro', 'Roboto', Arial, sans-serif",
[Font.INTER]: 'Inter, sans-serif',
[Font.MERRIWEATHER]: 'Merriweather, serif',
[Font.MULISH]: 'Mulish, sans-serif',
@@ -152,12 +154,15 @@ export function getRelatedFont(font: Font): Font | null {
}
}
-export function getGoogleFontName(font: Font): string {
+export function getGoogleFontName(font: Font): string | null {
switch (font) {
case Font.ALEGREYA:
return 'Alegreya';
case Font.ALEGREYA_SANS:
return 'Alegreya Sans';
+ case Font.FF_UNIT_PRO:
+ // Self-hosted via @font-face in src/styles/_fonts.scss.
+ return null;
case Font.MERRIWEATHER:
return 'Merriweather';
case Font.MULISH: