Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Media Cards: Use Pill component for video duration #12974

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { css } from '@emotion/react';
import { from, palette } from '@guardian/source/foundations';
import type { ThemeIcon } from '@guardian/source/react-components';
import { SvgMediaControlsPlay } from '@guardian/source/react-components';
import { SvgMediaControlsPlay } from '../../SvgMediaControlsPlay';
import type { ImagePositionType, ImageSizeType } from './ImageWrapper';

type PlayButtonSize = keyof typeof sizes;
Expand Down
77 changes: 57 additions & 20 deletions dotcom-rendering/src/components/Pill.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { css } from '@emotion/react';
import { palette, space } from '@guardian/source/foundations';
import { SvgCamera } from '@guardian/source/react-components';
import type { Meta, StoryObj } from '@storybook/react';
import { Pill } from './Pill';
Expand All @@ -6,37 +8,72 @@ import { SvgMediaControlsPlay } from './SvgMediaControlsPlay';
const meta: Meta<typeof Pill> = {
title: 'Components/Pill',
component: Pill,
args: {
content: 'Pill',
},
} satisfies Meta<typeof Pill>;

export default meta;

type Story = StoryObj<typeof Pill>;

export const Default = {} satisfies Story;
const liveStyles = css`
::before {
content: '';
display: inline-block;
width: 0.75em;
height: 0.75em;
border-radius: 100%;
background-color: ${palette.news[500]};
margin-right: ${space[1]}px;
}
`;

export const Default = {
render: () => (
<Pill>
<Pill.Segment>Pill</Pill.Segment>
</Pill>
),
} satisfies Story;

export const WithVideoIcon = {
args: {
content: <time>3:35</time>,
icon: <SvgMediaControlsPlay />,
},
render: () => (
<Pill>
<Pill.Segment>
<SvgMediaControlsPlay />
<time>3:35</time>
</Pill.Segment>
</Pill>
),
} satisfies Story;

export const WithGalleryIcon = {
args: {
content: '10',
icon: <SvgCamera />,
iconSide: 'right',
},
render: () => (
<Pill>
<Pill.Segment>
10
<SvgCamera />
</Pill.Segment>
</Pill>
),
} satisfies Story;

export const WithLiveIndicator = {
render: () => (
<Pill>
<Pill.Segment>
<span css={liveStyles}>Live</span>
</Pill.Segment>
</Pill>
),
} satisfies Story;

