diff --git a/dotcom-rendering/src/palette.ts b/dotcom-rendering/src/palette.ts new file mode 100644 index 00000000000..342a852f1e2 --- /dev/null +++ b/dotcom-rendering/src/palette.ts @@ -0,0 +1,175 @@ +// ----- Imports ----- // + +import { ArticleDesign, type ArticleFormat } from '@guardian/libs'; +import { palette as sourcePalette } from '@guardian/source-foundations'; + +// ----- Palette Functions ----- // + +const headlineColourLight = ({ design }: ArticleFormat): string => { + switch (design) { + case ArticleDesign.Feature: + return sourcePalette.news[300]; + default: + return sourcePalette.neutral[10]; + } +}; + +const headlineColourDark = ({ design }: ArticleFormat): string => { + switch (design) { + case ArticleDesign.Feature: + return sourcePalette.news[600]; + default: + return sourcePalette.neutral[97]; + } +}; + +const headlineBackgroundColourLight = ({ design }: ArticleFormat): string => { + switch (design) { + case ArticleDesign.LiveBlog: + return sourcePalette.news[400]; + default: + return sourcePalette.neutral[100]; + } +}; + +const headlineBackgroundColourDark = ({ design }: ArticleFormat): string => { + switch (design) { + case ArticleDesign.LiveBlog: + return sourcePalette.news[200]; + default: + return sourcePalette.neutral[7]; + } +}; + +// ----- Palette ----- // + +/** + * A template literal type used to make sure the keys of the palette use the + * correct CSS custom property syntax. + */ +type CSSCustomProperty = `--${string}`; +/** + * Ensures that all palette functions provide the same API, deriving a palette + * colour from an {@linkcode ArticleFormat}. + */ +type PaletteFunction = (f: ArticleFormat) => string; +/** + * Used to validate that the palette object always has the correct shape, + * without changing its type. + */ +type PaletteColours = Record< + CSSCustomProperty, + { + light: PaletteFunction; + dark: PaletteFunction; + } +>; + +/** + * Maps palette colour names (which are also CSS custom property names) to + * a pair of palette functions, which can be used to derive both light and dark + * mode colours from an {@linkcode ArticleFormat}. + * + * This is not accessed directly in components; the {@linkcode palette} function + * is used instead. + */ +const paletteColours = { + '--headline-colour': { + light: headlineColourLight, + dark: headlineColourDark, + }, + '--headline-background-colour': { + light: headlineBackgroundColourLight, + dark: headlineBackgroundColourDark, + }, +} satisfies PaletteColours; + +/** + * A union of all the keys of the palette object. In other words, all the + * possible colours that can be chosen. + */ +type ColourName = keyof typeof paletteColours; + +/** + * Looks up a palette colour by name. Retrieves a CSS value for the specified + * colour, for use in CSS declarations. See the examples for how this is + * commonly used with our Emotion-based styles. + * + * @param a The name of a palette colour; for example `--headline-colour`. + * @returns A CSS `var` function call; for example `var(--headline-colour)`. + * @example + * const styles = css` + * color: ${palette('--headline-colour')}; + * background-color: ${palette('--headline-background-colour')}; + * `; + */ +const palette = (colour: ColourName): string => `var(${colour})`; + +/** + * Builds a list of CSS custom property declarations representing colours. These + * can be used to set up the palette on any element, and then retrieved to apply + * styles via the {@linkcode palette} function. See the examples for ways the + * palette could be set up. + * + * @param format The `ArticleFormat` of the current article. + * @param colourScheme Get declarations for either `light` or `dark` mode. + * @returns A set of CSS custom property declarations for palette colours, + * in string format. For example: + * ``` + * [ '--headline-colour: #1a1a1a;', '--headline-background-colour: #ffffff;' ] + * ``` + * @example + * Create a single stylesheet to handle both colour schemes. + * const paletteStyles = css` + * :root { + * ${paletteDeclarations(format, 'light').join('\n')} + * } + * + * (@)media (prefers-color-scheme: dark) { + * :root { + * ${paletteDeclarations(format, 'dark').join('\n')} + * } + * } + * `; + * @example + * Load separate stylesheets based on user preference. + * // Use to build a file called 'light.css'. + * const lightPalette = css` + * :root { + * ${paletteDeclarations(format, 'light').join('\n')} + * } + * `; + * // Use to build a file called 'dark.css'. + * const darkPalette = css` + * :root { + * ${paletteDeclarations(format, 'dark').join('\n')} + * } + * `; + * + * const stylesheets = ( + * <> + * + * + * + * ); + */ +const paletteDeclarations = ( + format: ArticleFormat, + colourScheme: 'light' | 'dark', +): string[] => + Object.entries(paletteColours).map( + ([colourName, colour]) => + `${colourName}: ${colour[colourScheme](format)};`, + ); + +// ----- Exports ----- // + +export { palette, paletteDeclarations }; diff --git a/dotcom-rendering/src/web/components/HeadlineExample.stories.tsx b/dotcom-rendering/src/web/components/HeadlineExample.stories.tsx new file mode 100644 index 00000000000..eb130752b5e --- /dev/null +++ b/dotcom-rendering/src/web/components/HeadlineExample.stories.tsx @@ -0,0 +1,61 @@ +// ----- Imports ----- // + +import { css } from '@emotion/react'; +import { ArticleDesign, ArticleDisplay, ArticlePillar } from '@guardian/libs'; +import type { Meta, StoryObj, Decorator } from '@storybook/react'; +import { HeadlineExample } from './HeadlineExample'; +import { paletteDeclarations } from '../../../src/palette'; + +// ----- Meta ----- // + +const meta: Meta = { + title: 'components/HeadlineExample', + component: HeadlineExample, +}; + +export default meta; + +// ----- Decorators ----- // + +/** + * Creates storybook decorator used to wrap components in an element + * containing the light or dark mode palette colours. + * + * @param colourScheme Choose whether to use the light or dark palette. + * @returns A decorator that wraps the component in a `div` containing the + * palette colours as CSS custom properties. + */ +const colourSchemeDecorator = + (colourScheme: 'light' | 'dark') => + (format: ArticleFormat): Decorator => + (Story) => + ( +
+ +
+ ); + +const lightMode = colourSchemeDecorator('light'); +const darkMode = colourSchemeDecorator('dark'); + +// ----- Stories ----- // + +type Story = StoryObj; + +const articleFormat: ArticleFormat = { + design: ArticleDesign.Standard, + display: ArticleDisplay.Standard, + theme: ArticlePillar.News, +}; + +export const LightHeadline: Story = { + args: { + text: 'A short example headline', + }, + decorators: [lightMode(articleFormat)], +}; + +export const DarkHeadline: Story = { + args: LightHeadline.args, + decorators: [darkMode(articleFormat)], +}; diff --git a/dotcom-rendering/src/web/components/HeadlineExample.tsx b/dotcom-rendering/src/web/components/HeadlineExample.tsx new file mode 100644 index 00000000000..646e3fb6ceb --- /dev/null +++ b/dotcom-rendering/src/web/components/HeadlineExample.tsx @@ -0,0 +1,16 @@ +// ----- Imports ----- // + +import { css } from '@emotion/react'; +import { palette } from '../../../src/palette'; +import { headline } from '@guardian/source-foundations'; + +// ----- Component ----- // + +export const HeadlineExample = ({ text }: { text: string }) => { + const styles = css` + color: ${palette('--headline-colour')}; + background-color: ${palette('--headline-background-colour')}; + ${headline.large()} + `; + return

{text}

; +};