diff --git a/.gitignore b/.gitignore index 85f5d23511..5751797391 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ npm-debug.log* package-lock.json +packages/react-component-library/*.tgz diff --git a/packages/design-tokens/src/__tests__/selectors.test.tsx b/packages/design-tokens/src/__tests__/selectors.test.tsx index a065ec5476..473910e07c 100644 --- a/packages/design-tokens/src/__tests__/selectors.test.tsx +++ b/packages/design-tokens/src/__tests__/selectors.test.tsx @@ -25,6 +25,7 @@ const { fontSize, shadow, zIndex, + colorValue, } = selectors let wrapper: RenderResult @@ -211,15 +212,31 @@ describe('color', () => { color: ${color('neutral', '100')}; ` - it('should set styles correctly', () => { + it('resolves to a themeable CSS variable with the light value as fallback', () => { wrapper = render() expect(wrapper.getByTestId('test-element')).toHaveStyleRule( 'color', - '#e2e9ee' + 'var(--color-neutral-100, #e2e9ee)' + ) + }) + }) +}) + +describe('colorValue', () => { + describe('when the group or shade is invalid', () => { + it('throws a friendly error', () => { + expect(() => colorValue('invalid' as ColorGroup, '400')).toThrow( + 'Invalid color token' ) }) }) + + describe('when the group and shade are valid', () => { + it('returns the concrete light value with no theme', () => { + expect(colorValue('neutral', '100')).toBe('#e2e9ee') + }) + }) }) describe('fontSize', () => { diff --git a/packages/design-tokens/src/getters.ts b/packages/design-tokens/src/getters.ts index 2765f37e13..73ecd6d20f 100644 --- a/packages/design-tokens/src/getters.ts +++ b/packages/design-tokens/src/getters.ts @@ -102,7 +102,19 @@ export function getAnimation(index: AnimationTiming, theme?: Theme): string { throw new Error(`Invalid animation token for index: '${index}'`) } -export function getColor( +export function colorVariableName( + group: ColorGroup, + weight: ColorShade +): string { + return `--color-${group}-${weight}` +} + +/** + * Resolve a colour token to its concrete hex value for the given theme + * (falling back to the default light theme). Use this where the value + * must be a parseable colour, e.g. when passed to `polished` helpers. + */ +export function getColorValue( group: ColorGroup, weight: ColorShade, theme?: Theme @@ -119,6 +131,66 @@ export function getColor( ) } +/** + * Resolve a colour token. When an explicit theme is provided the concrete + * hex value is returned (preserving threaded-theme behaviour). Otherwise a + * CSS custom property reference is returned so the colour reacts to the + * theme injected by `GlobalStyleProvider`, with the light value as a static + * fallback for unprovided/SSR contexts. + */ +export function getColor( + group: ColorGroup, + weight: ColorShade, + theme?: Theme +): string { + if (theme?.colorsTokens) { + return getColorValue(group, weight, theme) + } + + const fallback = getColorValue(group, weight) + + return `var(${colorVariableName(group, weight)}, ${fallback})` +} + +/** + * Resolve a colour token, choosing a different shade per theme mode. Use this + * to tune a colour for dark mode without changing the light appearance: the + * light shade is used in light mode and the dark shade in dark mode. Pass the + * styled-component's theme, e.g. + * `({ theme }) => colorByMode('neutral', '000', '100', theme as Theme)`. + */ +export function getColorByMode( + group: ColorGroup, + lightWeight: ColorShade, + darkWeight: ColorShade, + theme?: Theme +): string { + const weight = theme?.mode === 'dark' ? darkWeight : lightWeight + + return getColor(group, weight, theme) +} + +/** + * Build the `--color-*` custom property declarations for a theme, for + * injection by `GlobalStyleProvider`. + */ +export function getColorVariables(theme?: Theme): Record { + const { color } = getTheme(theme).colorsTokens as unknown as { + color: Record> + } + + const entries = Object.entries(color).flatMap(([group, shades]) => + Object.entries(shades) + .filter(([, token]) => isTokenValid(token?.value)) + .map( + ([weight, token]) => + [`--color-${group}-${weight}`, token.value as string] as const + ) + ) + + return Object.fromEntries(entries) +} + export function getTypography(size: TypographySize, theme?: Theme): string { const value = get( getTheme(theme).typographyTokens, diff --git a/packages/design-tokens/src/index.ts b/packages/design-tokens/src/index.ts index 97aaa5cf3e..9aef2e3ca2 100644 --- a/packages/design-tokens/src/index.ts +++ b/packages/design-tokens/src/index.ts @@ -3,7 +3,7 @@ import selectors from './selectors' export * from './types' export * from './flat' -export { getTheme } from './getters' +export { getTheme, getColorVariables, colorVariableName } from './getters' export { selectors } @@ -14,6 +14,8 @@ const { animation, breakpoint, color, + colorValue, + colorByMode, fontSize, mediaQuery, mq, @@ -26,6 +28,8 @@ export { animation, breakpoint, color, + colorValue, + colorByMode, fontSize, mediaQuery, mq, diff --git a/packages/design-tokens/src/selectors.ts b/packages/design-tokens/src/selectors.ts index 9ce602a37b..168e0e5be6 100644 --- a/packages/design-tokens/src/selectors.ts +++ b/packages/design-tokens/src/selectors.ts @@ -3,6 +3,8 @@ import { getBreakpoint, getMediaQuery, getColor, + getColorValue, + getColorByMode, getShadow, getSpacing, getTypography, @@ -15,6 +17,8 @@ export default { breakpoint: getBreakpoint, animation: getAnimation, color: getColor, + colorValue: getColorValue, + colorByMode: getColorByMode, fontSize: getTypography, shadow: getShadow, spacing: getSpacing, diff --git a/packages/design-tokens/src/tokens/dark/colors.json b/packages/design-tokens/src/tokens/dark/colors.json index 0bacd800a9..890ac20f9a 100644 --- a/packages/design-tokens/src/tokens/dark/colors.json +++ b/packages/design-tokens/src/tokens/dark/colors.json @@ -2,80 +2,98 @@ "color": { "neutral": { "black": { - "value": "#0a141b" + "value": "#f6f8fa", + "description": "Foreground (inverted in dark mode)" }, "white": { - "value": "#ffffff" + "value": "#1f3140", + "description": "Raised surfaces, cards, panels (inverted in dark mode)" }, "000": { - "value": "#f8fafc" + "value": "#0a141b", + "description": "Page background, Disabled button" }, "100": { - "value": "#000000" + "value": "#243542", + "description": "Borders and dividers, Inactive tab" }, "200": { - "value": "#b8c7d2" + "value": "#4c6376", + "description": "Input borders, Disabled button border, Toggle off disabled" }, "300": { - "value": "#748999" + "value": "#6a8093", + "description": "Breadcrumb divider, Toggle off" }, "400": { - "value": "#3e5667" + "value": "#b7c5d0", + "description": "Body text, Input labels, Disabled button label" }, "500": { - "value": "#233745" + "value": "#cbd6dd", + "description": "Sidebar user menu background" }, "600": { - "value": "#1c2d39" + "value": "#e2e9ee", + "description": "Headings, Input text" }, "700": { - "value": "#12202b" + "value": "#edf1f4", + "description": "Sidebar" }, "800": { - "value": "#0c1720" + "value": "#f4f7f8" }, "900": { - "value": "#0a141b" + "value": "#f9fbfc" } }, "action": { "000": { - "value": "#ecf8ff" + "value": "#0e2233", + "description": "Checked/selected radio box background" }, "100": { - "value": "#ddf4ff" + "value": "#16395a", + "description": "Secondary button, Hovered primary button border, Phase banner" }, "200": { - "value": "#b7dff7" + "value": "#235d8c", + "description": "Hovered secondary and tertiary button, Toggle on disabled" }, "300": { - "value": "#85c6f2" + "value": "#3f86c9" }, "400": { - "value": "#58aae9" + "value": "#6fb2ec" }, "500": { - "value": "#3a8fdd" + "value": "#3a8fdd", + "description": "Info alert elements, Focus state, Checked/selected radio, Toggle on" }, "600": { - "value": "#2a77c7" + "value": "#2a77c7", + "description": "Primary button, Secondary and tertiary button border" }, "700": { - "value": "#2661a7" + "value": "#7cbcef", + "description": "Input interactions" }, "800": { - "value": "#274776" + "value": "#9fcdf3", + "description": "Primary button border, Hovered primary button" }, "900": { - "value": "#253b5b" + "value": "#bfe0fa", + "description": "Secondary and tertiary button label" } }, "success": { "000": { - "value": "#f4ffef" + "value": "#102516" }, "100": { - "value": "#e5ffd9" + "value": "#15331f" }, "200": { "value": "#c6f3b5" @@ -87,7 +105,8 @@ "value": "#8fd57f" }, "500": { - "value": "#76c767" + "value": "#76c767", + "description": "Success icon" }, "600": { "value": "#60b255" @@ -99,15 +118,16 @@ "value": "#3b6f33" }, "900": { - "value": "#3b612c" + "value": "#abe39b", + "description": "Success text" } }, "warning": { "000": { - "value": "#ffffee" + "value": "#2a2208" }, "100": { - "value": "#fffddc" + "value": "#33290a" }, "200": { "value": "#fefbb8" @@ -119,7 +139,8 @@ "value": "#f5db54" }, "500": { - "value": "#e8c242" + "value": "#e8c242", + "description": "Warning icon" }, "600": { "value": "#cf9328" @@ -131,15 +152,17 @@ "value": "#8c4f17" }, "900": { - "value": "#693a12" + "value": "#faed7e", + "description": "Warning text" } }, "danger": { "000": { - "value": "#fff3f4" + "value": "#2a1011" }, "100": { - "value": "#feeaec" + "value": "#331417", + "description": "Hovered danger button border" }, "200": { "value": "#fed1d1" @@ -151,27 +174,31 @@ "value": "#fc7c75" }, "500": { - "value": "#f45249" + "value": "#f45249", + "description": "Danger icon" }, "600": { "value": "#ec4138" }, "700": { - "value": "#d53229" + "value": "#d53229", + "description": "Danger button" }, "800": { - "value": "#B22820" + "value": "#B22820", + "description": "Hovered danger button, Input error border" }, "900": { - "value": "#841c1b" + "value": "#fea9a9", + "description": "Danger text" } }, "supa": { "000": { - "value": "#e8f2ff" + "value": "#14182e" }, "100": { - "value": "#deebff" + "value": "#181d3a" }, "200": { "value": "#bbd5fe" @@ -195,15 +222,15 @@ "value": "#3b3985" }, "900": { - "value": "#343160" + "value": "#99b7f9" } }, "supb": { "000": { - "value": "#f9f3ff" + "value": "#1c142e" }, "100": { - "value": "#f2e9ff" + "value": "#22193a" }, "200": { "value": "#e5d3fd" @@ -227,15 +254,15 @@ "value": "#4b358f" }, "900": { - "value": "#3b2d6e" + "value": "#d0b5f9" } }, "supc": { "000": { - "value": "#fff3ff" + "value": "#2a142b" }, "100": { - "value": "#fee9ff" + "value": "#331937" }, "200": { "value": "#fcd3fd" @@ -259,50 +286,50 @@ "value": "#8c358f" }, "900": { - "value": "#6c2d6e" + "value": "#f7b5f9" } }, "supd": { "000": { - "value": "#fff3ff" + "value": "#2e141d" }, "100": { - "value": "#fee9ff" + "value": "#3a1926" }, "200": { - "value": "#fcd3fd" + "value": "#ffcce5" }, "300": { - "value": "#f7b5f9" + "value": "#ffa2b5" }, "400": { - "value": "#ee89f1" + "value": "#f77f97" }, "500": { - "value": "#e46fe8" + "value": "#f35d7b" }, "600": { - "value": "#cc4fd0" + "value": "#d84b67" }, "700": { - "value": "#b43fb8" + "value": "#c43854" }, "800": { - "value": "#8c358f" + "value": "#972b41" }, "900": { - "value": "#6c2d6e" + "value": "#ffa2b5" } }, "supe": { "000": { - "value": "#fff8f3" + "value": "#2e1c10" }, "100": { - "value": "#fef2ea" + "value": "#3a2415" }, "200": { - "value": "#fee2d1" + "value": "#3a2415" }, "300": { "value": "#fecaa9" @@ -323,18 +350,18 @@ "value": "#9d4712" }, "900": { - "value": "#853a0c" + "value": "#fecaa9" } }, "supf": { "000": { - "value": "#eefff2" + "value": "#0e2016" }, "100": { - "value": "#dfffe9" + "value": "#13301f" }, "200": { - "value": "#bff4cf" + "value": "#13301f" }, "300": { "value": "#8fe2ab" @@ -355,7 +382,7 @@ "value": "#245c40" }, "900": { - "value": "#1f4a35" + "value": "#8fe2ab" } } } diff --git a/packages/react-component-library/.storybook/preview.js b/packages/react-component-library/.storybook/preview.js index a2ab7c2d86..220b50e87a 100644 --- a/packages/react-component-library/.storybook/preview.js +++ b/packages/react-component-library/.storybook/preview.js @@ -1,12 +1,77 @@ -import { DocsPage } from '@storybook/addon-docs/blocks' +import { DocsContainer, DocsPage } from '@storybook/addon-docs/blocks' +import { themes } from 'storybook/theming' import isChromatic from 'chromatic' import React from 'react' +import { lightTheme, darkTheme, color } from '@royalnavy/design-tokens' import '@royalnavy/fonts' import 'iframe-resizer/js/iframeResizer.contentWindow' import 'url-search-params-polyfill' import { GlobalStyleProvider } from '../src/styled-components/GlobalStyle' +const THEMES = { + light: lightTheme, + dark: darkTheme, +} + +function getThemeMode(context) { + const globals = + context?.store?.userGlobals?.globals ?? + context?.store?.globals?.get?.() ?? + context?.globals ?? + {} + + return globals.theme === 'dark' ? 'dark' : 'light' +} + +function getThemeFromContext(context) { + return THEMES[getThemeMode(context)] ?? lightTheme +} + +const ThemedDocsContainer = ({ context, children }) => { + const mode = getThemeMode(context) + + return ( + + {children} + + ) +} + +const ThemedCanvas = ({ theme, children }) => ( + +
+ {children} +
+
+) + +export const globalTypes = { + theme: { + name: 'Theme', + description: 'Design System token set', + defaultValue: 'light', + toolbar: { + icon: 'paintbrush', + items: [ + { value: 'light', title: 'Light' }, + { value: 'dark', title: 'Dark' }, + ], + dynamicTitle: true, + }, + }, +} + export const parameters = { jsx: { // Filter out callback props injected by the Actions addon @@ -21,11 +86,8 @@ export const parameters = { // there type: typeof WeakSet === undefined ? 'code' : 'auto', }, - page: () => ( - - - - ), + container: ThemedDocsContainer, + page: () => , }, options: { storySort: { @@ -54,14 +116,27 @@ export const parameters = { export const decorators = [ // https://github.com/storybookjs/storybook/issues/15223#issuecomment-1092837912 - (Story) => { + (Story, context) => { + const theme = getThemeFromContext(context) const queryParams = new URLSearchParams(window.location.search) if (queryParams.get('viewMode') === 'docs') { - return Story() + // In docs, the surrounding GlobalStyleProvider is supplied by the + // themed docs container; only give each embedded preview a themed + // background so it doesn't sit on Storybook's default light canvas. + return ( +
+ {Story()} +
+ ) } - return {Story()} + return {Story()} }, ] diff --git a/packages/react-component-library/src/components/Autocomplete/Autocomplete.test.tsx b/packages/react-component-library/src/components/Autocomplete/Autocomplete.test.tsx index 213def3acf..a18ba03656 100644 --- a/packages/react-component-library/src/components/Autocomplete/Autocomplete.test.tsx +++ b/packages/react-component-library/src/components/Autocomplete/Autocomplete.test.tsx @@ -1,4 +1,4 @@ -import { ColorDanger800, TypographyS } from '@royalnavy/design-tokens' +import { color, TypographyS } from '@royalnavy/design-tokens' import React from 'react' import { render, RenderResult, waitFor } from '@testing-library/react' @@ -8,9 +8,10 @@ import { COMPONENT_SIZE } from '../Forms' import { BORDER_WIDTH } from '../../styled-components' import { Autocomplete, AutocompleteOption } from '.' -const ERROR_BOX_SHADOW = `0 0 0 ${ - BORDER_WIDTH[COMPONENT_SIZE.FORMS] -} ${ColorDanger800.toUpperCase()}` +const ERROR_BOX_SHADOW = `0 0 0 ${BORDER_WIDTH[COMPONENT_SIZE.FORMS]} ${color( + 'danger', + '800' +)}` describe('Autocomplete', () => { let onBlurSpy: jest.Mock diff --git a/packages/react-component-library/src/components/Avatar/partials/StyledAvatar.tsx b/packages/react-component-library/src/components/Avatar/partials/StyledAvatar.tsx index 68a39989af..8b6fd4280d 100644 --- a/packages/react-component-library/src/components/Avatar/partials/StyledAvatar.tsx +++ b/packages/react-component-library/src/components/Avatar/partials/StyledAvatar.tsx @@ -1,5 +1,5 @@ import styled, { css } from 'styled-components' -import { color } from '@royalnavy/design-tokens' +import { color, colorValue } from '@royalnavy/design-tokens' interface StyledAvatarProps { $dark?: boolean @@ -20,14 +20,14 @@ export const StyledAvatar = styled.span` &:hover { background: ${color('action', '500')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; cursor: pointer; } ${({ $dark }) => $dark && css` - background: ${color('neutral', '400')}; - color: ${color('neutral', 'white')}; + background: ${colorValue('neutral', '400')}; + color: ${colorValue('neutral', 'white')}; `} ` diff --git a/packages/react-component-library/src/components/Badge/partials/StyledBadge.tsx b/packages/react-component-library/src/components/Badge/partials/StyledBadge.tsx index 0858507bcc..1a89de35c3 100644 --- a/packages/react-component-library/src/components/Badge/partials/StyledBadge.tsx +++ b/packages/react-component-library/src/components/Badge/partials/StyledBadge.tsx @@ -1,4 +1,12 @@ -import { color, fontSize, shadow, spacing } from '@royalnavy/design-tokens' +import { + color, + fontSize, + shadow, + spacing, + colorValue, + colorByMode, + Theme, +} from '@royalnavy/design-tokens' import styled, { css } from 'styled-components' import { @@ -80,8 +88,8 @@ const StyledBadgeColorVariantMap = { text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` - background-color: ${color('neutral', '500')}; - color: ${color('neutral', 'white')}; + background-color: ${colorValue('neutral', '500')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, @@ -93,26 +101,28 @@ const StyledBadgeColorVariantMap = { `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('action', '600')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, [BADGE_COLOR.SUCCESS]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('success', '100')}; - color: ${color('success', '800')}; + color: ${({ theme }) => + colorByMode('success', '800', '900', theme as Theme)}; text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('success', '800')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, [BADGE_COLOR.WARNING]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('warning', '000')}; - color: ${color('warning', '800')}; + color: ${({ theme }) => + colorByMode('warning', '800', '900', theme as Theme)}; text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` @@ -124,84 +134,91 @@ const StyledBadgeColorVariantMap = { [BADGE_COLOR.DANGER]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('danger', '100')}; - color: ${color('danger', '800')}; + color: ${({ theme }) => + colorByMode('danger', '800', '900', theme as Theme)}; text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('danger', '700')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, [BADGE_COLOR.SUPA]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('supa', '000')}; - color: ${color('supa', '600')}; + color: ${({ theme }) => + colorByMode('supa', '600', '900', theme as Theme)}; text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('supa', '600')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, [BADGE_COLOR.SUPB]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('supb', '100')}; - color: ${color('supb', '600')}; + color: ${({ theme }) => + colorByMode('supb', '600', '900', theme as Theme)}; text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('supb', '600')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, [BADGE_COLOR.SUPC]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('supc', '100')}; - color: ${color('supc', '800')}; + color: ${({ theme }) => + colorByMode('supc', '800', '900', theme as Theme)}; text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('supc', '700')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, [BADGE_COLOR.SUPD]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('supd', '100')}; - color: ${color('supd', '800')}; + color: ${({ theme }) => + colorByMode('supd', '800', '900', theme as Theme)}; text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('supd', '700')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, [BADGE_COLOR.SUPE]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('supe', '200')}; - color: ${color('supe', '800')}; + color: ${({ theme }) => + colorByMode('supe', '800', '900', theme as Theme)}; text-shadow: ${shadow('0')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('supe', '800')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, [BADGE_COLOR.SUPF]: { [BADGE_COLOR_VARIANT.FADED]: css` background-color: ${color('supf', '200')}; - color: ${color('supf', '800')}; + color: ${({ theme }) => + colorByMode('supf', '800', '900', theme as Theme)}; text-shadow: ${shadow('1')}; `, [BADGE_COLOR_VARIANT.SOLID]: css` background-color: ${color('supf', '700')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-shadow: ${shadow('1')}; `, }, diff --git a/packages/react-component-library/src/components/Button/partials/styles.ts b/packages/react-component-library/src/components/Button/partials/styles.ts index 44422a4b9f..0575f4dcb6 100644 --- a/packages/react-component-library/src/components/Button/partials/styles.ts +++ b/packages/react-component-library/src/components/Button/partials/styles.ts @@ -1,4 +1,4 @@ -import { color, fontSize } from '@royalnavy/design-tokens' +import { color, fontSize, colorValue } from '@royalnavy/design-tokens' import { BUTTON_VARIANT } from '../constants' import { COMPONENT_SIZE } from '../../Forms' @@ -38,12 +38,12 @@ export const STYLES_MAP: { [key: string]: ButtonStyles } = { [BUTTON_VARIANT.PRIMARY]: { default: { backgroundColor: color('action', '600'), - color: color('neutral', 'white'), + color: colorValue('neutral', 'white'), boxShadow: `0 0 0 1px ${color('action', '600')}`, textDecoration: 'none', }, hover: { - backgroundColor: color('action', '700'), + backgroundColor: colorValue('action', '700'), boxShadow: `0 0 0 2px ${color('action', '900')}`, }, focus: { @@ -51,7 +51,7 @@ export const STYLES_MAP: { [key: string]: ButtonStyles } = { boxShadow: `0 0 0 3px ${color('action', '900')}`, }, active: { - backgroundColor: color('action', '900'), + backgroundColor: colorValue('action', '900'), boxShadow: `0 0 0 3px ${color('action', '900')}`, }, disabled: defaultDisabledStyle, @@ -102,7 +102,7 @@ export const STYLES_MAP: { [key: string]: ButtonStyles } = { }, [BUTTON_VARIANT.DANGER]: { default: { - color: color('neutral', 'white'), + color: colorValue('neutral', 'white'), backgroundColor: color('danger', '700'), textDecoration: 'none', boxShadow: `0 0 0 1px ${color('danger', '700')}`, @@ -116,7 +116,7 @@ export const STYLES_MAP: { [key: string]: ButtonStyles } = { boxShadow: `0 0 0 3px ${color('danger', '900')}`, }, active: { - backgroundColor: color('danger', '900'), + backgroundColor: colorValue('danger', '900'), boxShadow: `0 0 0 3px ${color('danger', '900')}`, }, diff --git a/packages/react-component-library/src/components/Checkbox/Checkbox.test.tsx b/packages/react-component-library/src/components/Checkbox/Checkbox.test.tsx index c7d328f00a..35050fd1ec 100644 --- a/packages/react-component-library/src/components/Checkbox/Checkbox.test.tsx +++ b/packages/react-component-library/src/components/Checkbox/Checkbox.test.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import 'jest-styled-components' -import { ColorNeutral200 } from '@royalnavy/design-tokens' +import { color } from '@royalnavy/design-tokens' import { render, RenderResult } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -106,7 +106,7 @@ describe('Checkbox', () => { it('should render a container', () => { expect(checkbox.getByTestId('checkbox')).toHaveStyleRule( 'border', - `1px solid ${ColorNeutral200}` + `1px solid ${color('neutral', '200')}` ) expect(checkbox.getByTestId('checkbox')).toHaveStyleRule( 'border-radius', @@ -292,7 +292,7 @@ describe('Checkbox', () => { it('should not render a container', () => { expect(checkbox.getByTestId('checkbox')).not.toHaveStyleRule( 'border', - `1px solid ${ColorNeutral200}` + `1px solid ${color('neutral', '200')}` ) expect(checkbox.getByTestId('checkbox')).not.toHaveStyleRule( 'border-radius', diff --git a/packages/react-component-library/src/components/Checkbox/partials/StyledCheckmark.tsx b/packages/react-component-library/src/components/Checkbox/partials/StyledCheckmark.tsx index 3fffa24a31..6c12a04a35 100644 --- a/packages/react-component-library/src/components/Checkbox/partials/StyledCheckmark.tsx +++ b/packages/react-component-library/src/components/Checkbox/partials/StyledCheckmark.tsx @@ -1,6 +1,6 @@ import { position } from 'polished' import styled, { css } from 'styled-components' -import { color } from '@royalnavy/design-tokens' +import { color, colorValue } from '@royalnavy/design-tokens' import { StyledCheckbox } from './StyledCheckbox' import { CheckmarkProps } from '../../CheckboxRadioBase/CheckboxRadioBaseProps' @@ -32,7 +32,7 @@ export const StyledCheckmark = styled.span` content: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIiAvPgogICAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMywgNCkiIGQ9Ik04LjgyMiAwYzEuMDUtLjAyNyAxLjU3NCAxLjMwNC44MjcgMi4wOEw0LjI5OCA3Ljc0YS44My44MyAwIDAgMS0xLjIwNyAwTC4zNzYgNC44NTVDLS43NTIgMy43MTguOTE0IDEuOTU2IDEuOTkgMy4xNDlsMS41MDggMS41OTVjLjEwNS4xMS4yODguMTEuNDA3IDBsNC4xMy00LjM3QTEuMSAxLjEgMCAwIDEgOC44MjMgMHoiCiAgICAgICAgICBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KPC9zdmc+Cg==); display: none; border-radius: 2px; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; position: absolute; top: 50%; left: 50%; @@ -67,7 +67,7 @@ export const StyledCheckmark = styled.span` top: 50%; left: 50%; transform: translate(-50%, -50%); - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; } } diff --git a/packages/react-component-library/src/components/ClassificationBar/partials/StyledClassificationBar.tsx b/packages/react-component-library/src/components/ClassificationBar/partials/StyledClassificationBar.tsx index 4e5ab16f6d..65536baeb5 100644 --- a/packages/react-component-library/src/components/ClassificationBar/partials/StyledClassificationBar.tsx +++ b/packages/react-component-library/src/components/ClassificationBar/partials/StyledClassificationBar.tsx @@ -1,18 +1,18 @@ -import { color, fontSize, spacing } from '@royalnavy/design-tokens' +import { color, fontSize, spacing, colorValue } from '@royalnavy/design-tokens' import styled, { css } from 'styled-components' const getColorStyles = (isSecret?: boolean, inSidebar?: boolean) => { if (isSecret) { return css` background-color: ${color('danger', '600')}; - color: ${color('neutral', '000')}; + color: ${colorValue('neutral', '000')}; ` } if (inSidebar) { return css` - background-color: ${color('neutral', '400')}; - color: ${color('neutral', '000')}; + background-color: ${colorValue('neutral', '400')}; + color: ${colorValue('neutral', '000')}; ` } diff --git a/packages/react-component-library/src/components/DataGrid/partials/StyledHead.tsx b/packages/react-component-library/src/components/DataGrid/partials/StyledHead.tsx index 17805ed4b4..cfd8791684 100644 --- a/packages/react-component-library/src/components/DataGrid/partials/StyledHead.tsx +++ b/packages/react-component-library/src/components/DataGrid/partials/StyledHead.tsx @@ -1,14 +1,17 @@ import styled, { css } from 'styled-components' -import { color, zIndex } from '@royalnavy/design-tokens' +import { colorByMode, zIndex, Theme } from '@royalnavy/design-tokens' import { StyledLayoutProps } from './types' export const StyledHead = styled.thead` - background: ${color('neutral', '000')}; + background: ${({ theme }) => + colorByMode('neutral', '000', '100', theme as Theme)}; - ${({$hasScrolling}) => $hasScrolling && css` - position: sticky; - top: 0; - z-index: ${zIndex('header', 1)}; - `} + ${({ $hasScrolling }) => + $hasScrolling && + css` + position: sticky; + top: 0; + z-index: ${zIndex('header', 1)}; + `} ` diff --git a/packages/react-component-library/src/components/DataGrid/partials/StyledLoadingOverlay.tsx b/packages/react-component-library/src/components/DataGrid/partials/StyledLoadingOverlay.tsx index cf9e560f7c..5cb132d7dd 100644 --- a/packages/react-component-library/src/components/DataGrid/partials/StyledLoadingOverlay.tsx +++ b/packages/react-component-library/src/components/DataGrid/partials/StyledLoadingOverlay.tsx @@ -1,4 +1,5 @@ import styled from 'styled-components' +import { color } from '@royalnavy/design-tokens' export const StyledLoadingOverlay = styled.div` position: absolute; @@ -6,7 +7,6 @@ export const StyledLoadingOverlay = styled.div` left: 0; right: 0; bottom: 0; - background-color: rgba(255, 255, 255, 0.5); display: flex; justify-content: center; align-items: center; @@ -14,6 +14,17 @@ export const StyledLoadingOverlay = styled.div` border-radius: 4px; overflow: hidden; + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: ${color('neutral', 'white')}; + opacity: 0.5; + } + > div { position: absolute; top: 50%; diff --git a/packages/react-component-library/src/components/DatePicker/__tests__/DatePicker.test.tsx b/packages/react-component-library/src/components/DatePicker/__tests__/DatePicker.test.tsx index bda8000930..e7520ebde1 100644 --- a/packages/react-component-library/src/components/DatePicker/__tests__/DatePicker.test.tsx +++ b/packages/react-component-library/src/components/DatePicker/__tests__/DatePicker.test.tsx @@ -1,6 +1,6 @@ import { format, isValid } from 'date-fns' import React, { useState } from 'react' -import { ColorDanger800, ColorWarning800 } from '@royalnavy/design-tokens' +import { color } from '@royalnavy/design-tokens' import { act, fireEvent, @@ -17,9 +17,10 @@ import { DATE_VALIDITY } from '../constants' const NOW = '2019-12-05T11:00:00.000Z' -const ERROR_BOX_SHADOW = `0 0 0 ${ - BORDER_WIDTH[COMPONENT_SIZE.FORMS] -} ${ColorDanger800.toUpperCase()}` +const ERROR_BOX_SHADOW = `0 0 0 ${BORDER_WIDTH[COMPONENT_SIZE.FORMS]} ${color( + 'danger', + '800' +)}` const PREVIOUS_MONTH_BUTTON_LABEL = 'Go to previous month' @@ -162,11 +163,11 @@ describe('DatePicker', () => { }) }) - it('colours the current date', () => { + it('marks the current date as today', () => { return waitFor(() => { - expect(wrapper.getByRole('gridcell', { name: '5' })).toHaveStyle({ - color: ColorWarning800, - }) + expect(wrapper.getByRole('gridcell', { name: '5' })).toHaveClass( + 'rdp-day_today' + ) }) }) diff --git a/packages/react-component-library/src/components/DatePicker/__tests__/DatePickerWhenNavigateMonthYearIsEnabled.test.tsx b/packages/react-component-library/src/components/DatePicker/__tests__/DatePickerWhenNavigateMonthYearIsEnabled.test.tsx index c2dc46ab8a..3de16622df 100644 --- a/packages/react-component-library/src/components/DatePicker/__tests__/DatePickerWhenNavigateMonthYearIsEnabled.test.tsx +++ b/packages/react-component-library/src/components/DatePicker/__tests__/DatePickerWhenNavigateMonthYearIsEnabled.test.tsx @@ -1,4 +1,4 @@ -import { color } from '@royalnavy/design-tokens' +import { color, colorValue } from '@royalnavy/design-tokens' import { render, screen } from '@testing-library/react' import React from 'react' import userEvent from '@testing-library/user-event' @@ -115,7 +115,7 @@ it('applies correct styles to current and selected month', async () => { 'background-color', color('action', '700') ) - expect(selectedMonth).toHaveStyleRule('color', color('neutral', 'white')) + expect(selectedMonth).toHaveStyleRule('color', colorValue('neutral', 'white')) }) it('applies correct styles to current and selected year', async () => { @@ -144,7 +144,7 @@ it('applies correct styles to current and selected year', async () => { 'background-color', color('action', '700') ) - expect(selectedYear).toHaveStyleRule('color', color('neutral', 'white')) + expect(selectedYear).toHaveStyleRule('color', colorValue('neutral', 'white')) }) it('displays the initial month and allows navigation', async () => { diff --git a/packages/react-component-library/src/components/DatePicker/__tests__/DatePickerWhenTodayPropIsSet.test.tsx b/packages/react-component-library/src/components/DatePicker/__tests__/DatePickerWhenTodayPropIsSet.test.tsx index 6af7a2c303..3772b71ad7 100644 --- a/packages/react-component-library/src/components/DatePicker/__tests__/DatePickerWhenTodayPropIsSet.test.tsx +++ b/packages/react-component-library/src/components/DatePicker/__tests__/DatePickerWhenTodayPropIsSet.test.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { ColorWarning800 } from '@royalnavy/design-tokens' import { act, render, RenderResult } from '@testing-library/react' import { DatePicker, DatePickerOnChangeData } from '../index' @@ -26,15 +25,15 @@ describe('when today prop is set', () => { onChange.mockReset() }) - it('colours the override date', () => { - expect(wrapper.getByRole('gridcell', { name: '15' })).toHaveStyle({ - color: ColorWarning800, - }) + it('marks the override date as today', () => { + expect(wrapper.getByRole('gridcell', { name: '15' })).toHaveClass( + 'rdp-day_today' + ) }) - it('does not colour the actual current date', () => { - expect(wrapper.getByRole('gridcell', { name: '5' })).not.toHaveStyle({ - color: ColorWarning800, - }) + it('does not mark the actual current date as today', () => { + expect(wrapper.getByRole('gridcell', { name: '5' })).not.toHaveClass( + 'rdp-day_today' + ) }) }) diff --git a/packages/react-component-library/src/components/DatePicker/partials/StyledCalendarNavigation.tsx b/packages/react-component-library/src/components/DatePicker/partials/StyledCalendarNavigation.tsx index d88e95516f..3a5a08f6f6 100644 --- a/packages/react-component-library/src/components/DatePicker/partials/StyledCalendarNavigation.tsx +++ b/packages/react-component-library/src/components/DatePicker/partials/StyledCalendarNavigation.tsx @@ -1,5 +1,5 @@ import styled, { css } from 'styled-components' -import { color, fontSize, spacing } from '@royalnavy/design-tokens' +import { color, fontSize, spacing, colorValue } from '@royalnavy/design-tokens' import { Button } from '../../Button' @@ -105,11 +105,11 @@ export const StyledCalendarTiles = styled.button<{ $isSelected && css` background-color: ${color('action', '700')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; &:hover { background-color: ${color('action', '700')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; } `} ` diff --git a/packages/react-component-library/src/components/DatePicker/partials/StyledDayPicker.tsx b/packages/react-component-library/src/components/DatePicker/partials/StyledDayPicker.tsx index e3970c4178..556576ca44 100644 --- a/packages/react-component-library/src/components/DatePicker/partials/StyledDayPicker.tsx +++ b/packages/react-component-library/src/components/DatePicker/partials/StyledDayPicker.tsx @@ -1,5 +1,12 @@ import { DayPicker } from 'react-day-picker' -import { color, spacing, fontSize } from '@royalnavy/design-tokens' +import { + color, + spacing, + fontSize, + colorValue, + colorByMode, + Theme, +} from '@royalnavy/design-tokens' import styled from 'styled-components' import { BUTTON_VARIANT, getButtonStyles } from '../../Button' @@ -48,23 +55,44 @@ export const StyledDayPicker = styled(DayPicker)` position: absolute; top: ${spacing('6')}; width: 38px; - background-position: center; - background-size: 18px; - background-repeat: no-repeat; svg { display: none; } + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: ${color('neutral', '500')}; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: 18px; + mask-size: 18px; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + } } .rdp-nav_button_previous { left: ${spacing('6')}; - background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EIcons/Navigation/chevron-left%3C/title%3E%3Cdefs%3E%3Cpolygon id='path-1' points='10.2755556 4.9422222 9.33333334 3.99999998 5.33333332 8 9.33333334 12 10.2755556 11.0577778 7.21777777 8'%3E%3C/polygon%3E%3C/defs%3E%3Cg id='Icons/Navigation/chevron-left' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cuse id='Icons/Navigation/ic_chevron_left_18px' fill='%23233745' fill-rule='nonzero' xlink:href='%23path-1'%3E%3C/use%3E%3C/g%3E%3C/svg%3E"); + + &::after { + -webkit-mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EIcons/Navigation/chevron-left%3C/title%3E%3Cdefs%3E%3Cpolygon id='path-1' points='10.2755556 4.9422222 9.33333334 3.99999998 5.33333332 8 9.33333334 12 10.2755556 11.0577778 7.21777777 8'%3E%3C/polygon%3E%3C/defs%3E%3Cg id='Icons/Navigation/chevron-left' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cuse id='Icons/Navigation/ic_chevron_left_18px' fill='%23233745' fill-rule='nonzero' xlink:href='%23path-1'%3E%3C/use%3E%3C/g%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EIcons/Navigation/chevron-left%3C/title%3E%3Cdefs%3E%3Cpolygon id='path-1' points='10.2755556 4.9422222 9.33333334 3.99999998 5.33333332 8 9.33333334 12 10.2755556 11.0577778 7.21777777 8'%3E%3C/polygon%3E%3C/defs%3E%3Cg id='Icons/Navigation/chevron-left' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cuse id='Icons/Navigation/ic_chevron_left_18px' fill='%23233745' fill-rule='nonzero' xlink:href='%23path-1'%3E%3C/use%3E%3C/g%3E%3C/svg%3E"); + } } .rdp-nav_button_next { right: ${spacing('6')}; - background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EIcons/Navigation/chevron-right%3C/title%3E%3Cdefs%3E%3Cpolygon id='path-1' points='6.66666666 3.99999998 5.72444443 4.9422222 8.78222223 8 5.72444443 11.0577778 6.66666666 12 10.6666667 8'%3E%3C/polygon%3E%3C/defs%3E%3Cg id='Icons/Navigation/chevron-right' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cuse id='Icons/Navigation/ic_chevron_right_18px' fill='%23233745' fill-rule='nonzero' xlink:href='%23path-1'%3E%3C/use%3E%3C/g%3E%3C/svg%3E"); + + &::after { + -webkit-mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EIcons/Navigation/chevron-right%3C/title%3E%3Cdefs%3E%3Cpolygon id='path-1' points='6.66666666 3.99999998 5.72444443 4.9422222 8.78222223 8 5.72444443 11.0577778 6.66666666 12 10.6666667 8'%3E%3C/polygon%3E%3C/defs%3E%3Cg id='Icons/Navigation/chevron-right' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cuse id='Icons/Navigation/ic_chevron_right_18px' fill='%23233745' fill-rule='nonzero' xlink:href='%23path-1'%3E%3C/use%3E%3C/g%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EIcons/Navigation/chevron-right%3C/title%3E%3Cdefs%3E%3Cpolygon id='path-1' points='6.66666666 3.99999998 5.72444443 4.9422222 8.78222223 8 5.72444443 11.0577778 6.66666666 12 10.6666667 8'%3E%3C/polygon%3E%3C/defs%3E%3Cg id='Icons/Navigation/chevron-right' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cuse id='Icons/Navigation/ic_chevron_right_18px' fill='%23233745' fill-rule='nonzero' xlink:href='%23path-1'%3E%3C/use%3E%3C/g%3E%3C/svg%3E"); + } } .rdp-caption_label { @@ -119,7 +147,8 @@ export const StyledDayPicker = styled(DayPicker)` cursor: pointer; &.rdp-day_today { - color: ${color('warning', '800')}; + color: ${({ theme }) => + colorByMode('warning', '800', '900', theme as Theme)}; } &.rdp-day_selected { @@ -128,7 +157,7 @@ export const StyledDayPicker = styled(DayPicker)` &.rdp-day_start, &.rdp-day_end { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; background-color: ${color('action', '700')}; } diff --git a/packages/react-component-library/src/components/Field/partials/StyledError.tsx b/packages/react-component-library/src/components/Field/partials/StyledError.tsx index 8fb18c641c..c227a3bbc1 100644 --- a/packages/react-component-library/src/components/Field/partials/StyledError.tsx +++ b/packages/react-component-library/src/components/Field/partials/StyledError.tsx @@ -1,9 +1,9 @@ import styled from 'styled-components' -import { color, fontSize } from '@royalnavy/design-tokens' +import { colorByMode, fontSize, Theme } from '@royalnavy/design-tokens' export const StyledError = styled.span` display: inline-block; font-size: ${fontSize('s')}; - color: ${color('danger', '800')}; + color: ${({ theme }) => colorByMode('danger', '800', '900', theme as Theme)}; margin: 0 0 6px 12px; ` diff --git a/packages/react-component-library/src/components/Modal/partials/StyledModal.tsx b/packages/react-component-library/src/components/Modal/partials/StyledModal.tsx index 6e44b3071d..365e8f0d49 100644 --- a/packages/react-component-library/src/components/Modal/partials/StyledModal.tsx +++ b/packages/react-component-library/src/components/Modal/partials/StyledModal.tsx @@ -1,4 +1,4 @@ -import { zIndex } from '@royalnavy/design-tokens' +import { color, zIndex } from '@royalnavy/design-tokens' import styled from 'styled-components' export const StyledModal = styled.dialog` @@ -8,5 +8,8 @@ export const StyledModal = styled.dialog` bottom: 0; left: 0; background: rgba(0, 0, 0, 0.2); + /* Native defaults its text colour to CanvasText; set a themed + colour so content without an explicit colour stays legible in dark mode. */ + color: ${color('neutral', '600')}; z-index: ${zIndex('modal', 1)}; ` diff --git a/packages/react-component-library/src/components/Pagination/partials/StyledButton.tsx b/packages/react-component-library/src/components/Pagination/partials/StyledButton.tsx index 6f86578bad..25cf79f6be 100644 --- a/packages/react-component-library/src/components/Pagination/partials/StyledButton.tsx +++ b/packages/react-component-library/src/components/Pagination/partials/StyledButton.tsx @@ -1,5 +1,11 @@ import styled, { css } from 'styled-components' -import { animation, color, spacing } from '@royalnavy/design-tokens' +import { + animation, + color, + colorByMode, + spacing, + Theme, +} from '@royalnavy/design-tokens' interface StyledButtonProps { $isDisabled: boolean @@ -11,8 +17,17 @@ export const StyledButton = styled.button` outline: none; transition: all ${animation('default')}; - background-color: ${color('neutral', '000')}; - color: ${({ $isDisabled }) => color('neutral', $isDisabled ? '200' : '400')}; + background-color: ${({ theme }) => + (theme as Theme)?.mode === 'dark' + ? 'transparent' + : color('neutral', '000', theme as Theme)}; + color: ${({ $isDisabled, theme }) => + colorByMode( + 'neutral', + $isDisabled ? '200' : '400', + $isDisabled ? '300' : '600', + theme as Theme + )}; border: 1px solid transparent; border-radius: 4px; height: 33px; diff --git a/packages/react-component-library/src/components/ProgressBar/partials/StyledProgressBar.ts b/packages/react-component-library/src/components/ProgressBar/partials/StyledProgressBar.ts index ee1f14352e..37042a1665 100644 --- a/packages/react-component-library/src/components/ProgressBar/partials/StyledProgressBar.ts +++ b/packages/react-component-library/src/components/ProgressBar/partials/StyledProgressBar.ts @@ -1,4 +1,10 @@ -import { color, spacing, type Spacing } from '@royalnavy/design-tokens' +import { + color, + colorByMode, + spacing, + Theme, + type Spacing, +} from '@royalnavy/design-tokens' import styled, { css } from 'styled-components' export interface StyledProgressBarProps { @@ -7,7 +13,8 @@ export interface StyledProgressBarProps { } export const StyledProgressBar = styled.div` - background-color: ${color('neutral', '100')}; + background-color: ${({ theme }) => + colorByMode('neutral', '100', '200', theme as Theme)}; overflow: hidden; border-radius: 4px; height: ${({ $height }) => spacing($height)}; diff --git a/packages/react-component-library/src/components/Radio/Radio.test.tsx b/packages/react-component-library/src/components/Radio/Radio.test.tsx index 8630cefbb1..baba1e37b4 100644 --- a/packages/react-component-library/src/components/Radio/Radio.test.tsx +++ b/packages/react-component-library/src/components/Radio/Radio.test.tsx @@ -1,11 +1,7 @@ import React, { useState } from 'react' import 'jest-styled-components' -import { - ColorNeutral200, - ColorAction000, - ColorNeutralWhite, -} from '@royalnavy/design-tokens' +import { color } from '@royalnavy/design-tokens' import { render, RenderResult } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -51,7 +47,7 @@ describe('Radio', () => { it('should render a container', () => { expect(radio.getByTestId('radio')).toHaveStyleRule( 'border', - `1px solid ${ColorNeutral200}` + `1px solid ${color('neutral', '200')}` ) expect(radio.getByTestId('radio')).toHaveStyleRule( 'border-radius', @@ -60,7 +56,7 @@ describe('Radio', () => { expect(radio.getByTestId('radio')).toHaveStyleRule( 'background', - ColorNeutralWhite + color('neutral', 'white') ) }) @@ -101,7 +97,7 @@ describe('Radio', () => { it('styles the container as active', () => { expect(radio.getByTestId('radio')).toHaveStyleRule( 'background', - ColorAction000 + color('action', '000') ) }) }) @@ -213,7 +209,7 @@ describe('Radio', () => { it('should not render a container', () => { expect(radio.getByTestId('radio')).not.toHaveStyleRule( 'border', - `1px solid ${ColorNeutral200}` + `1px solid ${color('neutral', '200')}` ) expect(radio.getByTestId('radio')).not.toHaveStyleRule( 'border-radius', diff --git a/packages/react-component-library/src/components/RangeSlider/constants.ts b/packages/react-component-library/src/components/RangeSlider/constants.ts index 96e33efa7c..802d130435 100644 --- a/packages/react-component-library/src/components/RangeSlider/constants.ts +++ b/packages/react-component-library/src/components/RangeSlider/constants.ts @@ -1,9 +1,17 @@ -import { color } from '@royalnavy/design-tokens' +import { color, colorValue } from '@royalnavy/design-tokens' export const RANGE_SLIDER_BG_COLOR = color('neutral', '200') export const RANGE_SLIDER_HANDLE_COLOR = color('action', '500') export const RANGE_SLIDER_TRACK_COLOR = color('action', '200') -export const RANGE_SLIDER_TRACK_BELOW_FIRST_THRESHOLD = color('action', '500') -export const RANGE_SLIDER_TRACK_BETWEEN_THRESHOLDS = color('warning', '500') -export const RANGE_SLIDER_TRACK_ABOVE_THRESHOLDS = color('danger', '500') +// Threshold colours are passed through polished's `setLightness`, which +// needs a concrete colour rather than a CSS custom property reference. +export const RANGE_SLIDER_TRACK_BELOW_FIRST_THRESHOLD = colorValue( + 'action', + '500' +) +export const RANGE_SLIDER_TRACK_BETWEEN_THRESHOLDS = colorValue( + 'warning', + '500' +) +export const RANGE_SLIDER_TRACK_ABOVE_THRESHOLDS = colorValue('danger', '500') diff --git a/packages/react-component-library/src/components/RangeSlider/partials/StyledHandle.tsx b/packages/react-component-library/src/components/RangeSlider/partials/StyledHandle.tsx index 9ad1f363c5..5abc35fbcc 100644 --- a/packages/react-component-library/src/components/RangeSlider/partials/StyledHandle.tsx +++ b/packages/react-component-library/src/components/RangeSlider/partials/StyledHandle.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color } from '@royalnavy/design-tokens' +import { colorValue } from '@royalnavy/design-tokens' import { transparentize } from 'polished' import { RANGE_SLIDER_HANDLE_COLOR } from '../constants' @@ -20,7 +20,7 @@ export const StyledHandle = styled.div` &:focus { box-shadow: 1px 1px 2px 0px rgba(000, 000, 000, 0.25), - 0px 0px 0px 5px ${transparentize(0.5, color('action', '200'))}; + 0px 0px 0px 5px ${transparentize(0.5, colorValue('action', '200'))}; outline: none; } ` diff --git a/packages/react-component-library/src/components/Select/Select.test.tsx b/packages/react-component-library/src/components/Select/Select.test.tsx index 8b8310d2e4..a523e2d763 100644 --- a/packages/react-component-library/src/components/Select/Select.test.tsx +++ b/packages/react-component-library/src/components/Select/Select.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { render, RenderResult, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { color, ColorDanger800 } from '@royalnavy/design-tokens' +import { color } from '@royalnavy/design-tokens' import { IconAgriculture, IconAnchor, @@ -470,9 +470,7 @@ describe('Select', () => { it('displays in an error state', () => { expect(wrapper.getByTestId('select-outer-wrapper')).toHaveStyleRule( 'box-shadow', - `0 0 0 ${ - BORDER_WIDTH[COMPONENT_SIZE.FORMS] - } ${ColorDanger800.toUpperCase()}` + `0 0 0 ${BORDER_WIDTH[COMPONENT_SIZE.FORMS]} ${color('danger', '800')}` ) }) }) diff --git a/packages/react-component-library/src/components/Switch/partials/StyledSwitchOption.tsx b/packages/react-component-library/src/components/Switch/partials/StyledSwitchOption.tsx index 8cc599d70f..646a52164c 100644 --- a/packages/react-component-library/src/components/Switch/partials/StyledSwitchOption.tsx +++ b/packages/react-component-library/src/components/Switch/partials/StyledSwitchOption.tsx @@ -1,5 +1,5 @@ import styled, { css } from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { color, colorValue, spacing } from '@royalnavy/design-tokens' import { SWITCH_OPTION_VARIANT } from '../constants' import { SwitchOptionVariantType } from '../SwitchOption' @@ -21,7 +21,7 @@ type VariantActiveStyle = { const VARIANT_ACTIVE_STYLES: Record = { [SWITCH_OPTION_VARIANT.PRIMARY]: { backgroundColor: color('action', '600'), - color: color('neutral', 'white'), + color: colorValue('neutral', 'white'), borderColor: color('action', '600'), focusBorderColor: color('action', '500'), focusBoxShadow: `0 0 0 2px ${color('action', '500')}, 0 0 0 5px ${color( @@ -51,7 +51,7 @@ const VARIANT_ACTIVE_STYLES: Record = { }, [SWITCH_OPTION_VARIANT.DANGER]: { backgroundColor: color('danger', '700'), - color: color('neutral', 'white'), + color: colorValue('neutral', 'white'), borderColor: color('danger', '700'), focusBorderColor: color('danger', '700'), focusBoxShadow: `0 0 0 2px ${color('danger', '700')}, 0 0 0 5px ${color( diff --git a/packages/react-component-library/src/components/TabBase/partials/StyledTabs.tsx b/packages/react-component-library/src/components/TabBase/partials/StyledTabs.tsx index f4bb500fec..715a75ee89 100644 --- a/packages/react-component-library/src/components/TabBase/partials/StyledTabs.tsx +++ b/packages/react-component-library/src/components/TabBase/partials/StyledTabs.tsx @@ -1,4 +1,5 @@ import styled from 'styled-components' +import { color, Theme } from '@royalnavy/design-tokens' export const StyledTabs = styled.ol` display: flex; @@ -6,4 +7,8 @@ export const StyledTabs = styled.ol` list-style-type: none; padding: 0; margin: 0; + background-color: ${({ theme }) => + (theme as Theme)?.mode === 'dark' + ? color('neutral', '000', theme as Theme) + : 'transparent'}; ` diff --git a/packages/react-component-library/src/components/Tooltip/partials/StyledContent.tsx b/packages/react-component-library/src/components/Tooltip/partials/StyledContent.tsx index d6b0e8c0ad..c525761498 100644 --- a/packages/react-component-library/src/components/Tooltip/partials/StyledContent.tsx +++ b/packages/react-component-library/src/components/Tooltip/partials/StyledContent.tsx @@ -1,5 +1,11 @@ import styled, { css, CSSProp } from 'styled-components' -import { color, spacing, zIndex } from '@royalnavy/design-tokens' +import { + spacing, + zIndex, + color, + colorValue, + Theme, +} from '@royalnavy/design-tokens' import { TooltipPositionType } from '../Tooltip' @@ -7,7 +13,17 @@ const TOOLTIP_BORDER_THICK = 1 const TOOLTIP_INNER = 4 const TOOLTIP_OUTER = TOOLTIP_INNER - 4 const TOOLTIP_OFFSET = TOOLTIP_OUTER + TOOLTIP_BORDER_THICK -const TOOLTIP_DARK_BG = color('neutral', '700') +// The tooltip is a dark bubble in both themes. In light mode it stays +// near-black; in dark mode it lifts to an elevated slate so it reads clearly +// above the dark page and surfaces. +function tooltipDarkBg(theme?: Theme): string { + return theme?.mode === 'dark' + ? color('neutral', '200', theme) + : colorValue('neutral', '700') +} + +const TOOLTIP_DARK_BG = ({ theme }: { theme?: unknown }) => + tooltipDarkBg(theme as Theme) const TOOLTIP_DARK_BORDER = TOOLTIP_DARK_BG interface StyledContentProps { @@ -91,7 +107,7 @@ function getPositionStyles(position: TooltipPositionType): CSSProp { export const StyledContent = styled.div` z-index: ${zIndex('modal', 1)}; background: ${TOOLTIP_DARK_BG}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; border: ${TOOLTIP_DARK_BORDER} solid ${TOOLTIP_BORDER_THICK}px; border-radius: 5px; padding: ${spacing('6')} ${spacing('8')}; diff --git a/packages/react-component-library/src/components/Tooltip/partials/StyledMessage.tsx b/packages/react-component-library/src/components/Tooltip/partials/StyledMessage.tsx index e1a0902f1c..f6f678d4fe 100644 --- a/packages/react-component-library/src/components/Tooltip/partials/StyledMessage.tsx +++ b/packages/react-component-library/src/components/Tooltip/partials/StyledMessage.tsx @@ -1,7 +1,9 @@ import styled from 'styled-components' -import { color, fontSize } from '@royalnavy/design-tokens' +import { colorValue, fontSize } from '@royalnavy/design-tokens' export const StyledMessage = styled.div` font-size: ${fontSize('base')}; - color: ${color('neutral', '100')}; + // The tooltip bubble is always dark in both themes, so its message text + // stays a fixed light value rather than flipping with the theme. + color: ${colorValue('neutral', '100')}; ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledBanner.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledBanner.tsx index 8e6ca60359..981408394a 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledBanner.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledBanner.tsx @@ -1,5 +1,5 @@ import { rgba } from 'polished' -import { color, spacing } from '@royalnavy/design-tokens' +import { color, colorValue, spacing } from '@royalnavy/design-tokens' import styled from 'styled-components' import { MAIN_HEIGHT } from './constants' @@ -25,7 +25,7 @@ export const StyledBanner = styled.div<{ $withInlineNav?: boolean }>` } &:focus { - box-shadow: 0 0 0 0.2rem ${rgba(color('action', '700'), 0.5)}; + box-shadow: 0 0 0 0.2rem ${rgba(colorValue('action', '700'), 0.5)}; } } ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledOption.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledOption.tsx index bb8ee69379..11b806dd85 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledOption.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledOption.tsx @@ -1,4 +1,5 @@ -import { color } from '@royalnavy/design-tokens' +import { color, colorValue } from '@royalnavy/design-tokens' +import { rgba } from 'polished' import styled from 'styled-components' import { MAIN_HEIGHT } from './constants' @@ -22,7 +23,7 @@ export const StyledOption = styled.button` cursor: pointer; &:focus { - box-shadow: 0 0 0 0.2rem rgba(${color('action', '700')}, 0.5); + box-shadow: 0 0 0 0.2rem ${rgba(colorValue('action', '700'), 0.5)}; } > svg { diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledUserItemWrapper.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledUserItemWrapper.tsx index ec39051de9..7337db826c 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledUserItemWrapper.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Masthead/partials/StyledUserItemWrapper.tsx @@ -1,4 +1,4 @@ -import { color } from '@royalnavy/design-tokens' +import { color, colorValue } from '@royalnavy/design-tokens' import styled from 'styled-components' import { StyledUserItemIcon } from './StyledUserItemIcon' @@ -8,7 +8,7 @@ export const StyledUserItemWrapper = styled.div` position: relative; a { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-decoration: none; } diff --git a/packages/react-component-library/src/components/TopLevelNavigation/NotificationPanel/partials/StyledContent.tsx b/packages/react-component-library/src/components/TopLevelNavigation/NotificationPanel/partials/StyledContent.tsx index 459c9172b9..2c239fdeb0 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/NotificationPanel/partials/StyledContent.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/NotificationPanel/partials/StyledContent.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, fontSize, spacing } from '@royalnavy/design-tokens' +import { colorValue, fontSize, spacing } from '@royalnavy/design-tokens' export const StyledContent = styled.span` display: block; @@ -7,7 +7,9 @@ export const StyledContent = styled.span` font-size: ${fontSize('s')}; line-height: 1.3; + // The notification panel is an always-dark surface, so emphasised text + // stays a fixed light value rather than flipping with the theme. strong { - color: ${color('neutral', '000')}; + color: ${colorValue('neutral', '000')}; } ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/SearchBar/partials/StyledButton.tsx b/packages/react-component-library/src/components/TopLevelNavigation/SearchBar/partials/StyledButton.tsx index 2024962c0d..b0aa70bcea 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/SearchBar/partials/StyledButton.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/SearchBar/partials/StyledButton.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { color, colorValue, spacing } from '@royalnavy/design-tokens' import { rgba } from 'polished' export const StyledButton = styled.button` @@ -16,11 +16,11 @@ export const StyledButton = styled.button` cursor: pointer; > svg { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; } &:focus { - box-shadow: 0 0 0 0.2rem ${rgba(color('action', '700'), 0.5)}; + box-shadow: 0 0 0 0.2rem ${rgba(colorValue('action', '700'), 0.5)}; outline: 0; } ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/SearchBar/partials/StyledForm.tsx b/packages/react-component-library/src/components/TopLevelNavigation/SearchBar/partials/StyledForm.tsx index 39297ec69b..97a2b211cb 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/SearchBar/partials/StyledForm.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/SearchBar/partials/StyledForm.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, spacing, zIndex } from '@royalnavy/design-tokens' +import { spacing, zIndex, colorValue } from '@royalnavy/design-tokens' import { StyledInput } from '../../../TextInput/partials/StyledInput' import { StyledOuterWrapper } from '../../../TextInput/partials/StyledOuterWrapper' @@ -9,7 +9,7 @@ export const StyledForm = styled.form` z-index: ${zIndex('overlay', 1)}; display: flex; align-items: center; - background-color: ${color('neutral', '400')}; + background-color: ${colorValue('neutral', '400')}; height: 59px; ${StyledTextInput} { @@ -27,12 +27,12 @@ export const StyledForm = styled.form` ${StyledInput} { padding: ${spacing('8')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; background-color: transparent; border: 0; &::placeholder { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; } } ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHandle.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHandle.tsx index 95fc21e5f0..b7b7ed92fa 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHandle.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHandle.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, zIndex } from '@royalnavy/design-tokens' +import { color, zIndex, colorValue } from '@royalnavy/design-tokens' import { getTransitionOpacity, @@ -29,6 +29,6 @@ export const StyledHandle = styled.button` } > svg { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; } ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHead.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHead.tsx index 27e4ffb7ea..d1d69f1a36 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHead.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHead.tsx @@ -1,10 +1,10 @@ import styled from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { spacing, colorValue } from '@royalnavy/design-tokens' export const StyledHead = styled.div` position: relative; display: flex; align-items: center; padding: ${spacing('8')} ${spacing('6')}; - background-color: ${color('neutral', '800')}; + background-color: ${colorValue('neutral', '800')}; ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHeadIcon.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHeadIcon.tsx index d540331c6e..284d627eaa 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHeadIcon.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledHeadIcon.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { spacing, colorValue } from '@royalnavy/design-tokens' import { StyledHeadTitle } from './StyledHeadTitle' @@ -7,7 +7,7 @@ export const StyledHeadIcon = styled.div` display: inline-flex; align-items: center; border-radius: 4px; - background-color: ${color('neutral', '500')}; + background-color: ${colorValue('neutral', '500')}; padding: 0.55rem; min-width: 18px; diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNav.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNav.tsx index c40467b464..3864f47a52 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNav.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNav.tsx @@ -1,12 +1,12 @@ import styled from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { spacing, colorValue } from '@royalnavy/design-tokens' import { StyledContent } from '../../../../primitives/FloatingBox/partials/StyledContent' export const StyledNav = styled.nav` height: 100%; padding: ${spacing('1')} ${spacing('6')}; - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; ${StyledContent} { transform: translateX(0.5rem); diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItem.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItem.tsx index 6bacdd5e63..9588a29d25 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItem.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItem.tsx @@ -1,5 +1,5 @@ import styled, { css } from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { color, spacing, colorValue } from '@royalnavy/design-tokens' interface StyledNavItemProps { $isActive?: boolean @@ -28,7 +28,7 @@ export const StyledNavItem = styled.div` `} &:hover { - background-color: ${color('neutral', '400')}; + background-color: ${colorValue('neutral', '400')}; > * { text-decoration: none; diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItemIcon.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItemIcon.tsx index eafb4adde3..cd6e582c51 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItemIcon.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItemIcon.tsx @@ -1,5 +1,5 @@ import styled, { css } from 'styled-components' -import { color } from '@royalnavy/design-tokens' +import { colorValue } from '@royalnavy/design-tokens' import { StyledNavItem } from './StyledNavItem' @@ -17,10 +17,10 @@ export const StyledNavItemIcon = styled.div` svg { width: 18px; height: 18px; - color: ${color('neutral', '100')}; + color: ${colorValue('neutral', '100')}; ${StyledNavItem}:hover & { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; } } diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItemText.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItemText.tsx index a87fcb6e6e..cb896da629 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItemText.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNavItemText.tsx @@ -1,5 +1,5 @@ import styled, { css } from 'styled-components' -import { color, fontSize } from '@royalnavy/design-tokens' +import { fontSize, colorValue } from '@royalnavy/design-tokens' import { getTransitionOpacity, @@ -13,7 +13,7 @@ interface StyledTextProps extends TransitionProps { export const StyledNavItemText = styled.div` display: inline-block; - color: ${color('neutral', '100')}; + color: ${colorValue('neutral', '100')}; font-size: ${fontSize('m')}; opacity: ${({ $transitionStatus }) => getTransitionOpacity($transitionStatus)}; @@ -21,7 +21,7 @@ export const StyledNavItemText = styled.div` padding: 0.25rem 0.5rem; ${StyledNavItem}:hover & { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; } ${({ $isOpen }) => diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationDot.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationDot.tsx index a32cc25bc1..5e2f0a7363 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationDot.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationDot.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, fontSize } from '@royalnavy/design-tokens' +import { color, fontSize, colorValue } from '@royalnavy/design-tokens' export const StyledNotificationDot = styled.span` position: absolute; @@ -12,8 +12,8 @@ export const StyledNotificationDot = styled.span` height: 24px; border-radius: 9999px; background-color: ${color('danger', '600')}; - border: 3px solid ${color('neutral', '700')}; - color: ${color('neutral', 'white')}; + border: 3px solid ${colorValue('neutral', '700')}; + color: ${colorValue('neutral', 'white')}; font-size: ${fontSize('s')}; font-weight: 600; ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationsLabel.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationsLabel.tsx index 27122a015a..dc853272a5 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationsLabel.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationsLabel.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, fontSize, spacing } from '@royalnavy/design-tokens' +import { fontSize, spacing, colorValue } from '@royalnavy/design-tokens' import { getTransitionOpacity, @@ -9,7 +9,7 @@ import { export const StyledNotificationsLabel = styled.span` flex: 1; text-align: left; - color: ${color('neutral', '100')}; + color: ${colorValue('neutral', '100')}; font-size: ${fontSize('m')}; margin-left: ${spacing('4')}; white-space: nowrap; diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationsSheetButton.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationsSheetButton.tsx index 629eae6d5f..def4b7fc24 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationsSheetButton.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledNotificationsSheetButton.tsx @@ -1,6 +1,6 @@ import React from 'react' import styled, { css } from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { color, spacing, colorValue } from '@royalnavy/design-tokens' import { SheetButton, SheetButtonProps } from '../../Sheet/SheetButton' import { StyledNotificationDot } from './StyledNotificationDot' @@ -20,7 +20,7 @@ export const StyledNotificationsSheetButton = styled< white-space: nowrap; svg { - color: ${color('neutral', '100')}; + color: ${colorValue('neutral', '100')}; width: 20px; height: 20px; } @@ -46,7 +46,7 @@ export const StyledNotificationsSheetButton = styled< height: 12px; background-color: ${color('danger', '600')}; border-radius: 9999px; - border: 2px solid ${color('neutral', '700')}; + border: 2px solid ${colorValue('neutral', '700')}; } ${StyledNotificationDot} { diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSheetList.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSheetList.tsx index 3a19baf66c..ea9b0f9f0f 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSheetList.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSheetList.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color } from '@royalnavy/design-tokens' +import { colorValue } from '@royalnavy/design-tokens' export const StyledSheetList = styled.ol` list-style-type: none; @@ -8,7 +8,7 @@ export const StyledSheetList = styled.ol` a, a:hover { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; text-decoration: none; } ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSidebar.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSidebar.tsx index 9b7e640936..9704a9a599 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSidebar.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSidebar.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, zIndex } from '@royalnavy/design-tokens' +import { zIndex, colorValue } from '@royalnavy/design-tokens' import { StyledContent } from '../../../../primitives/FloatingBox/partials/StyledContent' @@ -31,8 +31,8 @@ export const StyledSidebar = styled.aside` height: 100%; left: 0; top: 0; - background-color: ${color('neutral', '700')}; - color: ${color('neutral', 'white')}; + background-color: ${colorValue('neutral', '700')}; + color: ${colorValue('neutral', 'white')}; transition: ${({ $isOpen }) => $isOpen diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSubNavSheetButton.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSubNavSheetButton.tsx index 05d0d126e1..c16eec796e 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSubNavSheetButton.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledSubNavSheetButton.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { spacing, colorValue } from '@royalnavy/design-tokens' import { SheetButton } from '../../Sheet/SheetButton' @@ -17,15 +17,15 @@ export const StyledSubNavSheetButton = styled(SheetButton)` overflow: hidden; &:hover { - background-color: ${color('neutral', '400')}; + background-color: ${colorValue('neutral', '400')}; cursor: pointer; svg { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; } } svg { - color: ${color('neutral', '100')}; + color: ${colorValue('neutral', '100')}; } ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledUser.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledUser.tsx index 854ceafce5..c4eb9249d6 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledUser.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledUser.tsx @@ -1,10 +1,10 @@ import styled from 'styled-components' -import { color, spacing } from '@royalnavy/design-tokens' +import { spacing, colorValue } from '@royalnavy/design-tokens' export const StyledUser = styled.div` display: flex; align-items: center; justify-content: center; padding: ${spacing('10')} ${spacing('8')}; - background-color: ${color('neutral', '500')}; + background-color: ${colorValue('neutral', '500')}; ` diff --git a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledUserText.tsx b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledUserText.tsx index 46ce69bc36..a6a0e2e5d5 100644 --- a/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledUserText.tsx +++ b/packages/react-component-library/src/components/TopLevelNavigation/Sidebar/partials/StyledUserText.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { color, fontSize, spacing } from '@royalnavy/design-tokens' +import { fontSize, spacing, colorValue } from '@royalnavy/design-tokens' import { getTransitionOpacity, @@ -27,13 +27,13 @@ export const StyledUserText = styled.div` } > span { - color: ${color('neutral', 'white')}; + color: ${colorValue('neutral', 'white')}; font-size: ${fontSize('m')}; } a span { margin-top: ${spacing('1')}; - color: ${color('neutral', '300')}; + color: ${colorValue('neutral', '300')}; font-size: ${fontSize('s')}; } } @@ -41,8 +41,8 @@ export const StyledUserText = styled.div` svg { width: 1.65rem; height: 1.65rem; - color: ${color('neutral', 'white')}; - background-color: ${color('neutral', '400')}; + color: ${colorValue('neutral', 'white')}; + background-color: ${colorValue('neutral', '400')}; border-radius: 2px; padding: ${spacing('2')}; } diff --git a/packages/react-component-library/src/primitives/FloatingBox/partials/StyledContent.tsx b/packages/react-component-library/src/primitives/FloatingBox/partials/StyledContent.tsx index 828fcd083e..9b51bea880 100644 --- a/packages/react-component-library/src/primitives/FloatingBox/partials/StyledContent.tsx +++ b/packages/react-component-library/src/primitives/FloatingBox/partials/StyledContent.tsx @@ -1,5 +1,5 @@ import styled, { css } from 'styled-components' -import { color, mq, spacing } from '@royalnavy/design-tokens' +import { color, mq, spacing, colorValue } from '@royalnavy/design-tokens' import { FloatingBoxSchemeType } from '../types' import { FLOATING_BOX_SCHEME } from '../constants' @@ -26,9 +26,9 @@ const schemeStyleMap = { `, [FLOATING_BOX_SCHEME.DARK]: css` - background: ${color('neutral', '700')}; - border: ${color('neutral', '700')} solid ${spacing('px')}; - color: ${color('neutral', 'white')}; + background: ${colorValue('neutral', '700')}; + border: ${colorValue('neutral', '700')} solid ${spacing('px')}; + color: ${colorValue('neutral', 'white')}; ${StyledArrow} { &::before { @@ -36,7 +36,7 @@ const schemeStyleMap = { } &::after { - border-color: ${color('neutral', '700')} transparent; + border-color: ${colorValue('neutral', '700')} transparent; } } `, diff --git a/packages/react-component-library/src/styled-components/GlobalStyle.tsx b/packages/react-component-library/src/styled-components/GlobalStyle.tsx index 53cfe14133..0702707b20 100644 --- a/packages/react-component-library/src/styled-components/GlobalStyle.tsx +++ b/packages/react-component-library/src/styled-components/GlobalStyle.tsx @@ -1,7 +1,14 @@ import React, { createContext, useMemo } from 'react' import { createGlobalStyle, ThemeProvider } from 'styled-components' import { Normalize } from 'styled-normalize' -import { lightTheme, color, fontSize } from '@royalnavy/design-tokens' +import { + lightTheme, + color, + colorByMode, + fontSize, + getColorVariables, + Theme, +} from '@royalnavy/design-tokens' export interface GlobalStyleContextDefaults { theme?: Record @@ -12,6 +19,23 @@ export interface GlobalStyleProviderProps { theme?: Record } +/** + * Expose the active theme's colour tokens as CSS custom properties on the + * root, so every `color(...)` reference (which resolves to a `var(--color-*)`) + * reacts to the theme — including content rendered through portals that sits + * outside the provider's DOM subtree. + */ +const ThemeVariables = createGlobalStyle<{ + $variables: Record +}>` + :root { + ${({ $variables }) => + Object.entries($variables) + .map(([name, value]) => `${name}: ${value};`) + .join('\n')} + } +` + /** * Globally setting `border-box` */ @@ -34,7 +58,8 @@ const BoxSizing = createGlobalStyle` */ const Hyperlinks = createGlobalStyle` a { - color: ${color('action', '500')}; + color: ${({ theme }) => + colorByMode('action', '500', '400', theme as Theme)}; text-decoration: none; } @@ -46,6 +71,25 @@ const Hyperlinks = createGlobalStyle` /** * Generate base font size */ +/** + * Dark-mode-only resets. Light mode keeps the original native defaults so it + * renders exactly as before; these only apply when the dark theme is active. + * - Give unstyled text an explicit light colour (native default is black). + * - Neutralise the native