diff --git a/packages/gamut-styles/src/index.ts b/packages/gamut-styles/src/index.ts index 2798e9e46d2..2c9e0399080 100644 --- a/packages/gamut-styles/src/index.ts +++ b/packages/gamut-styles/src/index.ts @@ -1,13 +1,14 @@ import '@emotion/react'; -import { Theme as GamutTheme } from './theme'; +import { theme } from './theme'; export * from './cache'; export * from './variables'; export * from './utilities'; -export * from './system'; export * from './theme'; +export type ThemeShape = typeof theme; + declare module '@emotion/react' { - export interface Theme extends GamutTheme {} + export interface Theme extends ThemeShape {} } diff --git a/packages/gamut-styles/src/theme/GamutThemeProvider.tsx b/packages/gamut-styles/src/theme/GamutThemeProvider.tsx new file mode 100644 index 00000000000..317f55136c6 --- /dev/null +++ b/packages/gamut-styles/src/theme/GamutThemeProvider.tsx @@ -0,0 +1,18 @@ +import { css, Global, ThemeProvider } from '@emotion/react'; +import React from 'react'; + +import { theme as rawTheme } from './theme'; +import { createThemeVariables } from './utils/createThemeVariables'; + +export const { theme, cssVariables } = createThemeVariables(rawTheme, [ + 'elements', +]); + +export const GamutThemeProvider: React.FC = ({ children }) => { + return ( + + + {children} + + ); +}; diff --git a/packages/gamut-styles/src/theme/index.ts b/packages/gamut-styles/src/theme/index.ts new file mode 100644 index 00000000000..b7f36d5e460 --- /dev/null +++ b/packages/gamut-styles/src/theme/index.ts @@ -0,0 +1,4 @@ +export * from './GamutThemeProvider'; +export * from './props'; +export { createVariables } from './utils/createVariables'; +export * from './utils/shouldForwardProp'; diff --git a/packages/gamut-styles/src/system.ts b/packages/gamut-styles/src/theme/props.ts similarity index 77% rename from packages/gamut-styles/src/system.ts rename to packages/gamut-styles/src/theme/props.ts index 45dd64db330..faedd19bc85 100644 --- a/packages/gamut-styles/src/system.ts +++ b/packages/gamut-styles/src/theme/props.ts @@ -1,7 +1,5 @@ import { system } from '@codecademy/gamut-system'; -import isPropValid from '@emotion/is-prop-valid'; - -import { Theme } from './theme'; +import { Theme } from '@emotion/react'; const { variant, @@ -71,16 +69,6 @@ const { }, }); -const allProps = Object.keys(properties).reduce( - (carry, prop: keyof typeof properties) => { - return [...carry, ...properties[prop].propNames]; - }, - [] -); - -const shouldForwardProp = (prop: string) => - isPropValid(prop) && !allProps.includes(prop); - export { variant, properties, @@ -94,5 +82,4 @@ export { shadow, space, border, - shouldForwardProp, }; diff --git a/packages/gamut-styles/src/theme.tsx b/packages/gamut-styles/src/theme/theme.tsx similarity index 69% rename from packages/gamut-styles/src/theme.tsx rename to packages/gamut-styles/src/theme/theme.tsx index e474f8de94e..e45bf549d78 100644 --- a/packages/gamut-styles/src/theme.tsx +++ b/packages/gamut-styles/src/theme/theme.tsx @@ -1,4 +1,4 @@ -import * as tokens from './variables'; +import * as tokens from '../variables'; export const theme = { boxShadows: tokens.boxShadows, @@ -9,8 +9,5 @@ export const theme = { fontWeight: tokens.fontWeight, colors: tokens.colors, spacing: tokens.spacing, + elements: tokens.elements, } as const; - -export type ThemeShape = typeof theme; - -export interface Theme extends ThemeShape {} diff --git a/packages/gamut-styles/src/theme/utils/createThemeVariables.ts b/packages/gamut-styles/src/theme/utils/createThemeVariables.ts new file mode 100644 index 00000000000..4575b472edd --- /dev/null +++ b/packages/gamut-styles/src/theme/utils/createThemeVariables.ts @@ -0,0 +1,75 @@ +import { AbstractTheme } from '@codecademy/gamut-system'; +import { CSSObject } from '@emotion/react'; +import { hasIn, isObject, mapKeys, mapValues, merge } from 'lodash'; + +import { createVariables } from './createVariables'; + +/** + * Returns an type of any object with { key: 'var(--key) } + */ +export type KeyAsVariable> = { + [V in keyof T]: `var(--${Extract})`; +}; + +/** + * Updates the theme type with the correct new values of css variable references + */ +export type ThemeWithVariables< + Theme extends AbstractTheme, + VariableKeys extends (keyof Theme)[] +> = { + [Key in keyof Theme]: Key extends VariableKeys[number] + ? KeyAsVariable + : Theme[Key]; +}; + +export interface CreateThemeVars { + , VariableKeys extends (keyof Theme)[]>( + theme: Theme, + keys: VariableKeys + ): { + cssVariables: CSSObject; + theme: ThemeWithVariables; + }; +} + +const isBreakpoint = ( + key: string, + breakpoints: Required['breakpoints'] +): key is keyof Required['breakpoints'] => + Object.keys(breakpoints).includes(key); + +export const createThemeVariables: CreateThemeVars = (theme, keys) => { + // Create an empty theme to merge with the base theme object + const updatedTheme = { ...theme }; + // Treat all CSS Variables as a plain emotion CSSObject to be added to. + const cssVariables: CSSObject = {}; + + keys.forEach((key) => { + const tokensToSerialize = { ...theme[key] }; + // Update the theme object with the new tokens + for (const variable in tokensToSerialize) { + if (hasIn(tokensToSerialize, variable)) { + const variableReference = `var(--${variable})`; + updatedTheme[key][variable] = variableReference; + } + } + // Replace breakpoint aliases with the actual selector + const replacedBreakpointAliases = mapValues(tokensToSerialize, (val) => { + if (isObject(val)) { + return mapKeys(val, (val, key) => { + return isBreakpoint(key, theme.breakpoints) + ? theme.breakpoints[key] + : key; + }); + } + + return val; + }); + + // Create the variables and merge with the rest of the vars + merge(cssVariables, createVariables(replacedBreakpointAliases)); + }); + + return { cssVariables, theme: updatedTheme }; +}; diff --git a/packages/gamut-styles/src/theme/utils/createVariables.ts b/packages/gamut-styles/src/theme/utils/createVariables.ts new file mode 100644 index 00000000000..e4c85e143aa --- /dev/null +++ b/packages/gamut-styles/src/theme/utils/createVariables.ts @@ -0,0 +1,50 @@ +import { CSSObject } from '@emotion/react'; +import { hasIn, merge } from 'lodash'; + +export const createVariables = ( + tokens: Record +) => { + const cssVariables: CSSObject = {}; + + for (const variable in tokens) { + if (!hasIn(tokens, variable)) continue; + + const varName = `--${variable}`; + const valuesToRegister = tokens[variable]; + + // For all variables in the theme scale add theme to the resulting CSS Object + switch (typeof valuesToRegister) { + // If the value is primitive just add it as is to the returned vars css object + case 'number': + case 'string': + cssVariables[varName] = valuesToRegister; + break; + // If the value is an object then attempt to parse it as a resposnive property + case 'object': + const { base, ...rest } = valuesToRegister; + + // If base key is defined add it to the root values + if (base) { + cssVariables[varName] = base; + } + + // If there are remaining selectors / queries that override the root value add them to style object + const selectors = Object.keys(rest); + if (selectors) { + const overridesBySelector: CSSObject = {}; + selectors.forEach((selector) => { + overridesBySelector[selector] = { + [varName]: valuesToRegister[selector], + }; + }); + + // Merge with the base object. + merge(cssVariables, overridesBySelector); + } + break; + default: + break; + } + } + return cssVariables; +}; diff --git a/packages/gamut-styles/src/theme/utils/shouldForwardProp.ts b/packages/gamut-styles/src/theme/utils/shouldForwardProp.ts new file mode 100644 index 00000000000..819ff5fc4e5 --- /dev/null +++ b/packages/gamut-styles/src/theme/utils/shouldForwardProp.ts @@ -0,0 +1,13 @@ +import isPropValid from '@emotion/is-prop-valid'; + +import { properties } from '../props'; + +const allProps = Object.keys(properties).reduce( + (carry, prop: keyof typeof properties) => { + return [...carry, ...properties[prop].propNames]; + }, + [] +); + +export const shouldForwardProp = (prop: string) => + isPropValid(prop) && !allProps.includes(prop); diff --git a/packages/gamut-styles/src/variables/elements.ts b/packages/gamut-styles/src/variables/elements.ts new file mode 100644 index 00000000000..03f3cda38f9 --- /dev/null +++ b/packages/gamut-styles/src/variables/elements.ts @@ -0,0 +1,3 @@ +export const elements = { + headerHeight: { base: '4rem', md: '5rem' }, +} as const; diff --git a/packages/gamut-styles/src/variables/index.ts b/packages/gamut-styles/src/variables/index.ts index 9a0a0a121af..38b0689a25d 100644 --- a/packages/gamut-styles/src/variables/index.ts +++ b/packages/gamut-styles/src/variables/index.ts @@ -5,5 +5,6 @@ export * from './typography'; export * from './effects'; export * from './shadows'; export * from './timing'; +export * from './elements'; // Deprecated variables export * from './deprecated-colors'; diff --git a/packages/gamut-styles/src/variables/responsive.ts b/packages/gamut-styles/src/variables/responsive.ts index a6820dae1c3..1237a50e529 100644 --- a/packages/gamut-styles/src/variables/responsive.ts +++ b/packages/gamut-styles/src/variables/responsive.ts @@ -8,9 +8,8 @@ export const breakpoints: Record = { xl: '1440px', }; -const createMediaQuery = (size: MediaSize, direction: 'min' | 'max') => ` - @media only screen and (${direction}-width: ${breakpoints[size]}) -`; +const createMediaQuery = (size: MediaSize, direction: 'min' | 'max') => + `@media only screen and (${direction}-width: ${breakpoints[size]})`; export const mediaQueries = { xs: createMediaQuery('xs', 'min'), diff --git a/packages/gamut-tests/src/index.tsx b/packages/gamut-tests/src/index.tsx index b9231786eac..93a3f754515 100644 --- a/packages/gamut-tests/src/index.tsx +++ b/packages/gamut-tests/src/index.tsx @@ -1,5 +1,4 @@ -import { theme } from '@codecademy/gamut-styles'; -import { ThemeProvider } from '@emotion/react'; +import { GamutThemeProvider } from '@codecademy/gamut-styles'; import { setupEnzyme as setupEnzymeBase, setupRtl as setupRtlBase, @@ -13,9 +12,9 @@ function withThemeProvider( WrappedComponent: React.ComponentType ) { const WithBoundaryComponent: React.FC = (props) => ( - + - + ); return WithBoundaryComponent; diff --git a/packages/styleguide/.storybook/addons/system/enhancers.ts b/packages/styleguide/.storybook/addons/system/enhancers.ts index 377b63fa5d6..4baf51c80d5 100644 --- a/packages/styleguide/.storybook/addons/system/enhancers.ts +++ b/packages/styleguide/.storybook/addons/system/enhancers.ts @@ -2,7 +2,8 @@ import { mapValues, isNumber, map } from 'lodash/fp'; import { ArgTypesEnhancer } from '@storybook/client-api'; import { kebabCase } from 'lodash'; import { ALL_PROPS, PROP_META, PROP_GROUPS } from './propMeta'; -import { Theme, theme } from '@codecademy/gamut-styles/src/theme'; +import { theme } from '@codecademy/gamut-styles/src/theme'; +import { Theme } from '@emotion/react'; export type SystemControls = 'text' | 'select' | 'radio' | 'inline-radio'; diff --git a/packages/styleguide/.storybook/addons/system/propMeta.ts b/packages/styleguide/.storybook/addons/system/propMeta.ts index f8be47b6013..7cf7bd3fa51 100644 --- a/packages/styleguide/.storybook/addons/system/propMeta.ts +++ b/packages/styleguide/.storybook/addons/system/propMeta.ts @@ -1,6 +1,6 @@ -import * as system from '@codecademy/gamut-styles/src/system'; +import * as system from '@codecademy/gamut-styles/src/theme/props'; -const { shouldForwardProp, properties, variant, ...groups } = system; +const { properties, variant, ...groups } = system; export const PROP_GROUPS = groups; diff --git a/packages/styleguide/.storybook/components/PropsTable/constants.ts b/packages/styleguide/.storybook/components/PropsTable/constants.ts index 86a11901db2..511b468a1a1 100644 --- a/packages/styleguide/.storybook/components/PropsTable/constants.ts +++ b/packages/styleguide/.storybook/components/PropsTable/constants.ts @@ -1,4 +1,4 @@ -import * as system from '@codecademy/gamut-styles/src/system'; +import * as system from '@codecademy/gamut-styles/src/theme/props'; const { shouldForwardProp, properties: props, variant, ...groups } = system; diff --git a/packages/styleguide/.storybook/decorators/theme.tsx b/packages/styleguide/.storybook/decorators/theme.tsx index 1b6f5c52e6b..45f47961157 100644 --- a/packages/styleguide/.storybook/decorators/theme.tsx +++ b/packages/styleguide/.storybook/decorators/theme.tsx @@ -1,7 +1,10 @@ import React from 'react'; -import { CacheProvider, ThemeContext } from '@emotion/react'; +import { CacheProvider } from '@emotion/react'; -import { theme, createEmotionCache } from '@codecademy/gamut-styles'; +import { + GamutThemeProvider, + createEmotionCache, +} from '@codecademy/gamut-styles'; const cache = createEmotionCache(); @@ -12,10 +15,10 @@ const cache = createEmotionCache(); export const withEmotion = (Story: any) => { return process.env.NODE_ENV === 'test' ? ( - {Story()} + {Story()} ) : ( - {Story()} + {Story()} ); };