From 156f7fe496cdec39786482a71738ef9102b601fa Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 17 Feb 2021 13:31:09 -0600 Subject: [PATCH] [CSS-in-JS] Context and foundation (#4440) * poc * tidy * proxy set handler * default theme; docs setup * table colors via css-in-js * exports * withEuiTheme; useEuiTheme return shape; some types * export and proxy fixes * use without provider; some test setup * prevent unnecessary updates * refactor colorMode calc; move computation to provider * utils refactor * clean up * compute default theme for initial context value * snapshot update preview * axe ignore temporary docs page * theme: colorVis and borders, sizes update * coupled types * some utils tests * start to amsterdam; name in theme obj * Revert "table colors via css-in-js" This reverts commit 5614b0fd41b6a8767ac8baf8fb6ad1f0237cc2a4. * eslintrc * readme Co-authored-by: cchaos --- .babelrc.js | 8 +- .eslintrc.js | 21 +- package.json | 5 + scripts/a11y-testing.js | 1 + scripts/jest/config.json | 3 +- .../components/with_theme/theme_context.tsx | 13 +- src-docs/src/routes.js | 17 + src-docs/src/views/emotion/canopy.tsx | 214 ++++++++ .../__snapshots__/accordion.test.tsx.snap | 8 +- .../expanded_item_actions.test.tsx.snap | 2 - .../basic_table/in_memory_table.test.tsx | 1 + .../__snapshots__/data_grid.test.tsx.snap | 6 - src/index.d.ts | 1 + src/services/index.ts | 29 + src/services/theme/README.md | 148 ++++++ src/services/theme/context.ts | 37 ++ src/services/theme/hooks.tsx | 79 +++ src/services/theme/index.ts | 52 ++ src/services/theme/provider.tsx | 136 +++++ src/services/theme/theme.ts | 494 ++++++++++++++++++ src/services/theme/types.ts | 63 +++ src/services/theme/utils.test.ts | 263 ++++++++++ src/services/theme/utils.ts | 299 +++++++++++ yarn.lock | 200 ++++++- 24 files changed, 2077 insertions(+), 23 deletions(-) create mode 100644 src-docs/src/views/emotion/canopy.tsx create mode 100644 src/services/theme/README.md create mode 100644 src/services/theme/context.ts create mode 100644 src/services/theme/hooks.tsx create mode 100644 src/services/theme/index.ts create mode 100644 src/services/theme/provider.tsx create mode 100644 src/services/theme/theme.ts create mode 100644 src/services/theme/types.ts create mode 100644 src/services/theme/utils.test.ts create mode 100644 src/services/theme/utils.ts diff --git a/.babelrc.js b/.babelrc.js index f7b986d99a3..56ae2a14425 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -12,7 +12,13 @@ module.exports = { "modules": process.env.BABEL_MODULES ? process.env.BABEL_MODULES === 'false' ? false : process.env.BABEL_MODULES : "commonjs" // babel's default is commonjs }], ["@babel/typescript", { isTSX: true, allExtensions: true }], - "@babel/react" + "@babel/react", + [ + "@emotion/babel-preset-css-prop", + { + "labelFormat": "[local]" + }, + ], ], "plugins": [ "@babel/plugin-syntax-dynamic-import", diff --git a/.eslintrc.js b/.eslintrc.js index f85b821ecb0..96c6a201351 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + const APACHE_2_0_LICENSE_HEADER = ` /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -48,7 +67,7 @@ module.exports = { 'prettier/standard', 'plugin:prettier/recommended', ], - plugins: ['jsx-a11y', 'prettier', 'local', 'react-hooks'], + plugins: ['jsx-a11y', 'prettier', 'local', 'react-hooks', '@emotion'], rules: { 'prefer-template': 'error', 'local/i18n': 'error', diff --git a/package.json b/package.json index 85b73cee4c4..7425b9dcc59 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,10 @@ "@elastic/charts": "^24.5.1", "@elastic/datemath": "^5.0.3", "@elastic/eslint-config-kibana": "^0.15.0", + "@emotion/babel-preset-css-prop": "^11.0.0", + "@emotion/eslint-plugin": "^11.0.0", + "@emotion/jest": "^11.1.0", + "@emotion/react": "^11.1.1", "@svgr/core": "5.4.0", "@svgr/plugin-svgo": "^4.0.3", "@types/classnames": "^2.2.10", @@ -213,6 +217,7 @@ }, "peerDependencies": { "@elastic/datemath": "^5.0.2", + "@emotion/react": "11.x", "@types/react": "^16.9.34", "@types/react-dom": "^16.9.6", "moment": "^2.13.0", diff --git a/scripts/a11y-testing.js b/scripts/a11y-testing.js index e9442604b79..0813bb8acce 100644 --- a/scripts/a11y-testing.js +++ b/scripts/a11y-testing.js @@ -47,6 +47,7 @@ const docsPages = async (root, page) => { `${root}#/tabular-content/data-grid-virtualization`, `${root}#/elastic-charts/creating-charts`, `${root}#/elastic-charts/part-to-whole-comparisons`, + `${root}#/temporary/canopy`, ]; return [ diff --git a/scripts/jest/config.json b/scripts/jest/config.json index 36fa62a369e..7a840101518 100644 --- a/scripts/jest/config.json +++ b/scripts/jest/config.json @@ -45,6 +45,7 @@ "^.+\\.(js|tsx?)$": "babel-jest" }, "snapshotSerializers": [ - "/node_modules/enzyme-to-json/serializer" + "/node_modules/enzyme-to-json/serializer", + "@emotion/jest/enzyme-serializer" ] } diff --git a/src-docs/src/components/with_theme/theme_context.tsx b/src-docs/src/components/with_theme/theme_context.tsx index fef4311c06a..e5e0cd36862 100644 --- a/src-docs/src/components/with_theme/theme_context.tsx +++ b/src-docs/src/components/with_theme/theme_context.tsx @@ -2,6 +2,11 @@ import React from 'react'; import { EUI_THEMES, EUI_THEME } from '../../../../src/themes'; // @ts-ignore importing from a JS file import { applyTheme } from '../../services'; +import { + EuiThemeProvider, + EuiThemeDefault, + EuiThemeAmsterdam, +} from '../../../../src/services'; const THEME_NAMES = EUI_THEMES.map(({ value }) => value); @@ -47,7 +52,13 @@ export class ThemeProvider extends React.Component { theme, changeTheme: this.changeTheme, }}> - {children} + + {children} + ); } diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index b19e7999f9c..edf09ca4e8f 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -224,6 +224,11 @@ import { ElasticChartsCategoryExample } from './views/elastic_charts/category_ex import { ElasticChartsSparklinesExample } from './views/elastic_charts/sparklines_example'; import { ElasticChartsPieExample } from './views/elastic_charts/pie_example'; + +/** ! Temporary ! */ + +import Canopy from './views/emotion/canopy'; + /** * Lowercases input and replaces spaces with hyphens: * e.g. 'GridView Example' -> 'gridview-example' @@ -306,6 +311,18 @@ const createExample = (example, customTitle) => { }; const navigation = [ + { + name: 'Temporary', + items: [ + createExample( + { + intro: , + sections: [], + }, + 'Canopy' + ), + ], + }, { name: 'Guidelines', items: [ diff --git a/src-docs/src/views/emotion/canopy.tsx b/src-docs/src/views/emotion/canopy.tsx new file mode 100644 index 00000000000..2b683e320c2 --- /dev/null +++ b/src-docs/src/views/emotion/canopy.tsx @@ -0,0 +1,214 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as React from 'react'; +import { css } from '@emotion/react'; +import chroma from 'chroma-js'; +import { EuiSpacer } from '../../../../src/components/spacer'; +import { EuiIcon } from '../../../../src/components/icon'; +import { + mergeDeep, + useEuiTheme, + withEuiTheme, + EuiThemeProvider, + computed, + euiThemeDefault, + buildTheme, +} from '../../../../src/services'; + +const View = () => { + const [theme, colorMode] = useEuiTheme(); + return ( +
+
+ {colorMode} +
+          {JSON.stringify(theme, null, 2)}
+        
+
+
+

+

+

+

+

+

+
+
+ ); +}; + +const View3 = () => { + const overrides = { + colors: { + light: { euiColorPrimary: '#8A07BD' }, + dark: { euiColorPrimary: '#bd07a5' }, + }, + }; + return ( + <> + + + + + Overriding primary + + + + ); +}; + +const View2 = () => { + const overrides = { + colors: { + light: { + euiColorSecondary: computed( + ['colors.euiColorPrimary'], + () => '#85e89d' + ), + }, + dark: { euiColorSecondary: '#f0fff4' }, + }, + }; + return ( + <> + + + Overriding secondary + + + + ); +}; + +// eslint-disable-next-line react/prefer-stateless-function +class Block extends React.Component { + render() { + const { theme, ...props } = this.props; + // TODO: TS autocomplete not working + const blockStyle = css` + color: ${theme.theme.colors.euiColorPrimary}; + border-radius: ${theme.theme.borders.euiBorderRadiusSmall}; + border: ${theme.theme.borders.euiBorderEditable}; + `; + return ( +
+
+ ); + } +} +const BlockWithTheme = withEuiTheme(Block); + +export default () => { + // const [colorMode, setColorMode] = React.useState('light'); + const toggleTheme = () => { + // setColorMode((mode) => (mode === 'light' ? 'dark' : 'light')); + }; + const [overrides, setOverrides] = React.useState({}); + const lightColors = () => { + setOverrides( + mergeDeep(overrides, { + colors: { + light: { + euiColorPrimary: chroma.random().hex(), + }, + }, + }) + ); + }; + const darkColors = () => { + setOverrides( + mergeDeep(overrides, { + colors: { + dark: { + euiColorPrimary: chroma.random().hex(), + }, + }, + }) + ); + }; + + const newTheme = buildTheme( + { + ...euiThemeDefault, + custom: '#000', + }, + 'CUSTOM' + ); + + return ( + <> + + + + + {' '} + + + Default view + + + + + theme={newTheme} + colorMode="inverse"> + Inverse colorMode + + withEuiTheme + + + + + + ); +}; diff --git a/src/components/accordion/__snapshots__/accordion.test.tsx.snap b/src/components/accordion/__snapshots__/accordion.test.tsx.snap index 4ad247d9e69..66873fda626 100644 --- a/src/components/accordion/__snapshots__/accordion.test.tsx.snap +++ b/src/components/accordion/__snapshots__/accordion.test.tsx.snap @@ -54,9 +54,7 @@ exports[`EuiAccordion behavior closes when clicked twice 1`] = ` onResize={[Function]} >
-
+
@@ -118,9 +116,7 @@ exports[`EuiAccordion behavior opens when clicked once 1`] = ` onResize={[Function]} >
-
+
diff --git a/src/components/basic_table/__snapshots__/expanded_item_actions.test.tsx.snap b/src/components/basic_table/__snapshots__/expanded_item_actions.test.tsx.snap index 9d461604127..5c3d2e236e8 100644 --- a/src/components/basic_table/__snapshots__/expanded_item_actions.test.tsx.snap +++ b/src/components/basic_table/__snapshots__/expanded_item_actions.test.tsx.snap @@ -10,7 +10,6 @@ exports[`ExpandedItemActions render 1`] = ` "onClick": [Function], } } - className="" enabled={true} item={ Object { @@ -27,7 +26,6 @@ exports[`ExpandedItemActions render 1`] = ` "render": [Function], } } - className="" enabled={true} index={1} item={ diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index 515b4d8c028..f27e893a016 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -1045,6 +1045,7 @@ describe('EuiInMemoryTable', () => { .find( '[data-test-subj*="tableHeaderCell_name_0"] [data-test-subj="tableHeaderSortButton"]' ) + .at(0) .simulate('click'); expect(props.onTableChange).toHaveBeenCalledTimes(1); expect(props.onTableChange).toHaveBeenCalledWith({ diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index a89cd309577..edad4fda6b7 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -217,7 +217,6 @@ exports[`EuiDataGrid render column actions renders various column actions config /// /// diff --git a/src/services/index.ts b/src/services/index.ts index 4b4da5d7420..fc982597295 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -117,3 +117,32 @@ export { export { EuiWindowEvent } from './window_event'; export { useCombinedRefs, useDependentState } from './hooks'; + +export { + EuiSystemContext, + EuiThemeContext, + EuiOverrideContext, + EuiColorModeContext, + useEuiTheme, + withEuiTheme, + EuiThemeProvider, + buildTheme, + computed, + isInverseColorMode, + getColorMode, + getComputed, + getOn, + mergeDeep, + setOn, + Computed, + euiThemeDefault, + EuiThemeDefault, + EuiThemeAmsterdam, + euiThemeAmsterdam, + EuiThemeColor, + EuiThemeColorMode, + EuiThemeComputed, + EuiThemeOverrides, + EuiThemeShape, + EuiThemeSystem, +} from './theme'; diff --git a/src/services/theme/README.md b/src/services/theme/README.md new file mode 100644 index 00000000000..ca88888a666 --- /dev/null +++ b/src/services/theme/README.md @@ -0,0 +1,148 @@ +# JavaScript-based theming in EUI + +The style system to replace Sass and Sass-based design tokens in EUI. + +* Theme construction via a Proxy-based system with cascading and computed values + * Proxy-based: allows for the theme system to reference its own values (for reuse or for computational manipulation) + * Cascading: conceptually similar to Sass, where variable location and order are important + * Extendable: allows for appending style variables to the EUI theme structure, scoped to a React context provider + * Override-able: all theme tokens/variables can be altered by consumers +* Theme consumption via React hook and HOC methods +* Color mode support as first-class consideration + * "Light" and "dark" mode (or defined through extensions/overrides) accounting + * Theme consumption is scoped to the current color mode (set in the context provider) + + +## Layers of the theme system + +### Unbuilt theme + +_See `euiThemeDefault`_ +An unbuilt theme is a composed object of style values or `computed` functions. + +#### Style values + +Think design tokens or CSS property values. Ready to be consumed as-is in an application environment, using some JavaScript method of applying styles (i.e., a CSS-in-JS library is not required). + +#### `computed` functions + +These properties specify that the value depends upon some other value in the theme, in the shape of: + +```js +computed( + ['sizes.euiSize'], // dependency array, referencing other properties in the theme object + ([size]) => size * 2 // predicate. What to do with the dependency values +) +``` + +### Theme system (built theme) + +_See `EuiThemeDefault`_ +A built theme by way of `buildTheme`, which transforms the object containing static style values and `computed` functions into a JavaScript Proxy object with handler traps. In this state, the theme is essentially inaccessible and immutable, that is, it requires `getComputed` to correctly order and access values and dependencies, and `set()` is disabled. + +### Computed theme + +_See `EuiThemeContext`_ +A consumable theme object in which all `computed` function values have been computed; all values are accessible and usable in an application environment. +Returned from `getComputed`, in the shape of: + +```js +getComputed( + EuiThemeDefault, // Theme system (Proxy) + {}, // Overrides object + 'light' // Color mode +) +``` + +#### Overrides + +Compute-time value overrides for theme property values. Because a theme system is unchangeable, this mechanism allows for changing values at certain points during consumption. +The overrides object must match the partial shape of the theme system: + +```js +{ + sizes: { + euiSize: 4 + } +} +``` + +#### Color mode + +Think light and dark mode. A theme has built-in color mode support, using the `colors` property as a marker: + +```js +colors: { + light: {...} + dark : {...} +} +``` +`getComputed` will only compute and return values in the specified current color mode. + + +## React-specific context + +### EuiThemeProvider + +Umbrella provider component that holds the various top-level theme configuration option providers: theme system, color mode, overrides; as well as the primary output provider: computed theme. +The actual computation for computed theme values takes place at this level, where the three inputs are known (theme system, color mode, overrides) and the output (computed theme) can be cached for consumption. Input changes are captured and the output is recomputed. + +```js + +``` + +All three props are optional. The default values for EUI will be used in the event that no configuration is provided. Note, however that colorMode switching will require consumers to maintain that app state. + +### useEuiTheme + +A custom React hook that is returns the computed theme. This hook it little more than a wrapper around the `useContext` hook, accessing three of the top-level providers: computed theme, color mode, and overrides. + +```js +const [theme, colorMode, overrides] = useEuiTheme(); +``` + +The `theme` variable has TypeScript support, which will result in IDE autocomplete availability. + +### WithEuiTheme +A higher-order-component that wraps `useEuiTheme` for React class components. + + +___ + + +## Emotion + +[Emotion](https://emotion.sh/docs/introduction) is the CSS-in-JS library currently selected for use in EUI. Nothing in the EUI theming system is dependent upon Emotion packages, but the Emotion ecosystem will have impacts on generated styles. + +### Composition + +* Prefer the use of [`css` prop](https://emotion.sh/docs/css-prop) construction over [styled-component-like](https://emotion.sh/docs/styled) component construction +* Babel-based build accommodation + +### Testing + +Snapshot testing ([as currently configured](https://emotion.sh/docs/testing#writing-a-test)) will result in generic `emotion-${n}` class names with the generated style object as part of the snapshot. + +* This seems good for EUI, but it also affects consumers + * Consumers will need to use the `@emotion/jest` snapshot serializer to avoid class name churn. + * Not ideal; unsure of any other solutions +* During the conversion process, the snapshot diffs will look less than ideal when using `shallow` (a single wrapper element; DOM itself is unchanged): + +```diff +-
+``` \ No newline at end of file diff --git a/src/services/theme/context.ts b/src/services/theme/context.ts new file mode 100644 index 00000000000..53746c38d56 --- /dev/null +++ b/src/services/theme/context.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createContext } from 'react'; +import { + EuiThemeColorMode, + EuiThemeSystem, + EuiThemeOverrides, + EuiThemeComputed, +} from './types'; +import { EuiThemeDefault } from './theme'; +import { DEFAULT_COLOR_MODE, getComputed } from './utils'; + +export const EuiSystemContext = createContext(EuiThemeDefault); +export const EuiOverrideContext = createContext({}); +export const EuiColorModeContext = createContext( + DEFAULT_COLOR_MODE +); +export const EuiThemeContext = createContext( + getComputed(EuiThemeDefault, {}, DEFAULT_COLOR_MODE) +); diff --git a/src/services/theme/hooks.tsx b/src/services/theme/hooks.tsx new file mode 100644 index 00000000000..ff8575d735a --- /dev/null +++ b/src/services/theme/hooks.tsx @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { forwardRef, useContext } from 'react'; + +import { + EuiThemeContext, + EuiOverrideContext, + EuiColorModeContext, +} from './context'; +import { + EuiThemeColorMode, + EuiThemeOverrides, + EuiThemeComputed, +} from './types'; + +export const useEuiTheme = (): [ + EuiThemeComputed, + EuiThemeColorMode, + EuiThemeOverrides +] => { + const theme = useContext(EuiThemeContext); + const overrides = useContext(EuiOverrideContext); + const colorMode = useContext(EuiColorModeContext); + + return [ + theme as EuiThemeComputed, + colorMode, + overrides as EuiThemeOverrides, + ]; +}; + +export const withEuiTheme = ( + Component: React.ComponentType< + T & { + theme: { + theme: EuiThemeComputed; + colorMode: EuiThemeColorMode; + }; + } + > +) => { + const componentName = Component.displayName || Component.name || 'Component'; + const Render = (props: T, ref: React.Ref) => { + const [theme, colorMode] = useEuiTheme(); + return ( + + ); + }; + + const WithEuiTheme = forwardRef(Render); + + WithEuiTheme.displayName = `WithEuiTheme(${componentName})`; + + return WithEuiTheme; +}; diff --git a/src/services/theme/index.ts b/src/services/theme/index.ts new file mode 100644 index 00000000000..e8960997d1c --- /dev/null +++ b/src/services/theme/index.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + EuiSystemContext, + EuiThemeContext, + EuiOverrideContext, + EuiColorModeContext, +} from './context'; +export { useEuiTheme, withEuiTheme } from './hooks'; +export { EuiThemeProvider } from './provider'; +export { + buildTheme, + computed, + isInverseColorMode, + getColorMode, + getComputed, + getOn, + mergeDeep, + setOn, + Computed, +} from './utils'; +export { + EuiThemeColor, + EuiThemeColorMode, + EuiThemeComputed, + EuiThemeOverrides, + EuiThemeShape, + EuiThemeSystem, +} from './types'; +export { + EuiThemeDefault, + euiThemeDefault, + EuiThemeAmsterdam, + euiThemeAmsterdam, +} from './theme'; diff --git a/src/services/theme/provider.tsx b/src/services/theme/provider.tsx new file mode 100644 index 00000000000..a4aff284c8f --- /dev/null +++ b/src/services/theme/provider.tsx @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { + useContext, + useEffect, + useRef, + useState, + PropsWithChildren, +} from 'react'; +import isEqual from 'lodash/isEqual'; + +import { + EuiSystemContext, + EuiThemeContext, + EuiOverrideContext, + EuiColorModeContext, +} from './context'; +import { buildTheme, getColorMode, getComputed, mergeDeep } from './utils'; +import { EuiThemeColorMode, EuiThemeSystem, EuiThemeOverrides } from './types'; + +export interface EuiThemeProviderProps { + theme?: EuiThemeSystem; + colorMode?: EuiThemeColorMode; + overrides?: EuiThemeOverrides; + children: any; +} + +export function EuiThemeProvider({ + theme: _system, + colorMode: _colorMode, + overrides: _overrides, + children, +}: PropsWithChildren>) { + const parentSystem = useContext(EuiSystemContext); + const parentOverrides = useContext(EuiOverrideContext); + const parentColorMode = useContext(EuiColorModeContext); + const parentTheme = useContext(EuiThemeContext); + + const [system, setSystem] = useState(_system || parentSystem); + const prevSystemKey = useRef(system.key); + + const [overrides, setOverrides] = useState( + mergeDeep(parentOverrides, _overrides) + ); + const prevOverrides = useRef(overrides); + + const [colorMode, setColorMode] = useState( + getColorMode(_colorMode, parentColorMode) + ); + const prevColorMode = useRef(colorMode); + + // TODO: Flip if return to using parent + const isParentTheme = useRef( + prevSystemKey.current === parentSystem.key && + colorMode === parentColorMode && + isEqual(parentOverrides, overrides) + ); + + const [theme, setTheme] = useState( + Object.keys(parentTheme).length + ? parentTheme + : getComputed( + system, + buildTheme(overrides, `_${system.key}`) as typeof system, + colorMode + ) + ); + + useEffect(() => { + const newSystem = _system || parentSystem; + if (prevSystemKey.current !== newSystem.key) { + setSystem(newSystem); + prevSystemKey.current = newSystem.key; + isParentTheme.current = false; + } + }, [_system, parentSystem]); + + useEffect(() => { + const newOverrides = mergeDeep(parentOverrides, _overrides); + if (!isEqual(prevOverrides.current, newOverrides)) { + setOverrides(newOverrides); + prevOverrides.current = newOverrides; + isParentTheme.current = false; + } + }, [_overrides, parentOverrides]); + + useEffect(() => { + const newColorMode = getColorMode(_colorMode, parentColorMode); + if (!isEqual(newColorMode, prevColorMode.current)) { + setColorMode(newColorMode); + prevColorMode.current = newColorMode; + isParentTheme.current = false; + } + }, [_colorMode, parentColorMode]); + + useEffect(() => { + if (!isParentTheme.current) { + setTheme( + getComputed( + system, + buildTheme(overrides, `_${system.key}`) as typeof system, + colorMode + ) + ); + } + }, [colorMode, system, overrides]); + + return ( + + + + + {children} + + + + + ); +} diff --git a/src/services/theme/theme.ts b/src/services/theme/theme.ts new file mode 100644 index 00000000000..eecef66ea27 --- /dev/null +++ b/src/services/theme/theme.ts @@ -0,0 +1,494 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chroma from 'chroma-js'; +import { buildTheme, computed, COLOR_MODE_KEY } from './utils'; + +export const tint = (color: string, ratio: number) => + chroma.mix(color, '#fff', ratio).hex(); +export const shade = (color: string, ratio: number) => + chroma.mix(color, '#000', ratio).hex(); +// TODO +const makeHighContrastColor = (color: string) => color; +// TODO +const makeDisabledContrastColor = (color: string) => color; +// TODO +const transparentize = (color: string, ratio: number) => + ratio ? color : color; + +const poles = { + euiColorGhost: '#FFF', + euiColorInk: '#000', +}; + +const graysLight = { + euiColorEmptyShade: '#FFF', + euiColorLightestShade: '#F5F7FA', + euiColorLightShade: '#D3DAE6', + euiColorMediumShade: '#98A2B3', + euiColorDarkShade: '#69707D', + euiColorDarkestShade: '#343741', + euiColorFullShade: '#000', +}; + +const textVariants = { + euiColorPrimaryText: computed( + ['colors.euiColorPrimary'], + ([euiColorPrimary]) => makeHighContrastColor(euiColorPrimary) + ), + euiColorSecondaryText: computed( + ['colors.euiColorSecondary'], + ([euiColorSecondary]) => makeHighContrastColor(euiColorSecondary) + ), + euiColorAccentText: computed(['colors.euiColorAccent'], ([euiColorAccent]) => + makeHighContrastColor(euiColorAccent) + ), + euiColorWarningText: computed( + ['colors.euiColorWarning'], + ([euiColorWarning]) => makeHighContrastColor(euiColorWarning) + ), + euiColorDangerText: computed(['colors.euiColorDanger'], ([euiColorDanger]) => + makeHighContrastColor(euiColorDanger) + ), + euiColorDisabledText: computed( + ['colors.euiColorDisabled'], + ([euiColorDisabled]) => makeDisabledContrastColor(euiColorDisabled) + ), + euiColorSuccessText: computed( + ['colors.euiColorSecondaryText'], + ([euiColorSecondaryText]) => euiColorSecondaryText + ), + euiLinkColor: computed( + ['colors.euiColorPrimaryText'], + ([euiColorPrimaryText]) => euiColorPrimaryText + ), +}; + +/* DEFAULT THEME */ + +export const light = { + euiColorPrimary: '#006BB4', + euiColorSecondary: '#017D73', + euiColorAccent: '#DD0A73', + + // These colors stay the same no matter the theme + ...poles, + + // Status + euiColorSuccess: computed( + ['colors.euiColorSecondary'], + ([euiColorSecondary]) => euiColorSecondary + ), + euiColorDanger: '#BD271E', + euiColorWarning: '#F5A700', + + // Grays + ...graysLight, + + // Backgrounds + euiPageBackgroundColor: computed( + ['colors.euiColorLightestShade'], + ([euiColorLightestShade]) => tint(euiColorLightestShade, 0.5) + ), + euiColorHighlight: '#FFFCDD', + + // Every color below must be based mathematically on the set above and in a particular order. + euiTextColor: computed( + ['colors.euiColorDarkestShade'], + ([euiColorDarkestShade]) => euiColorDarkestShade + ), + euiTitleColor: computed(['colors.euiTextColor'], ([euiTextColor]) => + shade(euiTextColor, 0.5) + ), + euiTextSubduedColor: computed( + ['colors.euiColorMediumShade'], + ([euiColorMediumShade]) => makeHighContrastColor(euiColorMediumShade) + ), + euiColorDisabled: computed(['colors.euiTextColor'], ([euiTextColor]) => + tint(euiTextColor, 0.7) + ), + + // Contrasty text variants + ...textVariants, + + // State + euiFocusTransparency: 0.1, + euiFocusBackgroundColor: computed( + ['colors.euiColorPrimary', 'colors.euiFocusTransparency'], + ([euiColorPrimary, euiFocusTransparency]) => + tint(euiColorPrimary, 1 - euiFocusTransparency) + ), +}; + +const graysDark = { + euiColorEmptyShade: '#1D1E24', + euiColorLightestShade: '#25262E', + euiColorLightShade: '#343741', + euiColorMediumShade: '#535966', + euiColorDarkShade: '#98A2B3', + euiColorDarkestShade: '#D4DAE5', + euiColorFullShade: '#FFF', +}; + +export const dark = { + // These colors stay the same no matter the theme + ...poles, + + // Core + euiColorPrimary: '#1BA9F5', + euiColorSecondary: '#7DE2D1', + euiColorAccent: '#F990C0', + + // Status + euiColorSuccess: computed( + ['colors.euiColorSecondary'], + ([euiColorSecondary]) => euiColorSecondary + ), + euiColorWarning: '#FFCE7A', + euiColorDanger: '#F66', + + // Grays + ...graysDark, + + // Backgrounds + euiPageBackgroundColor: computed( + ['colors.euiColorLightestShade'], + ([euiColorLightestShade]) => shade(euiColorLightestShade, 0.3) + ), + euiColorHighlight: '#2E2D25', + + // Variations from core + euiTextColor: '#DFE5EF', + euiTitleColor: computed( + ['colors.euiTextColor'], + ([euiTextColor]) => euiTextColor + ), + euiTextSubduedColor: computed( + ['colors.euiColorMediumShade'], + ([euiColorMediumShade]) => makeHighContrastColor(euiColorMediumShade) + ), + euiColorDisabled: computed(['colors.euiTextColor'], ([euiTextColor]) => + shade(euiTextColor, 0.7) + ), + + // Contrasty text variants + ...textVariants, + + // State + euiFocusTransparency: 0.3, + euiFocusBackgroundColor: computed( + ['colors.euiColorPrimary', 'colors.euiFocusTransparency'], + ([euiColorPrimary, euiFocusTransparency]) => + shade(euiColorPrimary, 1 - euiFocusTransparency) + ), +}; + +// Visualization colors + +// Maps allow for easier JSON usage +// Use map_merge(euiColorVisColors, $yourMap) to change individual colors after importing ths file +// The `behindText` variant is a direct copy of the hex output by the JS euiPaletteColorBlindBehindText() function +const euiPaletteColorBlind = { + euiColorVis0: { + graphic: '#54B399', + behindText: '#6DCCB1', + }, + euiColorVis1: { + graphic: '#6092C0', + behindText: '#79AAD9', + }, + euiColorVis2: { + graphic: '#D36086', + behindText: '#EE789D', + }, + euiColorVis3: { + graphic: '#9170B8', + behindText: '#A987D1', + }, + euiColorVis4: { + graphic: '#CA8EAE', + behindText: '#E4A6C7', + }, + euiColorVis5: { + graphic: '#D6BF57', + behindText: '#F1D86F', + }, + euiColorVis6: { + graphic: '#B9A888', + behindText: '#D2C0A0', + }, + euiColorVis7: { + graphic: '#DA8B45', + behindText: '#F5A35C', + }, + euiColorVis8: { + graphic: '#AA6556', + behindText: '#C47C6C', + }, + euiColorVis9: { + graphic: '#E7664C', + behindText: '#FF7E62', + }, +}; + +const colorVis = { + euiColorVis0: euiPaletteColorBlind.euiColorVis0.graphic, + euiColorVis1: euiPaletteColorBlind.euiColorVis1.graphic, + euiColorVis2: euiPaletteColorBlind.euiColorVis2.graphic, + euiColorVis3: euiPaletteColorBlind.euiColorVis3.graphic, + euiColorVis4: euiPaletteColorBlind.euiColorVis4.graphic, + euiColorVis5: euiPaletteColorBlind.euiColorVis5.graphic, + euiColorVis6: euiPaletteColorBlind.euiColorVis6.graphic, + euiColorVis7: euiPaletteColorBlind.euiColorVis7.graphic, + euiColorVis8: euiPaletteColorBlind.euiColorVis8.graphic, + euiColorVis9: euiPaletteColorBlind.euiColorVis9.graphic, + + euiColorVis0_behindText: euiPaletteColorBlind.euiColorVis0.behindText, + euiColorVis1_behindText: euiPaletteColorBlind.euiColorVis1.behindText, + euiColorVis2_behindText: euiPaletteColorBlind.euiColorVis2.behindText, + euiColorVis3_behindText: euiPaletteColorBlind.euiColorVis3.behindText, + euiColorVis4_behindText: euiPaletteColorBlind.euiColorVis4.behindText, + euiColorVis5_behindText: euiPaletteColorBlind.euiColorVis5.behindText, + euiColorVis6_behindText: euiPaletteColorBlind.euiColorVis6.behindText, + euiColorVis7_behindText: euiPaletteColorBlind.euiColorVis7.behindText, + euiColorVis8_behindText: euiPaletteColorBlind.euiColorVis8.behindText, + euiColorVis9_behindText: euiPaletteColorBlind.euiColorVis9.behindText, +}; + +const sizes = { + euiSize: 16, + euiSizeXS: computed(['sizes.euiSize'], ([euiSize]) => euiSize * 0.25), + euiSizeS: computed(['sizes.euiSize'], ([euiSize]) => euiSize * 0.5), + euiSizeM: computed(['sizes.euiSize'], ([euiSize]) => euiSize * 0.75), + euiSizeL: computed(['sizes.euiSize'], ([euiSize]) => euiSize * 1.5), + euiSizeXL: computed(['sizes.euiSize'], ([euiSize]) => euiSize * 2), + euiSizeXXL: computed(['sizes.euiSize'], ([euiSize]) => euiSize * 2.5), + + euiButtonMinWidth: computed(['sizes.euiSize'], ([euiSize]) => euiSize * 7), + + euiScrollBar: computed(['sizes.euiSize'], ([euiSize]) => euiSize), + euiScrollBarCorner: computed( + ['sizes.euiSizeS'], + ([euiSizeS]) => euiSizeS * 0.75 + ), +}; + +const borderRadius = { + euiBorderRadius: 4, + euiBorderRadiusSmall: computed( + ['borders.euiBorderRadius'], + ([euiBorderRadius]) => euiBorderRadius * 0.5 + ), +}; + +const borders = { + euiBorderWidthThin: '1px', + euiBorderWidthThick: '2px', + + euiBorderColor: computed( + ['colors.euiColorLightShade'], + ([euiColorLightShade]) => euiColorLightShade + ), + + euiBorderThick: computed( + ['borders.euiBorderWidthThick', 'borders.euiBorderColor'], + ([euiBorderWidthThick, euiBorderColor]) => + `${euiBorderWidthThick} solid ${euiBorderColor}` + ), + euiBorderThin: computed( + ['borders.euiBorderWidthThin', 'borders.euiBorderColor'], + ([euiBorderWidthThin, euiBorderColor]) => + `${euiBorderWidthThin} solid ${euiBorderColor}` + ), + euiBorderEditable: computed( + ['borders.euiBorderWidthThick', 'borders.euiBorderColor'], + ([euiBorderWidthThick, euiBorderColor]) => + `${euiBorderWidthThick} dotted ${euiBorderColor}` + ), +}; + +export const euiThemeDefault = { + [COLOR_MODE_KEY]: { + light, + dark, + }, + colorVis, + sizes, + borders: { + ...borderRadius, + ...borders, + }, + buttons: { + [COLOR_MODE_KEY]: { + light: { + custom: computed( + ['colors.euiColorPrimary'], + ([primary]) => primary /*'#000'*/ + ), + }, + dark: { custom: '#fff' }, + }, + }, +}; + +export const EuiThemeDefault = buildTheme(euiThemeDefault, 'EUI_THEME_DEFAULT'); + +/* AMSTERDAM THEME */ + +export const amsterdam_light = { + euiColorPrimary: '#07C', + euiColorSecondary: '#00BFB3', + euiColorAccent: '#F04E98', + + // These colors stay the same no matter the theme + ...poles, + + // Status + euiColorSuccess: computed( + ['colors.euiColorSecondary'], + ([euiColorSecondary]) => euiColorSecondary + ), + euiColorDanger: '#BD271E', + euiColorWarning: '#FEC514', + euiColorDisabled: '#ABB4C4', + + // Grays + ...graysLight, + + // Backgrounds + euiPageBackgroundColor: computed( + ['colors.euiColorLightestShade'], + ([euiColorLightestShade]) => tint(euiColorLightestShade, 0.5) + ), + euiColorHighlight: computed(['colors.euiColorWarning'], ([euiColorWarning]) => + tint(euiColorWarning, 0.9) + ), + + // Every color below must be based mathematically on the set above and in a particular order. + euiTextColor: computed( + ['colors.euiColorDarkestShade'], + ([euiColorDarkestShade]) => euiColorDarkestShade + ), + euiTitleColor: computed(['colors.euiTextColor'], ([euiTextColor]) => + shade(euiTextColor, 0.5) + ), + euiTextSubduedColor: computed( + ['colors.euiColorDarkShade'], + ([euiColorDarkShade]) => euiColorDarkShade + ), + + // Contrasty text variants + ...textVariants, + + // State + euiFocusTransparency: 0.9, + euiFocusBackgroundColor: computed( + ['colors.euiColorPrimary', 'colors.euiFocusTransparency'], + ([euiColorPrimary, euiFocusTransparency]) => + transparentize(euiColorPrimary, euiFocusTransparency) + ), +}; + +export const amsterdam_dark = { + // These colors stay the same no matter the theme + ...poles, + + // Core + euiColorPrimary: '#36A2EF', + euiColorSecondary: '#7DDED8', + euiColorAccent: '#F68FBE', + + // Status + euiColorSuccess: computed( + ['colors.euiColorSecondary'], + ([euiColorSecondary]) => euiColorSecondary + ), + euiColorWarning: '#F3D371', + euiColorDanger: '#F86B63', + euiColorDisabled: '#515761', + + // Grays + ...graysDark, + + // Backgrounds + euiPageBackgroundColor: computed( + ['colors.euiColorLightestShade'], + ([euiColorLightestShade]) => shade(euiColorLightestShade, 0.3) + ), + euiColorHighlight: '#2E2D25', + + // Variations from core + euiTextColor: '#DFE5EF', + euiTitleColor: computed( + ['colors.euiTextColor'], + ([euiTextColor]) => euiTextColor + ), + euiTextSubduedColor: computed( + ['colors.euiColorMediumShade'], + ([euiColorMediumShade]) => makeHighContrastColor(euiColorMediumShade) + ), + + // Contrasty text variants + ...textVariants, + + // State + euiFocusTransparency: 0.7, + euiFocusBackgroundColor: computed( + ['colors.euiColorPrimary', 'colors.euiFocusTransparency'], + ([euiColorPrimary, euiFocusTransparency]) => + transparentize(euiColorPrimary, euiFocusTransparency) + ), +}; + +const amsterdam_borderRadius = { + euiBorderRadius: computed( + ['sizes.euiSizeS'], + ([euiSizeS]) => euiSizeS * 0.75 + ), + euiBorderRadiusSmall: computed( + ['sizes.euiSizeS'], + ([euiSizeS]) => euiSizeS * 0.5 + ), +}; + +export const euiThemeAmsterdam = { + [COLOR_MODE_KEY]: { + light: amsterdam_light, + dark: amsterdam_dark, + }, + colorVis, + sizes, + borders: { + ...amsterdam_borderRadius, + ...borders, + }, + buttons: { + [COLOR_MODE_KEY]: { + light: { + custom: '#000', + }, + dark: { custom: '#fff' }, + }, + }, +}; + +export const EuiThemeAmsterdam = buildTheme( + euiThemeAmsterdam, + 'EUI_THEME_AMSTERDAM' +); diff --git a/src/services/theme/types.ts b/src/services/theme/types.ts new file mode 100644 index 00000000000..e5da512a442 --- /dev/null +++ b/src/services/theme/types.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { euiThemeDefault } from './theme'; + +type EuiThemeColorModeInverse = 'inverse'; +type EuiThemeColorModeStandard = 'light' | 'dark'; +export type EuiThemeColorMode = + | string + | EuiThemeColorModeStandard + | EuiThemeColorModeInverse; + +export type EuiThemeShape = typeof euiThemeDefault; +export type EuiThemeColor = EuiThemeShape['colors']['light']; + +export type EuiThemeSystem = { + root: EuiThemeShape & T; + model: EuiThemeShape & T; + key: string; +}; + +type DeepPartial = T extends Function + ? T + : T extends object + ? { [P in keyof T]?: DeepPartial } + : T; +export type EuiThemeOverrides = DeepPartial; + +type OmitDistributive = T extends any + ? T extends object + ? OmitRecursively + : T + : never; +type OmitRecursively = Omit< + { [P in keyof T]: OmitDistributive }, + K +>; + +type Colorless = OmitRecursively; +export type EuiThemeComputed = Colorless & { + themeName: string; + colors: EuiThemeColor; + // I don't like this + buttons: Colorless & { + colors: EuiThemeShape['buttons']['colors']['light']; + }; +}; diff --git a/src/services/theme/utils.test.ts b/src/services/theme/utils.test.ts new file mode 100644 index 00000000000..182b4d6d1b5 --- /dev/null +++ b/src/services/theme/utils.test.ts @@ -0,0 +1,263 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + isInverseColorMode, + getColorMode, + getOn, + setOn, + computed, + Computed, + getComputed, + buildTheme, + mergeDeep, + currentColorModeOnly, +} from './utils'; + +describe('isInverseColorMode', () => { + it("true only if 'inverse'", () => { + expect(isInverseColorMode('light')).toBe(false); + expect(isInverseColorMode('dark')).toBe(false); + expect(isInverseColorMode('custom')).toBe(false); + expect(isInverseColorMode()).toBe(false); + expect(isInverseColorMode('inverse')).toBe(true); + }); +}); + +describe('getColorMode', () => { + it("defaults to 'light'", () => { + expect(getColorMode()).toEqual('light'); + }); + it('uses `parentMode` as fallback', () => { + expect(getColorMode(undefined, 'dark')).toEqual('dark'); + }); + it("understands 'inverse'", () => { + expect(getColorMode('inverse', 'dark')).toEqual('light'); + expect(getColorMode('inverse', 'light')).toEqual('dark'); + expect(getColorMode('inverse')).toEqual('light'); + }); + it('respects custom modes', () => { + expect(getColorMode('custom')).toEqual('custom'); + expect(getColorMode('custom', 'light')).toEqual('custom'); + expect(getColorMode(undefined, 'custom')).toEqual('custom'); + expect(getColorMode('light', 'custom')).toEqual('light'); + }); +}); + +describe('getOn', () => { + const obj = { + parent: { + child: 'childVal', + }, + other: { + thing: { + string: 'stringVal', + nested: ['array'], + number: 0, + func: () => {}, + }, + }, + colors: { + light: { primary: '#000' }, + dark: { primary: '#FFF' }, + custom: { primary: '#333' }, + }, + }; + it('gets values at the given path', () => { + expect(getOn(obj, 'parent', '')).toEqual({ + child: 'childVal', + }); + expect(getOn(obj, 'parent.child', '')).toEqual('childVal'); + expect(getOn(obj, 'other.thing.string', '')).toEqual('stringVal'); + }); + it('gets values of various kinds', () => { + expect(getOn(obj, 'other.thing.nested', '')).toEqual(['array']); + expect(getOn(obj, 'other.thing.number', '')).toEqual(0); + expect(getOn(obj, 'other.thing.func', '')).toBeInstanceOf(Function); + }); + it('does can shortcut color modes', () => { + expect(getOn(obj, 'colors.primary', 'light')).toEqual('#000'); + expect(getOn(obj, 'colors.primary', 'dark')).toEqual('#FFF'); + expect(getOn(obj, 'colors.primary', 'custom')).toEqual('#333'); + }); + it('will not error', () => { + expect(getOn(obj, 'nope', '')).toBe(undefined); + expect(getOn(obj, 'other.nope', '')).toBe(undefined); + expect(getOn(obj, 'other.thing.nope', '')).toBe(undefined); + }); +}); + +describe('setOn', () => { + let obj: {}; + beforeEach(() => { + obj = { + existing: { + nested: { + val: 'value', + }, + }, + }; + }); + it('sets values at the given path', () => { + setOn(obj, 'existing.new', 'value'); + expect(obj).toEqual({ + existing: { nested: { val: 'value' }, new: 'value' }, + }); + setOn(obj, 'existing.nested.new', 'value'); + expect(obj).toEqual({ + existing: { nested: { val: 'value', new: 'value' }, new: 'value' }, + }); + }); + it('deep arbitrary creation', () => { + setOn(obj, 'trail.blazing.happening.now', 'wow'); + expect(obj).toEqual({ + existing: { nested: { val: 'value' } }, + trail: { blazing: { happening: { now: 'wow' } } }, + }); + }); + it('overrides existing path value', () => { + setOn(obj, 'existing.nested', 'diff'); + expect(obj).toEqual({ + existing: { + nested: 'diff', + }, + }); + }); +}); + +describe('computed', () => { + it('should transform to Computed', () => { + const output = computed(['path.to'], ([path]) => path); + expect(output).toBeInstanceOf(Computed); + expect(output.computer).toBeInstanceOf(Function); + expect(output.dependencies).toEqual(['path.to']); + }); +}); + +const theme = buildTheme( + { + colors: { + light: { + primary: '#000', + secondary: computed(['colors.primary'], ([primary]) => `${primary}000`), + }, + dark: { + primary: '#FFF', + secondary: computed(['colors.primary'], ([primary]) => `${primary}FFF`), + }, + }, + sizes: { + small: 8, + }, + }, + 'minimal' +); +describe('getComputed', () => { + it('computes all values and returns only the current color mode', () => { + // @ts-ignore intentionally not using a full EUI theme definition + expect(getComputed(theme, {}, 'light')).toEqual({ + colors: { primary: '#000', secondary: '#000000' }, + sizes: { small: 8 }, + themeName: 'minimal', + }); + // @ts-ignore intentionally not using a full EUI theme definition + expect(getComputed(theme, {}, 'dark')).toEqual({ + colors: { primary: '#FFF', secondary: '#FFFFFF' }, + sizes: { small: 8 }, + themeName: 'minimal', + }); + }); + it('respects simple overrides', () => { + expect( + // @ts-ignore intentionally not using a full EUI theme definition + getComputed(theme, buildTheme({ sizes: { small: 4 } }, ''), 'light') + ).toEqual({ + colors: { primary: '#000', secondary: '#000000' }, + sizes: { small: 4 }, + themeName: 'minimal', + }); + }); + it('respects overrides in computation', () => { + expect( + getComputed( + // @ts-ignore intentionally not using a full EUI theme definition + theme, + buildTheme({ colors: { light: { primary: '#CCC' } } }, ''), + 'light' + ) + ).toEqual({ + colors: { primary: '#CCC', secondary: '#CCC000' }, + sizes: { small: 8 }, + themeName: 'minimal', + }); + }); +}); + +describe('buildTheme', () => { + it('builds an EUI theme system', () => { + // TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'length' which is either non-existant or configurable in the proxy target + // expect(theme).toEqual(Proxy); // get() trap returns theme.model + // expect(theme.root).toEqual(Proxy); + expect(theme.key).toEqual('minimal'); + }); +}); + +describe('mergeDeep', () => { + it('merge simple objects, second into first', () => { + expect(mergeDeep({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 }); + expect(mergeDeep({ a: 1 }, { a: 2 })).toEqual({ a: 2 }); + }); + it('merge complex objects, second into first', () => { + expect( + mergeDeep({ a: 1, b: { c: { d: 3 } } }, { b: { c: { d: 4 } } }) + ).toEqual({ a: 1, b: { c: { d: 4 } } }); + expect( + mergeDeep({ a: 1, b: { c: { d: 3 } } }, { b: { c: { e: 5 } } }) + ).toEqual({ a: 1, b: { c: { d: 3, e: 5 } } }); + }); +}); + +describe('currentColorModeOnly', () => { + const theme = { + colors: { + light: { + primary: '#000', + }, + dark: { + primary: '#FFF', + }, + }, + sizes: { + small: 8, + }, + themeName: 'minimal', + }; + it('object with only the current color mode colors', () => { + expect(currentColorModeOnly('light', theme)).toEqual({ + colors: { primary: '#000' }, + sizes: { small: 8 }, + themeName: 'minimal', + }); + expect(currentColorModeOnly('dark', theme)).toEqual({ + colors: { primary: '#FFF' }, + sizes: { small: 8 }, + themeName: 'minimal', + }); + }); +}); diff --git a/src/services/theme/utils.ts b/src/services/theme/utils.ts new file mode 100644 index 00000000000..82b090bfb3f --- /dev/null +++ b/src/services/theme/utils.ts @@ -0,0 +1,299 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + EuiThemeColorMode, + EuiThemeOverrides, + EuiThemeSystem, + EuiThemeShape, + EuiThemeComputed, +} from './types'; + +export const COLOR_MODE_KEY = 'colors'; +export const DEFAULT_COLOR_MODE = 'light'; + +const isObject = (obj: any) => obj && typeof obj === 'object'; + +export const isInverseColorMode = (colorMode?: EuiThemeColorMode) => { + return colorMode === 'inverse'; +}; + +export const getColorMode = ( + colorMode?: EuiThemeColorMode, + parentColorMode?: EuiThemeColorMode +) => { + if (colorMode == null) { + return parentColorMode || DEFAULT_COLOR_MODE; + } else if (isInverseColorMode(colorMode)) { + return parentColorMode === 'dark' || parentColorMode === undefined + ? 'light' + : 'dark'; + } else { + return colorMode; + } +}; + +export const getOn = ( + model: { [key: string]: any }, + _path: string, + colorMode: EuiThemeColorMode +) => { + const path = _path.split('.'); + let node = model; + while (path.length) { + const segment = path.shift()!; + if (node.hasOwnProperty(segment) === false) { + return undefined; + } + if (segment === COLOR_MODE_KEY) { + if (node[segment].hasOwnProperty(colorMode) === false) { + return undefined; + } else { + node = node[segment][colorMode]; + } + } else { + if (node[segment] instanceof Computed) { + node = node[segment].getValue(null, null, node, colorMode); + } else { + node = node[segment]; + } + } + } + + return node; +}; + +export const setOn = ( + model: { [key: string]: any }, + _path: string, + value: any +) => { + const path = _path.split('.'); + const propertyName = path.pop()!; + let node = model; + + while (path.length) { + const segment = path.shift()!; + if (node.hasOwnProperty(segment) === false) { + node[segment] = {}; + } + node = node[segment]; + } + + node[propertyName] = value; + return true; +}; + +export class Computed { + constructor( + public dependencies: string[], + public computer: (...values: any[]) => T + ) {} + + getValue( + base: EuiThemeSystem | EuiThemeShape, + overrides: EuiThemeOverrides = {}, + working: EuiThemeComputed, + colorMode: EuiThemeColorMode + ) { + return this.computer( + this.dependencies.map((dependency) => { + return ( + getOn(working, dependency, colorMode) ?? + getOn(overrides, dependency, colorMode) ?? + getOn(base, dependency, colorMode) + ); + }) + ); + } +} + +export const computed = ( + dependencies: string[], + computer: (values: any[]) => T +) => { + return (new Computed(dependencies, computer) as unknown) as T; +}; + +export const getComputed = ( + base: EuiThemeSystem, + over: Partial>, + colorMode: EuiThemeColorMode +): EuiThemeComputed => { + const output = { themeName: base.key }; + + function loop( + base: { [key: string]: any }, + over: { [key: string]: any }, + path?: string + ) { + Object.keys(base).forEach((key) => { + const arr = path?.split('.') || []; + const last = arr[arr.length - 1]; + if (last === COLOR_MODE_KEY && key !== colorMode) { + // Intentional no-op + } else { + const newPath = path ? `${path}.${key}` : `${key}`; + const baseValue = + base[key] instanceof Computed + ? base[key].getValue(base.root, over.root, output, colorMode) + : base[key]; + const overValue = + over[key] instanceof Computed + ? over[key].getValue(base.root, over.root, output, colorMode) + : over[key]; + if (isObject(baseValue)) { + loop(baseValue, overValue ?? {}, newPath); + } else { + setOn(output, newPath, overValue ?? baseValue); + } + } + }); + } + loop(base, over); + return currentColorModeOnly(colorMode, (output as unknown) as T); +}; + +export const buildTheme = (model: T, key: string) => { + const handler: ProxyHandler> = { + getPrototypeOf(target) { + return Reflect.getPrototypeOf(target.model); + }, + + setPrototypeOf(target, prototype) { + return Reflect.setPrototypeOf(target.model, prototype); + }, + + isExtensible(target) { + return Reflect.isExtensible(target); + }, + + preventExtensions(target) { + return Reflect.preventExtensions(target.model); + }, + + getOwnPropertyDescriptor(target, key) { + return Reflect.getOwnPropertyDescriptor(target.model, key); + }, + + defineProperty(target, property, attributes) { + return Reflect.defineProperty(target.model, property, attributes); + }, + + has(target, property) { + return Reflect.has(target.model, property); + }, + + get(_target, property) { + if (property === 'key') { + return _target[property]; + } + + // prevent Safari from locking up when the proxy is used in dev tools + // as it doesn't support getPrototypeOf + if (property === '__proto__') return {}; + + const target = property === 'root' ? _target : _target.model || _target; + // @ts-ignore `string` index signature + const value = target[property]; + if (typeof value === 'object' && value !== null) { + return new Proxy( + { + model: value, + root: _target.root, + key: `_${_target.key}`, + }, + handler + ); + } else { + return value; + } + }, + + set(target: any) { + return target; + }, + + deleteProperty(target: any) { + return target; + }, + + ownKeys(target) { + return Reflect.ownKeys(target.model); + }, + + apply(target: any) { + return target; + }, + + construct(target: any) { + return target; + }, + }; + const themeProxy = new Proxy({ model, root: model, key }, handler); + + return themeProxy; +}; + +export const mergeDeep = ( + _target: { [key: string]: any }, + source: { [key: string]: any } = {} +) => { + const target = { ..._target }; + + if (!isObject(target) || !isObject(source)) { + return source; + } + + Object.keys(source).forEach((key) => { + const targetValue = target[key]; + const sourceValue = source[key]; + + if (isObject(targetValue) && isObject(sourceValue)) { + target[key] = mergeDeep({ ...targetValue }, { ...sourceValue }); + } else { + target[key] = sourceValue; + } + }); + + return target; +}; + +export const currentColorModeOnly = ( + colorMode: EuiThemeColorMode, + _theme: { [key: string]: any } +): EuiThemeComputed => { + const theme: { [key: string]: any } = {}; + + Object.keys(_theme).forEach((key) => { + if (key === COLOR_MODE_KEY) { + theme[key] = _theme[key][colorMode]; + } else { + const themeValue = _theme[key]; + + if (isObject(themeValue)) { + theme[key] = currentColorModeOnly(colorMode, themeValue); + } else { + theme[key] = themeValue; + } + } + }); + + return theme as EuiThemeComputed; +}; diff --git a/yarn.lock b/yarn.lock index 1affa187aa6..36a2e825101 100755 --- a/yarn.lock +++ b/yarn.lock @@ -103,6 +103,15 @@ "@babel/helper-module-imports" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-builder-react-jsx-experimental@^7.12.1": + version "7.12.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz#55fc1ead5242caa0ca2875dcb8eed6d311e50f48" + integrity sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-module-imports" "^7.12.1" + "@babel/types" "^7.12.1" + "@babel/helper-builder-react-jsx@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d" @@ -195,6 +204,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.7.0": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" + integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== + dependencies: + "@babel/types" "^7.12.5" + "@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" @@ -456,6 +472,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-jsx@^7.12.1", "@babel/plugin-syntax-jsx@^7.2.0": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926" + integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -738,6 +761,16 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" +"@babel/plugin-transform-react-jsx@^7.12.1": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.5.tgz#39ede0e30159770561b6963be143e40af3bde00c" + integrity sha512-2xkcPqqrYiOQgSlM/iwto1paPijjsDbUynN13tI6bosDz/jOW3CRzYguIE8wKX32h+msbBM22Dv5fwrFkUOZjQ== + dependencies: + "@babel/helper-builder-react-jsx" "^7.10.4" + "@babel/helper-builder-react-jsx-experimental" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.12.1" + "@babel/plugin-transform-react-pure-annotations@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501" @@ -961,7 +994,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.12.1": +"@babel/runtime@^7.12.1", "@babel/runtime@^7.7.2": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw== @@ -992,7 +1025,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.10", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.7.1", "@babel/types@^7.9.5": +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.10", "@babel/types@^7.12.7", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.7.1", "@babel/types@^7.9.5": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.10.tgz#7965e4a7260b26f09c56bcfcb0498af1f6d9b260" integrity sha512-sf6wboJV5mGyip2hIpDSKsr80RszPinEFjsHTalMxZAZkoQ2/2yQzxlcFN52SJqsyPfLtPmenL4g2KB3KJXPDw== @@ -1001,6 +1034,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.1", "@babel/types@^7.12.5": + version "7.12.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" + integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -1046,6 +1088,130 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-config-kibana/-/eslint-config-kibana-0.15.0.tgz#a552793497cdfc1829c2f9b7cd7018eb008f1606" integrity sha1-pVJ5NJfN/Bgpwvm3zXAY6wCPFgY= +"@emotion/babel-plugin-jsx-pragmatic@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin-jsx-pragmatic/-/babel-plugin-jsx-pragmatic-0.1.5.tgz#27debfe9c27c4d83574d509787ae553bf8a34d7e" + integrity sha512-y+3AJ0SItMDaAgGPVkQBC/S/BaqaPACkQ6MyCI2CUlrjTxKttTVfD3TMtcs7vLEcLxqzZ1xiG0vzwCXjhopawQ== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@emotion/babel-plugin@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.0.0.tgz#e6f40fa81ef52775773a53d50220c597ebc5c2ef" + integrity sha512-w3YP0jlqrNwBBaSI6W+r80fOKF6l9QmsPfLNx5YWSHwrxjVZhM+L50gY7YCVAvlfr1/qdD1vsFN+PDZmLvt42Q== + dependencies: + "@babel/helper-module-imports" "^7.7.0" + "@babel/plugin-syntax-jsx" "^7.12.1" + "@babel/runtime" "^7.7.2" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/serialize" "^1.0.0" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "^4.0.3" + +"@emotion/babel-preset-css-prop@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-preset-css-prop/-/babel-preset-css-prop-11.0.0.tgz#25b868affa620b9e97024b67f67ad32c03a0510e" + integrity sha512-E7z3jMf1OyThGpp3ngYGxOSGX5AdoSQTuqM9QgJNAHFh3Fi8N5CbWx6g+IdySJ8bjPiMgFQsIeEhkyy+4mDpCQ== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.12.1" + "@babel/runtime" "^7.7.2" + "@emotion/babel-plugin" "^11.0.0" + "@emotion/babel-plugin-jsx-pragmatic" "^0.1.5" + +"@emotion/cache@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.0.0.tgz#473adcaf9e04c6a0e30fb1421e79a209a96818f8" + integrity sha512-NStfcnLkL5vj3mBILvkR2m/5vFxo3G0QEreYKDGHNHm9IMYoT/t3j6xwjx6lMI/S1LUJfVHQqn0m9wSINttTTQ== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.0.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "^4.0.3" + +"@emotion/css-prettifier@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/css-prettifier/-/css-prettifier-1.0.0.tgz#3ed4240d93c9798c001cedf27dd0aa960bdddd1a" + integrity sha512-efxSrRTiTqHTQVKW15Gz5H4pNAw8OqcG8NaiwkJIkqIdNXTD4Qr1zC1Ou6r2acd1oJJ2s56nb1ClnXMiWoj6gQ== + dependencies: + "@emotion/memoize" "^0.7.4" + stylis "^4.0.3" + +"@emotion/eslint-plugin@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@emotion/eslint-plugin/-/eslint-plugin-11.0.0.tgz#7666b750df62dc33a93bb1e09086f1caaecadc6f" + integrity sha512-V5w/LgV61xta+U6LKht3WQqfjTLueU2mh1aRTcK5OfkRhZ4OZFE0Inq/oVwLCq5g3Hzoaq27PRm+Tk9W18QScw== + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/jest@^11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@emotion/jest/-/jest-11.1.0.tgz#ee498838f5f8cfc3c31be38543036372f36ebf4b" + integrity sha512-nkJY5U/cM3WjFYoKozy4WSKILmV3Y2P/uMynI9HVoCTNuYHSdu0djQBHL6zqBmpRya0sjVT1Sv7lasYYwVoMFg== + dependencies: + "@babel/runtime" "^7.7.2" + "@emotion/css-prettifier" "^1.0.0" + chalk "^4.1.0" + specificity "^0.4.1" + stylis "^4.0.3" + +"@emotion/memoize@^0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + +"@emotion/react@^11.1.1": + version "11.1.1" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.1.1.tgz#4b304d494af321b0179e6763830e07cf674f0423" + integrity sha512-otA0Np8OnOeU9ChkOS9iuLB6vIxiM+bJiU0id33CsQn3R2Pk9ijVHnxevENIKV/P2S7AhrD8cFbUGysEciWlEA== + dependencies: + "@babel/runtime" "^7.7.2" + "@emotion/cache" "^11.0.0" + "@emotion/serialize" "^1.0.0" + "@emotion/sheet" "^1.0.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.0.tgz#1a61f4f037cf39995c97fc80ebe99abc7b191ca9" + integrity sha512-zt1gm4rhdo5Sry8QpCOpopIUIKU+mUSpV9WNmFILUraatm5dttNEaYzUWWSboSMUE6PtN2j1cAsuvcugfdI3mw== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.0.tgz#a0ef06080f339477ad4ba7f56e1c931f7ba50822" + integrity sha512-cdCHfZtf/0rahPDCZ9zyq+36EqfD/6c0WUqTFZ/hv9xadTUv2lGE5QK7/Z6Dnx2oRxC0usfVM2/BYn9q9B9wZA== + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" + integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + +"@emotion/weak-memoize@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@eslint/eslintrc@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" @@ -2902,6 +3068,15 @@ babel-plugin-jest-hoist@^24.9.0: dependencies: "@types/babel__traverse" "^7.0.6" +babel-plugin-macros@^2.6.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + babel-plugin-pegjs-inline-precompile@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-pegjs-inline-precompile/-/babel-plugin-pegjs-inline-precompile-0.1.1.tgz#f12d1aa9f947945c2bd8c9c1ae503a8fba7d810d" @@ -4278,7 +4453,7 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.7.0: +convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== @@ -4706,6 +4881,11 @@ csstype@^2.2.0: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== +csstype@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8" + integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -7796,7 +7976,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -14713,7 +14893,7 @@ source-map@^0.4.2: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6: +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -14786,6 +14966,11 @@ specificity@^0.3.1: resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.2.tgz#99e6511eceef0f8d9b57924937aac2cb13d13c42" integrity sha512-Nc/QN/A425Qog7j9aHmwOrlwX2e7pNI47ciwxwy4jOlvbbMHkNNJchit+FX+UjF3IAdiaaV5BKeWuDUnws6G1A== +specificity@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" + integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -15248,6 +15433,11 @@ stylelint@^8.1.1: svg-tags "^1.0.0" table "^4.0.1" +stylis@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.3.tgz#0d714765f3f694a685550f0c45411ebf90a9bded" + integrity sha512-iAxdFyR9cHKp4H5M2dJlDnvcb/3TvPprzlKjvYVbH7Sh+y8hjY/mUu/ssdcvVz6Z4lKI3vsoS0jAkMYmX7ozfA== + sudo-block@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/sudo-block/-/sudo-block-1.2.0.tgz#cc539bf8191624d4f507d83eeb45b4cea27f3463"