Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/gamut-styles/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 {}
}
18 changes: 18 additions & 0 deletions packages/gamut-styles/src/theme/GamutThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ThemeProvider theme={theme}>
<Global styles={css({ ':root': cssVariables })} />
{children}
</ThemeProvider>
);
};
4 changes: 4 additions & 0 deletions packages/gamut-styles/src/theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './GamutThemeProvider';
export * from './props';
export { createVariables } from './utils/createVariables';
export * from './utils/shouldForwardProp';
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -71,16 +69,6 @@ const {
},
});

const allProps = Object.keys(properties).reduce<string[]>(
(carry, prop: keyof typeof properties) => {
return [...carry, ...properties[prop].propNames];
},
[]
);

const shouldForwardProp = (prop: string) =>
isPropValid(prop) && !allProps.includes(prop);

export {
variant,
properties,
Expand All @@ -94,5 +82,4 @@ export {
shadow,
space,
border,
shouldForwardProp,
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as tokens from './variables';
import * as tokens from '../variables';

export const theme = {
boxShadows: tokens.boxShadows,
Expand All @@ -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 {}
75 changes: 75 additions & 0 deletions packages/gamut-styles/src/theme/utils/createThemeVariables.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Record<string, any>> = {
[V in keyof T]: `var(--${Extract<V, string>})`;
};

/**
* 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]>
: Theme[Key];
};

export interface CreateThemeVars {
<Theme extends Required<AbstractTheme>, VariableKeys extends (keyof Theme)[]>(
theme: Theme,
keys: VariableKeys
): {
cssVariables: CSSObject;
theme: ThemeWithVariables<Theme, VariableKeys>;
};
}

const isBreakpoint = (
key: string,
breakpoints: Required<AbstractTheme>['breakpoints']
): key is keyof Required<AbstractTheme>['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 };
};
50 changes: 50 additions & 0 deletions packages/gamut-styles/src/theme/utils/createVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CSSObject } from '@emotion/react';
import { hasIn, merge } from 'lodash';

export const createVariables = (
tokens: Record<string, string | number | CSSObject>
) => {
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;
};
13 changes: 13 additions & 0 deletions packages/gamut-styles/src/theme/utils/shouldForwardProp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import isPropValid from '@emotion/is-prop-valid';

import { properties } from '../props';

const allProps = Object.keys(properties).reduce<string[]>(
(carry, prop: keyof typeof properties) => {
return [...carry, ...properties[prop].propNames];
},
[]
);

export const shouldForwardProp = (prop: string) =>
isPropValid(prop) && !allProps.includes(prop);
3 changes: 3 additions & 0 deletions packages/gamut-styles/src/variables/elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const elements = {
headerHeight: { base: '4rem', md: '5rem' },
} as const;
1 change: 1 addition & 0 deletions packages/gamut-styles/src/variables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
5 changes: 2 additions & 3 deletions packages/gamut-styles/src/variables/responsive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ export const breakpoints: Record<MediaSize, string> = {
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]})`;
Comment on lines +11 to +12
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes new line characters from gunking up the strings. This is a no op.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo, a small comment/warning about that would be helpful for future us's


export const mediaQueries = {
xs: createMediaQuery('xs', 'min'),
Expand Down
7 changes: 3 additions & 4 deletions packages/gamut-tests/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,9 +12,9 @@ function withThemeProvider<Props>(
WrappedComponent: React.ComponentType<Props>
) {
const WithBoundaryComponent: React.FC<Props> = (props) => (
<ThemeProvider theme={theme}>
<GamutThemeProvider>
<WrappedComponent {...props} />
</ThemeProvider>
</GamutThemeProvider>
);

return WithBoundaryComponent;
Expand Down
3 changes: 2 additions & 1 deletion packages/styleguide/.storybook/addons/system/enhancers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
4 changes: 2 additions & 2 deletions packages/styleguide/.storybook/addons/system/propMeta.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
11 changes: 7 additions & 4 deletions packages/styleguide/.storybook/decorators/theme.tsx
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -12,10 +15,10 @@ const cache = createEmotionCache();

export const withEmotion = (Story: any) => {
return process.env.NODE_ENV === 'test' ? (
<ThemeContext.Provider value={theme}>{Story()}</ThemeContext.Provider>
<GamutThemeProvider>{Story()}</GamutThemeProvider>
) : (
<CacheProvider value={cache}>
<ThemeContext.Provider value={theme}>{Story()}</ThemeContext.Provider>
<GamutThemeProvider>{Story()}</GamutThemeProvider>
</CacheProvider>
);
};