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

New Palette Proposal #7766

Closed
wants to merge 3 commits into from
Closed
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
175 changes: 175 additions & 0 deletions dotcom-rendering/src/palette.ts
Original file line number Diff line number Diff line change
@@ -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
* <caption>Create a single stylesheet to handle both colour schemes.</caption>
* const paletteStyles = css`
* :root {
* ${paletteDeclarations(format, 'light').join('\n')}
* }
*
* (@)media (prefers-color-scheme: dark) {
* :root {
* ${paletteDeclarations(format, 'dark').join('\n')}
* }
* }
* `;
* @example
* <caption>Load separate stylesheets based on user preference.</caption>
* // 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 = (
* <>
* <link
* media="(prefers-color-scheme: light)"
* rel="stylesheet"
* href="light.css"
* />
* <link
* media="(prefers-color-scheme: dark)"
* rel="stylesheet"
* href="dark.css"
* />
* </>
* );
*/
const paletteDeclarations = (
format: ArticleFormat,
colourScheme: 'light' | 'dark',
): string[] =>
Object.entries(paletteColours).map(
([colourName, colour]) =>
`${colourName}: ${colour[colourScheme](format)};`,
);

// ----- Exports ----- //

export { palette, paletteDeclarations };
61 changes: 61 additions & 0 deletions dotcom-rendering/src/web/components/HeadlineExample.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof HeadlineExample> = {
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) =>
(
<div css={css(paletteDeclarations(format, colourScheme))}>
<Story />
</div>
);

const lightMode = colourSchemeDecorator('light');
const darkMode = colourSchemeDecorator('dark');

// ----- Stories ----- //

type Story = StoryObj<typeof HeadlineExample>;

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)],
};
16 changes: 16 additions & 0 deletions dotcom-rendering/src/web/components/HeadlineExample.tsx
Original file line number Diff line number Diff line change
@@ -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 <h1 css={styles}>{text}</h1>;
};