From f3adbf1fc4c4b5c3fbd97276ec0b90580e302308 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Fri, 30 Jan 2026 15:31:48 +0000 Subject: [PATCH 01/48] move the follow and notification buttons from top to bottom --- .../src/components/ArticleMeta.apps.tsx | 4 +-- .../src/layouts/CommentLayout.tsx | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index fe34af72e1e..6757ba8590b 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -297,9 +297,7 @@ export const ArticleMetaApps = ({ format={format} /> )} - {shouldShowFollowButtons( - isComment || isAnalysis || isImmersive, - ) && + {shouldShowFollowButtons(isAnalysis || isImmersive) && soleContributor && ( { const showComments = article.isCommentable && !isPaidContent; - const avatarUrl = getSoleContributor(article.tags, article.byline) - ?.bylineLargeImageUrl; + const soleContributor = getSoleContributor(article.tags, article.byline); + const avatarUrl = soleContributor?.bylineLargeImageUrl; const { branding } = article.commercialProperties[article.editionId]; @@ -639,6 +644,22 @@ export const CommentLayout = (props: WebProps | AppsProps) => { `} color={themePalette('--straight-lines')} /> + {isApps && + format.design === + ArticleDesign.Comment && + soleContributor && ( + + + + )} Date: Mon, 2 Feb 2026 15:27:18 +0000 Subject: [PATCH 02/48] add bio and description to tag models --- dotcom-rendering/src/frontend/schemas/feArticle.json | 6 ++++++ dotcom-rendering/src/model/enhanceTags.ts | 4 ++++ dotcom-rendering/src/types/tag.ts | 2 ++ 3 files changed, 12 insertions(+) diff --git a/dotcom-rendering/src/frontend/schemas/feArticle.json b/dotcom-rendering/src/frontend/schemas/feArticle.json index 1d97ba0bd8b..9b11cba3d8b 100644 --- a/dotcom-rendering/src/frontend/schemas/feArticle.json +++ b/dotcom-rendering/src/frontend/schemas/feArticle.json @@ -109,6 +109,12 @@ "type": "string" } } + }, + "bio": { + "type": "string" + }, + "description": { + "type": "string" } }, "required": [ diff --git a/dotcom-rendering/src/model/enhanceTags.ts b/dotcom-rendering/src/model/enhanceTags.ts index 84c857e49ce..1927e831e1c 100644 --- a/dotcom-rendering/src/model/enhanceTags.ts +++ b/dotcom-rendering/src/model/enhanceTags.ts @@ -11,6 +11,8 @@ const enhanceTag = ({ twitterHandle, bylineImageUrl, contributorLargeImagePath: bylineLargeImageUrl, + bio, + description, }, }: FETagType): TagType => ({ id, @@ -19,4 +21,6 @@ const enhanceTag = ({ twitterHandle, bylineImageUrl, bylineLargeImageUrl, + bio, + description, }); diff --git a/dotcom-rendering/src/types/tag.ts b/dotcom-rendering/src/types/tag.ts index aca68f30f28..2470f874a09 100644 --- a/dotcom-rendering/src/types/tag.ts +++ b/dotcom-rendering/src/types/tag.ts @@ -57,4 +57,6 @@ export type TagType = { bylineImageUrl?: string; bylineLargeImageUrl?: string; podcast?: Podcast; + bio?: string; + description?: string; }; From f5feecbde45283d674962be56eea1a7832367538 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 2 Feb 2026 16:24:04 +0000 Subject: [PATCH 03/48] implement new design --- .../src/components/FollowButtons.stories.tsx | 87 ++++++++++++++++++- .../src/components/FollowButtons.tsx | 56 ++++++++++++ dotcom-rendering/src/paletteDeclarations.ts | 20 +++++ 3 files changed, 162 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/FollowButtons.stories.tsx b/dotcom-rendering/src/components/FollowButtons.stories.tsx index f6b4a0cb2bb..f9657caa602 100644 --- a/dotcom-rendering/src/components/FollowButtons.stories.tsx +++ b/dotcom-rendering/src/components/FollowButtons.stories.tsx @@ -1,6 +1,11 @@ +import { css } from '@emotion/react'; import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; -import { FollowNotificationsButton, FollowTagButton } from './FollowButtons'; +import { + FollowNotificationsButton, + FollowTagButton, + FollowTagButtonPill, +} from './FollowButtons'; export default { component: [FollowNotificationsButton, FollowTagButton], @@ -22,6 +27,10 @@ export const Default = ({ isFollowing }: { isFollowing: boolean }) => { isFollowing={isFollowing} onClickHandler={() => undefined} /> + undefined} + /> ); }; @@ -70,3 +79,79 @@ export const FollowContributorBothStates = () => { ); }; FollowContributorBothStates.decorators = [splitTheme()]; + +const pillContainerStyles = css` + display: flex; + gap: 16px; + margin-bottom: 16px; +`; + +export const FollowTagButtonPillBothStates = () => { + return ( +
+ undefined} + /> + undefined} + /> +
+ ); +}; +FollowTagButtonPillBothStates.storyName = 'Pill Button - Both States'; +FollowTagButtonPillBothStates.decorators = [ + splitTheme([ + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Standard, + theme: Pillar.News, + }, + ]), +]; + +export const FollowTagButtonPillAllPillars = () => { + return ( +
+ undefined} + /> + undefined} + /> +
+ ); +}; +FollowTagButtonPillAllPillars.storyName = 'Pill Button - All Pillars'; +FollowTagButtonPillAllPillars.decorators = [ + splitTheme([ + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Standard, + theme: Pillar.News, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Standard, + theme: Pillar.Opinion, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Standard, + theme: Pillar.Sport, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Standard, + theme: Pillar.Culture, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Standard, + theme: Pillar.Lifestyle, + }, + ]), +]; diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index d2dce658e10..525c2c30ab4 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -136,3 +136,59 @@ export const FollowTagButton = ({ ); }; + +// Pill-style button (with visible border, pillar-aware colors) +// Not following: filled with pillar color, white bold text +// Following: transparent, neutral border, pillar text +const pillButtonStyles = (isFollowing: boolean) => css` + ${textSans15} + display: inline-flex; + align-items: center; + gap: ${space[1]}px; + padding: ${space[2]}px ${space[3]}px; + border-radius: ${space[5]}px; + border: 1px solid + ${isFollowing + ? palette('--follow-button-border-following') + : palette('--follow-button-border')}; + background: ${isFollowing + ? 'transparent' + : palette('--follow-button-fill')}; + color: ${isFollowing + ? palette('--follow-button-text') + : palette('--follow-button-text-not-following')}; + font-weight: 700; + cursor: pointer; + + svg { + width: 16px; + height: 16px; + fill: ${isFollowing + ? palette('--follow-button-text') + : palette('--follow-button-text-not-following')}; + stroke: ${isFollowing + ? palette('--follow-button-text') + : palette('--follow-button-text-not-following')}; + stroke-width: 1px; + } +`; + +export const FollowTagButtonPill = ({ + isFollowing, + onClickHandler, +}: ButtonProps) => { + return ( + + ); +}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index cc604489aea..d2cf9aaae48 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -7096,6 +7096,26 @@ const paletteColours = { light: followTextLight, dark: followTextDark, }, + '--follow-button-border': { + light: followIconFillLight, + dark: followIconFillDark, + }, + '--follow-button-border-following': { + light: () => sourcePalette.neutral[73], + dark: () => sourcePalette.neutral[73], + }, + '--follow-button-fill': { + light: followIconFillLight, + dark: followIconFillLight, + }, + '--follow-button-text': { + light: () => sourcePalette.neutral[7], + dark: () => sourcePalette.neutral[100], + }, + '--follow-button-text-not-following': { + light: () => sourcePalette.neutral[100], + dark: () => sourcePalette.neutral[100], + }, '--football-competition-select-text': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[86], From 5fae7778c33e5a2b6887630e8a4a558f1ab4e268 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 2 Feb 2026 16:38:24 +0000 Subject: [PATCH 04/48] create a follow button wrapper with a pill variant --- .../components/FollowWrapper.importable.tsx | 55 +++++++++++++------ .../src/layouts/CommentLayout.tsx | 1 + 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index 7208609214c..d821d833fd1 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -6,14 +6,23 @@ import { useEffect, useState } from 'react'; import { getNotificationsClient, getTagClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; -import { FollowNotificationsButton, FollowTagButton } from './FollowButtons'; +import { + FollowNotificationsButton, + FollowTagButton, + FollowTagButtonPill, +} from './FollowButtons'; type Props = { id: string; displayName: string; + variant?: 'default' | 'pill'; }; -export const FollowWrapper = ({ id, displayName }: Props) => { +export const FollowWrapper = ({ + id, + displayName, + variant = 'default', +}: Props) => { const [isFollowingNotifications, setIsFollowingNotifications] = useState< boolean | undefined >(undefined); @@ -177,26 +186,38 @@ export const FollowWrapper = ({ id, displayName }: Props) => { `} data-gu-name="follow" > - {showFollowTagButton && ( - undefined + } + /> + ) : ( + undefined + } + withExtraBottomMargin={true} + /> + ))} + {variant === 'default' && ( + undefined } - withExtraBottomMargin={true} /> )} - undefined - } - /> ); }; diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index c1555543780..42b0cd2b4d0 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -657,6 +657,7 @@ export const CommentLayout = (props: WebProps | AppsProps) => { soleContributor.title } id={soleContributor.id} + variant={'pill'} />
)} From c5bbae4bbae4f06eb2b9b567d67986f3e979d3ac Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 2 Feb 2026 17:18:28 +0000 Subject: [PATCH 05/48] set up contributor profile --- .../ContributorProfile.importable.tsx | 136 ++++++++++++ .../components/ContributorProfile.stories.tsx | 209 ++++++++++++++++++ .../src/components/FollowButtons.tsx | 8 +- 3 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 dotcom-rendering/src/components/ContributorProfile.importable.tsx create mode 100644 dotcom-rendering/src/components/ContributorProfile.stories.tsx diff --git a/dotcom-rendering/src/components/ContributorProfile.importable.tsx b/dotcom-rendering/src/components/ContributorProfile.importable.tsx new file mode 100644 index 00000000000..c4eb05299e5 --- /dev/null +++ b/dotcom-rendering/src/components/ContributorProfile.importable.tsx @@ -0,0 +1,136 @@ +import { css } from '@emotion/react'; +import { + from, + headlineBold17, + space, + textEgyptian14, +} from '@guardian/source/foundations'; +import sanitise from 'sanitize-html'; +import { palette } from '../palette'; +import { Avatar } from './Avatar'; +import { FollowWrapper } from './FollowWrapper.importable'; + +type Props = { + id: string; + displayName: string; + avatarUrl?: string; + bio?: string; +}; + +const containerStyles = css` + display: flex; + flex-direction: column; + gap: ${space[2]}px; + padding: ${space[2]}px 0; +`; + +const topRowStyles = css` + display: flex; + flex-direction: row; + gap: ${space[3]}px; +`; + +const avatarContainerStyles = css` + width: 60px; + height: 60px; + flex-shrink: 0; + + ${from.tablet} { + width: 80px; + height: 80px; + } +`; + +const contentStyles = css` + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +`; + +const titleStyles = css` + ${headlineBold17}; + color: ${palette('--byline-anchor')}; + margin: 0 0 ${space[1]}px; +`; + +const bioStyles = css` + ${textEgyptian14}; + font-weight: 500; + line-height: 1.3; + letter-spacing: -0.01em; + color: ${palette('--article-text')}; + margin: 0; + + p { + margin: 0 0 ${space[1]}px; + } + + a { + color: ${palette('--link-kicker-text')}; + text-underline-offset: 3px; + } + + a:not(:hover) { + text-decoration-color: ${palette('--bio-link-underline')}; + } + + a:hover { + text-decoration: underline; + } +`; + +const followButtonContainerStyles = css` + display: flex; +`; + +const containsText = (html: string) => { + const htmlWithoutTags = sanitise(html, { + allowedTags: [], + allowedAttributes: {}, + }); + return htmlWithoutTags.length > 0; +}; + +export const ContributorProfile = ({ + id, + displayName, + avatarUrl, + bio, +}: Props) => { + const hasBio = bio && containsText(bio); + const sanitizedBio = hasBio ? sanitise(bio, {}) : undefined; + + return ( +
+
+ {avatarUrl && ( +
+ +
+ )} +
+

{displayName}

+ {sanitizedBio && ( +
+ )} +
+
+
+ +
+
+ ); +}; diff --git a/dotcom-rendering/src/components/ContributorProfile.stories.tsx b/dotcom-rendering/src/components/ContributorProfile.stories.tsx new file mode 100644 index 00000000000..4156b8cb4e6 --- /dev/null +++ b/dotcom-rendering/src/components/ContributorProfile.stories.tsx @@ -0,0 +1,209 @@ +import { css } from '@emotion/react'; +import { + headlineBold17, + space, + textEgyptian14, +} from '@guardian/source/foundations'; +import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; +import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; +import { palette } from '../palette'; +import { Avatar } from './Avatar'; +import { ContributorProfile } from './ContributorProfile.importable'; +import { FollowTagButtonPill } from './FollowButtons'; + +export default { + component: ContributorProfile, + title: 'Components/ContributorProfile', +}; + +// Mock contributor data +const mockContributor = { + id: 'profile/george-monbiot', + displayName: 'George Monbiot', + avatarUrl: 'https://i.guim.co.uk/img/uploads/2025/05/21/George_Monbiot.png', + bio: `

A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)

`, + bioText: + 'A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)', +}; + +const longBio = `

Marina Hyde is a Guardian columnist. She has won multiple awards for her writing, including the British Press Awards' Columnist of the Year in 2022. Her work covers politics, sport, and celebrity culture with a distinctive satirical edge.

She is also the author of several books and appears regularly on podcasts discussing current affairs.