export const WithGalleryIconAndPrefix = {
args: {
content: '10',
prefix: 'Gallery',
icon: <SvgCamera />,
iconSide: 'right',
},
export const Segmented = {
render: () => (
<Pill>
<Pill.Segment>Gallery</Pill.Segment>
<Pill.Segment>
10
<SvgCamera />
</Pill.Segment>
</Pill>
),
} satisfies Story;
71 changes: 22 additions & 49 deletions dotcom-rendering/src/components/Pill.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,41 @@
import { css } from '@emotion/react';
import { space, textSansBold12 } from '@guardian/source/foundations';
import { cloneElement, type ReactElement } from 'react';
import type { ReactNode } from 'react';
import { palette } from '../palette';

type IconSide = 'left' | 'right';

interface Props {
/**
* Main content of pill. This can be a string or an element. eg.
* <time>2:35</time>
*/
content: string | ReactElement;
/** Optional prefix displayed before main content with a dividing line */
prefix?: string;
/** Optional icon displayed before or after content */
icon?: ReactElement;
/** Optional icon position (icon is on the left by default) */
iconSide?: IconSide;
}

const pillStyles = css`
display: inline-flex;
align-items: center;
gap: ${space[1]}px;
padding: 0 10px;
border-radius: ${space[3]}px;
${textSansBold12};
color: ${palette('--pill-text')};
background-color: ${palette('--pill-background')};
`;

const pillSegmentStyles = css`
display: flex;
align-items: center;
gap: ${space[1]}px;
min-height: 24px;
& + & {
margin-left: 6px;
padding-left: 6px;
border-left: 1px solid ${palette('--pill-divider')};
}
svg {
flex: none;
fill: currentColor;
width: auto;
height: 20px;
margin: 0 -3px; /* Compensate for whitespace around icon */
}
`;

const pillContentStyles = css`
padding: ${space[1]}px 0;
`;

const pillPrefixStyles = css`
margin-right: 2px;
padding-right: 6px;
border-right: 1px solid ${palette('--pill-divider')};
`;

export const Pill = ({ content, prefix, icon, iconSide = 'left' }: Props) => {
const Icon = () =>
icon
? cloneElement(icon, {
size: 'xsmall',
theme: { fill: 'currentColor' },
})
: null;
export const Pill = ({ children }: { children: ReactNode }) => (
<div css={pillStyles}>{children}</div>
);

return (
<div css={pillStyles}>
{iconSide === 'left' && <Icon />}
{!!prefix && (
<span css={[pillContentStyles, pillPrefixStyles]}>
{prefix}
</span>
)}
<span css={pillContentStyles}>{content}</span>
{iconSide === 'right' && <Icon />}
</div>
);
};
Pill.Segment = ({ children }: { children: ReactNode }) => (
<span css={pillSegmentStyles}>{children}</span>
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
headlineMedium20,
palette as sourcePalette,
space,
textSansBold12,
} from '@guardian/source/foundations';
import type { ArticleFormat } from '../../lib/articleFormat';
import { palette } from '../../palette';
Expand All @@ -19,6 +18,8 @@ import { PlayIcon } from '../Card/components/PlayIcon';
import { FormatBoundary } from '../FormatBoundary';
import { Kicker } from '../Kicker';
import { secondsToDuration } from '../MediaDuration';
import { Pill } from '../Pill';
import { SvgMediaControlsPlay } from '../SvgMediaControlsPlay';
import { YoutubeAtomPicture } from './YoutubeAtomPicture';

export type VideoCategory = 'live' | 'documentary' | 'explainer';
Expand Down Expand Up @@ -71,40 +72,28 @@ const overlayStyles = css`
}
`;

const pillStyles = css`
const pillPositionStyles = (smallCard: boolean) => css`
position: absolute;
top: ${space[2]}px;
right: ${space[2]}px;
${textSansBold12};
background-color: rgba(0, 0, 0, 0.7);
color: ${sourcePalette.neutral[100]};
border-radius: ${space[3]}px;
padding: 0 6px;
display: inline-flex;
`;

const pillItemStyles = css`
/* Target all but the first element, and add a border */
:nth-of-type(n + 2) {
border-left: 1px solid rgba(255, 255, 255, 0.5);
}
`;

const pillTextStyles = css`
line-height: ${space[4]}px;
padding: ${space[1]}px 6px;
${smallCard
? css`
bottom: ${space[1]}px;
left: ${space[1]}px;
`
: css`
top: ${space[2]}px;
right: ${space[2]}px;
`};
`;

const liveStyles = css`
::before {
content: '';
width: 9px;
height: 9px;
border-radius: 50%;
background-color: ${sourcePalette.news[500]};
display: inline-block;
position: relative;
margin-right: 0.1875rem;
width: 0.75em;
height: 0.75em;
border-radius: 100%;
background-color: ${sourcePalette.news[500]};
margin-right: ${space[1]}px;
}
`;

Expand Down Expand Up @@ -158,7 +147,7 @@ export const YoutubeAtomOverlay = ({
const showPill = !!videoCategory || hasDuration;
const isLive = videoCategory === 'live';
const image = overrideImage ?? posterImage;
const hidePillOnMobile =
const isSmallCard =
imagePositionOnMobile === 'right' || imagePositionOnMobile === 'left';

return (
Expand All @@ -180,37 +169,30 @@ export const YoutubeAtomOverlay = ({
/>
)}
{showPill && (
<div
css={
hidePillOnMobile
? css`
display: none;
`
: pillStyles
}
>
{!!videoCategory && (
<div css={pillItemStyles}>
<div
css={[pillTextStyles, isLive && liveStyles]}
>
{capitalise(videoCategory)}
</div>
</div>
)}
{!!hasDuration && (
<div css={pillItemStyles}>
<div css={pillTextStyles}>
<div css={pillPositionStyles(isSmallCard)}>
<Pill>
{!!videoCategory && !isSmallCard && (
<Pill.Segment>
<span css={isLive && liveStyles}>
{capitalise(videoCategory)}
</span>
</Pill.Segment>
)}
{!!hasDuration && (
<Pill.Segment>
{isSmallCard && <SvgMediaControlsPlay />}
{secondsToDuration(duration)}
</div>
</div>
)}
</Pill.Segment>
)}
</Pill>
</div>
)}
<PlayIcon
imageSize={imageSize}
imagePositionOnMobile={imagePositionOnMobile}
/>
{!isSmallCard && (
<PlayIcon
imageSize={imageSize}
imagePositionOnMobile={imagePositionOnMobile}
/>
)}
{showTextOverlay && (
<div css={textOverlayStyles}>
{!!kicker && (
Expand Down
Loading