`; + +// Shared styles +const containerStyles = css` + max-width: 400px; + padding: 16px; +`; + +const opinionDecorator = splitTheme([ + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Opinion, + }, +]); + +const allPillarsDecorator = splitTheme([ + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.News, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Opinion, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Sport, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Culture, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Lifestyle, + }, +]); + +// Component stories +export const WithAvatarAndBio = () => ( +
+ +
+); +WithAvatarAndBio.storyName = 'With Avatar and Bio'; +WithAvatarAndBio.decorators = [opinionDecorator]; + +export const WithoutAvatar = () => ( +
+ +
+); +WithoutAvatar.storyName = 'Without Avatar'; +WithoutAvatar.decorators = [opinionDecorator]; + +export const AllPillars = () => ( +
+ +
+); +AllPillars.storyName = 'All Pillars'; +AllPillars.decorators = [allPillarsDecorator]; + +export const LongBio = () => ( +
+ +
+); +LongBio.storyName = 'Long Bio'; +LongBio.decorators = [opinionDecorator]; + +const previewContainerStyles = css` + display: flex; + flex-direction: column; + gap: ${space[3]}px; +`; + +const previewTopRowStyles = css` + display: flex; + flex-direction: row; + gap: ${space[3]}px; +`; + +const previewAvatarContainerStyles = css` + width: 80px; + height: 80px; + flex-shrink: 0; +`; + +const previewContentStyles = css` + display: flex; + flex-direction: column; + flex: 1; +`; + +const previewTitleStyles = css` + ${headlineBold17}; + color: ${palette('--byline-anchor')}; + margin: 0 0 ${space[1]}px; +`; + +const previewBioStyles = css` + ${textEgyptian14}; + font-weight: 500; + line-height: 1.3; + letter-spacing: -0.01em; + color: ${palette('--article-text')}; + margin: 0; +`; + +const ProfilePreview = ({ isFollowing }: { isFollowing: boolean }) => ( +
+
+
+
+ +
+
+

+ {mockContributor.displayName} +

+

{mockContributor.bioText}

+
+
+
+ undefined} + /> +
+
+
+); + +export const VisualPreviewWithButton = () => ( + +); +VisualPreviewWithButton.storyName = 'With Follow Button (Follow)'; +VisualPreviewWithButton.decorators = [opinionDecorator]; + +export const VisualPreviewFollowingState = () => ( + +); +VisualPreviewFollowingState.storyName = 'With Follow Button (Following)'; +VisualPreviewFollowingState.decorators = [opinionDecorator]; + +export const VisualPreviewAllPillars = () => ( + +); +VisualPreviewAllPillars.storyName = 'With Follow Button (All Pillars)'; +VisualPreviewAllPillars.decorators = [allPillarsDecorator]; diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index 525c2c30ab4..e09e9997126 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -137,9 +137,6 @@ export const FollowTagButton = ({ ); }; -// Pill-style button (with visible border, pillar-aware colors) -// Not following: filled with pillar color, white bold text -// Following: transparent, neutral border, pillar text const pillButtonStyles = (isFollowing: boolean) => css` ${textSans15} display: inline-flex; @@ -161,15 +158,14 @@ const pillButtonStyles = (isFollowing: boolean) => css` cursor: pointer; svg { - width: 16px; - height: 16px; + width: 24px; + height: 24px; fill: ${isFollowing ? palette('--follow-button-text') : palette('--follow-button-text-not-following')}; stroke: ${isFollowing ? palette('--follow-button-text') : palette('--follow-button-text-not-following')}; - stroke-width: 1px; } `; From 006693d5dfb479b27dfa4472573e3040e41670f9 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 2 Feb 2026 17:24:58 +0000 Subject: [PATCH 06/48] add to commentlayout --- dotcom-rendering/src/layouts/CommentLayout.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index 42b0cd2b4d0..e466ebf62b5 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -17,8 +17,8 @@ import { ArticleTitle } from '../components/ArticleTitle'; import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.importable'; import { ContributorAvatar } from '../components/ContributorAvatar'; +import { ContributorProfile } from '../components/ContributorProfile.importable'; import { DiscussionLayout } from '../components/DiscussionLayout'; -import { FollowWrapper } from '../components/FollowWrapper.importable'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; @@ -652,12 +652,15 @@ export const CommentLayout = (props: WebProps | AppsProps) => { priority="feature" defer={{ until: 'visible' }} > - )} From 8f66db4fee82696bcb267d88e270392f1930df56 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 3 Feb 2026 13:06:47 +0000 Subject: [PATCH 07/48] rename contributor profile name and component --- .../components/ContributorProfile.stories.tsx | 12 +- ...ttonWithContributorProfile.importable.tsx} | 2 +- ...wButtonsWithContributorProfile.stories.tsx | 209 ++++++++++++++++++ .../src/layouts/CommentLayout.tsx | 4 +- 4 files changed, 218 insertions(+), 9 deletions(-) rename dotcom-rendering/src/components/{ContributorProfile.importable.tsx => FollowButtonWithContributorProfile.importable.tsx} (97%) create mode 100644 dotcom-rendering/src/components/FollowButtonsWithContributorProfile.stories.tsx diff --git a/dotcom-rendering/src/components/ContributorProfile.stories.tsx b/dotcom-rendering/src/components/ContributorProfile.stories.tsx index 4156b8cb4e6..c907efed9eb 100644 --- a/dotcom-rendering/src/components/ContributorProfile.stories.tsx +++ b/dotcom-rendering/src/components/ContributorProfile.stories.tsx @@ -8,11 +8,11 @@ import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; import { palette } from '../palette'; import { Avatar } from './Avatar'; -import { ContributorProfile } from './ContributorProfile.importable'; +import { FollowButtonWithContributorProfile } from './FollowButtonWithContributorProfile.importable'; import { FollowTagButtonPill } from './FollowButtons'; export default { - component: ContributorProfile, + component: FollowButtonWithContributorProfile, title: 'Components/ContributorProfile', }; @@ -73,7 +73,7 @@ const allPillarsDecorator = splitTheme([ // Component stories export const WithAvatarAndBio = () => (
- (
- (
- (
- { return htmlWithoutTags.length > 0; }; -export const ContributorProfile = ({ +export const FollowButtonWithContributorProfile = ({ id, displayName, avatarUrl, diff --git a/dotcom-rendering/src/components/FollowButtonsWithContributorProfile.stories.tsx b/dotcom-rendering/src/components/FollowButtonsWithContributorProfile.stories.tsx new file mode 100644 index 00000000000..c907efed9eb --- /dev/null +++ b/dotcom-rendering/src/components/FollowButtonsWithContributorProfile.stories.tsx @@ -0,0 +1,209 @@ +import { css } from '@emotion/react'; +import { + headlineBold17, + space, + textEgyptian14, +} from '@guardian/source/foundations'; +import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; +import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; +import { palette } from '../palette'; +import { Avatar } from './Avatar'; +import { FollowButtonWithContributorProfile } from './FollowButtonWithContributorProfile.importable'; +import { FollowTagButtonPill } from './FollowButtons'; + +export default { + component: FollowButtonWithContributorProfile, + title: 'Components/ContributorProfile', +}; + +// Mock contributor data +const mockContributor = { + id: 'profile/george-monbiot', + displayName: 'George Monbiot', + avatarUrl: 'https://i.guim.co.uk/img/uploads/2025/05/21/George_Monbiot.png', + bio: `

A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)

`, + bioText: + 'A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)', +}; + +const longBio = `

Marina Hyde is a Guardian columnist. She has won multiple awards for her writing, including the British Press Awards' Columnist of the Year in 2022. Her work covers politics, sport, and celebrity culture with a distinctive satirical edge.

She is also the author of several books and appears regularly on podcasts discussing current affairs.

`; + +// Shared styles +const containerStyles = css` + max-width: 400px; + padding: 16px; +`; + +const opinionDecorator = splitTheme([ + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Opinion, + }, +]); + +const allPillarsDecorator = splitTheme([ + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.News, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Opinion, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Sport, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Culture, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Lifestyle, + }, +]); + +// Component stories +export const WithAvatarAndBio = () => ( +
+ +
+); +WithAvatarAndBio.storyName = 'With Avatar and Bio'; +WithAvatarAndBio.decorators = [opinionDecorator]; + +export const WithoutAvatar = () => ( +
+ +
+); +WithoutAvatar.storyName = 'Without Avatar'; +WithoutAvatar.decorators = [opinionDecorator]; + +export const AllPillars = () => ( +
+ +
+); +AllPillars.storyName = 'All Pillars'; +AllPillars.decorators = [allPillarsDecorator]; + +export const LongBio = () => ( +
+ +
+); +LongBio.storyName = 'Long Bio'; +LongBio.decorators = [opinionDecorator]; + +const previewContainerStyles = css` + display: flex; + flex-direction: column; + gap: ${space[3]}px; +`; + +const previewTopRowStyles = css` + display: flex; + flex-direction: row; + gap: ${space[3]}px; +`; + +const previewAvatarContainerStyles = css` + width: 80px; + height: 80px; + flex-shrink: 0; +`; + +const previewContentStyles = css` + display: flex; + flex-direction: column; + flex: 1; +`; + +const previewTitleStyles = css` + ${headlineBold17}; + color: ${palette('--byline-anchor')}; + margin: 0 0 ${space[1]}px; +`; + +const previewBioStyles = css` + ${textEgyptian14}; + font-weight: 500; + line-height: 1.3; + letter-spacing: -0.01em; + color: ${palette('--article-text')}; + margin: 0; +`; + +const ProfilePreview = ({ isFollowing }: { isFollowing: boolean }) => ( +
+
+
+
+ +
+
+

+ {mockContributor.displayName} +

+

{mockContributor.bioText}

+
+
+
+ undefined} + /> +
+
+
+); + +export const VisualPreviewWithButton = () => ( + +); +VisualPreviewWithButton.storyName = 'With Follow Button (Follow)'; +VisualPreviewWithButton.decorators = [opinionDecorator]; + +export const VisualPreviewFollowingState = () => ( + +); +VisualPreviewFollowingState.storyName = 'With Follow Button (Following)'; +VisualPreviewFollowingState.decorators = [opinionDecorator]; + +export const VisualPreviewAllPillars = () => ( + +); +VisualPreviewAllPillars.storyName = 'With Follow Button (All Pillars)'; +VisualPreviewAllPillars.decorators = [allPillarsDecorator]; diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index e466ebf62b5..ccb6a91983c 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -17,7 +17,7 @@ import { ArticleTitle } from '../components/ArticleTitle'; import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.importable'; import { ContributorAvatar } from '../components/ContributorAvatar'; -import { ContributorProfile } from '../components/ContributorProfile.importable'; +import { FollowButtonWithContributorProfile } from '../components/FollowButtonWithContributorProfile.importable'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; @@ -652,7 +652,7 @@ export const CommentLayout = (props: WebProps | AppsProps) => { priority="feature" defer={{ until: 'visible' }} > - Date: Tue, 3 Feb 2026 13:28:50 +0000 Subject: [PATCH 08/48] fix storybook config --- ...uttonWithContributorProfile.importable.tsx | 4 +- ...wButtonWithContributorProfile.stories.tsx} | 75 ++----- ...wButtonsWithContributorProfile.stories.tsx | 209 ------------------ .../src/layouts/CommentLayout.tsx | 2 +- 4 files changed, 16 insertions(+), 274 deletions(-) rename dotcom-rendering/src/components/{ContributorProfile.stories.tsx => FollowButtonWithContributorProfile.stories.tsx} (67%) delete mode 100644 dotcom-rendering/src/components/FollowButtonsWithContributorProfile.stories.tsx diff --git a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx b/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx index 379d38c4e1f..e9478cf93ac 100644 --- a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx +++ b/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx @@ -104,7 +104,7 @@ export const FollowButtonWithContributorProfile = ({ return (
- {avatarUrl && ( + {!!avatarUrl && (

{displayName}

- {sanitizedBio && ( + {!!sanitizedBio && (
( -
- -
-); -WithAvatarAndBio.storyName = 'With Avatar and Bio'; -WithAvatarAndBio.decorators = [opinionDecorator]; - -export const WithoutAvatar = () => ( -
- -
-); -WithoutAvatar.storyName = 'Without Avatar'; -WithoutAvatar.decorators = [opinionDecorator]; - -export const AllPillars = () => ( -
- -
-); -AllPillars.storyName = 'All Pillars'; -AllPillars.decorators = [allPillarsDecorator]; - -export const LongBio = () => ( -
- -
-); -LongBio.storyName = 'Long Bio'; -LongBio.decorators = [opinionDecorator]; - const previewContainerStyles = css` display: flex; flex-direction: column; @@ -161,7 +108,11 @@ const previewBioStyles = css` margin: 0; `; -const ProfilePreview = ({ isFollowing }: { isFollowing: boolean }) => ( +const FollowButtonWithProfilePreview = ({ + isFollowing, +}: { + isFollowing: boolean; +}) => (
@@ -191,19 +142,19 @@ const ProfilePreview = ({ isFollowing }: { isFollowing: boolean }) => ( ); export const VisualPreviewWithButton = () => ( - + ); -VisualPreviewWithButton.storyName = 'With Follow Button (Follow)'; +VisualPreviewWithButton.storyName = 'Follow'; VisualPreviewWithButton.decorators = [opinionDecorator]; export const VisualPreviewFollowingState = () => ( - + ); -VisualPreviewFollowingState.storyName = 'With Follow Button (Following)'; +VisualPreviewFollowingState.storyName = 'Following'; VisualPreviewFollowingState.decorators = [opinionDecorator]; export const VisualPreviewAllPillars = () => ( - + ); -VisualPreviewAllPillars.storyName = 'With Follow Button (All Pillars)'; +VisualPreviewAllPillars.storyName = 'All Pillars'; VisualPreviewAllPillars.decorators = [allPillarsDecorator]; diff --git a/dotcom-rendering/src/components/FollowButtonsWithContributorProfile.stories.tsx b/dotcom-rendering/src/components/FollowButtonsWithContributorProfile.stories.tsx deleted file mode 100644 index c907efed9eb..00000000000 --- a/dotcom-rendering/src/components/FollowButtonsWithContributorProfile.stories.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { css } from '@emotion/react'; -import { - headlineBold17, - space, - textEgyptian14, -} from '@guardian/source/foundations'; -import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; -import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; -import { palette } from '../palette'; -import { Avatar } from './Avatar'; -import { FollowButtonWithContributorProfile } from './FollowButtonWithContributorProfile.importable'; -import { FollowTagButtonPill } from './FollowButtons'; - -export default { - component: FollowButtonWithContributorProfile, - title: 'Components/ContributorProfile', -}; - -// Mock contributor data -const mockContributor = { - id: 'profile/george-monbiot', - displayName: 'George Monbiot', - avatarUrl: 'https://i.guim.co.uk/img/uploads/2025/05/21/George_Monbiot.png', - bio: `

A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)

`, - bioText: - 'A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)', -}; - -const longBio = `

Marina Hyde is a Guardian columnist. She has won multiple awards for her writing, including the British Press Awards' Columnist of the Year in 2022. Her work covers politics, sport, and celebrity culture with a distinctive satirical edge.

She is also the author of several books and appears regularly on podcasts discussing current affairs.

`; - -// Shared styles -const containerStyles = css` - max-width: 400px; - padding: 16px; -`; - -const opinionDecorator = splitTheme([ - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Opinion, - }, -]); - -const allPillarsDecorator = splitTheme([ - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.News, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Opinion, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Sport, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Culture, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Lifestyle, - }, -]); - -// Component stories -export const WithAvatarAndBio = () => ( -
- -
-); -WithAvatarAndBio.storyName = 'With Avatar and Bio'; -WithAvatarAndBio.decorators = [opinionDecorator]; - -export const WithoutAvatar = () => ( -
- -
-); -WithoutAvatar.storyName = 'Without Avatar'; -WithoutAvatar.decorators = [opinionDecorator]; - -export const AllPillars = () => ( -
- -
-); -AllPillars.storyName = 'All Pillars'; -AllPillars.decorators = [allPillarsDecorator]; - -export const LongBio = () => ( -
- -
-); -LongBio.storyName = 'Long Bio'; -LongBio.decorators = [opinionDecorator]; - -const previewContainerStyles = css` - display: flex; - flex-direction: column; - gap: ${space[3]}px; -`; - -const previewTopRowStyles = css` - display: flex; - flex-direction: row; - gap: ${space[3]}px; -`; - -const previewAvatarContainerStyles = css` - width: 80px; - height: 80px; - flex-shrink: 0; -`; - -const previewContentStyles = css` - display: flex; - flex-direction: column; - flex: 1; -`; - -const previewTitleStyles = css` - ${headlineBold17}; - color: ${palette('--byline-anchor')}; - margin: 0 0 ${space[1]}px; -`; - -const previewBioStyles = css` - ${textEgyptian14}; - font-weight: 500; - line-height: 1.3; - letter-spacing: -0.01em; - color: ${palette('--article-text')}; - margin: 0; -`; - -const ProfilePreview = ({ isFollowing }: { isFollowing: boolean }) => ( -
-
-
-
- -
-
-

- {mockContributor.displayName} -

-

{mockContributor.bioText}

-
-
-
- undefined} - /> -
-
-
-); - -export const VisualPreviewWithButton = () => ( - -); -VisualPreviewWithButton.storyName = 'With Follow Button (Follow)'; -VisualPreviewWithButton.decorators = [opinionDecorator]; - -export const VisualPreviewFollowingState = () => ( - -); -VisualPreviewFollowingState.storyName = 'With Follow Button (Following)'; -VisualPreviewFollowingState.decorators = [opinionDecorator]; - -export const VisualPreviewAllPillars = () => ( - -); -VisualPreviewAllPillars.storyName = 'With Follow Button (All Pillars)'; -VisualPreviewAllPillars.decorators = [allPillarsDecorator]; diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index ccb6a91983c..939ee32e3d7 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -17,8 +17,8 @@ import { ArticleTitle } from '../components/ArticleTitle'; import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.importable'; import { ContributorAvatar } from '../components/ContributorAvatar'; -import { FollowButtonWithContributorProfile } from '../components/FollowButtonWithContributorProfile.importable'; import { DiscussionLayout } from '../components/DiscussionLayout'; +import { FollowButtonWithContributorProfile } from '../components/FollowButtonWithContributorProfile.importable'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; From 9c64e50554d38890bf60af1675f35c80c954ea03 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 3 Feb 2026 13:45:23 +0000 Subject: [PATCH 09/48] fix lint issues --- ...owButtonWithContributorProfile.stories.tsx | 3 --- dotcom-rendering/src/paletteDeclarations.ts | 24 +++++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx b/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx index a3319254eec..fa7fb58a0ed 100644 --- a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx +++ b/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx @@ -25,9 +25,6 @@ const mockContributor = { 'A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)', }; -const longBio = `

Marina Hyde is a Guardian columnist. She has won multiple awards for her writing, including the British Press Awards' Columnist of the Year in 2022. Her work covers politics, sport, and celebrity culture with a distinctive satirical edge.

She is also the author of several books and appears regularly on podcasts discussing current affairs.

`; - -// Shared styles const containerStyles = css` max-width: 400px; padding: 16px; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index d2cf9aaae48..f2d69699e84 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -7084,18 +7084,6 @@ const paletteColours = { light: () => sourcePalette.neutral[86], dark: () => sourcePalette.neutral[20], }, - '--follow-icon-background': { - light: followIconBackgroundLight, - dark: followIconBackgroundDark, - }, - '--follow-icon-fill': { - light: followIconFillLight, - dark: followIconFillDark, - }, - '--follow-text': { - light: followTextLight, - dark: followTextDark, - }, '--follow-button-border': { light: followIconFillLight, dark: followIconFillDark, @@ -7116,6 +7104,18 @@ const paletteColours = { light: () => sourcePalette.neutral[100], dark: () => sourcePalette.neutral[100], }, + '--follow-icon-background': { + light: followIconBackgroundLight, + dark: followIconBackgroundDark, + }, + '--follow-icon-fill': { + light: followIconFillLight, + dark: followIconFillDark, + }, + '--follow-text': { + light: followTextLight, + dark: followTextDark, + }, '--football-competition-select-text': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[86], From b1e3300b65a55a25de7ce54c375997364e8b80bb Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Wed, 4 Feb 2026 12:12:12 +0000 Subject: [PATCH 10/48] create a variant and update code --- .../src/components/ArticleMeta.apps.tsx | 8 +++- .../src/layouts/CommentLayout.tsx | 45 ++++++++++--------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index 6757ba8590b..a4901ec149d 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -37,6 +37,7 @@ type Props = { isCommentable: boolean; pageId?: string; headline?: string; + inArticleFollowButtonVariant?: boolean; }; const metaGridContainer = css` @@ -230,6 +231,7 @@ export const ArticleMetaApps = ({ isCommentable, pageId, headline, + inArticleFollowButtonVariant, }: Props) => { const soleContributor = getSoleContributor(tags, byline); const authorName = soleContributor?.title ?? 'Author Image'; @@ -297,7 +299,11 @@ export const ArticleMetaApps = ({ format={format} /> )} - {shouldShowFollowButtons(isAnalysis || isImmersive) && + {shouldShowFollowButtons( + (isComment && !inArticleFollowButtonVariant) || + isAnalysis || + isImmersive, + ) && soleContributor && ( { const showComments = article.isCommentable && !isPaidContent; const soleContributor = getSoleContributor(article.tags, article.byline); + const inArticleFollowButtonVariant = + isApps && + format.design === ArticleDesign.Comment && + !!soleContributor && + article.config.abTests.inArticleFollowButtonVariant === 'variant'; const avatarUrl = soleContributor?.bylineLargeImageUrl; const { branding } = article.commercialProperties[article.editionId]; @@ -498,6 +503,9 @@ export const CommentLayout = (props: WebProps | AppsProps) => { article.config.shortUrlId } pageId={article.config.pageId} + inArticleFollowButtonVariant={ + inArticleFollowButtonVariant + } > @@ -644,26 +652,23 @@ export const CommentLayout = (props: WebProps | AppsProps) => { `} color={themePalette('--straight-lines')} /> - {isApps && - format.design === - ArticleDesign.Comment && - soleContributor && ( - - - - )} + {inArticleFollowButtonVariant && ( + + + + )} Date: Wed, 4 Feb 2026 14:48:33 +0000 Subject: [PATCH 11/48] add thick border line below profile --- ...wButtonWithContributorProfile.importable.tsx | 12 ++++++++---- .../src/components/FollowButtons.tsx | 4 ++-- dotcom-rendering/src/layouts/CommentLayout.tsx | 17 +++++++++-------- dotcom-rendering/src/paletteDeclarations.ts | 12 ++++++++++-- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx b/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx index e9478cf93ac..3425efdc698 100644 --- a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx +++ b/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx @@ -50,7 +50,7 @@ const contentStyles = css` const titleStyles = css` ${headlineBold17}; - color: ${palette('--byline-anchor')}; + color: ${palette('--follow-accent-color')}; margin: 0 0 ${space[1]}px; `; @@ -58,8 +58,7 @@ const bioStyles = css` ${textEgyptian14}; font-weight: 500; line-height: 1.3; - letter-spacing: -0.01em; - color: ${palette('--article-text')}; + color: ${palette('--follow-bio-text')}; margin: 0; p { @@ -83,7 +82,11 @@ const bioStyles = css` const followButtonContainerStyles = css` display: flex; `; - +const borderStyles = css` + height: 13px; + background-color: ${palette('--follow-bottom-border')}; + margin-top: ${space[3]}px; +`; const containsText = (html: string) => { const htmlWithoutTags = sanitise(html, { allowedTags: [], @@ -131,6 +134,7 @@ export const FollowButtonWithContributorProfile = ({ variant="pill" />
+
); }; diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index e09e9997126..b272cf42e35 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -142,7 +142,7 @@ const pillButtonStyles = (isFollowing: boolean) => css` display: inline-flex; align-items: center; gap: ${space[1]}px; - padding: ${space[2]}px ${space[3]}px; + padding: ${space[1] + 1}px ${space[3] + 2}px; border-radius: ${space[5]}px; border: 1px solid ${isFollowing @@ -150,7 +150,7 @@ const pillButtonStyles = (isFollowing: boolean) => css` : palette('--follow-button-border')}; background: ${isFollowing ? 'transparent' - : palette('--follow-button-fill')}; + : palette('--follow-accent-color')}; color: ${isFollowing ? palette('--follow-button-text') : palette('--follow-button-text-not-following')}; diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index 4cd0bb5722e..ca05927dc12 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -297,11 +297,11 @@ export const CommentLayout = (props: WebProps | AppsProps) => { const showComments = article.isCommentable && !isPaidContent; const soleContributor = getSoleContributor(article.tags, article.byline); - const inArticleFollowButtonVariant = - isApps && - format.design === ArticleDesign.Comment && - !!soleContributor && - article.config.abTests.inArticleFollowButtonVariant === 'variant'; + const inArticleFollowButtonVariant = true; + // isApps && + // format.design === ArticleDesign.Comment && + // !!soleContributor && + // article.config.abTests.inArticleFollowButtonVariant === 'variant'; const avatarUrl = soleContributor?.bylineLargeImageUrl; const { branding } = article.commercialProperties[article.editionId]; @@ -658,12 +658,13 @@ export const CommentLayout = (props: WebProps | AppsProps) => { defer={{ until: 'visible' }} > diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index f2d69699e84..e712d70de1f 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -1,4 +1,4 @@ -// ----- Imports ----- // +/// ----- Imports ----- // /* eslint sort-keys: ["error", "asc", { minKeys: 12, natural: true }] -- the palette object is large and ordering helps knowing where to insert new elements @@ -7092,7 +7092,7 @@ const paletteColours = { light: () => sourcePalette.neutral[73], dark: () => sourcePalette.neutral[73], }, - '--follow-button-fill': { + '--follow-accent-color': { light: followIconFillLight, dark: followIconFillLight, }, @@ -7104,6 +7104,10 @@ const paletteColours = { light: () => sourcePalette.neutral[100], dark: () => sourcePalette.neutral[100], }, + '--follow-bio-text': { + light: () => sourcePalette.neutral[10], + dark: () => sourcePalette.neutral[86], + }, '--follow-icon-background': { light: followIconBackgroundLight, dark: followIconBackgroundDark, @@ -7116,6 +7120,10 @@ const paletteColours = { light: followTextLight, dark: followTextDark, }, + '--follow-bottom-border': { + light: () => sourcePalette.neutral[97], + dark: () => sourcePalette.neutral[97], + }, '--football-competition-select-text': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[86], From 449d88499f3353483637b934b2f03e0d436dd1fe Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 5 Feb 2026 13:28:07 +0000 Subject: [PATCH 12/48] add notification icon and update copy --- ...owButtonWithContributorProfile.stories.tsx | 20 +++- .../src/components/FollowButtons.stories.tsx | 30 ++--- .../src/components/FollowButtons.tsx | 108 +++++++++++++++++- .../components/FollowWrapper.importable.tsx | 25 ++-- dotcom-rendering/src/paletteDeclarations.ts | 4 + 5 files changed, 153 insertions(+), 34 deletions(-) diff --git a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx b/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx index fa7fb58a0ed..233294ebb30 100644 --- a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx +++ b/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx @@ -8,7 +8,11 @@ import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; import { palette } from '../palette'; import { Avatar } from './Avatar'; -import { FollowTagButtonPill } from './FollowButtons'; +import { + FollowNotificationsButton, + FollowNotificationsButtonVariant, + FollowTagButtonVariant, +} from './FollowButtons'; import { FollowButtonWithContributorProfile } from './FollowButtonWithContributorProfile.importable'; export default { @@ -129,11 +133,23 @@ const FollowButtonWithProfilePreview = ({
- undefined} />
+
+
+ undefined} + /> +
+ undefined} + /> +
); diff --git a/dotcom-rendering/src/components/FollowButtons.stories.tsx b/dotcom-rendering/src/components/FollowButtons.stories.tsx index f9657caa602..61d6542d3ad 100644 --- a/dotcom-rendering/src/components/FollowButtons.stories.tsx +++ b/dotcom-rendering/src/components/FollowButtons.stories.tsx @@ -4,7 +4,7 @@ import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; import { FollowNotificationsButton, FollowTagButton, - FollowTagButtonPill, + FollowTagButtonVariant, } from './FollowButtons'; export default { @@ -27,7 +27,7 @@ export const Default = ({ isFollowing }: { isFollowing: boolean }) => { isFollowing={isFollowing} onClickHandler={() => undefined} /> - undefined} /> @@ -80,28 +80,28 @@ export const FollowContributorBothStates = () => { }; FollowContributorBothStates.decorators = [splitTheme()]; -const pillContainerStyles = css` +const variantContainerStyles = css` display: flex; gap: 16px; margin-bottom: 16px; `; -export const FollowTagButtonPillBothStates = () => { +export const VariantFollowTagButtonBothStates = () => { return ( -
- + undefined} /> - undefined} />
); }; -FollowTagButtonPillBothStates.storyName = 'Pill Button - Both States'; -FollowTagButtonPillBothStates.decorators = [ +VariantFollowTagButtonBothStates.storyName = 'Variant Button - Both States'; +VariantFollowTagButtonBothStates.decorators = [ splitTheme([ { display: ArticleDisplay.Standard, @@ -111,22 +111,22 @@ FollowTagButtonPillBothStates.decorators = [ ]), ]; -export const FollowTagButtonPillAllPillars = () => { +export const VariantFollowTagButtonAllPillars = () => { return ( -
- + undefined} /> - undefined} />
); }; -FollowTagButtonPillAllPillars.storyName = 'Pill Button - All Pillars'; -FollowTagButtonPillAllPillars.decorators = [ +VariantFollowTagButtonAllPillars.storyName = 'Variant Button - All Pillars'; +VariantFollowTagButtonAllPillars.decorators = [ splitTheme([ { display: ArticleDisplay.Standard, diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index b272cf42e35..eb993f0b060 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/react'; -import { space, textSans15 } from '@guardian/source/foundations'; +import { space, textSans12, textSans15 } from '@guardian/source/foundations'; import { SvgCheckmark, SvgNotificationsOff, @@ -8,6 +8,7 @@ import { } from '@guardian/source/react-components'; import type { ReactNode } from 'react'; import { palette } from '../palette'; +import { ToggleSwitch } from '@guardian/source-development-kitchen/react-components'; type IconProps = { isFollowing?: boolean; @@ -137,7 +138,17 @@ export const FollowTagButton = ({ ); }; -const pillButtonStyles = (isFollowing: boolean) => css` +// Variant style + +const containerStylesVariant = css` + display: flex; + align-items: flex-start; + column-gap: 0.2em; + justify-content: space-between; + width: 100%; +`; + +const buttonStylesVariant = (isFollowing: boolean) => css` ${textSans15} display: inline-flex; align-items: center; @@ -169,7 +180,7 @@ const pillButtonStyles = (isFollowing: boolean) => css` } `; -export const FollowTagButtonPill = ({ +export const FollowTagButtonVariant = ({ isFollowing, onClickHandler, }: ButtonProps) => { @@ -177,14 +188,101 @@ export const FollowTagButtonPill = ({ + ); +}; + +const NotificationIconVariant = ({ + isFollowing, + iconIsFollowing, + iconIsNotFollowing, +}: IconProps) => ( +
+ {isFollowing ? iconIsFollowing : iconIsNotFollowing} +
+); + +const notificationTextStylesVariant = css` + ${textSans12} +`; + +const notificationsTextSpanVariant = ({ + isFollowing, +}: Pick) => ( + + {isFollowing + ? 'Notifications on' + : 'Turn on notifications to be alerted whenever {contributor} publishes an article'} + +); + +const toggleSwitchStyles = css` + [aria-checked='false'] { + background-color: ${palette( + '--follow-button-border-following', + )} !important; + border-color: ${palette('--follow-button-border-following')} !important; + } + [aria-checked='true'] { + background-color: ${palette('--follow-button-border')} !important; + border-color: ${palette('--follow-button-border')} !important; + } +`; + +const buttonStylesVariantNotification = css` + ${buttonStyles(true)} + width: 100%; +`; + +const iconTextWrapperStyles = css` + display: flex; + align-items: flex-start; +`; + +export const FollowNotificationsButtonVariant = ({ + isFollowing, + onClickHandler, +}: ButtonProps) => { + return ( + ); }; diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index d821d833fd1..0c75fa202f2 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -8,8 +8,9 @@ import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; import { FollowNotificationsButton, + FollowNotificationsButtonVariant, FollowTagButton, - FollowTagButtonPill, + FollowTagButtonVariant, } from './FollowButtons'; type Props = { @@ -188,7 +189,7 @@ export const FollowWrapper = ({ > {showFollowTagButton && (variant === 'pill' ? ( - ))} - {variant === 'default' && ( - undefined - } - /> - )} + {/* {variant === 'default' && ( */} + undefined + } + /> + {/* )} */}
); }; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index e712d70de1f..31e55eb600d 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -7116,6 +7116,10 @@ const paletteColours = { light: followIconFillLight, dark: followIconFillDark, }, + '--follow-icon-variant-fill': { + light: () => sourcePalette.neutral[10], + dark: () => sourcePalette.neutral[86], + }, '--follow-text': { light: followTextLight, dark: followTextDark, From 4a57c1b1b52e48473737721ba0d26c3884438a57 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 5 Feb 2026 15:05:30 +0000 Subject: [PATCH 13/48] rename files --- .../src/components/FollowButtons.stories.tsx | 6 +- .../src/components/FollowButtons.tsx | 5 +- ...> FollowContributorProfile.importable.tsx} | 2 +- ...x => FollowContributorProfile.stories.tsx} | 63 ++++++++----------- .../src/layouts/CommentLayout.tsx | 4 +- 5 files changed, 33 insertions(+), 47 deletions(-) rename dotcom-rendering/src/components/{FollowButtonWithContributorProfile.importable.tsx => FollowContributorProfile.importable.tsx} (97%) rename dotcom-rendering/src/components/{FollowButtonWithContributorProfile.stories.tsx => FollowContributorProfile.stories.tsx} (65%) diff --git a/dotcom-rendering/src/components/FollowButtons.stories.tsx b/dotcom-rendering/src/components/FollowButtons.stories.tsx index 61d6542d3ad..6905487208f 100644 --- a/dotcom-rendering/src/components/FollowButtons.stories.tsx +++ b/dotcom-rendering/src/components/FollowButtons.stories.tsx @@ -8,7 +8,7 @@ import { } from './FollowButtons'; export default { - component: [FollowNotificationsButton, FollowTagButton], + component: FollowTagButton, title: 'Components/FollowStatus', args: { isFollowing: false, @@ -23,10 +23,6 @@ export const Default = ({ isFollowing }: { isFollowing: boolean }) => { displayName={'John Doe'} onClickHandler={() => undefined} /> - undefined} - /> undefined} diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index eb993f0b060..5202abd4888 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -281,7 +281,10 @@ export const FollowNotificationsButtonVariant = ({ /> {notificationsTextSpanVariant({ isFollowing })} - + ); diff --git a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx b/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx similarity index 97% rename from dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx rename to dotcom-rendering/src/components/FollowContributorProfile.importable.tsx index 3425efdc698..67b1d48571e 100644 --- a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.importable.tsx +++ b/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx @@ -95,7 +95,7 @@ const containsText = (html: string) => { return htmlWithoutTags.length > 0; }; -export const FollowButtonWithContributorProfile = ({ +export const FollowContributorProfile = ({ id, displayName, avatarUrl, diff --git a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx b/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx similarity index 65% rename from dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx rename to dotcom-rendering/src/components/FollowContributorProfile.stories.tsx index 233294ebb30..ed5c4b395c9 100644 --- a/dotcom-rendering/src/components/FollowButtonWithContributorProfile.stories.tsx +++ b/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx @@ -9,15 +9,14 @@ import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; import { palette } from '../palette'; import { Avatar } from './Avatar'; import { - FollowNotificationsButton, FollowNotificationsButtonVariant, FollowTagButtonVariant, } from './FollowButtons'; -import { FollowButtonWithContributorProfile } from './FollowButtonWithContributorProfile.importable'; +import { FollowContributorProfile } from './FollowContributorProfile.importable'; export default { - component: FollowButtonWithContributorProfile, - title: 'Components/FollowButtonWithProfile', + component: FollowContributorProfile, + title: 'Components/FollowContributorProfile', }; const mockContributor = { @@ -70,37 +69,37 @@ const allPillarsDecorator = splitTheme([ }, ]); -const previewContainerStyles = css` +const contributorContainerStyles = css` display: flex; flex-direction: column; gap: ${space[3]}px; `; -const previewTopRowStyles = css` +const topRowStyles = css` display: flex; flex-direction: row; gap: ${space[3]}px; `; -const previewAvatarContainerStyles = css` +const avatarContainerStyles = css` width: 80px; height: 80px; flex-shrink: 0; `; -const previewContentStyles = css` +const contentStyles = css` display: flex; flex-direction: column; flex: 1; `; -const previewTitleStyles = css` +const titleStyles = css` ${headlineBold17}; color: ${palette('--byline-anchor')}; margin: 0 0 ${space[1]}px; `; -const previewBioStyles = css` +const bioStyles = css` ${textEgyptian14}; font-weight: 500; line-height: 1.3; @@ -109,15 +108,11 @@ const previewBioStyles = css` margin: 0; `; -const FollowButtonWithProfilePreview = ({ - isFollowing, -}: { - isFollowing: boolean; -}) => ( +const Wrapper = ({ isFollowing }: { isFollowing: boolean }) => (
-
-
-
+
+
+
-
-

- {mockContributor.displayName} -

-

{mockContributor.bioText}

+
+

{mockContributor.displayName}

+

{mockContributor.bioText}

@@ -154,20 +147,14 @@ const FollowButtonWithProfilePreview = ({
); -export const VisualPreviewWithButton = () => ( - -); -VisualPreviewWithButton.storyName = 'Follow'; -VisualPreviewWithButton.decorators = [opinionDecorator]; +export const ContributorFollow = () => ; +ContributorFollow.storyName = 'Follow'; +ContributorFollow.decorators = [opinionDecorator]; -export const VisualPreviewFollowingState = () => ( - -); -VisualPreviewFollowingState.storyName = 'Following'; -VisualPreviewFollowingState.decorators = [opinionDecorator]; +export const ContributorFollowing = () => ; +ContributorFollowing.storyName = 'Following'; +ContributorFollowing.decorators = [opinionDecorator]; -export const VisualPreviewAllPillars = () => ( - -); -VisualPreviewAllPillars.storyName = 'All Pillars'; -VisualPreviewAllPillars.decorators = [allPillarsDecorator]; +export const ContributorAllPillars = () => ; +ContributorAllPillars.storyName = 'All Pillars'; +ContributorAllPillars.decorators = [allPillarsDecorator]; diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index ca05927dc12..ea227f6921a 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -18,7 +18,7 @@ import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.importable'; import { ContributorAvatar } from '../components/ContributorAvatar'; import { DiscussionLayout } from '../components/DiscussionLayout'; -import { FollowButtonWithContributorProfile } from '../components/FollowButtonWithContributorProfile.importable'; +import { FollowContributorProfile } from '../components/FollowContributorProfile.importable'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; @@ -657,7 +657,7 @@ export const CommentLayout = (props: WebProps | AppsProps) => { priority="feature" defer={{ until: 'visible' }} > - Date: Thu, 5 Feb 2026 15:38:40 +0000 Subject: [PATCH 14/48] update the notification text to be non-button --- .../src/components/FollowButtons.tsx | 9 +++----- .../FollowContributorProfile.stories.tsx | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index 5202abd4888..aca90030ac2 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -265,11 +265,7 @@ export const FollowNotificationsButtonVariant = ({ onClickHandler, }: ButtonProps) => { return ( - +
); }; diff --git a/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx b/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx index ed5c4b395c9..d622d031624 100644 --- a/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx +++ b/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx @@ -132,16 +132,18 @@ const Wrapper = ({ isFollowing }: { isFollowing: boolean }) => ( />
-
- undefined} - /> -
- undefined} - /> + {isFollowing && ( +
+ undefined} + /> + undefined} + /> +
+ )}
From 30ed2573968775fe25512c3262dd89b8ded28090 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 5 Feb 2026 15:54:52 +0000 Subject: [PATCH 15/48] display contributor name in the notification button --- dotcom-rendering/src/components/FollowButtons.tsx | 12 ++++++++---- .../src/components/FollowWrapper.importable.tsx | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index aca90030ac2..81249e6909c 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -229,11 +229,14 @@ const notificationTextStylesVariant = css` const notificationsTextSpanVariant = ({ isFollowing, -}: Pick) => ( + displayName, +}: Pick & { displayName?: string }) => ( {isFollowing ? 'Notifications on' - : 'Turn on notifications to be alerted whenever {contributor} publishes an article'} + : `Turn on notifications to be alerted whenever ${ + displayName ?? 'this author' + } publishes an article`} ); @@ -263,7 +266,8 @@ const iconTextWrapperStyles = css` export const FollowNotificationsButtonVariant = ({ isFollowing, onClickHandler, -}: ButtonProps) => { + displayName, +}: ButtonProps & { displayName?: string }) => { return (
@@ -275,7 +279,7 @@ export const FollowNotificationsButtonVariant = ({ } /> - {notificationsTextSpanVariant({ isFollowing })} + {notificationsTextSpanVariant({ isFollowing, displayName })} undefined } + displayName={displayName} /> {/* )} */}
From 20a8272fd52349b2d2f414c52701ab8f2d66144a Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 5 Feb 2026 16:04:22 +0000 Subject: [PATCH 16/48] hide notification when not following --- .../components/FollowWrapper.importable.tsx | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index a8d67855ce7..a8664db9db1 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -13,6 +13,10 @@ import { FollowTagButtonVariant, } from './FollowButtons'; +const notificationContainerStyles = css` + margin-top: ${space[3]}px; +`; + type Props = { id: string; displayName: string; @@ -210,15 +214,19 @@ export const FollowWrapper = ({ /> ))} {/* {variant === 'default' && ( */} - undefined - } - displayName={displayName} - /> + {isFollowingTag && ( +
+ undefined + } + displayName={displayName} + /> +
+ )} {/* )} */}
); From bded4e25d50392533fafcc0fdadbeb5c55c401b4 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 5 Feb 2026 16:31:28 +0000 Subject: [PATCH 17/48] tweak toggle button positioning and add straight line --- .../src/components/FollowButtons.tsx | 21 ++++++++++--------- .../FollowContributorProfile.importable.tsx | 9 +++----- .../components/FollowWrapper.importable.tsx | 2 ++ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index 81249e6909c..ef240ecac06 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -142,7 +142,6 @@ export const FollowTagButton = ({ const containerStylesVariant = css` display: flex; - align-items: flex-start; column-gap: 0.2em; justify-content: space-between; width: 100%; @@ -270,8 +269,8 @@ export const FollowNotificationsButtonVariant = ({ }: ButtonProps & { displayName?: string }) => { return (
- - +
+
} @@ -280,13 +279,15 @@ export const FollowNotificationsButtonVariant = ({ } /> {notificationsTextSpanVariant({ isFollowing, displayName })} - - - +
+
+ +
+
); }; diff --git a/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx b/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx index 67b1d48571e..b83837888b5 100644 --- a/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx +++ b/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx @@ -9,6 +9,7 @@ import sanitise from 'sanitize-html'; import { palette } from '../palette'; import { Avatar } from './Avatar'; import { FollowWrapper } from './FollowWrapper.importable'; +import { StraightLines } from '@guardian/source-development-kitchen/react-components'; type Props = { id: string; @@ -82,11 +83,7 @@ const bioStyles = css` const followButtonContainerStyles = css` display: flex; `; -const borderStyles = css` - height: 13px; - background-color: ${palette('--follow-bottom-border')}; - margin-top: ${space[3]}px; -`; + const containsText = (html: string) => { const htmlWithoutTags = sanitise(html, { allowedTags: [], @@ -134,7 +131,7 @@ export const FollowContributorProfile = ({ variant="pill" />
-
+
); }; diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index a8664db9db1..0809b056fe4 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -15,6 +15,7 @@ import { const notificationContainerStyles = css` margin-top: ${space[3]}px; + width: 100%; `; type Props = { @@ -179,6 +180,7 @@ export const FollowWrapper = ({
Date: Thu, 5 Feb 2026 17:09:39 +0000 Subject: [PATCH 18/48] insert after 4th para --- .../.storybook/mocks/bridgetApi.ts | 7 +- .../src/components/ArticleMeta.apps.tsx | 8 +- ...ibutorProfileBlockComponent.importable.tsx | 139 ++++++++++++++++++ .../src/components/FollowButtons.tsx | 2 +- .../components/FollowWrapper.importable.tsx | 1 - .../src/layouts/CommentLayout.tsx | 33 +---- dotcom-rendering/src/lib/renderElement.tsx | 12 ++ .../src/model/enhance-contributor-profile.ts | 68 +++++++++ dotcom-rendering/src/model/enhanceBlocks.ts | 7 + dotcom-rendering/src/types/article.ts | 2 +- dotcom-rendering/src/types/content.ts | 10 ++ 11 files changed, 246 insertions(+), 43 deletions(-) create mode 100644 dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx create mode 100644 dotcom-rendering/src/model/enhance-contributor-profile.ts diff --git a/dotcom-rendering/.storybook/mocks/bridgetApi.ts b/dotcom-rendering/.storybook/mocks/bridgetApi.ts index 7026003236a..d7c6ed36a92 100644 --- a/dotcom-rendering/.storybook/mocks/bridgetApi.ts +++ b/dotcom-rendering/.storybook/mocks/bridgetApi.ts @@ -107,7 +107,12 @@ export const getNativeABTestingClient: BridgetApi< 'getNativeABTestingClient' > = () => ({ getParticipations: async () => - new Map(Object.entries({ 'test-id': 'variant' })), + new Map( + Object.entries({ + 'test-id': 'variant', + contributor_profile_test: 'variant', + }), + ), }); export const ensure_all_exports_are_present = { diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index a4901ec149d..6757ba8590b 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -37,7 +37,6 @@ type Props = { isCommentable: boolean; pageId?: string; headline?: string; - inArticleFollowButtonVariant?: boolean; }; const metaGridContainer = css` @@ -231,7 +230,6 @@ export const ArticleMetaApps = ({ isCommentable, pageId, headline, - inArticleFollowButtonVariant, }: Props) => { const soleContributor = getSoleContributor(tags, byline); const authorName = soleContributor?.title ?? 'Author Image'; @@ -299,11 +297,7 @@ export const ArticleMetaApps = ({ format={format} /> )} - {shouldShowFollowButtons( - (isComment && !inArticleFollowButtonVariant) || - isAnalysis || - isImmersive, - ) && + {shouldShowFollowButtons(isAnalysis || isImmersive) && soleContributor && ( { + const htmlWithoutTags = sanitise(html, { + allowedTags: [], + allowedAttributes: {}, + }); + return htmlWithoutTags.length > 0; +}; + +export const ContributorProfileBlockComponent = ({ + contributorId, + displayName, + avatarUrl, + bio, +}: Props) => { + const hasBio = bio && containsText(bio); + const sanitizedBio = hasBio ? sanitise(bio, {}) : undefined; + + return ( +
+ +
+ {!!avatarUrl && ( +
+ +
+ )} +
+

{displayName}

+ {!!sanitizedBio && ( +
+ )} +
+
+
+ +
+ +
+ ); +}; diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index ef240ecac06..6774bfcd8c5 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -194,7 +194,7 @@ export const FollowTagButtonVariant = ({ ) : ( )} - {isFollowing ? 'Following in My Guardian' : 'Follow'} + {isFollowing ? 'Following' : 'Follow'} in My Guardian ); }; diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index 0809b056fe4..c9687488245 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -7,7 +7,6 @@ import { getNotificationsClient, getTagClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; import { - FollowNotificationsButton, FollowNotificationsButtonVariant, FollowTagButton, FollowTagButtonVariant, diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index ea227f6921a..8aa70def138 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -18,7 +18,6 @@ import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.importable'; import { ContributorAvatar } from '../components/ContributorAvatar'; import { DiscussionLayout } from '../components/DiscussionLayout'; -import { FollowContributorProfile } from '../components/FollowContributorProfile.importable'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; @@ -38,11 +37,7 @@ import { Standfirst } from '../components/Standfirst'; import { StickyBottomBanner } from '../components/StickyBottomBanner.importable'; import { SubMeta } from '../components/SubMeta'; import { SubNav } from '../components/SubNav.importable'; -import { - ArticleDesign, - ArticleDisplay, - type ArticleFormat, -} from '../lib/articleFormat'; +import { ArticleDisplay, type ArticleFormat } from '../lib/articleFormat'; import { getSoleContributor } from '../lib/byline'; import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; @@ -297,11 +292,6 @@ export const CommentLayout = (props: WebProps | AppsProps) => { const showComments = article.isCommentable && !isPaidContent; const soleContributor = getSoleContributor(article.tags, article.byline); - const inArticleFollowButtonVariant = true; - // isApps && - // format.design === ArticleDesign.Comment && - // !!soleContributor && - // article.config.abTests.inArticleFollowButtonVariant === 'variant'; const avatarUrl = soleContributor?.bylineLargeImageUrl; const { branding } = article.commercialProperties[article.editionId]; @@ -503,9 +493,6 @@ export const CommentLayout = (props: WebProps | AppsProps) => { article.config.shortUrlId } pageId={article.config.pageId} - inArticleFollowButtonVariant={ - inArticleFollowButtonVariant - } > @@ -652,24 +639,6 @@ export const CommentLayout = (props: WebProps | AppsProps) => { `} color={themePalette('--straight-lines')} /> - {inArticleFollowButtonVariant && ( - - - - )} ); + case 'model.dotcomrendering.pageElements.ContributorProfileBlockElement': + return ( + + + + ); case 'model.dotcomrendering.pageElements.DividerBlockElement': return ( + element._type === 'model.dotcomrendering.pageElements.TextBlockElement'; + +export const enhanceContributorProfile = + ( + format: ArticleFormat, + renderingTarget: RenderingTarget, + tags: TagType[] | undefined, + byline: string | undefined, + ) => + (elements: FEElement[]): FEElement[] => { + if (renderingTarget !== 'Apps') { + return elements; + } + + if (format.design !== ArticleDesign.Comment) { + return elements; + } + + const soleContributor = getSoleContributor(tags ?? [], byline); + if (!soleContributor) { + return elements; + } + + let paragraphCount = 0; + let insertPosition: number | null = null; + + for (let i = 0; i < elements.length; i++) { + if (isParagraph(elements[i]!)) { + paragraphCount++; + if (paragraphCount === PARAGRAPH_POSITION) { + insertPosition = i + 1; + break; + } + } + } + + // If an article has <= 4 paragraphs, insert at the end + if (insertPosition === null) { + insertPosition = elements.length; + } + + const contributorProfileElement: ContributorProfileBlockElement = { + _type: 'model.dotcomrendering.pageElements.ContributorProfileBlockElement', + elementId: `contributor-profile-${soleContributor.id}`, + contributorId: soleContributor.id, + displayName: soleContributor.title, + avatarUrl: soleContributor.bylineLargeImageUrl, + bio: soleContributor.bio, + }; + + return [ + ...elements.slice(0, insertPosition), + contributorProfileElement, + ...elements.slice(insertPosition), + ]; + }; diff --git a/dotcom-rendering/src/model/enhanceBlocks.ts b/dotcom-rendering/src/model/enhanceBlocks.ts index b8d63221638..5d679e53171 100644 --- a/dotcom-rendering/src/model/enhanceBlocks.ts +++ b/dotcom-rendering/src/model/enhanceBlocks.ts @@ -12,6 +12,7 @@ import type { RenderingTarget } from '../types/renderingTarget'; import type { TagType } from '../types/tag'; import { enhanceAdPlaceholders } from './enhance-ad-placeholders'; import { enhanceBlockquotes } from './enhance-blockquotes'; +import { enhanceContributorProfile } from './enhance-contributor-profile'; import { enhanceDisclaimer } from './enhance-disclaimer'; import { enhanceDividers } from './enhance-dividers'; import { enhanceDots } from './enhance-dots'; @@ -98,6 +99,12 @@ export const enhanceElements = options.renderingTarget, options.shouldHideAds, ), + enhanceContributorProfile( + format, + options.renderingTarget, + options.tags, + options.byline, + ), enhanceDisclaimer(options.hasAffiliateLinksDisclaimer, isNested), enhanceProductSummary({ pageId: options.pageId, diff --git a/dotcom-rendering/src/types/article.ts b/dotcom-rendering/src/types/article.ts index fce46392341..f08d8335edd 100644 --- a/dotcom-rendering/src/types/article.ts +++ b/dotcom-rendering/src/types/article.ts @@ -105,7 +105,7 @@ export const enhanceArticleType = ( shouldHideAds: data.shouldHideAds, pageId: data.pageId, serverSideABTests: data.config.serverSideABTests, - switches: data.config.switches, + byline: data.byline, }); const crosswordBlock = buildCrosswordBlock(data); diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index 3fbd75dcc98..be4419f7e4a 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -606,6 +606,15 @@ export interface TextBlockElement { html: string; } +export interface ContributorProfileBlockElement { + _type: 'model.dotcomrendering.pageElements.ContributorProfileBlockElement'; + elementId: string; + contributorId: string; + displayName: string; + avatarUrl?: string; + bio?: string; +} + export type DCRTimelineEvent = { date: string; title?: string; @@ -832,6 +841,7 @@ export type FEElement = | CodeBlockElement | CommentBlockElement | ContentAtomBlockElement + | ContributorProfileBlockElement | DisclaimerBlockElement | DividerBlockElement | DocumentBlockElement From 1d65d5e91929005d699cd0cd985071366fc6dcf4 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 5 Feb 2026 17:10:58 +0000 Subject: [PATCH 19/48] delete old files and add a new storybook --- ...ntributorProfileBlockComponent.stories.tsx | 81 +++++++++ .../FollowContributorProfile.importable.tsx | 137 --------------- .../FollowContributorProfile.stories.tsx | 162 ------------------ 3 files changed, 81 insertions(+), 299 deletions(-) create mode 100644 dotcom-rendering/src/components/ContributorProfileBlockComponent.stories.tsx delete mode 100644 dotcom-rendering/src/components/FollowContributorProfile.importable.tsx delete mode 100644 dotcom-rendering/src/components/FollowContributorProfile.stories.tsx diff --git a/dotcom-rendering/src/components/ContributorProfileBlockComponent.stories.tsx b/dotcom-rendering/src/components/ContributorProfileBlockComponent.stories.tsx new file mode 100644 index 00000000000..8ab6e0c06bd --- /dev/null +++ b/dotcom-rendering/src/components/ContributorProfileBlockComponent.stories.tsx @@ -0,0 +1,81 @@ +import { css } from '@emotion/react'; +import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; +import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; +import { ContributorProfileBlockComponent } from './ContributorProfileBlockComponent.importable'; + +export default { + component: ContributorProfileBlockComponent, + title: 'Components/ContributorProfileBlockComponent', +}; + +const mockContributor = { + contributorId: 'profile/george-monbiot', + displayName: 'George Monbiot', + avatarUrl: + 'https://i.guim.co.uk/img/uploads/2017/10/06/George-Monbiot,-L.png?width=300&quality=85&auto=format&fit=max&s=b4786b498dac53ea736ef05160a2a172', + bio: '

George Monbiot is a Guardian columnist and author of The Invisible Doctrine: The Secret History of Neoliberalism

', +}; + +const containerStyles = css` + max-width: 620px; + padding: 16px; +`; + +const opinionDecorator = splitTheme([ + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Opinion, + }, +]); + +const allPillarsDecorator = splitTheme([ + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.News, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Opinion, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Sport, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Culture, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Lifestyle, + }, +]); + +const Wrapper = ({ withBio = true }: { withBio?: boolean }) => ( +
+ +
+); + +export const Default = () => ; +Default.storyName = 'Default (Opinion)'; +Default.decorators = [opinionDecorator]; + +export const WithoutBio = () => ; +WithoutBio.storyName = 'Without Bio'; +WithoutBio.decorators = [opinionDecorator]; + +export const AllPillars = () => ; +AllPillars.storyName = 'All Pillars'; +AllPillars.decorators = [allPillarsDecorator]; diff --git a/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx b/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx deleted file mode 100644 index b83837888b5..00000000000 --- a/dotcom-rendering/src/components/FollowContributorProfile.importable.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { css } from '@emotion/react'; -import { - from, - headlineBold17, - space, - textEgyptian14, -} from '@guardian/source/foundations'; -import sanitise from 'sanitize-html'; -import { palette } from '../palette'; -import { Avatar } from './Avatar'; -import { FollowWrapper } from './FollowWrapper.importable'; -import { StraightLines } from '@guardian/source-development-kitchen/react-components'; - -type Props = { - id: string; - displayName: string; - avatarUrl?: string; - bio?: string; -}; - -const containerStyles = css` - display: flex; - flex-direction: column; - gap: ${space[2]}px; - padding: ${space[2]}px 0; -`; - -const topRowStyles = css` - display: flex; - flex-direction: row; - gap: ${space[3]}px; -`; - -const avatarContainerStyles = css` - width: 60px; - height: 60px; - flex-shrink: 0; - - ${from.tablet} { - width: 80px; - height: 80px; - } -`; - -const contentStyles = css` - display: flex; - flex-direction: column; - flex: 1; - min-width: 0; -`; - -const titleStyles = css` - ${headlineBold17}; - color: ${palette('--follow-accent-color')}; - margin: 0 0 ${space[1]}px; -`; - -const bioStyles = css` - ${textEgyptian14}; - font-weight: 500; - line-height: 1.3; - color: ${palette('--follow-bio-text')}; - margin: 0; - - p { - margin: 0 0 ${space[1]}px; - } - - a { - color: ${palette('--link-kicker-text')}; - text-underline-offset: 3px; - } - - a:not(:hover) { - text-decoration-color: ${palette('--bio-link-underline')}; - } - - a:hover { - text-decoration: underline; - } -`; - -const followButtonContainerStyles = css` - display: flex; -`; - -const containsText = (html: string) => { - const htmlWithoutTags = sanitise(html, { - allowedTags: [], - allowedAttributes: {}, - }); - return htmlWithoutTags.length > 0; -}; - -export const FollowContributorProfile = ({ - id, - displayName, - avatarUrl, - bio, -}: Props) => { - const hasBio = bio && containsText(bio); - const sanitizedBio = hasBio ? sanitise(bio, {}) : undefined; - - return ( -
-
- {!!avatarUrl && ( -
- -
- )} -
-

{displayName}

- {!!sanitizedBio && ( -
- )} -
-
-
- -
- -
- ); -}; diff --git a/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx b/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx deleted file mode 100644 index d622d031624..00000000000 --- a/dotcom-rendering/src/components/FollowContributorProfile.stories.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { css } from '@emotion/react'; -import { - headlineBold17, - space, - textEgyptian14, -} from '@guardian/source/foundations'; -import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; -import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; -import { palette } from '../palette'; -import { Avatar } from './Avatar'; -import { - FollowNotificationsButtonVariant, - FollowTagButtonVariant, -} from './FollowButtons'; -import { FollowContributorProfile } from './FollowContributorProfile.importable'; - -export default { - component: FollowContributorProfile, - title: 'Components/FollowContributorProfile', -}; - -const mockContributor = { - id: 'profile/george-monbiot', - displayName: 'George Monbiot', - avatarUrl: 'https://i.guim.co.uk/img/uploads/2025/05/21/George_Monbiot.png', - bio: `

A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)

`, - bioText: - 'A Guardian columnist, and author of The Invisible Doctrine: The Secret History of Neoliberalism (with Peter Hutchison)', -}; - -const containerStyles = css` - max-width: 400px; - padding: 16px; -`; - -const opinionDecorator = splitTheme([ - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Opinion, - }, -]); - -const allPillarsDecorator = splitTheme([ - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.News, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Opinion, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Sport, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Culture, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Lifestyle, - }, -]); - -const contributorContainerStyles = css` - display: flex; - flex-direction: column; - gap: ${space[3]}px; -`; - -const topRowStyles = css` - display: flex; - flex-direction: row; - gap: ${space[3]}px; -`; - -const avatarContainerStyles = css` - width: 80px; - height: 80px; - flex-shrink: 0; -`; - -const contentStyles = css` - display: flex; - flex-direction: column; - flex: 1; -`; - -const titleStyles = css` - ${headlineBold17}; - color: ${palette('--byline-anchor')}; - margin: 0 0 ${space[1]}px; -`; - -const bioStyles = css` - ${textEgyptian14}; - font-weight: 500; - line-height: 1.3; - letter-spacing: -0.01em; - color: ${palette('--article-text')}; - margin: 0; -`; - -const Wrapper = ({ isFollowing }: { isFollowing: boolean }) => ( -
-
-
-
- -
-
-

{mockContributor.displayName}

-

{mockContributor.bioText}

-
-
-
- undefined} - /> -
-
- {isFollowing && ( -
- undefined} - /> - undefined} - /> -
- )} -
-
-
-); - -export const ContributorFollow = () => ; -ContributorFollow.storyName = 'Follow'; -ContributorFollow.decorators = [opinionDecorator]; - -export const ContributorFollowing = () => ; -ContributorFollowing.storyName = 'Following'; -ContributorFollowing.decorators = [opinionDecorator]; - -export const ContributorAllPillars = () => ; -ContributorAllPillars.storyName = 'All Pillars'; -ContributorAllPillars.decorators = [allPillarsDecorator]; From ba92c92f0d5fea2f5ee43e9e48a51328998fec8b Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 9 Feb 2026 13:26:49 +0000 Subject: [PATCH 20/48] update use effect --- .../src/components/FollowWrapper.importable.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index c9687488245..adb6726d940 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -45,9 +45,15 @@ export const FollowWrapper = ({ return !blockList.includes(tagId); }; - if (isBridgetCompatible && isMyGuardianEnabled && isNotInBlockList(id)) { - setShowFollowTagButton(true); - } + useEffect(() => { + if ( + isBridgetCompatible && + isMyGuardianEnabled && + isNotInBlockList(id) + ) { + setShowFollowTagButton(true); + } + }, [isBridgetCompatible, isMyGuardianEnabled, id]); useEffect(() => { const topic = new Topic({ From 75732f77a59819475dbffcbe0a44edbe00a8390e Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 9 Feb 2026 13:32:47 +0000 Subject: [PATCH 21/48] separate out the new follow wrapper --- ...ibutorProfileBlockComponent.importable.tsx | 7 +- .../ContributorProfileFollow.importable.tsx | 214 ++++++++++++++++++ .../components/FollowWrapper.importable.tsx | 46 ++-- 3 files changed, 231 insertions(+), 36 deletions(-) create mode 100644 dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx diff --git a/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx b/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx index ec3d0e73bc7..fdb352af1bd 100644 --- a/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx +++ b/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx @@ -9,7 +9,7 @@ import { import sanitise from 'sanitize-html'; import { palette } from '../palette'; import { Avatar } from './Avatar'; -import { FollowWrapper } from './FollowWrapper.importable'; +import { ContributorProfileFollow } from './ContributorProfileFollow.importable'; type Props = { contributorId: string; @@ -127,10 +127,9 @@ export const ContributorProfileBlockComponent = ({
-
diff --git a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx b/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx new file mode 100644 index 00000000000..04601114087 --- /dev/null +++ b/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx @@ -0,0 +1,214 @@ +import { css } from '@emotion/react'; +import { Topic } from '@guardian/bridget/Topic'; +import { isUndefined, log } from '@guardian/libs'; +import { space } from '@guardian/source/foundations'; +import { useEffect, useState } from 'react'; +import { getNotificationsClient, getTagClient } from '../lib/bridgetApi'; +import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; +import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; +import { + FollowNotificationsButtonVariant, + FollowTagButtonVariant, +} from './FollowButtons'; + +const containerStyles = css` + display: flex; + flex-direction: column; + gap: ${space[2]}px; + width: 100%; +`; + +const notificationContainerStyles = css` + width: 100%; +`; + +type Props = { + contributorId: string; + displayName: string; +}; + +const isNotInBlockList = (tagId: string) => { + const blockList = ['profile/anas-al-sharif']; + return !blockList.includes(tagId); +}; + +/** + * Follow button specifically for contributor profiles. + * Always renders when the contributor profile is shown. + */ +export const ContributorProfileFollow = ({ + contributorId, + displayName, +}: Props) => { + const [isFollowingNotifications, setIsFollowingNotifications] = useState< + boolean | undefined + >(undefined); + const [isFollowingTag, setIsFollowingTag] = useState( + undefined, + ); + + const isMyGuardianEnabled = useIsMyGuardianEnabled(); + const isBridgetCompatible = useIsBridgetCompatible('2.5.0'); + + const shouldShowFollow = + isBridgetCompatible && + isMyGuardianEnabled && + isNotInBlockList(contributorId); + + useEffect(() => { + if (!shouldShowFollow) return; + + const topic = new Topic({ + id: contributorId, + displayName, + type: 'tag-contributor', + }); + + void getNotificationsClient() + .isFollowing(topic) + .then(setIsFollowingNotifications) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getNotificationsClient-isFollowing-error', + ); + log( + 'dotcom', + 'Bridget getNotificationsClient.isFollowing Error:', + error, + ); + }); + + void getTagClient() + .isFollowing(topic) + .then(setIsFollowingTag) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getTagClient-isFollowing-error', + ); + log('dotcom', 'Bridget getTagClient.isFollowing Error:', error); + }); + }, [contributorId, displayName, shouldShowFollow]); + + const tagHandler = () => { + const topic = new Topic({ + id: contributorId, + displayName, + type: 'tag-contributor', + }); + + if (isFollowingTag) { + void getTagClient() + .unfollow(topic) + .then((success) => { + if (success) { + setIsFollowingTag(false); + } + }) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getTagClient-unfollow-error', + ); + log( + 'dotcom', + 'Bridget getTagClient.unfollow Error:', + error, + ); + }); + } else { + void getTagClient() + .follow(topic) + .then((success) => { + if (success) { + setIsFollowingTag(true); + } + }) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getTagClient-follow-error', + ); + log('dotcom', 'Bridget getTagClient.follow Error:', error); + }); + } + }; + + const notificationsHandler = () => { + const topic = new Topic({ + id: contributorId, + displayName, + type: 'tag-contributor', + }); + + if (isFollowingNotifications) { + void getNotificationsClient() + .unfollow(topic) + .then((success) => { + if (success) { + setIsFollowingNotifications(false); + } + }) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'briidget-getNotificationsClient-unfollow-error', + ); + log( + 'dotcom', + 'Bridget getNotificationsClient.unfollow Error:', + error, + ); + }); + } else { + void getNotificationsClient() + .follow(topic) + .then((success) => { + if (success) { + setIsFollowingNotifications(true); + } + }) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getNotificationsClient-follow-error', + ); + log( + 'dotcom', + 'Bridget getNotificationsClient.follow Error:', + error, + ); + }); + } + }; + + // Don't render if feature flags aren't met + if (!shouldShowFollow) { + return null; + } + + return ( +
+ undefined + } + /> + {isFollowingTag && ( +
+ undefined + } + displayName={displayName} + /> +
+ )} +
+ ); +}; diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index adb6726d940..ed4f19898fe 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -9,7 +9,6 @@ import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; import { FollowNotificationsButtonVariant, FollowTagButton, - FollowTagButtonVariant, } from './FollowButtons'; const notificationContainerStyles = css` @@ -20,14 +19,9 @@ const notificationContainerStyles = css` type Props = { id: string; displayName: string; - variant?: 'default' | 'pill'; }; -export const FollowWrapper = ({ - id, - displayName, - variant = 'default', -}: Props) => { +export const FollowWrapper = ({ id, displayName }: Props) => { const [isFollowingNotifications, setIsFollowingNotifications] = useState< boolean | undefined >(undefined); @@ -198,30 +192,19 @@ export const FollowWrapper = ({ `} data-gu-name="follow" > - {showFollowTagButton && - (variant === 'pill' ? ( - undefined - } - /> - ) : ( - undefined - } - withExtraBottomMargin={true} - /> - ))} - {/* {variant === 'default' && ( */} - {isFollowingTag && ( + {showFollowTagButton && ( + undefined + } + withExtraBottomMargin={true} + /> + )} + {showFollowTagButton && isFollowingTag && (
)} - {/* )} */}
); }; From 7616a081a64d973aa67e9409db40696e340d8faa Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 9 Feb 2026 15:23:38 +0000 Subject: [PATCH 22/48] fix button from stretching --- .../src/components/ContributorProfileFollow.importable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx b/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx index 04601114087..030ac841b84 100644 --- a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx +++ b/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx @@ -16,6 +16,7 @@ const containerStyles = css` flex-direction: column; gap: ${space[2]}px; width: 100%; + align-items: flex-start; `; const notificationContainerStyles = css` From 4b2721488cb56f2797f1fbe7e717d966f3a030a8 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 9 Feb 2026 15:54:41 +0000 Subject: [PATCH 23/48] use shared blocklist --- .../components/ContributorProfileFollow.importable.tsx | 6 +----- .../src/components/FollowWrapper.importable.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx b/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx index 030ac841b84..41f488b35be 100644 --- a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx +++ b/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx @@ -10,6 +10,7 @@ import { FollowNotificationsButtonVariant, FollowTagButtonVariant, } from './FollowButtons'; +import { isNotInBlockList } from './FollowWrapper.importable'; const containerStyles = css` display: flex; @@ -28,11 +29,6 @@ type Props = { displayName: string; }; -const isNotInBlockList = (tagId: string) => { - const blockList = ['profile/anas-al-sharif']; - return !blockList.includes(tagId); -}; - /** * Follow button specifically for contributor profiles. * Always renders when the contributor profile is shown. diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index ed4f19898fe..82ebbbc9eac 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -21,6 +21,11 @@ type Props = { displayName: string; }; +export const isNotInBlockList = (tagId: string) => { + const blockList = ['profile/anas-al-sharif']; + return !blockList.includes(tagId); +}; + export const FollowWrapper = ({ id, displayName }: Props) => { const [isFollowingNotifications, setIsFollowingNotifications] = useState< boolean | undefined @@ -34,11 +39,6 @@ export const FollowWrapper = ({ id, displayName }: Props) => { const isMyGuardianEnabled = useIsMyGuardianEnabled(); const isBridgetCompatible = useIsBridgetCompatible('2.5.0'); - const isNotInBlockList = (tagId: string) => { - const blockList = ['profile/anas-al-sharif']; - return !blockList.includes(tagId); - }; - useEffect(() => { if ( isBridgetCompatible && From 6d6053bdd50787344c01ceb6dd89cc737d2852e1 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 10 Feb 2026 15:12:33 +0000 Subject: [PATCH 24/48] wrap island around the follow button --- ...ContributorProfileBlockComponent.importable.tsx | 13 ++++++++----- dotcom-rendering/src/lib/renderElement.tsx | 14 ++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx b/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx index fdb352af1bd..a75de551f19 100644 --- a/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx +++ b/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx @@ -10,6 +10,7 @@ import sanitise from 'sanitize-html'; import { palette } from '../palette'; import { Avatar } from './Avatar'; import { ContributorProfileFollow } from './ContributorProfileFollow.importable'; +import { Island } from './Island'; type Props = { contributorId: string; @@ -100,7 +101,7 @@ export const ContributorProfileBlockComponent = ({ bio, }: Props) => { const hasBio = bio && containsText(bio); - const sanitizedBio = hasBio ? sanitise(bio, {}) : undefined; + const sanitizedBio = hasBio ? sanitise(bio, {}) : {}; return (
@@ -127,10 +128,12 @@ export const ContributorProfileBlockComponent = ({
- + + +
diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index d693ab249b0..f83fa0a7656 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -288,14 +288,12 @@ export const renderElement = ({ ); case 'model.dotcomrendering.pageElements.ContributorProfileBlockElement': return ( - - - + ); case 'model.dotcomrendering.pageElements.DividerBlockElement': return ( From c2cf6f841ac96a79844c8f8cdc98c31bc01f7828 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 10 Feb 2026 15:21:36 +0000 Subject: [PATCH 25/48] fix lint issues --- ...ibutorProfileBlockComponent.importable.tsx | 6 ++--- .../src/components/FollowButtons.tsx | 2 +- dotcom-rendering/src/paletteDeclarations.ts | 24 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx b/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx index a75de551f19..2a803e922fa 100644 --- a/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx +++ b/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx @@ -1,11 +1,11 @@ import { css } from '@emotion/react'; -import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { from, headlineBold17, space, textEgyptian14, } from '@guardian/source/foundations'; +import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import sanitise from 'sanitize-html'; import { palette } from '../palette'; import { Avatar } from './Avatar'; @@ -100,8 +100,8 @@ export const ContributorProfileBlockComponent = ({ avatarUrl, bio, }: Props) => { - const hasBio = bio && containsText(bio); - const sanitizedBio = hasBio ? sanitise(bio, {}) : {}; + const hasBio = bio !== undefined && containsText(bio); + const sanitizedBio = hasBio ? sanitise(bio, {}) : undefined; return (
diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index 6774bfcd8c5..88d9dc7700a 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -6,9 +6,9 @@ import { SvgNotificationsOn, SvgPlus, } from '@guardian/source/react-components'; +import { ToggleSwitch } from '@guardian/source-development-kitchen/react-components'; import type { ReactNode } from 'react'; import { palette } from '../palette'; -import { ToggleSwitch } from '@guardian/source-development-kitchen/react-components'; type IconProps = { isFollowing?: boolean; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 31e55eb600d..a4489abab18 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -7084,6 +7084,18 @@ const paletteColours = { light: () => sourcePalette.neutral[86], dark: () => sourcePalette.neutral[20], }, + '--follow-accent-color': { + light: followIconFillLight, + dark: followIconFillLight, + }, + '--follow-bio-text': { + light: () => sourcePalette.neutral[10], + dark: () => sourcePalette.neutral[86], + }, + '--follow-bottom-border': { + light: () => sourcePalette.neutral[97], + dark: () => sourcePalette.neutral[97], + }, '--follow-button-border': { light: followIconFillLight, dark: followIconFillDark, @@ -7092,10 +7104,6 @@ const paletteColours = { light: () => sourcePalette.neutral[73], dark: () => sourcePalette.neutral[73], }, - '--follow-accent-color': { - light: followIconFillLight, - dark: followIconFillLight, - }, '--follow-button-text': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[100], @@ -7104,10 +7112,6 @@ const paletteColours = { light: () => sourcePalette.neutral[100], dark: () => sourcePalette.neutral[100], }, - '--follow-bio-text': { - light: () => sourcePalette.neutral[10], - dark: () => sourcePalette.neutral[86], - }, '--follow-icon-background': { light: followIconBackgroundLight, dark: followIconBackgroundDark, @@ -7124,10 +7128,6 @@ const paletteColours = { light: followTextLight, dark: followTextDark, }, - '--follow-bottom-border': { - light: () => sourcePalette.neutral[97], - dark: () => sourcePalette.neutral[97], - }, '--football-competition-select-text': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[86], From 45964b53ba15801f4a5e15ec0533007b560574f0 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 10 Feb 2026 15:21:50 +0000 Subject: [PATCH 26/48] undo unnecessary changes --- .../src/components/FollowButtons.stories.tsx | 4 +- .../components/FollowWrapper.importable.tsx | 43 ++++++------------- .../src/frontend/schemas/feArticle.json | 5 +-- .../src/layouts/CommentLayout.tsx | 4 +- dotcom-rendering/src/model/enhanceTags.ts | 2 - dotcom-rendering/src/paletteDeclarations.ts | 2 +- dotcom-rendering/src/types/tag.ts | 1 - 7 files changed, 18 insertions(+), 43 deletions(-) diff --git a/dotcom-rendering/src/components/FollowButtons.stories.tsx b/dotcom-rendering/src/components/FollowButtons.stories.tsx index 6905487208f..c120ec3c5d3 100644 --- a/dotcom-rendering/src/components/FollowButtons.stories.tsx +++ b/dotcom-rendering/src/components/FollowButtons.stories.tsx @@ -8,7 +8,7 @@ import { } from './FollowButtons'; export default { - component: FollowTagButton, + component: [FollowNotificationsButton, FollowTagButton], title: 'Components/FollowStatus', args: { isFollowing: false, @@ -23,7 +23,7 @@ export const Default = ({ isFollowing }: { isFollowing: boolean }) => { displayName={'John Doe'} onClickHandler={() => undefined} /> - undefined} /> diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index 82ebbbc9eac..43a50946a0f 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -6,15 +6,7 @@ import { useEffect, useState } from 'react'; import { getNotificationsClient, getTagClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; -import { - FollowNotificationsButtonVariant, - FollowTagButton, -} from './FollowButtons'; - -const notificationContainerStyles = css` - margin-top: ${space[3]}px; - width: 100%; -`; +import { FollowNotificationsButton, FollowTagButton } from './FollowButtons'; type Props = { id: string; @@ -39,15 +31,9 @@ export const FollowWrapper = ({ id, displayName }: Props) => { const isMyGuardianEnabled = useIsMyGuardianEnabled(); const isBridgetCompatible = useIsBridgetCompatible('2.5.0'); - useEffect(() => { - if ( - isBridgetCompatible && - isMyGuardianEnabled && - isNotInBlockList(id) - ) { - setShowFollowTagButton(true); - } - }, [isBridgetCompatible, isMyGuardianEnabled, id]); + if (isBridgetCompatible && isMyGuardianEnabled && isNotInBlockList(id)) { + setShowFollowTagButton(true); + } useEffect(() => { const topic = new Topic({ @@ -204,19 +190,14 @@ export const FollowWrapper = ({ id, displayName }: Props) => { withExtraBottomMargin={true} /> )} - {showFollowTagButton && isFollowingTag && ( -
- undefined - } - displayName={displayName} - /> -
- )} + undefined + } + />
); }; diff --git a/dotcom-rendering/src/frontend/schemas/feArticle.json b/dotcom-rendering/src/frontend/schemas/feArticle.json index 9b11cba3d8b..3f604b084c2 100644 --- a/dotcom-rendering/src/frontend/schemas/feArticle.json +++ b/dotcom-rendering/src/frontend/schemas/feArticle.json @@ -112,9 +112,6 @@ }, "bio": { "type": "string" - }, - "description": { - "type": "string" } }, "required": [ @@ -6043,4 +6040,4 @@ } }, "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file +} diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index 8aa70def138..17341643b98 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -291,8 +291,8 @@ export const CommentLayout = (props: WebProps | AppsProps) => { const showComments = article.isCommentable && !isPaidContent; - const soleContributor = getSoleContributor(article.tags, article.byline); - const avatarUrl = soleContributor?.bylineLargeImageUrl; + const avatarUrl = getSoleContributor(article.tags, article.byline) + ?.bylineLargeImageUrl; const { branding } = article.commercialProperties[article.editionId]; diff --git a/dotcom-rendering/src/model/enhanceTags.ts b/dotcom-rendering/src/model/enhanceTags.ts index 1927e831e1c..5df9b3b2db2 100644 --- a/dotcom-rendering/src/model/enhanceTags.ts +++ b/dotcom-rendering/src/model/enhanceTags.ts @@ -12,7 +12,6 @@ const enhanceTag = ({ bylineImageUrl, contributorLargeImagePath: bylineLargeImageUrl, bio, - description, }, }: FETagType): TagType => ({ id, @@ -22,5 +21,4 @@ const enhanceTag = ({ bylineImageUrl, bylineLargeImageUrl, bio, - description, }); diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index a4489abab18..6e0004ceadc 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -1,4 +1,4 @@ -/// ----- Imports ----- // +// ----- Imports ----- // /* eslint sort-keys: ["error", "asc", { minKeys: 12, natural: true }] -- the palette object is large and ordering helps knowing where to insert new elements diff --git a/dotcom-rendering/src/types/tag.ts b/dotcom-rendering/src/types/tag.ts index 2470f874a09..1d4c3534735 100644 --- a/dotcom-rendering/src/types/tag.ts +++ b/dotcom-rendering/src/types/tag.ts @@ -58,5 +58,4 @@ export type TagType = { bylineLargeImageUrl?: string; podcast?: Podcast; bio?: string; - description?: string; }; From 12b0cb58f3b7ebd0156c70894dc894352c2bdba7 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 10 Feb 2026 16:03:14 +0000 Subject: [PATCH 27/48] update ContributorProfileFOllow --- .../components/ContributorProfileFollow.importable.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx b/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx index 41f488b35be..8a4c1319ee3 100644 --- a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx +++ b/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx @@ -29,10 +29,6 @@ type Props = { displayName: string; }; -/** - * Follow button specifically for contributor profiles. - * Always renders when the contributor profile is shown. - */ export const ContributorProfileFollow = ({ contributorId, displayName, @@ -53,8 +49,6 @@ export const ContributorProfileFollow = ({ isNotInBlockList(contributorId); useEffect(() => { - if (!shouldShowFollow) return; - const topic = new Topic({ id: contributorId, displayName, @@ -86,7 +80,7 @@ export const ContributorProfileFollow = ({ ); log('dotcom', 'Bridget getTagClient.isFollowing Error:', error); }); - }, [contributorId, displayName, shouldShowFollow]); + }, [contributorId, displayName]); const tagHandler = () => { const topic = new Topic({ @@ -180,7 +174,6 @@ export const ContributorProfileFollow = ({ } }; - // Don't render if feature flags aren't met if (!shouldShowFollow) { return null; } From e733a28e8dcb483ef1f0de7cf7a2f388906c4c61 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 10 Feb 2026 18:13:54 +0000 Subject: [PATCH 28/48] update notification button copy --- dotcom-rendering/src/components/FollowButtons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index 88d9dc7700a..83f0f5cf963 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -233,7 +233,7 @@ const notificationsTextSpanVariant = ({ {isFollowing ? 'Notifications on' - : `Turn on notifications to be alerted whenever ${ + : `Receive an alert when ${ displayName ?? 'this author' } publishes an article`} From 15a7d723d7e82b9a737c23fa1de5f1009c3c665d Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 10 Feb 2026 18:14:15 +0000 Subject: [PATCH 29/48] rename to ContributorFollowCard --- ...x => ContributorFollowCard.importable.tsx} | 2 +- ...orFollowCardBlockComponent.importable.tsx} | 8 +++---- ...butorFollowCardBlockComponent.stories.tsx} | 18 +++++++-------- dotcom-rendering/src/lib/renderElement.tsx | 6 ++--- ....ts => enhance-contributor-follow-card.ts} | 23 ++++++++++--------- dotcom-rendering/src/model/enhanceBlocks.ts | 4 ++-- dotcom-rendering/src/types/content.ts | 6 ++--- 7 files changed, 34 insertions(+), 33 deletions(-) rename dotcom-rendering/src/components/{ContributorProfileFollow.importable.tsx => ContributorFollowCard.importable.tsx} (99%) rename dotcom-rendering/src/components/{ContributorProfileBlockComponent.importable.tsx => ContributorFollowCardBlockComponent.importable.tsx} (93%) rename dotcom-rendering/src/components/{ContributorProfileBlockComponent.stories.tsx => ContributorFollowCardBlockComponent.stories.tsx} (80%) rename dotcom-rendering/src/model/{enhance-contributor-profile.ts => enhance-contributor-follow-card.ts} (73%) diff --git a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx b/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx similarity index 99% rename from dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx rename to dotcom-rendering/src/components/ContributorFollowCard.importable.tsx index 8a4c1319ee3..0ad62507eba 100644 --- a/dotcom-rendering/src/components/ContributorProfileFollow.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx @@ -29,7 +29,7 @@ type Props = { displayName: string; }; -export const ContributorProfileFollow = ({ +export const ContributorFollowCard = ({ contributorId, displayName, }: Props) => { diff --git a/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx b/dotcom-rendering/src/components/ContributorFollowCardBlockComponent.importable.tsx similarity index 93% rename from dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx rename to dotcom-rendering/src/components/ContributorFollowCardBlockComponent.importable.tsx index 2a803e922fa..35bd9dd6649 100644 --- a/dotcom-rendering/src/components/ContributorProfileBlockComponent.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowCardBlockComponent.importable.tsx @@ -9,7 +9,7 @@ import { StraightLines } from '@guardian/source-development-kitchen/react-compon import sanitise from 'sanitize-html'; import { palette } from '../palette'; import { Avatar } from './Avatar'; -import { ContributorProfileFollow } from './ContributorProfileFollow.importable'; +import { ContributorFollowCard } from './ContributorFollowCard.importable'; import { Island } from './Island'; type Props = { @@ -53,7 +53,7 @@ const contentStyles = css` const titleStyles = css` ${headlineBold17}; color: ${palette('--follow-accent-color')}; - margin: 0 0 ${space[1]}px; + margin: 0 0 ${space[2]}px; `; const bioStyles = css` @@ -94,7 +94,7 @@ const containsText = (html: string) => { return htmlWithoutTags.length > 0; }; -export const ContributorProfileBlockComponent = ({ +export const ContributorFollowCardBlockComponent = ({ contributorId, displayName, avatarUrl, @@ -129,7 +129,7 @@ export const ContributorProfileBlockComponent = ({
- diff --git a/dotcom-rendering/src/components/ContributorProfileBlockComponent.stories.tsx b/dotcom-rendering/src/components/ContributorFollowCardBlockComponent.stories.tsx similarity index 80% rename from dotcom-rendering/src/components/ContributorProfileBlockComponent.stories.tsx rename to dotcom-rendering/src/components/ContributorFollowCardBlockComponent.stories.tsx index 8ab6e0c06bd..d0a0ad7827d 100644 --- a/dotcom-rendering/src/components/ContributorProfileBlockComponent.stories.tsx +++ b/dotcom-rendering/src/components/ContributorFollowCardBlockComponent.stories.tsx @@ -1,14 +1,14 @@ import { css } from '@emotion/react'; import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; -import { ContributorProfileBlockComponent } from './ContributorProfileBlockComponent.importable'; +import { ContributorFollowCardBlockComponent } from './ContributorFollowCardBlockComponent.importable'; export default { - component: ContributorProfileBlockComponent, - title: 'Components/ContributorProfileBlockComponent', + component: ContributorFollowCardBlockComponent, + title: 'Components/ContributorFollowCardBlockComponent', }; -const mockContributor = { +const contributor = { contributorId: 'profile/george-monbiot', displayName: 'George Monbiot', avatarUrl: @@ -59,11 +59,11 @@ const allPillarsDecorator = splitTheme([ const Wrapper = ({ withBio = true }: { withBio?: boolean }) => (
-
); diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index f83fa0a7656..5973a119010 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -9,7 +9,7 @@ import { CartoonComponent } from '../components/CartoonComponent'; import { ChartAtom } from '../components/ChartAtom.importable'; import { CodeBlockComponent } from '../components/CodeBlockComponent'; import { CommentBlockComponent } from '../components/CommentBlockComponent'; -import { ContributorProfileBlockComponent } from '../components/ContributorProfileBlockComponent.importable'; +import { ContributorFollowCardBlockComponent } from '../components/ContributorFollowCardBlockComponent.importable'; import { CrosswordComponent } from '../components/CrosswordComponent.importable'; import { DividerBlockComponent } from '../components/DividerBlockComponent'; import { DocumentBlockComponent } from '../components/DocumentBlockComponent.importable'; @@ -286,9 +286,9 @@ export const renderElement = ({ permalink={element.permalink} /> ); - case 'model.dotcomrendering.pageElements.ContributorProfileBlockElement': + case 'model.dotcomrendering.pageElements.ContributorFollowCardBlockElement': return ( - element._type === 'model.dotcomrendering.pageElements.TextBlockElement'; -export const enhanceContributorProfile = +export const enhanceContributorFollowCard = ( format: ArticleFormat, renderingTarget: RenderingTarget, @@ -51,18 +51,19 @@ export const enhanceContributorProfile = insertPosition = elements.length; } - const contributorProfileElement: ContributorProfileBlockElement = { - _type: 'model.dotcomrendering.pageElements.ContributorProfileBlockElement', - elementId: `contributor-profile-${soleContributor.id}`, - contributorId: soleContributor.id, - displayName: soleContributor.title, - avatarUrl: soleContributor.bylineLargeImageUrl, - bio: soleContributor.bio, - }; + const contributorFollowCardElement: ContributorFollowCardBlockElement = + { + _type: 'model.dotcomrendering.pageElements.ContributorFollowCardBlockElement', + elementId: `contributor-profile-${soleContributor.id}`, + contributorId: soleContributor.id, + displayName: soleContributor.title, + avatarUrl: soleContributor.bylineLargeImageUrl, + bio: soleContributor.bio, + }; return [ ...elements.slice(0, insertPosition), - contributorProfileElement, + contributorFollowCardElement, ...elements.slice(insertPosition), ]; }; diff --git a/dotcom-rendering/src/model/enhanceBlocks.ts b/dotcom-rendering/src/model/enhanceBlocks.ts index 5d679e53171..a2500bda586 100644 --- a/dotcom-rendering/src/model/enhanceBlocks.ts +++ b/dotcom-rendering/src/model/enhanceBlocks.ts @@ -12,7 +12,7 @@ import type { RenderingTarget } from '../types/renderingTarget'; import type { TagType } from '../types/tag'; import { enhanceAdPlaceholders } from './enhance-ad-placeholders'; import { enhanceBlockquotes } from './enhance-blockquotes'; -import { enhanceContributorProfile } from './enhance-contributor-profile'; +import { enhanceContributorFollowCard } from './enhance-contributor-follow-card'; import { enhanceDisclaimer } from './enhance-disclaimer'; import { enhanceDividers } from './enhance-dividers'; import { enhanceDots } from './enhance-dots'; @@ -99,7 +99,7 @@ export const enhanceElements = options.renderingTarget, options.shouldHideAds, ), - enhanceContributorProfile( + enhanceContributorFollowCard( format, options.renderingTarget, options.tags, diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index be4419f7e4a..ae60068def6 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -606,8 +606,8 @@ export interface TextBlockElement { html: string; } -export interface ContributorProfileBlockElement { - _type: 'model.dotcomrendering.pageElements.ContributorProfileBlockElement'; +export interface ContributorFollowCardBlockElement { + _type: 'model.dotcomrendering.pageElements.ContributorFollowCardBlockElement'; elementId: string; contributorId: string; displayName: string; @@ -841,7 +841,7 @@ export type FEElement = | CodeBlockElement | CommentBlockElement | ContentAtomBlockElement - | ContributorProfileBlockElement + | ContributorFollowCardBlockElement | DisclaimerBlockElement | DividerBlockElement | DocumentBlockElement From b01eb65bd0b3b040be532331aaa9f203aed0c82f Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 10 Feb 2026 18:26:21 +0000 Subject: [PATCH 30/48] move button and notifications to inside the new component --- .../ContributorFollowCard.importable.tsx | 199 +++++++++++++++++- .../src/components/FollowButtons.stories.tsx | 83 +------- .../src/components/FollowButtons.tsx | 157 +------------- 3 files changed, 193 insertions(+), 246 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx b/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx index 0ad62507eba..5610cd2042b 100644 --- a/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx @@ -1,17 +1,200 @@ import { css } from '@emotion/react'; import { Topic } from '@guardian/bridget/Topic'; import { isUndefined, log } from '@guardian/libs'; -import { space } from '@guardian/source/foundations'; +import { space, textSans12, textSans15 } from '@guardian/source/foundations'; +import { + SvgCheckmark, + SvgNotificationsOff, + SvgNotificationsOn, + SvgPlus, +} from '@guardian/source/react-components'; +import { ToggleSwitch } from '@guardian/source-development-kitchen/react-components'; +import type { ReactNode } from 'react'; import { useEffect, useState } from 'react'; import { getNotificationsClient, getTagClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; -import { - FollowNotificationsButtonVariant, - FollowTagButtonVariant, -} from './FollowButtons'; +import { palette } from '../palette'; import { isNotInBlockList } from './FollowWrapper.importable'; +// -- Follow button -- + +type ButtonProps = { + isFollowing: boolean; + onClickHandler: () => void; +}; + +const followButtonStyles = (isFollowing: boolean) => css` + ${textSans15} + display: inline-flex; + align-items: center; + gap: ${space[1]}px; + padding: ${space[1] + 1}px ${space[3] + 2}px; + border-radius: ${space[5]}px; + border: 1px solid + ${isFollowing + ? palette('--follow-button-border-following') + : palette('--follow-button-border')}; + background: ${isFollowing + ? 'transparent' + : palette('--follow-accent-color')}; + color: ${isFollowing + ? palette('--follow-button-text') + : palette('--follow-button-text-not-following')}; + font-weight: 700; + cursor: pointer; + + svg { + width: 24px; + height: 24px; + fill: ${isFollowing + ? palette('--follow-button-text') + : palette('--follow-button-text-not-following')}; + stroke: ${isFollowing + ? palette('--follow-button-text') + : palette('--follow-button-text-not-following')}; + } +`; + +const FollowTagButton = ({ isFollowing, onClickHandler }: ButtonProps) => { + return ( + + ); +}; + +// -- Notifications toggle -- + +type IconProps = { + isFollowing?: boolean; + iconIsFollowing: ReactNode; + iconIsNotFollowing: ReactNode; +}; + +const NotificationIcon = ({ + isFollowing, + iconIsFollowing, + iconIsNotFollowing, +}: IconProps) => ( +
+ {isFollowing ? iconIsFollowing : iconIsNotFollowing} +
+); + +const notificationsTextSpan = ({ + isFollowing, + displayName, +}: { + isFollowing: boolean; + displayName?: string; +}) => ( + + {isFollowing + ? 'Notifications on' + : `Receive an alert when ${ + displayName ?? 'this author' + } publishes an article`} + +); + +const toggleSwitchStyles = css` + [aria-checked='false'] { + background-color: ${palette( + '--follow-button-border-following', + )} !important; + border-color: ${palette('--follow-button-border-following')} !important; + } + [aria-checked='true'] { + background-color: ${palette('--follow-button-border')} !important; + border-color: ${palette('--follow-button-border')} !important; + } +`; + +const notificationRowStyles = css` + ${textSans15} + color: ${palette('--follow-text')}; + background: none; + border: none; + display: block; + margin-left: 0; + min-height: ${space[6]}px; + padding: 0; + text-align: left; + margin-bottom: ${space[2]}px; + width: 100%; +`; + +const notificationRowContainerStyles = css` + display: flex; + column-gap: 0.2em; + justify-content: space-between; + width: 100%; +`; + +const iconTextWrapperStyles = css` + display: flex; + align-items: flex-start; +`; + +const FollowNotificationsButton = ({ + isFollowing, + onClickHandler, + displayName, +}: ButtonProps & { displayName?: string }) => { + return ( +
+
+
+ } + iconIsNotFollowing={ + + } + /> + {notificationsTextSpan({ isFollowing, displayName })} +
+
+ +
+
+
+ ); +}; + +// -- Card layout -- + const containerStyles = css` display: flex; flex-direction: column; @@ -179,8 +362,8 @@ export const ContributorFollowCard = ({ } return ( -
- + undefined @@ -188,7 +371,7 @@ export const ContributorFollowCard = ({ /> {isFollowingTag && (
- { ); }; FollowContributorBothStates.decorators = [splitTheme()]; - -const variantContainerStyles = css` - display: flex; - gap: 16px; - margin-bottom: 16px; -`; - -export const VariantFollowTagButtonBothStates = () => { - return ( -
- undefined} - /> - undefined} - /> -
- ); -}; -VariantFollowTagButtonBothStates.storyName = 'Variant Button - Both States'; -VariantFollowTagButtonBothStates.decorators = [ - splitTheme([ - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Standard, - theme: Pillar.News, - }, - ]), -]; - -export const VariantFollowTagButtonAllPillars = () => { - return ( -
- undefined} - /> - undefined} - /> -
- ); -}; -VariantFollowTagButtonAllPillars.storyName = 'Variant Button - All Pillars'; -VariantFollowTagButtonAllPillars.decorators = [ - splitTheme([ - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Standard, - theme: Pillar.News, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Standard, - theme: Pillar.Opinion, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Standard, - theme: Pillar.Sport, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Standard, - theme: Pillar.Culture, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Standard, - theme: Pillar.Lifestyle, - }, - ]), -]; diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowButtons.tsx index 83f0f5cf963..d2dce658e10 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowButtons.tsx @@ -1,12 +1,11 @@ import { css } from '@emotion/react'; -import { space, textSans12, textSans15 } from '@guardian/source/foundations'; +import { space, textSans15 } from '@guardian/source/foundations'; import { SvgCheckmark, SvgNotificationsOff, SvgNotificationsOn, SvgPlus, } from '@guardian/source/react-components'; -import { ToggleSwitch } from '@guardian/source-development-kitchen/react-components'; import type { ReactNode } from 'react'; import { palette } from '../palette'; @@ -137,157 +136,3 @@ export const FollowTagButton = ({ ); }; - -// Variant style - -const containerStylesVariant = css` - display: flex; - column-gap: 0.2em; - justify-content: space-between; - width: 100%; -`; - -const buttonStylesVariant = (isFollowing: boolean) => css` - ${textSans15} - display: inline-flex; - align-items: center; - gap: ${space[1]}px; - padding: ${space[1] + 1}px ${space[3] + 2}px; - border-radius: ${space[5]}px; - border: 1px solid - ${isFollowing - ? palette('--follow-button-border-following') - : palette('--follow-button-border')}; - background: ${isFollowing - ? 'transparent' - : palette('--follow-accent-color')}; - color: ${isFollowing - ? palette('--follow-button-text') - : palette('--follow-button-text-not-following')}; - font-weight: 700; - cursor: pointer; - - svg { - width: 24px; - height: 24px; - fill: ${isFollowing - ? palette('--follow-button-text') - : palette('--follow-button-text-not-following')}; - stroke: ${isFollowing - ? palette('--follow-button-text') - : palette('--follow-button-text-not-following')}; - } -`; - -export const FollowTagButtonVariant = ({ - isFollowing, - onClickHandler, -}: ButtonProps) => { - return ( - - ); -}; - -const NotificationIconVariant = ({ - isFollowing, - iconIsFollowing, - iconIsNotFollowing, -}: IconProps) => ( -
- {isFollowing ? iconIsFollowing : iconIsNotFollowing} -
-); - -const notificationTextStylesVariant = css` - ${textSans12} -`; - -const notificationsTextSpanVariant = ({ - isFollowing, - displayName, -}: Pick & { displayName?: string }) => ( - - {isFollowing - ? 'Notifications on' - : `Receive an alert when ${ - displayName ?? 'this author' - } publishes an article`} - -); - -const toggleSwitchStyles = css` - [aria-checked='false'] { - background-color: ${palette( - '--follow-button-border-following', - )} !important; - border-color: ${palette('--follow-button-border-following')} !important; - } - [aria-checked='true'] { - background-color: ${palette('--follow-button-border')} !important; - border-color: ${palette('--follow-button-border')} !important; - } -`; - -const buttonStylesVariantNotification = css` - ${buttonStyles(true)} - width: 100%; -`; - -const iconTextWrapperStyles = css` - display: flex; - align-items: flex-start; -`; - -export const FollowNotificationsButtonVariant = ({ - isFollowing, - onClickHandler, - displayName, -}: ButtonProps & { displayName?: string }) => { - return ( -
-
-
- } - iconIsNotFollowing={ - - } - /> - {notificationsTextSpanVariant({ isFollowing, displayName })} -
-
- -
-
-
- ); -}; From 789bc26e1fb0b731ed88a1b919ad5b979dcf90c5 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 10 Feb 2026 18:46:59 +0000 Subject: [PATCH 31/48] tweaks on notification component --- .../ContributorFollowCard.importable.tsx | 157 ++++++------------ 1 file changed, 51 insertions(+), 106 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx b/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx index 5610cd2042b..970e6cfc62e 100644 --- a/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowCard.importable.tsx @@ -4,12 +4,13 @@ import { isUndefined, log } from '@guardian/libs'; import { space, textSans12, textSans15 } from '@guardian/source/foundations'; import { SvgCheckmark, - SvgNotificationsOff, SvgNotificationsOn, SvgPlus, } from '@guardian/source/react-components'; -import { ToggleSwitch } from '@guardian/source-development-kitchen/react-components'; -import type { ReactNode } from 'react'; +import { + StraightLines, + ToggleSwitch, +} from '@guardian/source-development-kitchen/react-components'; import { useEffect, useState } from 'react'; import { getNotificationsClient, getTagClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; @@ -19,7 +20,7 @@ import { isNotInBlockList } from './FollowWrapper.importable'; // -- Follow button -- -type ButtonProps = { +type FollowButtonProps = { isFollowing: boolean; onClickHandler: () => void; }; @@ -56,7 +57,7 @@ const followButtonStyles = (isFollowing: boolean) => css` } `; -const FollowTagButton = ({ isFollowing, onClickHandler }: ButtonProps) => { +const FollowButton = ({ isFollowing, onClickHandler }: FollowButtonProps) => { return ( ); }; // -- notifications -- -const notificationIconStyles = css` - display: flex; - margin: 0; - margin-right: ${space[1]}px; - - svg { - margin-top: -${space[1] - 1}px; - } -`; - const notificationsStatusStyles = css` ${textSans15} color: ${palette('--follow-text')}; @@ -96,23 +91,42 @@ const notificationsStatusStyles = css` min-height: ${space[6]}px; padding: 0; text-align: left; - margin: ${space[1]}px 0 0 ${space[1]}px; width: 100%; `; const notificationsStatusContainerStyles = css` display: flex; - column-gap: ${space[1]}px; + column-gap: ${space[6]}px; justify-content: space-between; width: 100%; `; +const notificationIconStyles = css` + display: flex; + margin: 0; + margin-right: ${space[1]}px; + + svg { + margin-top: -${space[1] - 1}px; + } +`; const notificationIconTextWrapperStyles = css` display: flex; align-items: flex-start; ${textSans12} `; +const toggleSwitchContainerStyles = css` + transform: scale(1.2); + button[aria-checked='false'] { + background-color: ${sourcePalette.neutral[60]}; + border-color: ${sourcePalette.neutral[60]}; + } + button[aria-checked='true']::before { + display: none; + } +`; + const NotificationsStatus = ({ isFollowing, onClickHandler, @@ -130,7 +144,7 @@ const NotificationsStatus = ({ publishes an article
-
+
{ +}: ContainerProps) => { const [isFollowingNotifications, setIsFollowingNotifications] = useState< boolean | undefined >(undefined); @@ -304,7 +317,7 @@ export const ContributorFollowBlock = ({ } return ( -
+
{ diff --git a/dotcom-rendering/src/components/ContributorFollowBlockComponent.stories.tsx b/dotcom-rendering/src/components/ContributorFollowBlockComponent.stories.tsx index 974966b6ace..de7c695987a 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlockComponent.stories.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlockComponent.stories.tsx @@ -1,14 +1,30 @@ import { css } from '@emotion/react'; -import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; +import { mocked } from 'storybook/test'; import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; +import { getNotificationsClient, getTagClient } from '../lib/bridgetApi'; +import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; +import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; import { ContributorFollowBlockComponent } from './ContributorFollowBlockComponent.importable'; +const opinionFormat = { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Opinion, +}; + export default { component: ContributorFollowBlockComponent, title: 'Components/ContributorFollowBlockComponent', + async beforeEach() { + mocked(useIsMyGuardianEnabled).mockReturnValue(true); + mocked(useIsBridgetCompatible).mockReturnValue(true); + }, + parameters: { + formats: [opinionFormat], + }, }; -const contributor = { +const contributorData = { contributorId: 'profile/george-monbiot', displayName: 'George Monbiot', avatarUrl: @@ -21,61 +37,78 @@ const containerStyles = css` padding: 16px; `; -const opinionDecorator = splitTheme([ - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Opinion, - }, -]); - -const allPillarsDecorator = splitTheme([ - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.News, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Opinion, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Sport, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Culture, - }, - { - display: ArticleDisplay.Standard, - design: ArticleDesign.Comment, - theme: Pillar.Lifestyle, - }, -]); +const mockBridgetClients = ( + isFollowingTag: boolean, + isFollowingNotifications: boolean, +) => { + mocked(getTagClient).mockReturnValue({ + isFollowing: () => Promise.resolve(isFollowingTag), + follow: () => Promise.resolve(true), + unfollow: () => Promise.resolve(true), + } as unknown as ReturnType); + mocked(getNotificationsClient).mockReturnValue({ + isFollowing: () => Promise.resolve(isFollowingNotifications), + follow: () => Promise.resolve(true), + unfollow: () => Promise.resolve(true), + } as unknown as ReturnType); +}; -const Wrapper = ({ withBio = true }: { withBio?: boolean }) => ( +const ContributorFollowBlockStory = () => (
); -export const Default = () => ; -Default.storyName = 'Default (Opinion)'; -Default.decorators = [opinionDecorator]; +export const NotFollowing = () => ; +NotFollowing.storyName = 'Not Following'; +NotFollowing.beforeEach = () => { + mockBridgetClients(false, false); +}; -export const WithoutBio = () => ; -WithoutBio.storyName = 'Without Bio'; -WithoutBio.decorators = [opinionDecorator]; +export const Following = () => ; +Following.storyName = 'Following'; +Following.beforeEach = () => { + mockBridgetClients(true, false); +}; -export const AllPillars = () => ; +export const FollowingWithNotifications = () => ; +FollowingWithNotifications.storyName = 'Following with Notifications'; +FollowingWithNotifications.beforeEach = () => { + mockBridgetClients(true, true); +}; + +export const AllPillars = () => ; AllPillars.storyName = 'All Pillars'; -AllPillars.decorators = [allPillarsDecorator]; +AllPillars.parameters = { + formats: [ + opinionFormat, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.News, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Sport, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Culture, + }, + { + display: ArticleDisplay.Standard, + design: ArticleDesign.Comment, + theme: Pillar.Lifestyle, + }, + ], +}; +AllPillars.beforeEach = () => { + mockBridgetClients(false, false); +}; From 48bd8723417782fb9c1eb943fbc8c6f2f1d17efb Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Wed, 11 Feb 2026 16:17:39 +0000 Subject: [PATCH 34/48] update palette definition names --- .../ContributorFollowBlock.importable.tsx | 42 +++++++++---------- ...ributorFollowBlockComponent.importable.tsx | 12 +++--- dotcom-rendering/src/paletteDeclarations.ts | 20 +++------ 3 files changed, 33 insertions(+), 41 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx index a4c25392760..6c064a44022 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx @@ -39,14 +39,14 @@ const followButtonStyles = (isFollowing: boolean) => css` border-radius: ${space[5]}px; border: 1px solid ${isFollowing - ? palette('--follow-button-border-following') - : palette('--follow-button-border')}; + ? palette('--contributor-follow-button-border-following') + : palette('--contributor-follow-button-border')}; background: ${isFollowing ? 'transparent' - : palette('--follow-accent-color')}; + : palette('--contributor-follow-accent-color')}; color: ${isFollowing - ? palette('--follow-button-text') - : palette('--follow-button-text-not-following')}; + ? palette('--contributor-follow-button-text') + : palette('--contributor-follow-button-text-not-following')}; font-weight: 700; cursor: pointer; @@ -54,11 +54,11 @@ const followButtonStyles = (isFollowing: boolean) => css` width: 24px; height: 24px; fill: ${isFollowing - ? palette('--follow-button-text') - : palette('--follow-button-text-not-following')}; + ? palette('--contributor-follow-button-text') + : palette('--contributor-follow-button-text-not-following')}; stroke: ${isFollowing - ? palette('--follow-button-text') - : palette('--follow-button-text-not-following')}; + ? palette('--contributor-follow-button-text') + : palette('--contributor-follow-button-text-not-following')}; } `; @@ -81,7 +81,7 @@ const FollowButton = ({ isFollowing, onClickHandler }: FollowButtonProps) => { // -- notifications -- -const notificationsStatusStyles = css` +const notificationAlertStyles = css` ${textSans15} color: ${palette('--follow-text')}; background: none; @@ -94,7 +94,7 @@ const notificationsStatusStyles = css` width: 100%; `; -const notificationsStatusContainerStyles = css` +const notificationAlertRowStyles = css` display: flex; column-gap: ${space[6]}px; justify-content: space-between; @@ -110,7 +110,7 @@ const notificationIconStyles = css` margin-top: -${space[1] - 1}px; } `; -const notificationIconTextWrapperStyles = css` +const notificationLabelStyles = css` display: flex; align-items: flex-start; ${textSans12} @@ -127,15 +127,15 @@ const toggleSwitchContainerStyles = css` } `; -const NotificationsStatus = ({ +const NotificationAlert = ({ isFollowing, onClickHandler, displayName, }: FollowButtonProps & { displayName?: string }) => { return ( -
-
-
+
+
+
@@ -155,14 +155,14 @@ const NotificationsStatus = ({ ); }; -const containerStyles = css` +const followBlockStyles = css` display: flex; flex-direction: column; width: 100%; align-items: flex-start; `; -type ContainerProps = { +type FollowBlockProps = { contributorId: string; displayName: string; }; @@ -170,7 +170,7 @@ type ContainerProps = { export const ContributorFollowBlock = ({ contributorId, displayName, -}: ContainerProps) => { +}: FollowBlockProps) => { const [isFollowingNotifications, setIsFollowingNotifications] = useState< boolean | undefined >(undefined); @@ -317,7 +317,7 @@ export const ContributorFollowBlock = ({ } return ( -
+
- +
{!!avatarUrl && ( @@ -117,7 +117,7 @@ export const ContributorFollowBlockComponent = ({ />
)} -
+

{displayName}

{!!sanitizedBio && (
sourcePalette.neutral[86], dark: () => sourcePalette.neutral[20], }, - '--follow-accent-color': { + '--contributor-follow-accent-color': { light: followIconFillLight, dark: followIconFillLight, }, - '--follow-bio-text': { + '--contributor-follow-bio-text': { light: () => sourcePalette.neutral[10], dark: () => sourcePalette.neutral[86], }, - '--follow-bottom-border': { - light: () => sourcePalette.neutral[97], - dark: () => sourcePalette.neutral[97], - }, - '--follow-button-border': { + '--contributor-follow-button-border': { light: followIconFillLight, dark: followIconFillDark, }, - '--follow-button-border-following': { + '--contributor-follow-button-border-following': { light: () => sourcePalette.neutral[73], dark: () => sourcePalette.neutral[73], }, - '--follow-button-text': { + '--contributor-follow-button-text': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[100], }, - '--follow-button-text-not-following': { + '--contributor-follow-button-text-not-following': { light: () => sourcePalette.neutral[100], dark: () => sourcePalette.neutral[100], }, @@ -7120,10 +7116,6 @@ const paletteColours = { light: followIconFillLight, dark: followIconFillDark, }, - '--follow-icon-variant-fill': { - light: () => sourcePalette.neutral[10], - dark: () => sourcePalette.neutral[86], - }, '--follow-text': { light: followTextLight, dark: followTextDark, From bde21c8e86f4741e925b2e851215beeb870e7c8f Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Wed, 11 Feb 2026 16:37:15 +0000 Subject: [PATCH 35/48] add dark mode colours --- .../components/ContributorFollowBlock.importable.tsx | 6 +++++- .../ContributorFollowBlockComponent.importable.tsx | 10 ++++++++-- dotcom-rendering/src/paletteDeclarations.ts | 8 ++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx index 6c064a44022..a4a3a64d108 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx @@ -108,6 +108,7 @@ const notificationIconStyles = css` svg { margin-top: -${space[1] - 1}px; + fill: currentColor; } `; const notificationLabelStyles = css` @@ -328,7 +329,10 @@ export const ContributorFollowBlock = ({ /> {isFollowingContributor && (
- + - +
{!!avatarUrl && (
@@ -135,7 +138,10 @@ export const ContributorFollowBlockComponent = ({ />
- +
); }; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 81248cf96e8..04399422e24 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -7094,11 +7094,11 @@ const paletteColours = { }, '--contributor-follow-button-border': { light: followIconFillLight, - dark: followIconFillDark, + dark: () => sourcePalette.neutral[20], }, '--contributor-follow-button-border-following': { light: () => sourcePalette.neutral[73], - dark: () => sourcePalette.neutral[73], + dark: () => sourcePalette.neutral[20], }, '--contributor-follow-button-text': { light: () => sourcePalette.neutral[7], @@ -7108,6 +7108,10 @@ const paletteColours = { light: () => sourcePalette.neutral[100], dark: () => sourcePalette.neutral[100], }, + '--contributor-follow-straight-lines': { + light: () => sourcePalette.neutral[86], + dark: () => sourcePalette.neutral[38], + }, '--follow-icon-background': { light: followIconBackgroundLight, dark: followIconBackgroundDark, From c919d3bbcf898797af0f30e6970bfebb115c260b Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 12 Feb 2026 11:59:10 +0000 Subject: [PATCH 36/48] recorder paletteDeclarations --- dotcom-rendering/src/paletteDeclarations.ts | 56 ++++++++++----------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 04399422e24..bf68d0099e7 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -6792,6 +6792,34 @@ const paletteColours = { light: commentFormInputBackgroundLight, dark: commentFormInputBackgroundDark, }, + '--contributor-follow-accent-color': { + light: followIconFillLight, + dark: followIconFillLight, + }, + '--contributor-follow-bio-text': { + light: () => sourcePalette.neutral[10], + dark: () => sourcePalette.neutral[86], + }, + '--contributor-follow-button-border': { + light: followIconFillLight, + dark: () => sourcePalette.neutral[20], + }, + '--contributor-follow-button-border-following': { + light: () => sourcePalette.neutral[73], + dark: () => sourcePalette.neutral[20], + }, + '--contributor-follow-button-text': { + light: () => sourcePalette.neutral[7], + dark: () => sourcePalette.neutral[100], + }, + '--contributor-follow-button-text-not-following': { + light: () => sourcePalette.neutral[100], + dark: () => sourcePalette.neutral[100], + }, + '--contributor-follow-straight-lines': { + light: () => sourcePalette.neutral[86], + dark: () => sourcePalette.neutral[38], + }, '--cricket-scoreboard-border-top': { light: cricketScoreboardBorderTop, dark: cricketScoreboardBorderTop, @@ -7084,34 +7112,6 @@ const paletteColours = { light: () => sourcePalette.neutral[86], dark: () => sourcePalette.neutral[20], }, - '--contributor-follow-accent-color': { - light: followIconFillLight, - dark: followIconFillLight, - }, - '--contributor-follow-bio-text': { - light: () => sourcePalette.neutral[10], - dark: () => sourcePalette.neutral[86], - }, - '--contributor-follow-button-border': { - light: followIconFillLight, - dark: () => sourcePalette.neutral[20], - }, - '--contributor-follow-button-border-following': { - light: () => sourcePalette.neutral[73], - dark: () => sourcePalette.neutral[20], - }, - '--contributor-follow-button-text': { - light: () => sourcePalette.neutral[7], - dark: () => sourcePalette.neutral[100], - }, - '--contributor-follow-button-text-not-following': { - light: () => sourcePalette.neutral[100], - dark: () => sourcePalette.neutral[100], - }, - '--contributor-follow-straight-lines': { - light: () => sourcePalette.neutral[86], - dark: () => sourcePalette.neutral[38], - }, '--follow-icon-background': { light: followIconBackgroundLight, dark: followIconBackgroundDark, From 33a2cf33a5d35ffc5527f9d206e923457df27363 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 12 Feb 2026 12:22:35 +0000 Subject: [PATCH 37/48] tweak margin/paddings --- ...ributorFollowBlockComponent.importable.tsx | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx index 365cd47b835..c27793437d7 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx @@ -22,14 +22,15 @@ type Props = { const contributorBlockStyles = css` display: flex; flex-direction: column; - padding: ${space[2]}px 0; + padding: ${space[2]}px; + padding-left: 0; `; const topRowStyles = css` display: flex; flex-direction: row; gap: ${space[3]}px; - margin: ${space[2]}px 0; + margin: ${space[2]}px ${space[3]}px ${space[4]}px 0; `; const avatarContainerStyles = css` @@ -62,23 +63,6 @@ const bioStyles = css` line-height: 1.3; color: ${palette('--contributor-follow-bio-text')}; margin: 0; - - p { - margin: 0 0 ${space[1]}px; - } - - a { - color: ${palette('--link-kicker-text')}; - text-underline-offset: 3px; - } - - a:not(:hover) { - text-decoration-color: ${palette('--bio-link-underline')}; - } - - a:hover { - text-decoration: underline; - } `; const followButtonContainerStyles = css` From af2d06d685b165cfe5a01333dab155a46f9ee4bf Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 12 Feb 2026 12:27:02 +0000 Subject: [PATCH 38/48] remove redundant css rules --- .../src/components/ContributorFollowBlock.importable.tsx | 7 ------- .../ContributorFollowBlockComponent.importable.tsx | 5 +---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx index a4a3a64d108..6288ceae9e6 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx @@ -84,13 +84,7 @@ const FollowButton = ({ isFollowing, onClickHandler }: FollowButtonProps) => { const notificationAlertStyles = css` ${textSans15} color: ${palette('--follow-text')}; - background: none; - border: none; - display: block; - margin-left: 0; min-height: ${space[6]}px; - padding: 0; - text-align: left; width: 100%; `; @@ -103,7 +97,6 @@ const notificationAlertRowStyles = css` const notificationIconStyles = css` display: flex; - margin: 0; margin-right: ${space[1]}px; svg { diff --git a/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx index c27793437d7..25d2312fba1 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx @@ -22,13 +22,11 @@ type Props = { const contributorBlockStyles = css` display: flex; flex-direction: column; - padding: ${space[2]}px; - padding-left: 0; + padding: ${space[2]}px ${space[2]}px ${space[2]}px 0; `; const topRowStyles = css` display: flex; - flex-direction: row; gap: ${space[3]}px; margin: ${space[2]}px ${space[3]}px ${space[4]}px 0; `; @@ -60,7 +58,6 @@ const titleStyles = css` const bioStyles = css` ${textEgyptian14}; font-weight: 500; - line-height: 1.3; color: ${palette('--contributor-follow-bio-text')}; margin: 0; `; From 7efcbfb5cf98b5758c30f8a375ad1596f5f78ac6 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 12 Feb 2026 12:36:44 +0000 Subject: [PATCH 39/48] remove a/b test conditions --- dotcom-rendering/.storybook/mocks/bridgetApi.ts | 7 +------ dotcom-rendering/src/components/ArticleMeta.apps.tsx | 4 +++- .../src/model/enhance-contributor-follow-block.ts | 3 +++ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dotcom-rendering/.storybook/mocks/bridgetApi.ts b/dotcom-rendering/.storybook/mocks/bridgetApi.ts index d7c6ed36a92..7026003236a 100644 --- a/dotcom-rendering/.storybook/mocks/bridgetApi.ts +++ b/dotcom-rendering/.storybook/mocks/bridgetApi.ts @@ -107,12 +107,7 @@ export const getNativeABTestingClient: BridgetApi< 'getNativeABTestingClient' > = () => ({ getParticipations: async () => - new Map( - Object.entries({ - 'test-id': 'variant', - contributor_profile_test: 'variant', - }), - ), + new Map(Object.entries({ 'test-id': 'variant' })), }); export const ensure_all_exports_are_present = { diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index 6757ba8590b..fe34af72e1e 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -297,7 +297,9 @@ export const ArticleMetaApps = ({ format={format} /> )} - {shouldShowFollowButtons(isAnalysis || isImmersive) && + {shouldShowFollowButtons( + isComment || isAnalysis || isImmersive, + ) && soleContributor && ( (elements: FEElement[]): FEElement[] => { + // TODO: Gate on A/B test before enabling + return elements; + if (renderingTarget !== 'Apps') { return elements; } From 55e534c15bc8be65c5a95afa49a9aad0552266fd Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 12 Feb 2026 12:46:09 +0000 Subject: [PATCH 40/48] disable until a/b test is set up --- .../src/model/enhance-contributor-follow-block.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/model/enhance-contributor-follow-block.ts b/dotcom-rendering/src/model/enhance-contributor-follow-block.ts index 56a22428aec..bcf384b65e1 100644 --- a/dotcom-rendering/src/model/enhance-contributor-follow-block.ts +++ b/dotcom-rendering/src/model/enhance-contributor-follow-block.ts @@ -20,8 +20,11 @@ export const enhanceContributorFollowBlock = byline: string | undefined, ) => (elements: FEElement[]): FEElement[] => { - // TODO: Gate on A/B test before enabling - return elements; + // TODO: replace with A/B test check + const enabled = false; + if (!enabled) { + return elements; + } if (renderingTarget !== 'Apps') { return elements; From e04df2db231465641cb44ac65b0607ef98b8c131 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 12 Feb 2026 12:52:17 +0000 Subject: [PATCH 41/48] update schema --- .../src/frontend/schemas/feArticle.json | 35 ++++++++++++++++++- dotcom-rendering/src/model/block-schema.json | 33 +++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/frontend/schemas/feArticle.json b/dotcom-rendering/src/frontend/schemas/feArticle.json index 3f604b084c2..431753f9487 100644 --- a/dotcom-rendering/src/frontend/schemas/feArticle.json +++ b/dotcom-rendering/src/frontend/schemas/feArticle.json @@ -632,6 +632,9 @@ { "$ref": "#/definitions/ContentAtomBlockElement" }, + { + "$ref": "#/definitions/ContributorFollowBlockElement" + }, { "$ref": "#/definitions/DisclaimerBlockElement" }, @@ -1867,6 +1870,36 @@ "elementId" ] }, + "ContributorFollowBlockElement": { + "type": "object", + "properties": { + "_type": { + "type": "string", + "const": "model.dotcomrendering.pageElements.ContributorFollowBlockElement" + }, + "elementId": { + "type": "string" + }, + "contributorId": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "bio": { + "type": "string" + } + }, + "required": [ + "_type", + "contributorId", + "displayName", + "elementId" + ] + }, "DisclaimerBlockElement": { "type": "object", "properties": { @@ -6040,4 +6073,4 @@ } }, "$schema": "http://json-schema.org/draft-07/schema#" -} +} \ No newline at end of file diff --git a/dotcom-rendering/src/model/block-schema.json b/dotcom-rendering/src/model/block-schema.json index 97c8187254a..cfe3b40cd25 100644 --- a/dotcom-rendering/src/model/block-schema.json +++ b/dotcom-rendering/src/model/block-schema.json @@ -117,6 +117,9 @@ { "$ref": "#/definitions/ContentAtomBlockElement" }, + { + "$ref": "#/definitions/ContributorFollowBlockElement" + }, { "$ref": "#/definitions/DisclaimerBlockElement" }, @@ -1352,6 +1355,36 @@ "elementId" ] }, + "ContributorFollowBlockElement": { + "type": "object", + "properties": { + "_type": { + "type": "string", + "const": "model.dotcomrendering.pageElements.ContributorFollowBlockElement" + }, + "elementId": { + "type": "string" + }, + "contributorId": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "bio": { + "type": "string" + } + }, + "required": [ + "_type", + "contributorId", + "displayName", + "elementId" + ] + }, "DisclaimerBlockElement": { "type": "object", "properties": { From 68bd71170153efc035e123becd31359ec8deeb62 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 12 Feb 2026 12:55:37 +0000 Subject: [PATCH 42/48] add byline to enhanceBlocks --- dotcom-rendering/src/model/enhanceBlocks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/dotcom-rendering/src/model/enhanceBlocks.ts b/dotcom-rendering/src/model/enhanceBlocks.ts index c253d73091d..e2b0dbb1936 100644 --- a/dotcom-rendering/src/model/enhanceBlocks.ts +++ b/dotcom-rendering/src/model/enhanceBlocks.ts @@ -40,6 +40,7 @@ type Options = { pageId: string; serverSideABTests?: Record; switches?: Switches; + byline?: string; }; const enhanceNewsletterSignup = From 178fb3ed14aa9a127e138bb07fe43b1e81914292 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Mon, 16 Feb 2026 10:54:02 +0000 Subject: [PATCH 43/48] revert width change in follow wrapper --- dotcom-rendering/src/components/FollowWrapper.importable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/dotcom-rendering/src/components/FollowWrapper.importable.tsx b/dotcom-rendering/src/components/FollowWrapper.importable.tsx index 43a50946a0f..7bed3a45498 100644 --- a/dotcom-rendering/src/components/FollowWrapper.importable.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.importable.tsx @@ -165,7 +165,6 @@ export const FollowWrapper = ({ id, displayName }: Props) => {
Date: Mon, 16 Feb 2026 11:39:04 +0000 Subject: [PATCH 44/48] add comment about repeated code in new block --- .../src/components/ContributorFollowBlock.importable.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx index 6288ceae9e6..27f1660856e 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx @@ -1,3 +1,8 @@ +/** + * There is a lot of repeated code between this component and FollowWrapper. + * This is intentional so that we can implement it as an A/B test. + * We will clean this up once the test is complete. + */ import { css } from '@emotion/react'; import { Topic } from '@guardian/bridget/Topic'; import { isUndefined, log } from '@guardian/libs'; From aa63860187621333001ab552be88d79a497b3f85 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Tue, 17 Feb 2026 10:09:48 +0000 Subject: [PATCH 45/48] revert article.ts --- dotcom-rendering/src/types/article.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/dotcom-rendering/src/types/article.ts b/dotcom-rendering/src/types/article.ts index f08d8335edd..32bec0b9b73 100644 --- a/dotcom-rendering/src/types/article.ts +++ b/dotcom-rendering/src/types/article.ts @@ -105,6 +105,7 @@ export const enhanceArticleType = ( shouldHideAds: data.shouldHideAds, pageId: data.pageId, serverSideABTests: data.config.serverSideABTests, + switches: data.config.switches, byline: data.byline, }); From c40b3556d2f99a389dd5773f0ffedc62cc05e3e3 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 19 Feb 2026 16:19:05 +0000 Subject: [PATCH 46/48] use source toggle switch --- .../ContributorFollowBlock.importable.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx index 27f1660856e..4b22b5e088d 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx @@ -115,17 +115,6 @@ const notificationLabelStyles = css` ${textSans12} `; -const toggleSwitchContainerStyles = css` - transform: scale(1.2); - button[aria-checked='false'] { - background-color: ${sourcePalette.neutral[60]}; - border-color: ${sourcePalette.neutral[60]}; - } - button[aria-checked='true']::before { - display: none; - } -`; - const NotificationAlert = ({ isFollowing, onClickHandler, @@ -143,7 +132,7 @@ const NotificationAlert = ({ publishes an article
-
+
Date: Thu, 19 Feb 2026 16:28:37 +0000 Subject: [PATCH 47/48] use source button --- .../ContributorFollowBlock.importable.tsx | 24 +++++++++++-------- dotcom-rendering/src/paletteDeclarations.ts | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx index 4b22b5e088d..ae60145b18c 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx @@ -13,6 +13,7 @@ import { textSans15, } from '@guardian/source/foundations'; import { + Button, SvgCheckmark, SvgNotificationsOn, SvgPlus, @@ -67,20 +68,23 @@ const followButtonStyles = (isFollowing: boolean) => css` } `; -const FollowButton = ({ isFollowing, onClickHandler }: FollowButtonProps) => { +const FollowButtonPillStyle = ({ + isFollowing, + onClickHandler, +}: FollowButtonProps) => { return ( - + ); }; @@ -306,7 +310,7 @@ export const ContributorFollowBlock = ({ return (
- sourcePalette.neutral[10], From db78d9bc194bd3702b4e2ad8f2b85ef7a8b06063 Mon Sep 17 00:00:00 2001 From: Ara Cho Date: Thu, 19 Feb 2026 16:30:45 +0000 Subject: [PATCH 48/48] remove unused css --- .../ContributorFollowBlock.importable.tsx | 41 +------------------ ...ributorFollowBlockComponent.importable.tsx | 2 +- dotcom-rendering/src/paletteDeclarations.ts | 18 +------- 3 files changed, 4 insertions(+), 57 deletions(-) diff --git a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx index ae60145b18c..f2399bb0c4e 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx @@ -6,12 +6,7 @@ import { css } from '@emotion/react'; import { Topic } from '@guardian/bridget/Topic'; import { isUndefined, log } from '@guardian/libs'; -import { - palette as sourcePalette, - space, - textSans12, - textSans15, -} from '@guardian/source/foundations'; +import { space, textSans12, textSans15 } from '@guardian/source/foundations'; import { Button, SvgCheckmark, @@ -36,38 +31,6 @@ type FollowButtonProps = { onClickHandler: () => void; }; -const followButtonStyles = (isFollowing: boolean) => css` - ${textSans15} - display: inline-flex; - align-items: center; - gap: ${space[1]}px; - padding: ${space[1] + 1}px ${space[3] + 2}px; - border-radius: ${space[5]}px; - border: 1px solid - ${isFollowing - ? palette('--contributor-follow-button-border-following') - : palette('--contributor-follow-button-border')}; - background: ${isFollowing - ? 'transparent' - : palette('--contributor-follow-accent-color')}; - color: ${isFollowing - ? palette('--contributor-follow-button-text') - : palette('--contributor-follow-button-text-not-following')}; - font-weight: 700; - cursor: pointer; - - svg { - width: 24px; - height: 24px; - fill: ${isFollowing - ? palette('--contributor-follow-button-text') - : palette('--contributor-follow-button-text-not-following')}; - stroke: ${isFollowing - ? palette('--contributor-follow-button-text') - : palette('--contributor-follow-button-text-not-following')}; - } -`; - const FollowButtonPillStyle = ({ isFollowing, onClickHandler, @@ -77,7 +40,7 @@ const FollowButtonPillStyle = ({ onClick={onClickHandler} type="button" theme={{ - backgroundPrimary: palette('--contributor-follow-accent-color'), + backgroundPrimary: palette('--contributor-follow-fill'), textPrimary: palette('--quiz-atom-button-text'), }} iconSide="left" diff --git a/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx b/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx index 25d2312fba1..5045b17a521 100644 --- a/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx +++ b/dotcom-rendering/src/components/ContributorFollowBlockComponent.importable.tsx @@ -51,7 +51,7 @@ const nameAndBioStyles = css` const titleStyles = css` ${headlineBold17}; - color: ${palette('--contributor-follow-accent-color')}; + color: ${palette('--contributor-follow-fill')}; margin: 0 0 ${space[2]}px; `; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 47d71fceda4..61daa0107ae 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -6792,7 +6792,7 @@ const paletteColours = { light: commentFormInputBackgroundLight, dark: commentFormInputBackgroundDark, }, - '--contributor-follow-accent-color': { + '--contributor-follow-fill': { light: followIconFillLight, dark: followIconFillDark, }, @@ -6800,22 +6800,6 @@ const paletteColours = { light: () => sourcePalette.neutral[10], dark: () => sourcePalette.neutral[86], }, - '--contributor-follow-button-border': { - light: followIconFillLight, - dark: () => sourcePalette.neutral[20], - }, - '--contributor-follow-button-border-following': { - light: () => sourcePalette.neutral[73], - dark: () => sourcePalette.neutral[20], - }, - '--contributor-follow-button-text': { - light: () => sourcePalette.neutral[7], - dark: () => sourcePalette.neutral[100], - }, - '--contributor-follow-button-text-not-following': { - light: () => sourcePalette.neutral[100], - dark: () => sourcePalette.neutral[100], - }, '--contributor-follow-straight-lines': { light: () => sourcePalette.neutral[86], dark: () => sourcePalette.neutral[38],