diff --git a/.babelrc.js b/.babelrc.js index f7b986d99a3..dd277c23d08 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -12,7 +12,7 @@ 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", ], "plugins": [ "@babel/plugin-syntax-dynamic-import", diff --git a/.eslintrc.js b/.eslintrc.js index 7e5e616d77f..56613ea54ce 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -37,7 +37,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/CHANGELOG.md b/CHANGELOG.md index d0d7cf9989e..ca5b1f045aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `37.4.0`. +## Feature: Emotion ([#4511](https://github.com/elastic/eui/pull/4511)) + +- Added `EuiThemeProvider`, a React context provider for theme values and color mode selection +- Added `useEuiTheme` React hook, and `withEuiTheme` React HOC for consuming the EuiTheme +- Added global `EuiTheme` tokens for `colors`, `size`, `font`, `border`, `animation`, and `breakpoint` +- Added color services for `makeHighContrastColor`, `makeDisabledContrastColor`, `shade`, `tint`, `transparentize`, `saturate`, `desaturate`, `lightness` ## [`37.4.0`](https://github.com/elastic/eui/tree/v37.4.0) diff --git a/package.json b/package.json index bc4c6a05e7f..95509644e37 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,9 @@ "@elastic/charts": "^34.2.0", "@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/react": "^11.1.1", "@svgr/core": "5.4.0", "@svgr/plugin-svgo": "^4.0.3", "@types/classnames": "^2.2.10", diff --git a/scripts/babel/proptypes-from-ts-props/index.js b/scripts/babel/proptypes-from-ts-props/index.js index c6c41a1395c..b02a9114394 100644 --- a/scripts/babel/proptypes-from-ts-props/index.js +++ b/scripts/babel/proptypes-from-ts-props/index.js @@ -770,7 +770,7 @@ function getPropTypesForNode(node, optional, state) { types.arrayExpression( node.properties.map(property => types.stringLiteral( - property.key.name || property.key.name || property.key.value + property.key ? property.key.name || property.key.value : property.argument.name ) ) ), diff --git a/src-docs/.babelrc.js b/src-docs/.babelrc.js index a80333ae140..ab09003d064 100644 --- a/src-docs/.babelrc.js +++ b/src-docs/.babelrc.js @@ -2,6 +2,12 @@ const baseConfig = require('../.babelrc.js'); const index = baseConfig.plugins.indexOf( './scripts/babel/proptypes-from-ts-props' ); +baseConfig.presets.push([ + '@emotion/babel-preset-css-prop', + { + labelFormat: '[local]', + }, +]); baseConfig.plugins.splice( index + 1, 0, diff --git a/src-docs/src/components/with_theme/theme_context.tsx b/src-docs/src/components/with_theme/theme_context.tsx index 67a3cd74901..5df045cee42 100644 --- a/src-docs/src/components/with_theme/theme_context.tsx +++ b/src-docs/src/components/with_theme/theme_context.tsx @@ -2,6 +2,9 @@ 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 } from '../../../../src/services'; +import { EuiThemeAmsterdam } from '../../../../src/themes/eui-amsterdam/theme'; +import { EuiThemeDefault } from '../../../../src/themes/eui/theme'; const THEME_NAMES = EUI_THEMES.map(({ value }) => value); @@ -48,7 +51,14 @@ export class ThemeProvider extends React.Component { changeTheme: this.changeTheme, }} > - {children} + + {children} + ); } diff --git a/src-docs/src/index.d.ts b/src-docs/src/index.d.ts new file mode 100644 index 00000000000..60c8ded2efa --- /dev/null +++ b/src-docs/src/index.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// TODO: Delete this file when the @emotion reference is added to `src/index.d.ts` + +/* eslint-disable @typescript-eslint/triple-slash-reference */ +/// diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 4889e2fb7c6..d51adee1d3b 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -24,7 +24,7 @@ import WritingGuidelines from './views/guidelines/writing'; import { ColorPaletteExample } from './views/color_palette/color_palette_example'; -import { IsColorDarkExample } from './views/is_color_dark/is_color_dark_example'; +import { ColorExample } from './views/color/color_example'; import { PrettyDurationExample } from './views/pretty_duration/pretty_duration_example'; @@ -222,6 +222,9 @@ import { I18nTokens } from './views/package/i18n_tokens'; import { SuperSelectExample } from './views/super_select/super_select_example'; +import { ThemeExample } from './views/theme/theme_example'; +import ThemeValues from './views/theme/values'; + /** Elastic Charts */ import { ElasticChartsThemingExample } from './views/elastic_charts/theming_example'; @@ -460,7 +463,7 @@ const navigation = [ items: [ AccessibilityExample, BeaconExample, - IsColorDarkExample, + ColorExample, ColorPaletteExample, CopyExample, UtilityClassesExample, @@ -482,6 +485,17 @@ const navigation = [ WindowEventExample, ].map((example) => createExample(example)), }, + { + name: 'Theming', + items: [ + createExample(ThemeExample, 'Theme provider'), + { + name: 'Global values', + component: ThemeValues, + isNew: true, + }, + ], + }, { name: 'Package', items: [Changelog, I18nTokens], diff --git a/src-docs/src/services/playground/knobs.js b/src-docs/src/services/playground/knobs.js index 5dbacd61925..cc9b6f30c65 100644 --- a/src-docs/src/services/playground/knobs.js +++ b/src-docs/src/services/playground/knobs.js @@ -96,7 +96,38 @@ export const humanizeType = (type) => { humanizedType = type.name; } - return humanizedType; + let typeMarkup; + + if (humanizedType) { + typeMarkup = humanizedType; + + const functionMatches = [ + ...humanizedType.matchAll(/\([^=]*\) =>\s\w*\)*/g), + ]; + + const types = humanizedType.split(/\([^=]*\) =>\s\w*\)*/); + + if (functionMatches.length > 0) { + let elements = ''; + let j = 0; + for (let i = 0; i < types.length; i++) { + if (functionMatches[j]) { + elements = + `${elements}` + + `${types[i]}` + + '\n' + + `${functionMatches[j][0]}` + + '\n'; + j++; + } else { + elements = `${elements}` + `${types[i]}` + '\n'; + } + } + typeMarkup = elements; + } + } + + return typeMarkup || humanizedType; }; const getTooltip = (description, type, name) => ( @@ -364,70 +395,19 @@ const Knob = ({ }; const KnobColumn = ({ state, knobNames, error, set, isPlayground }) => { - return knobNames.map((name, idx) => { - const codeBlockProps = { - className: 'guideSection__tableCodeBlock', - paddingSize: 'none', - language: 'ts', - }; - - /** - * TS Type - */ - let humanizedType; - - if ( - state[name].custom && - state[name].custom.origin && - state[name].custom.origin.type - ) - humanizedType = humanizeType(state[name].custom.origin.type); - - let typeMarkup; - - if (humanizedType) { - typeMarkup = humanizedType && ( - {humanizedType} - ); - - const functionMatches = [ - ...humanizedType.matchAll(/\([^=]*\) =>\s\w*\)*/g), - ]; - - const types = humanizedType.split(/\([^=]*\) =>\s\w*\)*/); - - if (functionMatches.length > 0) { - let elements = ''; - let j = 0; - for (let i = 0; i < types.length; i++) { - if (functionMatches[j]) { - elements = - `${elements}` + - `${types[i]}` + - '\n' + - `${functionMatches[j][0]}` + - '\n'; - j++; - } else { - elements = `${elements}` + `${types[i]}` + '\n'; - } - } - typeMarkup = ( - {elements} - ); - } - } + const codeBlockProps = { + className: 'guideSection__tableCodeBlock', + paddingSize: 'none', + language: 'ts', + }; + return knobNames.map((name, idx) => { /** * Prop name */ let humanizedName = {name}; - if ( - state[name].custom && - state[name].custom.origin && - state[name].custom.origin.required - ) { + if (state[name].custom?.origin?.required) { humanizedName = ( <> {humanizedName} (required) @@ -435,16 +415,26 @@ const KnobColumn = ({ state, knobNames, error, set, isPlayground }) => { ); } + /** + * TS Type + */ + let typeMarkup; + + if (state[name].custom?.origin?.type) { + const humanizedType = humanizeType(state[name].custom.origin.type); + + if (humanizedType) { + typeMarkup = ( + {humanizedType} + ); + } + } + /** * Default value */ let defaultValueMarkup; - if ( - // !isPlayground && - state[name].custom && - state[name].custom.origin && - state[name].custom.origin.defaultValue - ) { + if (state[name].custom?.origin?.defaultValue) { const defaultValue = state[name].custom.origin.defaultValue; defaultValueMarkup = ( diff --git a/src-docs/src/views/button/button_example.js b/src-docs/src/views/button/button_example.js index 336a9b0cbd4..5421aacffa6 100644 --- a/src-docs/src/views/button/button_example.js +++ b/src-docs/src/views/button/button_example.js @@ -151,7 +151,7 @@ const buttonGroupSnippet = [ options={[ { id, - label' + label } ]} />`, @@ -163,7 +163,7 @@ const buttonGroupSnippet = [ options={[ { id, - label' + label } ]} />`, diff --git a/src-docs/src/views/code/code_example.js b/src-docs/src/views/code/code_example.js index b69df50309b..47bdd55e2d5 100644 --- a/src-docs/src/views/code/code_example.js +++ b/src-docs/src/views/code/code_example.js @@ -68,9 +68,7 @@ export const CodeExample = { library - . -
- The language prop can also be omitted to simply + . The language prop can also be omitted to simply render formatted but unhighlighted code.

diff --git a/src-docs/src/views/color/color_example.js b/src-docs/src/views/color/color_example.js new file mode 100644 index 00000000000..5055ffe598b --- /dev/null +++ b/src-docs/src/views/color/color_example.js @@ -0,0 +1,284 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { GuideSectionTypes } from '../../components'; + +import { + EuiCode, + EuiText, + EuiLink, + EuiCallOut, +} from '../../../../src/components'; + +import IsColorDark from './is_color_dark'; +const isColorDarkSource = require('!!raw-loader!./is_color_dark'); + +import Contrast from './contrast'; +const ContrastSource = require('!!raw-loader!./contrast'); +import ContrastBody from './contrast_body'; +const ContrastBodySource = require('!!raw-loader!./contrast_body'); + +import Transparency from './transparency'; +const TransparencySource = require('!!raw-loader!./transparency'); + +import Tint from './tint'; +const TintSource = require('!!raw-loader!./tint'); +import Shade from './shade'; +const ShadeSource = require('!!raw-loader!./shade'); + +import Saturate from './saturate'; +const SaturateSource = require('!!raw-loader!./saturate'); +import Desaturate from './desaturate'; +const DesaturateSource = require('!!raw-loader!./desaturate'); + +import ContrastSimulated from './contrast_simulated'; +const ContrastSimulatedSource = require('!!raw-loader!./contrast_simulated'); +import ContrastSimulatedBody from './contrast_simulated_body'; +const ContrastSimulatedBodySource = require('!!raw-loader!./contrast_simulated_body'); + +export const ColorExample = { + title: 'Color', + intro: ( + +

+ EUI's color functions use the lightweight color library{' '} + chroma.js for + calculations. This means that most functions accept most Chroma{' '} + Color{' '} + types. +

+
+ ), + sections: [ + { + title: 'Contrast', + source: [ + { + type: GuideSectionTypes.JS, + code: ContrastSource, + }, + ], + text: ( + <> +

+ Use{' '} + + makeHighContrastColor(foreground, ratio = 4.5)(background) + {' '} + to calculate the appropriate foreground color (usually text) based + on a background color. +

+ +

+ Note that color contrast cannot be accurately detected when using + transparency (colors with alpha channels). See the{' '} + + "Simulating contrast with transparency" + {' '} + example for details on how handle this internally. +

+
+ + ), + demo: , + snippet: + 'const textColor = makeHighContrastColor(foreground)(background);', + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: ContrastBodySource, + }, + ], + text: ( +

+ If you want to use the same background color that the EUI theme uses + for all of its contrast calculations, you can pass in the{' '} + euiTheme as the background. +

+ ), + demo: , + snippet: `const { euiTheme } = useEuiTheme(); +const textColor = makeHighContrastColor(foreground)(euiTheme);`, + }, + { + title: 'Transparency', + source: [ + { + type: GuideSectionTypes.JS, + code: TransparencySource, + }, + ], + text: ( +

+ Use transparentize(color, alpha: 0-1){' '} + to convert any color to rgba() with the provided + alpha value. +

+ ), + demo: , + snippet: 'const rgba = transparentize(color, 0.75);', + }, + { + title: 'Tint and shade', + source: [ + { + type: GuideSectionTypes.JS, + code: TintSource, + }, + ], + text: ( +

+ Use tint(color, ratio: 0-1) to mix + any color with white. The higher the ratio, the more + white will be mixed. +

+ ), + demo: , + snippet: 'const tinted = tint(color, 0.75);', + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: ShadeSource, + }, + ], + text: ( +

+ Use shade(color, ratio: 0-1) to mix + any color with black. The higher the ratio, the more + black will be mixed. +

+ ), + demo: , + snippet: 'const shaded = shade(color, 0.5);', + }, + { + title: 'Saturation', + source: [ + { + type: GuideSectionTypes.JS, + code: SaturateSource, + }, + ], + text: ( +

+ Use saturate(color, ratio: 0-1) to + increase the saturation of a color by manipulating the hsl variant. +

+ ), + demo: , + snippet: 'const saturated = saturate(color, 0.75);', + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: DesaturateSource, + }, + ], + text: ( +

+ Use desaturate(color, ratio: 0-1) to + decrease the saturation of a color by manipulating the hsl variant. +

+ ), + demo: , + snippet: 'const desaturated = desaturate(color, 0.75);', + }, + { + title: 'Is color dark', + source: [ + { + type: GuideSectionTypes.JS, + code: isColorDarkSource, + }, + ], + text: ( + <> +

+ Use isColorDark(red, green, blue){' '} + to determine whether or not to use light or dark text against a + background of a given color. It requires the values to be passed in + as rgb integers and will return a boolean if the + color is dark based on luminance. +

+

+ If the function returns true, use{' '} + euiTheme.colors.ghost otherwise use{' '} + euiTheme.colors.ink as the text + color. +

+ + ), + demo: , + snippet: `const { euiTheme } = useEuiTheme(); +const textColor = isColorDark(color) ? euiTheme.colors.ghost : euiTheme.colors.ink;`, + }, + { + title: 'Simulating contrast with transparency', + source: [ + { + type: GuideSectionTypes.JS, + code: ContrastSimulatedSource, + }, + ], + text: ( + <> +

+ The makeHighContrastColor function does not take + alpha channel into account and can therefore lead to incorrect + contrast calculations. +

+

+ You can, instead, simulate transparency by first + tinting or shading (depending on the color mode) the background + color with the same transparency ratio. Then use the simulated color + to calculate the contrast with the foreground. +

+ +

+ This method is not 100% accurate and has some inconstencies in the + way mixing colors are calculated versus transparency. However, it + is more likely to create the right contrast ratio + than using transparency alone. +

+
+ + ), + demo: , + snippet: `const { colorMode } = useEuiTheme(); +const transparency = 0.4; +const simulated = colorMode === 'DARK' + ? shade(background, 1 - transparency) + : tint(background, 1 - transparency); +const backgroundColor = transparentize(background, transparency); +const color = makeHighContrastColor(foreground)(simulated);`, + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: ContrastSimulatedBodySource, + }, + ], + text: ( + <> +

+ When EUI simulates or calculates contrast, we typically mix the + background color with euiTheme.colors.body which + is the darkest version of backgrounds we allow. +

+

+ This effectively increases the contrast ratio when used on the empty + shade, but ensures proper contrast when the element sits directly on + the body color. +

+ + ), + demo: , + }, + ], +}; diff --git a/src-docs/src/views/color/contrast.js b/src-docs/src/views/color/contrast.js new file mode 100644 index 00000000000..75a429461d4 --- /dev/null +++ b/src-docs/src/views/color/contrast.js @@ -0,0 +1,49 @@ +import React, { Fragment } from 'react'; +import chroma from 'chroma-js'; +import { css } from '@emotion/react'; + +import { EuiFlexGrid, EuiFlexItem } from '../../../../src/components'; + +import { useEuiTheme, makeHighContrastColor } from '../../../../src/services'; + +export default () => { + const { euiTheme } = useEuiTheme(); + + const BACKGROUND = ['#006837', 'rgb(165,0,38)', 'rgba(0,0,0,1)', 'pink']; + + const FOREGROUND = [ + '#bfa180', + 'rgb(249,133,16)', + 'rgba(0,179,164,1)', + 'white', + ]; + + return ( + + + {BACKGROUND.map((background, i) => { + const color = makeHighContrastColor(FOREGROUND[i])(background); + + return ( + +
+ + {chroma.contrast(color, background).toFixed(2)} + {`: makeHighContrastColor(${FOREGROUND[i]}, ${background})`} + +
+
+ ); + })} +
+
+ ); +}; diff --git a/src-docs/src/views/color/contrast_body.js b/src-docs/src/views/color/contrast_body.js new file mode 100644 index 00000000000..a26d78aa4f7 --- /dev/null +++ b/src-docs/src/views/color/contrast_body.js @@ -0,0 +1,47 @@ +import React, { Fragment } from 'react'; +import chroma from 'chroma-js'; +import { css } from '@emotion/react'; + +import { EuiFlexGrid, EuiFlexItem } from '../../../../src/components'; + +import { useEuiTheme, makeHighContrastColor } from '../../../../src/services'; + +export default () => { + const { euiTheme } = useEuiTheme(); + + const FOREGROUND = [ + '#bfa180', + 'rgb(249,133,16)', + 'rgba(0,179,164,1)', + 'white', + ]; + + return ( + + + {FOREGROUND.map((foreground) => { + const color = makeHighContrastColor(foreground)(euiTheme); + + return ( + +
+ + {chroma.contrast(color, euiTheme.colors.body).toFixed(2)} + {`: makeHighContrastColor(${foreground}, euiTheme)`} + +
+
+ ); + })} +
+
+ ); +}; diff --git a/src-docs/src/views/color/contrast_simulated.js b/src-docs/src/views/color/contrast_simulated.js new file mode 100644 index 00000000000..0cbc6b72389 --- /dev/null +++ b/src-docs/src/views/color/contrast_simulated.js @@ -0,0 +1,109 @@ +import React, { Fragment, useState } from 'react'; +import chroma from 'chroma-js'; +import { css } from '@emotion/react'; + +import { + EuiFlexGrid, + EuiFlexItem, + EuiButtonGroup, + EuiHorizontalRule, + EuiIcon, +} from '../../../../src/components'; + +import { + useEuiTheme, + makeHighContrastColor, + transparentize, + tint, + shade, +} from '../../../../src/services'; + +export default () => { + const { euiTheme, colorMode } = useEuiTheme(); + const [contrastButtonSelected, setContrastButtonSelected] = useState( + 'transparent' + ); + + const transparency = 0.3; + const BACKGROUND = ['#006837ee', 'rgb(165,0,38)', 'rgb(0,0,0)', 'pink']; + const FOREGROUND = ['#bfa180', 'rgb(249,133,16)', 'rgb(0,179,164)', 'white']; + + return ( + + setContrastButtonSelected(optionId)} + options={[ + { + id: 'incorrect', + label: 'Transparency only', + }, + { + id: 'simulated', + label: 'Simulated background', + }, + { + id: 'transparent', + label: 'Simulated with transparency', + }, + ]} + /> + + + + + {BACKGROUND.map((background, i) => { + const simulated = + colorMode === 'DARK' + ? shade(background, 1 - transparency) + : tint(background, 1 - transparency); + + // Default incorrect calculation + let backgroundColor = transparentize(background, transparency); + let color = makeHighContrastColor(FOREGROUND[i])(backgroundColor); + let iconType = 'crossInACircleFilled'; + let contrastRatio = chroma + .contrast(color, backgroundColor) + .toFixed(2); + + switch (contrastButtonSelected) { + case 'simulated': + backgroundColor = simulated; + color = makeHighContrastColor(FOREGROUND[i])(simulated); + iconType = 'checkInCircleFilled'; + contrastRatio = chroma + .contrast(color, backgroundColor) + .toFixed(2); + break; + case 'transparent': + backgroundColor = transparentize(background, transparency); + color = makeHighContrastColor(FOREGROUND[i])(simulated); + iconType = 'questionInCircle'; + contrastRatio = 'Unknown'; + break; + } + + return ( + +
+ {contrastRatio}:{' '} + {contrastButtonSelected} +
+
+ ); + })} +
+
+ ); +}; diff --git a/src-docs/src/views/color/contrast_simulated_body.js b/src-docs/src/views/color/contrast_simulated_body.js new file mode 100644 index 00000000000..d2e60052925 --- /dev/null +++ b/src-docs/src/views/color/contrast_simulated_body.js @@ -0,0 +1,48 @@ +import React, { Fragment } from 'react'; +import chroma from 'chroma-js'; +import { css } from '@emotion/react'; + +import { EuiFlexGrid, EuiFlexItem } from '../../../../src/components'; + +import { + useEuiTheme, + makeHighContrastColor, + transparentize, +} from '../../../../src/services'; + +export default () => { + const { euiTheme } = useEuiTheme(); + + const transparency = 0.3; + const BACKGROUND = ['#006837ee', 'rgb(165,0,38)', 'rgb(0,0,0)', 'pink']; + const FOREGROUND = ['#bfa180', 'rgb(249,133,16)', 'rgb(0,179,164)', 'white']; + + return ( + + + {BACKGROUND.map((background, i) => { + const simulated = chroma + .mix(background, euiTheme.colors.body, 1 - transparency, 'rgb') + .css(); + const backgroundColor = transparentize(background, transparency); + const color = makeHighContrastColor(FOREGROUND[i])(simulated); + + return ( + +
+ {chroma.contrast(color, simulated).toFixed(2)}: Roughly +
+
+ ); + })} +
+
+ ); +}; diff --git a/src-docs/src/views/color/desaturate.js b/src-docs/src/views/color/desaturate.js new file mode 100644 index 00000000000..698a1314fff --- /dev/null +++ b/src-docs/src/views/color/desaturate.js @@ -0,0 +1,28 @@ +import React, { Fragment } from 'react'; + +import { desaturate } from '../../../../src/services'; +import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '../../../../src/components'; + +export default () => { + const COLORS = ['#006837', 'rgb(165,0,38)', 'rgba(0,0,0,0.5)', 'pink']; + + return ( + + + {COLORS.map((color) => { + const desaturated = desaturate(color, 0.75); + + return ( + + + desaturate( + {color}, 0.75) ={' '} + {desaturated} + + + ); + })} + + + ); +}; diff --git a/src-docs/src/views/color/is_color_dark.js b/src-docs/src/views/color/is_color_dark.js new file mode 100644 index 00000000000..ccb08973df1 --- /dev/null +++ b/src-docs/src/views/color/is_color_dark.js @@ -0,0 +1,53 @@ +import React, { Fragment } from 'react'; +import { css } from '@emotion/react'; + +import { EuiFlexGrid, EuiFlexItem } from '../../../../src/components'; + +import { isColorDark, useEuiTheme } from '../../../../src/services'; +const rgb = (r, g, b) => { + return `rgb(${r}, ${g}, ${b})`; +}; + +export default () => { + const { euiTheme } = useEuiTheme(); + + const SWATCH_STYLE = css` + padding: ${euiTheme.size.base}; + border-radius: ${euiTheme.border.radius.small}; + `; + + const COLORS = [ + [0, 104, 55], + [165, 0, 38], + [0, 0, 0], + [219, 19, 116], + [191, 161, 128], + [249, 133, 16], + [0, 179, 164], + [212, 157, 170], + ]; + + return ( + + + {COLORS.map((color) => ( + +
+ {isColorDark(...color) ? 'Dark' : 'Light'} +
+
+ ))} +
+
+ ); +}; diff --git a/src-docs/src/views/color/saturate.js b/src-docs/src/views/color/saturate.js new file mode 100644 index 00000000000..ced4a116614 --- /dev/null +++ b/src-docs/src/views/color/saturate.js @@ -0,0 +1,28 @@ +import React, { Fragment } from 'react'; + +import { saturate } from '../../../../src/services'; +import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '../../../../src/components'; + +export default () => { + const COLORS = ['#006837', 'rgb(165,0,38)', 'rgba(0,0,0,.5)', 'pink']; + + return ( + + + {COLORS.map((color) => { + const saturated = saturate(color, 0.75); + + return ( + + + saturate( + {color}, 0.75) ={' '} + {saturated} + + + ); + })} + + + ); +}; diff --git a/src-docs/src/views/color/shade.js b/src-docs/src/views/color/shade.js new file mode 100644 index 00000000000..dfe1b3b3936 --- /dev/null +++ b/src-docs/src/views/color/shade.js @@ -0,0 +1,29 @@ +import React, { Fragment } from 'react'; + +import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '../../../../src/components'; + +import { shade } from '../../../../src/services'; + +export default () => { + const COLORS = ['#006837', 'rgb(165,0,38)', 'rgba(0,0,0,.5)', 'pink']; + + return ( + + + {COLORS.map((color) => { + const shaded = shade(color, 0.5); + + return ( + + + shade( + {color}, 0.5) ={' '} + {shaded} + + + ); + })} + + + ); +}; diff --git a/src-docs/src/views/color/tint.js b/src-docs/src/views/color/tint.js new file mode 100644 index 00000000000..9a352a5bc38 --- /dev/null +++ b/src-docs/src/views/color/tint.js @@ -0,0 +1,29 @@ +import React, { Fragment } from 'react'; + +import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '../../../../src/components'; + +import { tint } from '../../../../src/services'; + +export default () => { + const COLORS = ['#006837', 'rgb(165,0,38)', 'rgba(0,0,0,.5)', 'pink']; + + return ( + + + {COLORS.map((color) => { + const tinted = tint(color, 0.75); + + return ( + + + tint( + {color}, 0.75) ={' '} + {tinted} + + + ); + })} + + + ); +}; diff --git a/src-docs/src/views/color/transparency.js b/src-docs/src/views/color/transparency.js new file mode 100644 index 00000000000..5491a469c87 --- /dev/null +++ b/src-docs/src/views/color/transparency.js @@ -0,0 +1,40 @@ +import React, { Fragment } from 'react'; + +import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '../../../../src/components'; + +import { useEuiTheme, transparentize } from '../../../../src/services'; + +export default () => { + const { euiTheme, colorMode } = useEuiTheme(); + const COLORS = ['#006837', 'rgb(165,0,38)', 'rgba(0,0,0,.5)', 'pink']; + + return ( + + + {COLORS.map((color) => { + const transparent = transparentize(color, 0.25); + + return ( + + + transparentize( + {color}, 0.75) ={' '} + + {transparent} + + + + ); + })} + + + ); +}; diff --git a/src-docs/src/views/guidelines/_index.scss b/src-docs/src/views/guidelines/_index.scss index 0e77f426458..304efad4212 100644 --- a/src-docs/src/views/guidelines/_index.scss +++ b/src-docs/src/views/guidelines/_index.scss @@ -238,6 +238,7 @@ height: $euiSize; margin-top: $euiSizeS; position: relative; + overflow: hidden; } .guideSass__animChild { @@ -251,6 +252,7 @@ .guideSass__animRow:hover .guideSass__animChild { transition-property: left; transition-timing-function: linear; + transition-duration: $euiAnimSpeedSlow; left: calc(100% - #{$euiSize}); } diff --git a/src-docs/src/views/guidelines/sass.js b/src-docs/src/views/guidelines/sass.js index 57d65876ee3..e32f64f8664 100644 --- a/src-docs/src/views/guidelines/sass.js +++ b/src-docs/src/views/guidelines/sass.js @@ -364,7 +364,27 @@ export const SassGuidelines = ({ selectedTheme }) => { const palette = getSassVars(selectedTheme); return ( - + +

+ EUI is highly tokenized and highly using the following + Sass variables when + customizing on top of EUI. This way your customizations stay up to + date with EUI's theming. +

+

+ For more information on how to consume these Sass variables in your + project, see the{' '} + + Consuming wiki page + + . +

+ + } + >

Core variables

diff --git a/src-docs/src/views/is_color_dark/is_color_dark.js b/src-docs/src/views/is_color_dark/is_color_dark.js deleted file mode 100644 index 4a91b5dc323..00000000000 --- a/src-docs/src/views/is_color_dark/is_color_dark.js +++ /dev/null @@ -1,71 +0,0 @@ -import React, { Fragment } from 'react'; - -import { EuiFlexGrid, EuiFlexItem } from '../../../../src/components'; - -import { isColorDark } from '../../../../src/services'; - -const SWATCH_STYLE = { - width: 100, - height: 100, - padding: 16, -}; - -const rgb = (r, g, b) => { - return `rgb(${r}, ${g}, ${b})`; -}; - -export default () => { - const DARK_COLORS = [ - [0, 104, 55], - [165, 0, 38], - [0, 0, 0], - [219, 19, 116], - [73, 0, 146], - [70, 26, 10], - [146, 0, 0], - ]; - - const LIGHT_COLORS = [ - [191, 161, 128], - [249, 133, 16], - [0, 179, 164], - [212, 157, 170], - [255, 255, 255], - [254, 182, 219], - [230, 194, 32], - ]; - - return ( - - - {DARK_COLORS.map((color) => ( - - {isColorDark(...color) ? ( -
Dark
- ) : ( -
Light
- )} -
- ))} -
- - - {LIGHT_COLORS.map((color) => ( - - {isColorDark(...color) ? ( -
Dark
- ) : ( -
Light
- )} -
- ))} -
-
- ); -}; diff --git a/src-docs/src/views/is_color_dark/is_color_dark_example.js b/src-docs/src/views/is_color_dark/is_color_dark_example.js deleted file mode 100644 index a52bf52a401..00000000000 --- a/src-docs/src/views/is_color_dark/is_color_dark_example.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -import { renderToHtml } from '../../services'; - -import { GuideSectionTypes } from '../../components'; - -import { EuiCode } from '../../../../src/components'; - -import IsColorDark from './is_color_dark'; -const isColorDarkSource = require('!!raw-loader!./is_color_dark'); -const isColorDarkHtml = renderToHtml(IsColorDark); - -export const IsColorDarkExample = { - title: 'Color', - sections: [ - { - title: 'Is color dark', - source: [ - { - type: GuideSectionTypes.JS, - code: isColorDarkSource, - }, - { - type: GuideSectionTypes.HTML, - code: isColorDarkHtml, - }, - ], - text: ( -

- Use isColorDark to determine whether or not to use - light or dark text against a background of a given color. -

- ), - demo: , - }, - ], -}; diff --git a/src-docs/src/views/theme/_animation.js b/src-docs/src/views/theme/_animation.js new file mode 100644 index 00000000000..a0e74c8195a --- /dev/null +++ b/src-docs/src/views/theme/_animation.js @@ -0,0 +1,199 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { useEuiTheme } from '../../../../src/services'; +import { transparentize } from '../../../../src/services/color'; + +import { + EuiText, + EuiSpacer, + EuiFlexItem, + EuiTabbedContent, + EuiCode, +} from '../../../../src/components'; + +import { useDebouncedUpdate } from './hooks'; + +import { ThemeSection } from './_theme_section'; +import { ThemeValue } from './_values'; + +import { + getPropsFromThemeKey, + EuiThemeAnimationSpeed, + EuiThemeAnimationEasing, +} from './_props'; + +export default ({ onThemeUpdate }) => { + const { euiTheme } = useEuiTheme(); + const animation = euiTheme.animation; + const [animationClone, updateAnimation] = useDebouncedUpdate({ + property: 'animation', + value: animation, + onUpdate: onThemeUpdate, + time: 1000, + }); + + const speedTypes = getPropsFromThemeKey(EuiThemeAnimationSpeed); + const easingTypes = getPropsFromThemeKey(EuiThemeAnimationEasing); + + return ( +
+ +

+ Animation EuiThemeAnimation +

+

+ The animation values provide some easy and + consistent ways for adding transition or animation effects and timing. +

+
+ + + + + + + + These are general properties that can be used to create + subtle animations or transitions that share similar timing + and easing functions. +

+ } + themeValues={Object.keys(speedTypes).map((prop) => { + return ( + + updateAnimation(prop, value)} + /> + +
+
+
+ + ); + })} + /> + + + + EUI utilizes the following easing constants to maintain a + similar 'bounce' or slight resistance to its + animations. +

+ } + themeValues={Object.keys(easingTypes).map((prop) => { + return ( + + updateAnimation(prop, value)} + /> + +
+
+
+ + ); + })} + /> + + ), + }, + { + id: 'themeAnimationTabUsage', + name: 'Usage', + content: ( + <> + + + The simplest and most common usage of the animation speeds + is to apply them to custom transitions like hover effects. +

+ } + example={ +
+ Hover me +
+ } + snippet={'transition: background ${euiTheme.animation.slow};'} + /> + + + When moving or changing the{' '} + size of elements on the page, it's + good to add a slight ease to the transition or animation. +

+ } + example={ +
+ Hover me +
+ } + snippet={ + 'transition: padding ${euiTheme.animation.slow} ${euiTheme.animation.resistance}' + } + /> + + ), + }, + ]} + /> +
+ ); +}; diff --git a/src-docs/src/views/theme/_border.js b/src-docs/src/views/theme/_border.js new file mode 100644 index 00000000000..3c17ba0ba07 --- /dev/null +++ b/src-docs/src/views/theme/_border.js @@ -0,0 +1,257 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { useEuiTheme } from '../../../../src/services'; + +import { + EuiText, + EuiSpacer, + EuiFlexItem, + EuiColorPickerSwatch, + EuiCode, + EuiTabbedContent, +} from '../../../../src/components'; + +import { useDebouncedUpdate } from './hooks'; + +import { ThemeSection } from './_theme_section'; +import { ThemeValue } from './_values'; + +import { + getPropsFromThemeKey, + EuiThemeBorderTypes, + EuiThemeBorderColorValues, + EuiThemeBorderWidthValues, + EuiThemeBorderRadiusValues, +} from './_props'; + +export default ({ onThemeUpdate }) => { + const { euiTheme } = useEuiTheme(); + const border = euiTheme.border; + const [borderClone, updateBorder] = useDebouncedUpdate({ + property: 'border', + value: border, + onUpdate: onThemeUpdate, + time: 1000, + }); + const [radiusClone, updateRadius] = useDebouncedUpdate({ + property: ['border', 'radius'], + value: border, + onUpdate: onThemeUpdate, + time: 1000, + }); + const [widthClone, updateWidth] = useDebouncedUpdate({ + property: ['border', 'width'], + value: border, + onUpdate: onThemeUpdate, + time: 1000, + }); + + const colorProps = getPropsFromThemeKey(EuiThemeBorderColorValues); + const widthProps = getPropsFromThemeKey(EuiThemeBorderWidthValues); + const radiusProps = getPropsFromThemeKey(EuiThemeBorderRadiusValues); + const typeProps = getPropsFromThemeKey(EuiThemeBorderTypes); + + const style = css` + width: ${euiTheme.size.xl}; + height: ${euiTheme.size.xl}; + border-radius: ${euiTheme.border.radius.small}; + `; + + const wrappingExampleStyle = { + padding: euiTheme.size.s, + }; + + return ( +
+ +

+ Border EuiThemeBorder +

+

+ The border theme key contains both individual + border property values and full shorthand border properties. +

+
+ + + + + + EUI only has one base color it uses for all borders (or + calculated borders). +

+ } + themeValues={Object.keys(colorProps).map((prop) => ( + + updateBorder(prop, value)} + example={ + + } + /> + + ))} + /> + + + These basic properties make up the border thickness which + can be used individually. +

+ } + themeValues={Object.keys(widthProps).map((prop) => ( + + updateWidth(prop, value)} + /> + + ))} + /> + + + These basic properties make up the corner radii which can + be used individually. +

+ } + themeValues={Object.keys(radiusProps).map((prop) => ( + + updateRadius(prop, value)} + /> + + ))} + /> + + + + These common border types string together the base + properties to form common full border{' '} + properties. +

+ } + themeValues={Object.keys(typeProps).map((prop) => ( + + updateBorder(prop, value)} + stringProps={{ style: { width: 160 } }} + buttonStyle={[ + style, + css` + border: ${borderClone[prop]}; + `, + ]} + /> + + ))} + /> + + ), + }, + { + id: 'themeBorderTabUsage', + name: 'Usage', + content: ( + <> + + + The simplest form of consuming border styles is using one + of the full types which provides the color, width and + style. +

+ } + example={ +
+ {`border: ${euiTheme.border.thick}`} +
+ } + snippet={'border: ${euiTheme.border.thick};'} + /> + + + You can also strictly use the border values within a + single shorthand property. +

+ } + example={ +
+ {`border: ${euiTheme.border.width.thick} dashed ${euiTheme.border.color}`} +
+ } + snippet={ + 'border: ${euiTheme.border.width.thick} dashed ${euiTheme.border.color};' + } + /> + + + {`border-radius: ${euiTheme.border.radius.medium}`} +
+ } + snippet={'border-radius: ${euiTheme.border.radius.medium};'} + /> + + + ), + }, + ]} + /> +
+ ); +}; diff --git a/src-docs/src/views/theme/_breakpoints.js b/src-docs/src/views/theme/_breakpoints.js new file mode 100644 index 00000000000..9e846be171b --- /dev/null +++ b/src-docs/src/views/theme/_breakpoints.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useEuiTheme } from '../../../../src/services'; + +import { EuiText, EuiSpacer, EuiFlexItem } from '../../../../src/components'; + +import { useDebouncedUpdate } from './hooks'; + +import { ThemeSection } from './_theme_section'; +import { ThemeValue } from './_values'; + +import { getPropsFromThemeKey, _EuiThemeBreakpoint } from './_props'; + +export default ({ onThemeUpdate }) => { + const { euiTheme } = useEuiTheme(); + const breakpoint = euiTheme.breakpoint; + const [breakpointClone, updateBreakpoint] = useDebouncedUpdate({ + property: 'breakpoint', + value: breakpoint, + onUpdate: onThemeUpdate, + }); + + const breakpointTypes = getPropsFromThemeKey(_EuiThemeBreakpoint); + + return ( +
+ +

Breakpoints

+

+ It is not recommended to consume these values directly, but to use one + of our responsive components. +

+
+ + + + These original set of breakpoint keys specify the minimum window + size and are required. However, you can adjust and/or add more keys + as needed. +

+ } + themeValues={Object.keys(breakpointTypes).map((prop) => { + return ( + + updateBreakpoint(prop, value)} + groupProps={{ + alignItems: 'center', + }} + /> + + ); + })} + /> +
+ ); +}; diff --git a/src-docs/src/views/theme/_colors.js b/src-docs/src/views/theme/_colors.js new file mode 100644 index 00000000000..caeffc2a8a6 --- /dev/null +++ b/src-docs/src/views/theme/_colors.js @@ -0,0 +1,506 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { Link } from 'react-router-dom'; +import { useEuiTheme, transparentize } from '../../../../src/services'; + +import { + brand_colors, + brand_text_colors, + shade_colors, + special_colors, + text_colors, +} from '../../../../src/global_styling/variables/_colors'; + +import { + EuiText, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiCode, + EuiIcon, + EuiColorPickerSwatch, + EuiTabbedContent, +} from '../../../../src/components'; + +import { ThemeValue } from './_values'; +import { ThemeSection } from './_theme_section'; + +import { + getPropsFromThemeKey, + EuiThemeColors, + EuiThemeConstantColors, +} from './_props'; +import { makeHighContrastColor } from '../../../../src/services/color/contrast'; +import { EuiCodeBlock } from '../../../../src/components/code'; + +const brandKeys = Object.keys(brand_colors); +const brandTextKeys = Object.keys(brand_text_colors); +const shadeKeys = Object.keys(shade_colors); +const specialKeys = Object.keys(special_colors); +const textKeys = Object.keys(text_colors); + +export default ({ onThemeUpdate }) => { + const { euiTheme, colorMode } = useEuiTheme(); + const colors = euiTheme.colors; + const props = getPropsFromThemeKey(EuiThemeColors); + const constantProps = getPropsFromThemeKey(EuiThemeConstantColors); + + const updateColor = (property, value) => { + onThemeUpdate({ + colors: { + [colorMode]: { + [property]: value, + }, + }, + }); + }; + + return ( +
+ +

+ Colors EuiThemeColors +

+
+ + + + + + +

+ The colors theme key is a mix of hard-coded hex + values and computed colors. The colorMode{' '} + determines which values to return based on{' '} + LIGHT or DARK mode. +

+

+ When switching between light and dark color modes, the theme keys + do not change, only their values do. This is why most keys are not + named for their evaluated value but by their{' '} + purpose. +

+
+
+ + + + +
+
+
+ + + colorMode + + + + +

+ {colorMode} +

+
+
+
+
+
+
+ + + + + +

+ ColorModeSwitch type +

+

+ EUI created this custom type to indicate when a key can accept + either a singular {''} or separate + strings for each color mode. +

+
+
+ + + + {`key: { + LIGHT: , + DARK: , +}`} + + + +
+ + + + + + + + Elastic has two main brand colors. The other three are + used for statefulness like indicating between successful + and dangerous actions. +

+ } + property="colors" + themeValues={brandKeys.map((color) => ( + + } + onUpdate={(hex) => updateColor(color, hex)} + /> + + ))} + /> + + + + + Each brand color also has a corresponding text variant + that has been calculated for proper (4.5) contrast against{' '} + colors.body and should be used + specifically when coloring text. As is used in{' '} + + EuiTextColor + + . +

+ } + property="colors" + themeValues={brandTextKeys.map((color, index) => ( + + updateColor(color, hex)} + example={ + + } + /> + + ))} + /> + + + + + A six-color grayscale palette. Variation beyond these + colors is minimal and always done through computations + against this set. +

+ } + property="colors" + themeValues={shadeKeys.map((color) => ( + + } + onUpdate={(hex) => updateColor(color, hex)} + /> + + ))} + /> + + + + + Specific text colors calculated off either the brand or + shade colors. +

+ } + property="colors" + themeValues={textKeys.map((color) => ( + + updateColor(color, hex)} + example={ + + } + /> + + ))} + /> + + + + These are used a lot for special cases.

} + property="colors" + themeValues={specialKeys.map((color) => { + if (color.includes('Text')) { + return ( + + updateColor(color, hex)} + example={ + + } + /> + + ); + } else { + return ( + + + } + onUpdate={(hex) => updateColor(color, hex)} + /> + + ); + } + })} + /> + + + + These are constant no matter the theme or color mode.

+ } + property="colors" + themeValues={ + <> + + + } + /> + + + + } + /> + + + } + /> + + ), + }, + { + id: 'themeColorsTabUsage', + name: 'Usage', + content: ( + <> + + + Most usages of the colors can be implemented simply by + pulling and applying the values. +

+ } + example={ +
+ background: {euiTheme.colors.warning} +
+ } + snippet={'background: ${euiTheme.colors.warning};'} + /> + + + Since the EUI colors usually evaluate to a hex value, the + easiest way to perform color operations like transparency, + shading, or tinting is by using the EUI provided methods + of transparentize(),{' '} + shade(), and tint(){' '} + respectively. +

+ } + example={ +
+ + background:{' '} + {transparentize(euiTheme.colors.warning, 0.25)} + +
+ } + snippet={ + 'background: ${transparentize(euiTheme.colors.warning, .25)};' + } + /> + + + Remember, when using any of the EUI colors for text, use + the text specific variant. +

+ } + example={ +
+ color: {euiTheme.colors.warningText} +
+ } + snippet={'color: ${euiTheme.colors.warningText};'} + /> + + + If your background color is anything other than or darker + than the body color, you will want to + re-calculate the high contrast version by using the{' '} + + makeHighContrastColor(foreground)(background) + {' '} + method. +

+ } + example={ +
+ + color:{' '} + {makeHighContrastColor(euiTheme.colors.warning)( + euiTheme.colors.darkShade + )} + +
+ } + snippet={`background: \${euiTheme.colors.darkShade}; + color: \${makeHighContrastColor(euiTheme.colors.warning)(euiTheme.colors.darkShade)};`} + /> + + ), + }, + ]} + /> +
+ ); +}; diff --git a/src-docs/src/views/theme/_focus.js b/src-docs/src/views/theme/_focus.js new file mode 100644 index 00000000000..90079e72bfd --- /dev/null +++ b/src-docs/src/views/theme/_focus.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { useEuiTheme } from '../../../../src/services'; + +import { + EuiTitle, + EuiSpacer, + EuiColorPickerSwatch, + EuiFlexItem, + EuiCodeBlock, +} from '../../../../src/components'; + +import { ThemeSection } from './_theme_section'; +import { ThemeValue } from './_values'; + +import { getPropsFromThemeKey, EuiThemeFocus } from './_props'; + +export default ({ onThemeUpdate }) => { + const { euiTheme } = useEuiTheme(); + const focus = euiTheme.focus; + const focusProps = getPropsFromThemeKey(EuiThemeFocus); + + const updateFocus = (property, value) => { + onThemeUpdate({ + focus: { + [property]: value, + }, + }); + }; + + const style = css` + width: ${euiTheme.size.xl}; + height: ${euiTheme.size.xl}; + border-radius: ${euiTheme.border.radius.small}; + `; + + return ( +
+ +

Focus

+
+ + + + + These are general properties that apply to the focus state of + interactable components. Some components have their own specific + implementation, but most use these variables. +

+ } + themeValues={Object.keys(focus).map((prop) => { + const isColor = prop.toLowerCase().includes('color'); + if (prop === 'outline') { + return ( + + + + {`${JSON.stringify( + focus[prop] + ).replace(/[{}"]/g, '')};`} + + ); + } + return ( + + updateFocus(prop, value)} + example={ + isColor ? ( + + ) : undefined + } + colorProps={ + isColor ? { showAlpha: true, format: 'rgba' } : undefined + } + /> + + ); + })} + /> +
+ ); +}; diff --git a/src-docs/src/views/theme/_props.tsx b/src-docs/src/views/theme/_props.tsx new file mode 100644 index 00000000000..c5cd7a7caf2 --- /dev/null +++ b/src-docs/src/views/theme/_props.tsx @@ -0,0 +1,90 @@ +import React, { FunctionComponent } from 'react'; +import { useView } from 'react-view'; +// @ts-ignore NOT TS +import { propUtilityForPlayground } from '../../services/playground'; + +export function getPropsFromThemeKey(component: any) { + const docgenInfo = Array.isArray(component.__docgenInfo) + ? component.__docgenInfo[0] + : component.__docgenInfo; + const { props } = docgenInfo; + // eslint-disable-next-line react-hooks/rules-of-hooks + const params = useView({ props: propUtilityForPlayground(props) }); + return params.knobProps.state; +} + +import { EuiThemeShape } from '../../../../src/services'; + +export const EuiTheme: FunctionComponent = () =>
; + +import { + _EuiThemeColors, + _EuiThemeConstantColors, +} from '../../../../src/global_styling/variables/_colors'; + +export const EuiThemeColors: FunctionComponent<_EuiThemeColors> = () =>
; +export const EuiThemeConstantColors: FunctionComponent<_EuiThemeConstantColors> = () => ( +
+); + +import { EuiThemeSize } from '../../../../src/global_styling/variables/_size'; + +export const _EuiThemeSize: FunctionComponent = () =>
; + +import { + _EuiThemeFontBase, + _EuiThemeFontWeight, + _EuiThemeFontScale, +} from '../../../../src/global_styling/variables/_typography'; + +export const EuiThemeFontBase: FunctionComponent<_EuiThemeFontBase> = () => ( +
+); +export const EuiThemeFontWeight: FunctionComponent<_EuiThemeFontWeight> = () => ( +
+); +export const EuiThemeFontScale: FunctionComponent<_EuiThemeFontScale> = () => ( +
+); + +import { + _EuiThemeBorderColorValues, + _EuiThemeBorderTypes, + _EuiThemeBorderRadiusValues, + _EuiThemeBorderWidthValues, +} from '../../../../src/global_styling/variables/_borders'; + +export const EuiThemeBorderRadiusValues: FunctionComponent<_EuiThemeBorderRadiusValues> = () => ( +
+); +export const EuiThemeBorderWidthValues: FunctionComponent<_EuiThemeBorderWidthValues> = () => ( +
+); +export const EuiThemeBorderColorValues: FunctionComponent<_EuiThemeBorderColorValues> = () => ( +
+); +export const EuiThemeBorderTypes: FunctionComponent<_EuiThemeBorderTypes> = () => ( +
+); + +import { _EuiThemeFocus } from '../../../../src/global_styling/variables/_states'; + +export const EuiThemeFocus: FunctionComponent<_EuiThemeFocus> = () =>
; + +import { + _EuiThemeAnimationSpeed, + _EuiThemeAnimationEasing, +} from '../../../../src/global_styling/variables/_animations'; + +export const EuiThemeAnimationSpeed: FunctionComponent<_EuiThemeAnimationSpeed> = () => ( +
+); +export const EuiThemeAnimationEasing: FunctionComponent<_EuiThemeAnimationEasing> = () => ( +
+); + +import { EuiThemeBreakpoint } from '../../../../src/global_styling/variables/_breakpoint'; + +export const _EuiThemeBreakpoint: FunctionComponent = () => ( +
+); diff --git a/src-docs/src/views/theme/_size.js b/src-docs/src/views/theme/_size.js new file mode 100644 index 00000000000..53049f58c4a --- /dev/null +++ b/src-docs/src/views/theme/_size.js @@ -0,0 +1,204 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { useEuiTheme } from '../../../../src/services'; + +import { + EuiText, + EuiSpacer, + EuiCode, + EuiLink, + EuiTabbedContent, +} from '../../../../src/components'; + +import { useDebouncedUpdate } from './hooks'; +import { getPropsFromThemeKey, EuiTheme, _EuiThemeSize } from './_props'; +import { ThemeSection } from './_theme_section'; +import { ThemeValue } from './_values'; +import { EuiFlexItem } from '../../../../src/components/flex'; + +export default ({ onThemeUpdate }) => { + const { euiTheme } = useEuiTheme(); + const sizes = euiTheme.size; + const base = euiTheme.base; + const [baseClone, updateBase] = useDebouncedUpdate({ + base: 'base', + value: base, + onUpdate: onThemeUpdate, + }); + + const themeProps = getPropsFromThemeKey(EuiTheme); + const themeSizeProps = getPropsFromThemeKey(_EuiThemeSize); + + const wrappingExampleStyle = { + background: euiTheme.colors.highlight, + fontWeight: euiTheme.font.weight.bold, + }; + + return ( +
+ +

Sizing

+

+ All sizing values, including font size, are calculated from a single{' '} + base integer and converted to pixel or rem string + values. +

+
+ + + + + + This base integer sets the scale for + the entire theme. Adjust cautiously. +

+ } + themeValues={ + + updateBase('base', value)} + /> + + } + /> + + + It is not recommended to adjust the computed sizes but to + only adjust the top level base value in order to keep + proper proportions. +

+ } + property="size" + themeValues={Object.keys(sizes).map((size) => ( + + + + ))} + /> + + ), + }, + { + id: 'themeSizingTabUsage', + name: 'Usage', + content: ( + <> + + + You can use calculations on top of the base value, just be + sure to append the px unit to the end. +

+ } + example={ +
+ {`padding: ${euiTheme.base * 2}px`} +
+ } + snippet={'padding: ${euiTheme.base * 2}px;'} + /> + + + Using the named keys as values is straight forward.

+ } + example={ +
+ {`padding: ${euiTheme.size.xl}`} +
+ } + snippet={'padding: ${euiTheme.size.xl};'} + /> + + + + +

+ When doing calculations on top of the named key values, + you have to use the{' '} + + CSS calc() method + {' '} + because the value that is returned is a string value + with the appended unit. +

+ + } + example={ +
+ {`padding: calc(${euiTheme.size.base} * 2)`} +
+ } + snippet={'padding: calc(${euiTheme.size.base} * 2);'} + /> + + ), + }, + ]} + /> +
+ ); +}; diff --git a/src-docs/src/views/theme/_theme_section.tsx b/src-docs/src/views/theme/_theme_section.tsx new file mode 100644 index 00000000000..23b85a412b1 --- /dev/null +++ b/src-docs/src/views/theme/_theme_section.tsx @@ -0,0 +1,83 @@ +import React, { FunctionComponent, ReactNode } from 'react'; +import { EuiCode, EuiCodeBlock } from '../../../../src/components/code'; +import { EuiFlexGroup, EuiFlexItem } from '../../../../src/components/flex'; +import { EuiText } from '../../../../src/components/text'; +import { EuiSplitPanel, EuiPanel } from '../../../../src/components/panel'; +import { GuideSectionExample } from '../../components/guide_section/guide_section_parts/guide_section_example'; + +export const LANGUAGES = ['javascript', 'html'] as const; + +type ThemeSection = { + code?: string; + description?: ReactNode; + themeValues?: ReactNode; + property?: string; + example?: GuideSectionExample['example']; + snippet?: GuideSectionExample['tabContent']; + customSnippet?: string; +}; + +export const ThemeSection: FunctionComponent = ({ + code, + description, + themeValues, + example, + snippet, + customSnippet, +}) => { + const finalSnippet = customSnippet + ? customSnippet + : `css\` + ${snippet} +\``; + + return ( + + + + {code && ( +

+ + {code} + +

+ )} + {description} +
+
+ {themeValues && ( + + + + {themeValues} + + + + )} + {example && ( + + + {example} + + {finalSnippet && ( + + {finalSnippet} + + )} + + + + )} +
+ ); +}; diff --git a/src-docs/src/views/theme/_typography.js b/src-docs/src/views/theme/_typography.js new file mode 100644 index 00000000000..837b3c71864 --- /dev/null +++ b/src-docs/src/views/theme/_typography.js @@ -0,0 +1,347 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { css } from '@emotion/react'; +import { useEuiTheme } from '../../../../src/services'; + +import { + EuiText, + EuiSpacer, + EuiFlexItem, + EuiCode, + EuiLink, + EuiTabbedContent, +} from '../../../../src/components'; + +import { + fontWeight, + fontScale, +} from '../../../../src/global_styling/variables/_typography'; + +import { ThemeValue } from './_values'; +import { ThemeSection } from './_theme_section'; + +import { + getPropsFromThemeKey, + EuiThemeFontBase, + EuiThemeFontWeight, + EuiThemeFontScale, +} from './_props'; + +import { useDebouncedUpdate } from './hooks'; + +const weightKeys = Object.keys(fontWeight); +const scaleKeys = Object.keys(fontScale); + +export default ({ onThemeUpdate }) => { + const { euiTheme } = useEuiTheme(); + const font = euiTheme.font; + + const [fontClone, updateFont] = useDebouncedUpdate({ + property: 'font', + value: font, + onUpdate: onThemeUpdate, + }); + const [scaleClone, updateScale] = useDebouncedUpdate({ + property: ['font', 'scale'], + value: font, + onUpdate: onThemeUpdate, + }); + const [weightClone, updateWeight] = useDebouncedUpdate({ + property: ['font', 'weight'], + value: font, + onUpdate: onThemeUpdate, + }); + + const baseProps = getPropsFromThemeKey(EuiThemeFontBase); + const weightProps = getPropsFromThemeKey(EuiThemeFontWeight); + const scaleProps = getPropsFromThemeKey(EuiThemeFontScale); + + const fontFamilies = fontClone.family.split(','); + const codeFontFamilies = fontClone.familyCode.split(','); + + return ( +
+ +

+ Typography EuiThemeFont +

+

+ The typography specific theme keys start with the{' '} + font key. +

+
+ + + + + + + +

+ The base font settings determine things like{' '} + family and{' '} + featureSettings. +

+

+ The lineHeightMultiplier establishes + the line-height in percentages compared to the + font-size, but it is the baseline{' '} + integer that establishes the final pixel/rem value by + ensuring it falls on a multiplier of this baseline. +

+ + } + property="font" + themeValues={ + <> + + { + const out = [...fontFamilies]; + out.splice(0, 1, value); + updateFont('family', out.join(',')); + }} + /> + + {/* The loop below renders each font family applied to a span. */} + + {fontFamilies.map((family, i) => ( + + {family} + {i < fontFamilies.length - 1 ? ', ' : ''} + + ))} + + + + { + const out = [...codeFontFamilies]; + out.splice(0, 1, value); + updateFont('familyCode', out.join(',')); + }} + /> + + {/* The loop below renders each font family applied to a span. */} + + {codeFontFamilies.map((family, i) => ( + + {family} + {i < codeFontFamilies.length - 1 ? ', ' : ''} + + ))} + + + + + + + {font.featureSettings} + + + + updateFont('baseline', value)} + /> + + + + updateFont('lineHeightMultiplier', value) + } + numberProps={{ step: 0.1 }} + /> + + + } + /> + + + + +

+ Matches up colloqual weight names with their appropriate + number values. +

+

+ These default weights are what is manually pulled from + Google fonts. If you intend to change these numbers, + switch to a variable font or change your font import to + include those you've selected. +

+ + } + property="font" + themeValues={weightKeys.map((key) => ( + + updateWeight(key, value)} + numberProps={{ step: 10 }} + /> + + ))} + /> + + + + +

+ The typographic scale that is used to calculate the font + size variables. These are multipliers applied the{' '} + euiTheme.base value. +

+

+ The default scale is loosely based on the{' '} + + Major Third (1.250) typographic scale + + . +

+

+ For regular text sizing, we recommend using the{' '} + + EuiText + {' '} + component directly and sticking to the sizing props + provided. +

+ + } + property="font" + themeValues={scaleKeys.map((key) => ( + + updateScale(key, value)} + numberProps={{ step: 0.1, style: { width: '6em' } }} + groupProps={{ alignItems: 'center' }} + /> + + ))} + /> + + ), + }, + { + id: 'themeTypographyTabUsage', + name: 'Usage', + content: ( + <> + + + All of EUI defaults to the base{' '} + font.family. However, you change + certain instance to other available families like{' '} + font.familyCode. +

+ } + example={ +

+ {'I am a paragraph rendered using the code font family'} +

+ } + snippet={'font-family: ${euiTheme.font.familyCode};'} + /> + + + To maintain consistency, EUI establishes the font weight + patterns directly in the text and title components. + However, we recommend using the theme keys instead of + font-weight: bold in + your css to ensure proper rendering with the imported font + family. +

+ } + example={ +
+ {'I am proper bold'} +
+ } + snippet={'font-weight: ${euiTheme.font.weight.bold};'} + /> + + + ), + }, + ]} + /> +
+ ); +}; diff --git a/src-docs/src/views/theme/_values.tsx b/src-docs/src/views/theme/_values.tsx new file mode 100644 index 00000000000..9cbe2674dde --- /dev/null +++ b/src-docs/src/views/theme/_values.tsx @@ -0,0 +1,213 @@ +import React, { + FunctionComponent, + ReactElement, + ReactNode, + useCallback, +} from 'react'; +import { css, SerializedStyles } from '@emotion/react'; +import debounce from 'lodash/debounce'; +import { EuiCode } from '../../../../src/components/code'; +import { + EuiColorPicker, + EuiColorPickerProps, +} from '../../../../src/components/color_picker'; +import { EuiSpacer } from '../../../../src/components/spacer'; +import { + EuiFlexGroup, + EuiFlexGroupProps, + EuiFlexItem, +} from '../../../../src/components/flex'; +import { EuiText } from '../../../../src/components/text'; +import { + EuiFieldNumber, + EuiFieldNumberProps, + EuiFieldText, + EuiFieldTextProps, +} from '../../../../src/components/form'; +import { + isValidHex, + useColorPickerState, + EuiSetColorMethod, + useEuiTheme, +} from '../../../../src/services'; +// @ts-ignore NOT TS yet +import { humanizeType, markup } from '../../services/playground/knobs'; +import { EuiCopy } from '../../../../src/components/copy'; + +export const LANGUAGES = ['javascript', 'html'] as const; + +type ThemeValue = { + property: string; + name: string; + value?: ReactNode; + example?: ReactNode; + groupProps?: EuiFlexGroupProps; + buttonStyle?: SerializedStyles; + onUpdate?: (color: string | number) => void; + type?: any; + numberProps?: EuiFieldNumberProps; + stringProps?: EuiFieldTextProps; + colorProps?: Partial; +}; + +export const ThemeValue: FunctionComponent = ({ + property, + name, + value, + example, + groupProps, + buttonStyle, + onUpdate, + type, + numberProps, + stringProps, + colorProps, +}) => { + const { euiTheme } = useEuiTheme(); + + const [color, setColor, errors] = useColorPickerState( + isValidHex(String(value)) ? String(value) : '' + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedOnUpdate = useCallback( + debounce<(...args: any[]) => void>((hex, isValid) => { + if (isValid) { + onUpdate && onUpdate(hex); + } + }, 100), + [onUpdate] + ); + + const handleColorChange: EuiSetColorMethod = (text, { hex, isValid }) => { + setColor(text, { hex, isValid }); + debouncedOnUpdate(hex, isValid); + }; + let exampleRender; + if ( + (property === 'colors' || name.toLowerCase().includes('color')) && + onUpdate + ) { + exampleRender = ( + + + + ); + } else if (example || buttonStyle) { + exampleRender = ( + + {example} + + ); + } + + let typeRender: ReactNode; + if (type?.custom?.origin?.type) { + typeRender = ( + + : {humanizeType(type.custom.origin.type)} + + ); + } + + let descriptionRender; + if (type?.description) { + descriptionRender = ( + <> + + + {markup(type.description)} + + + ); + } + + let valueRender; + if (typeof value === 'number' && onUpdate) { + valueRender = ( + onUpdate(Number(e.target.value))} + style={{ width: 64 }} + {...numberProps} + /> + ); + } else if ( + property !== 'colors' && + !name.toLowerCase().includes('color') && + typeof value === 'string' && + onUpdate + ) { + valueRender = ( + onUpdate(e.target.value)} + style={{ width: 120 }} + {...stringProps} + /> + ); + } else { + valueRender = ( + + {value} + + ); + } + + name = property ? `${property}.${name}` : name; + + return ( + + + + + {(copy) => ( + + )} + + + {descriptionRender} + + {valueRender} + {exampleRender} + + ); +}; diff --git a/src-docs/src/views/theme/computed.tsx b/src-docs/src/views/theme/computed.tsx new file mode 100644 index 00000000000..266da5ff125 --- /dev/null +++ b/src-docs/src/views/theme/computed.tsx @@ -0,0 +1,49 @@ +import React, { FunctionComponent, ReactNode } from 'react'; +import { EuiIcon } from '../../../../src/components/icon'; +import { EuiCode } from '../../../../src/components/code'; +import { EuiText } from '../../../../src/components/text'; +import { EuiThemeProvider, useEuiTheme } from '../../../../src/services'; + +const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { + const { euiTheme } = useEuiTheme(); + + return ( + +

+ {children} +

+
+ ); +}; + +export default () => { + const primaryOverrides = { + colors: { + LIGHT: { + primary: '#db1dde', + }, + DARK: { + primary: '#e378e4', + }, + }, + }; + + return ( +
+ + + The colors.primary value has been changed to{' '} + #db1dde (#e378e4 for dark mode) + and so the calculated value of colors.primaryText{' '} + has also been updated. + + +
+ ); +}; diff --git a/src-docs/src/views/theme/consuming.tsx b/src-docs/src/views/theme/consuming.tsx new file mode 100644 index 00000000000..31916fec0df --- /dev/null +++ b/src-docs/src/views/theme/consuming.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { EuiSpacer } from '../../../../src/components/spacer'; +import { EuiIcon } from '../../../../src/components/icon'; +import { EuiCode } from '../../../../src/components/code'; +import { EuiText } from '../../../../src/components/text'; +import { useEuiTheme } from '../../../../src/services'; + +export default () => { + const { euiTheme } = useEuiTheme(); + return ( + +

+ {' '} + This primary color will adjust based on the light or dark theme value +

+ +
+

+ The padding of this box is created using calc(){' '} + because EUI's theme sizes are string pixel values that are + calculated off the theme's base +

+
+
+ ); +}; diff --git a/src-docs/src/views/theme/consuming_hoc.tsx b/src-docs/src/views/theme/consuming_hoc.tsx new file mode 100644 index 00000000000..8dc9237774e --- /dev/null +++ b/src-docs/src/views/theme/consuming_hoc.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { withEuiTheme, WithEuiThemeProps } from '../../../../src/services'; +import { EuiIcon } from '../../../../src/components/icon'; +import { EuiText } from '../../../../src/components/text'; + +// eslint-disable-next-line react/prefer-stateless-function +class Block extends React.Component { + render() { + const { theme } = this.props; + + const divStyle = css` + background: ${theme.euiTheme.colors.lightShade}; + padding: ${theme.euiTheme.size.xl}; + border-radius: ${theme.euiTheme.border.radius.medium}; + `; + + return ( + +

+ This box has it's background, + padding, and border-radius controlled by custom css +

+
+ ); + } +} + +export const ConsumingHOC = withEuiTheme(Block); diff --git a/src-docs/src/views/theme/create_computed.tsx b/src-docs/src/views/theme/create_computed.tsx new file mode 100644 index 00000000000..7b91fae3220 --- /dev/null +++ b/src-docs/src/views/theme/create_computed.tsx @@ -0,0 +1,81 @@ +import React, { FunctionComponent, ReactNode } from 'react'; +import { EuiIcon } from '../../../../src/components/icon'; +import { EuiCode } from '../../../../src/components/code'; +import { EuiText } from '../../../../src/components/text'; +import { + computed, + EuiThemeProvider, + useEuiTheme, +} from '../../../../src/services'; +import { shade, tint } from '../../../../src/services/color'; + +interface ThemeExtensions { + colors: { + customColorPrimary: string; + customColorPrimaryHighlight: string; + customColorPrimaryText: string; + }; +} + +const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { + const { euiTheme } = useEuiTheme(); + + return ( + +

+ {' '} + {children} +

+
+ ); +}; + +export default () => { + const primaryOverrides = { + colors: { + LIGHT: { + customColorPrimary: 'rgb(29, 222, 204)', + customColorPrimaryHighlight: computed( + (customColorPrimary) => tint(customColorPrimary, 0.8), + 'colors.customColorPrimary' + ), + customColorPrimaryText: computed( + (customColorPrimary) => shade(customColorPrimary, 0.8), + 'colors.customColorPrimary' + ), + }, + DARK: { + customColorPrimary: 'rgb(29, 222, 204)', + customColorPrimaryHighlight: computed( + ([customColorPrimary]) => shade(customColorPrimary, 0.8), + ['colors.customColorPrimary'] + ), + customColorPrimaryText: computed( + ([customColorPrimary]) => tint(customColorPrimary, 0.8), + ['colors.customColorPrimary'] + ), + }, + }, + }; + + return ( +
+ + + A new key of customColorPrimary has been added as{' '} + rgb(29, 222, 204). +
+
+ There is also two new computed color keys create off of this for + better contrast. +
+
+
+ ); +}; diff --git a/src-docs/src/views/theme/hooks.tsx b/src-docs/src/views/theme/hooks.tsx new file mode 100644 index 00000000000..fa5acefa15c --- /dev/null +++ b/src-docs/src/views/theme/hooks.tsx @@ -0,0 +1,72 @@ +import { useCallback, useEffect, useState } from 'react'; +import debounce from 'lodash/debounce'; +import { mergeDeep } from '../../../../src/services'; +import { ExclusiveUnion } from '../../../../src/components/common'; + +type Base = ExclusiveUnion<{ property: string | string[] }, { base: string }>; + +type Params = Base & { + value: object; + onUpdate: (args: object) => void; + time?: number; +}; + +export const useDebouncedUpdate = ({ + base, + property, + value, + onUpdate, + time = 300, +}: Params) => { + const [valueClone, setValueClone] = useState(value); + + useEffect(() => { + setValueClone(value); + }, [value]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedUpdate = useCallback( + debounce((key: string, value: any) => { + let obj = { + [base ?? key]: value, + }; + if (property) { + obj = Array.isArray(property) + ? { + [property[0]]: { + [property[1]]: { + [key]: value, + }, + }, + } + : { + [property]: { + [key]: value, + }, + }; + } + onUpdate(obj); + }, time), + [onUpdate] + ); + const updateValue = (key: string, value: any) => { + if (!property) { + setValueClone(value); + debouncedUpdate(key, value); + } else { + const obj = Array.isArray(property) + ? { + [property[1]]: { + [key]: value, + }, + } + : { + [key]: value, + }; + setValueClone(mergeDeep(valueClone, obj)); + debouncedUpdate(key, value); + } + }; + + return [valueClone, updateValue]; +}; diff --git a/src-docs/src/views/theme/inverse.tsx b/src-docs/src/views/theme/inverse.tsx new file mode 100644 index 00000000000..3cc4cb8743b --- /dev/null +++ b/src-docs/src/views/theme/inverse.tsx @@ -0,0 +1,48 @@ +import React, { FunctionComponent, ReactNode } from 'react'; +import { EuiIcon } from '../../../../src/components/icon'; +import { EuiSpacer } from '../../../../src/components/spacer'; +import { EuiText } from '../../../../src/components/text'; +import { EuiThemeProvider, useEuiTheme } from '../../../../src/services'; + +const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { + const { euiTheme } = useEuiTheme(); + + return ( + +

{children}

+
+ ); +}; + +export default () => { + return ( +
+ + + The colors of this box are will always be + in light mode + + + + + + The colors of this box are will always be + in dark mode + + + + + + The colors of this box are the opposite ( + inverse) of the current color mode + + +
+ ); +}; diff --git a/src-docs/src/views/theme/override_simple.tsx b/src-docs/src/views/theme/override_simple.tsx new file mode 100644 index 00000000000..93f1d2f364e --- /dev/null +++ b/src-docs/src/views/theme/override_simple.tsx @@ -0,0 +1,39 @@ +import React, { FunctionComponent, ReactNode } from 'react'; +import { EuiCode } from '../../../../src/components/code'; +import { EuiText } from '../../../../src/components/text'; +import { EuiThemeProvider, useEuiTheme } from '../../../../src/services'; + +const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { + const { euiTheme } = useEuiTheme(); + + return ( + +

{children}

+
+ ); +}; + +export default () => { + const overrides = { + colors: { + LIGHT: { lightShade: '#d3e6df' }, + DARK: { lightShade: '#394c4b' }, + }, + }; + + return ( +
+ + + The background of this box is using the locally overridden value of{' '} + euiTheme.colors.lightShade + + +
+ ); +}; diff --git a/src-docs/src/views/theme/theme_example.js b/src-docs/src/views/theme/theme_example.js new file mode 100644 index 00000000000..f1f98e8b92f --- /dev/null +++ b/src-docs/src/views/theme/theme_example.js @@ -0,0 +1,252 @@ +import React from 'react'; + +import { GuideSectionTypes } from '../../components'; + +import { + EuiText, + EuiSpacer, + EuiCallOut, + EuiCode, + EuiLink, +} from '../../../../src/components'; +import { EuiThemeProvider } from '../../../../src/services'; + +import Consuming from './consuming'; +const consumingSource = require('!!raw-loader!./consuming'); + +import { ConsumingHOC } from './consuming_hoc'; +const consumingHOCSource = require('!!raw-loader!./consuming_hoc'); + +import Inverse from './inverse'; +const InverseSource = require('!!raw-loader!./inverse'); + +import OverrideSimple from './override_simple'; +const overrideSimpleSource = require('!!raw-loader!./override_simple'); + +import Computed from './computed'; +const computedSource = require('!!raw-loader!./computed'); + +import CreateComputed from './create_computed'; +const createComputedSource = require('!!raw-loader!./create_computed'); + +export const ThemeExample = { + title: 'Theme provider', + isNew: true, + beta: true, + intro: ( + <> + +

+ EUI is in the progress of switching it's core styles processor + from Sass to Emotion. It + requires that all consumer applications wrap their core application + with EuiThemeProvider. +

+
+ + + + ), + sections: [ + { + title: 'EuiThemeProvider', + text: ( + <> +

+ The context layer that enables theming (including the default theme + styles) comes from EuiThemeProvider. It is a thin + wrapper around and caching layer built onto{' '} + React.Context.Provider. +

+

+ Typically your app will only need a single instance at the top level + and the functionality will flow down the component tree. It is also + possible to use several nested theme providers. In this case each + nested provider will inherit from its closest ancestor provider. +

+

+ EuiThemeProvider accepts three props, all of + which have default values and are therefore optional. To use the + default EUI theme, no configuration is required. +

+
    +
  • + theme: EuiThemeSystem Raw theme + values. Calculated values are acceptable. +
  • +
  • + colorMode: EuiThemeColorMode{' '} + Simply {"'light'"} or {"'dark'"} +
  • +
  • + modify: EuiThemeModifications{' '} + Overrides and modifications for theme values. +
  • +
+

+ The concept for each prop is explained in subsequent sections. More + information on the full shape of an EUI theme, see the{' '} + Global Values{' '} + page. +

+ + ), + props: { EuiThemeProvider }, + }, + { + title: 'Consuming with the React hook', + source: [ + { + type: GuideSectionTypes.JS, + code: consumingSource, + }, + ], + text: ( + <> +

+ Using the react hook useEuiTheme() makes it very + easy to consume the EUI static and computed variables like colors + and sizing. It simply passes back an object of the current theme + which includes +

+
    +
  • + euiTheme: EuiThemeComputed All + the calculated keys including any modifications +
  • +
  • + colorMode: EuiThemeColorMode{' '} + Simply {"'light'"} or {"'dark'"} +
  • +
  • + + modifications: EuiThemeModifications + {' '} + Only the modification keys +
  • +
+

+ When consuming the theme's keys like{' '} + euiTheme.colors.primary, you'll want to pass + them via the css property to take advantage of + Emotion's compilation. +

+ + ), + demo: , + }, + { + title: 'Consuming with the React HOC', + source: [ + { + type: GuideSectionTypes.JS, + code: consumingHOCSource, + }, + ], + text: ( + <> +

+ When using inside of a React Component, you can wrap your exported + component with withEuiTheme(). +

+ + ), + demo: , + }, + { + title: 'Rendering a specific color mode', + source: [ + { + type: GuideSectionTypes.JS, + code: InverseSource, + }, + ], + text: ( + <> +

+ While it is usually best to keep all consumptions of the global + variables rendering in the same light or dark color mode, some + instances benefit from an exaggerated change in contrast from the + current theme. For this you can specify{' '} + EuiThemeProvider's{' '} + colorMode to always be{' '} + {'"light"'}, {'"dark"'}, or{' '} + {'"inverse"'} which sets it to the opposite of + the current color mode. +

+ + ), + demo: , + }, + { + title: 'Simple instance overrides', + source: [ + { + type: GuideSectionTypes.JS, + code: overrideSimpleSource, + }, + ], + text: ( + <> +

+ Usually, you won't need to actually override an EUI theme + variable at the instance level. Instead, you'd just create a + new variable local to that component. However, if you cannot alter + the component that is using the EUI variable then you can wrap that + component with the EuiThemeProvider and pass your + custom object to modify. +

+ + ), + demo: , + }, + { + title: 'Understanding computed values', + source: [ + { + type: GuideSectionTypes.JS, + code: computedSource, + }, + ], + text: ( + <> +

+ The benefit of EUI's theme structure is that it only hard-codes + a few color and size variables. The rest are{' '} + computed values based on this base few. When you + update a core variable, this will cascade into the other computed + values. +

+

+ For instance, we compute text variants of our base colors. So + locally overriding the colors.primary color will + automatically cascade to the colors.primaryText. + You can however, directly override computed values as well by + passing a custom value to this theme variable. +

+ + ), + demo: , + }, + { + title: 'Creating custom keys', + source: [ + { + type: GuideSectionTypes.JS, + code: createComputedSource, + }, + ], + text: ( + <> +

+ Because of the computed values and possible cascade effects, it may + not be advisable to locally override any EUI + specific theme variables. Instead, you should append custom keys to + the theme. +

+ + ), + demo: , + }, + ], +}; diff --git a/src-docs/src/views/theme/values.js b/src-docs/src/views/theme/values.js new file mode 100644 index 00000000000..1cf8c3df3d6 --- /dev/null +++ b/src-docs/src/views/theme/values.js @@ -0,0 +1,152 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + useEuiTheme, + mergeDeep, + EuiThemeProvider, +} from '../../../../src/services'; + +import { GuidePage } from '../../components'; + +import Colors from './_colors'; +import Size from './_size'; +import Typography from './_typography'; +import Border from './_border'; +import Animation from './_animation'; +import Breakpoints from './_breakpoints'; + +import { + EuiSpacer, + EuiCodeBlock, + EuiBottomBar, + EuiFlexGroup, + EuiFlexItem, + EuiCode, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, +} from '../../../../src/components'; +import { EuiHorizontalRule } from '../../../../src/components/horizontal_rule'; +import { EuiButton, EuiButtonEmpty } from '../../../../src/components/button'; +import { EuiCopy } from '../../../../src/components/copy'; +import { EuiCallOut } from '../../../../src/components/call_out'; + +const JsonFlyout = ({ setIsOpen }) => { + const { euiTheme } = useEuiTheme(); + return ( + setIsOpen(false)}> + + +

Calculated EuiTheme JSON

+
+
+ + + {JSON.stringify(euiTheme, null, 2)} + + +
+ ); +}; + +export default () => { + const [jsonFlyoutIsOpen, setJsonFlyoutIsOpen] = React.useState(false); + const [overrides, setOverrides] = React.useState({}); + + const updateTheme = (newOverrides) => { + setOverrides(mergeDeep(overrides, newOverrides)); + }; + + const clearOverrides = () => { + setOverrides({}); + }; + + return ( + + +

+ The euiTheme() hook is only available for + consuming the values. Modifying or overriding the values will not + have any effect on the individual EUI components, yet. Instead, + you still need to use the{' '} + Sass method. +

+
+ } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {Object.keys(overrides).length > 0 && ( + <> + + + Clear overrides + + + + + {(copy) => ( + + Copy overrides + + )} + + + + )} + + + setJsonFlyoutIsOpen(true)} + color="ghost" + > + View theme JSON + + + + + + + {jsonFlyoutIsOpen && } + + + ); +}; diff --git a/src/components/color_picker/_color_picker_swatch.scss b/src/components/color_picker/_color_picker_swatch.scss index a9fbbe25751..ba8c5adebfd 100644 --- a/src/components/color_picker/_color_picker_swatch.scss +++ b/src/components/color_picker/_color_picker_swatch.scss @@ -8,7 +8,11 @@ border: solid 1px transparentize($euiColorFullShade, .9); box-shadow: inset 0 0 0 1px transparentize($euiColorEmptyShade, .95); + &:disabled { + cursor: default; + } + &:focus { @include euiFocusRing; } -} \ No newline at end of file +} diff --git a/src/components/common.ts b/src/components/common.ts index e5b7376716e..9cb88edb806 100644 --- a/src/components/common.ts +++ b/src/components/common.ts @@ -47,6 +47,13 @@ export function keysOf(obj: T): K[] { return Object.keys(obj) as K[]; } +/** + * Like `keyof typeof`, but for getting values instead of keys + * ValueOf + * Results in `'value1' | 'value2'` + */ +export type ValueOf = T[keyof T]; + export type PropsOf = C extends SFC ? SFCProps : C extends FunctionComponent @@ -97,6 +104,15 @@ export type DistributivePick> = T extends any export type DistributiveOmit> = T extends any ? Omit> : never; +type RecursiveDistributiveOmit = T extends any + ? T extends object + ? RecursiveOmit + : T + : never; +export type RecursiveOmit = Omit< + { [P in keyof T]: RecursiveDistributiveOmit }, + K +>; /* TypeScript's discriminated unions are overly permissive: as long as one type of the union is satisfied @@ -215,8 +231,8 @@ export type RecursivePartial = { ? T[P] : T[P] extends Array ? Array> - : T[P] extends ReadonlyArray // eslint-disable-line @typescript-eslint/array-type - ? ReadonlyArray> // eslint-disable-line @typescript-eslint/array-type + : T[P] extends ReadonlyArray + ? ReadonlyArray> : T[P] extends Set // checks for Sets ? Set> : T[P] extends Map // checks for Maps diff --git a/src/global_styling/mixins/_helpers.ts b/src/global_styling/mixins/_helpers.ts new file mode 100644 index 00000000000..83138f69804 --- /dev/null +++ b/src/global_styling/mixins/_helpers.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chroma from 'chroma-js'; +import { useEuiTheme } from '../../services/theme/hooks'; +import { transparentize } from '../../services/color'; +import { useOverflowShadow } from './_shadow'; + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +// Useful border shade when dealing with images of unknown color. +export const useInnerBorder = ({ + type = 'dark', + borderRadius = 0, + alpha = 0.1, +}: { + type?: 'light' | 'dark'; + borderRadius?: number; + alpha?: number; +}) => { + const { + euiTheme: { colors }, + } = useEuiTheme(); + const color = chroma( + type === 'dark' ? colors.darkestShade : colors.emptyShade + ) + .alpha(alpha) + .css(); + + return ` + position: relative; + + &:after { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: ${borderRadius}; + content: ''; + pointer-events: none; + border: 1px solid ${color}; + } + `; +}; + +// Set scroll bar appearance on Chrome (and firefox). +export const useScrollBar = ({ + thumbColor: _thumbColor, + trackBackgroundColor: _trackBackgroundColor, +}: { + thumbColor?: string; + trackBackgroundColor?: string; +} = {}) => { + const { + euiTheme: { colors, size }, + } = useEuiTheme(); + const thumbColor = _thumbColor || colors.darkShade; + const trackBackgroundColor = _trackBackgroundColor || 'transparent'; + // Firefox's scrollbar coloring cascades, but the sizing does not, + // so it's being added to this mixin for allowing support wherever custom scrollbars are + return ` + scrollbar-width: thin; + &::-webkit-scrollbar { + width: ${size.base}; + height: ${size.base}; + } + &::-webkit-scrollbar-thumb { + background-color: ${transparentize(thumbColor, 0.5)}; + border: calc(${size.base} * 0.75) solid ${trackBackgroundColor}; + background-clip: content-box; + } + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: ${trackBackgroundColor}; + } + `; +}; + +/** + * 1. Focus rings shouldn't be visible on scrollable regions, but a11y requires them to be focusable. + * Browser's supporting `:focus-visible` will still show outline on keyboard focus only. + * Others like Safari, won't show anything at all. + */ + +// Just overflow and scrollbars +export const useYScroll = () => ` + ${useScrollBar()} + height: 100%; + overflow-y: auto; + overflow-x: hidden; + &:focus { + outline: none; /* 1 */ + } +`; +export const useXScroll = () => ` + ${useScrollBar()} + overflow-x: auto; + + &:focus { + outline: none; /* 1 */ + } +`; + +// // The full overflow with shadow +export const useYScrollWithShadows = () => ` + ${useYScroll()} + ${useOverflowShadow({ direction: 'y' })} +`; + +export const useXScrollWithShadows = () => ` + ${useXScroll()} + ${useOverflowShadow({ direction: 'x' })} +`; + +// Hiding elements offscreen to only be read by screen reader +export const useScreenReaderOnly = () => ` + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; +`; + +// Doesn't have reduced motion turned on +export const useCanAnimate = (content: string) => ` + @media screen and (prefers-reduced-motion: no-preference) { + ${content} + } +`; diff --git a/src/global_styling/mixins/_shadow.ts b/src/global_styling/mixins/_shadow.ts new file mode 100644 index 00000000000..47883404d2e --- /dev/null +++ b/src/global_styling/mixins/_shadow.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chroma from 'chroma-js'; +import { useEuiTheme } from '../../services/theme/hooks'; +import { lightness, tint, transparentize } from '../../services/color'; + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +export const useSlightShadow = ({ + color, + opacity, +}: { + color?: string; + opacity?: number; +} = {}) => { + const { + euiTheme: { colors }, + } = useEuiTheme(); + const rgba = chroma(color || colors.shadow) + .alpha(opacity || 0.3) + .css(); + return `box-shadow: 0 2px 2px -1px ${rgba};`; +}; + +export const useBottomShadowSmall = ({ + color, + opacity, +}: { + color?: string; + opacity?: number; +} = {}) => { + const { + euiTheme: { colors }, + } = useEuiTheme(); + const rgba = chroma(color || colors.shadow) + .alpha(opacity || 0.3) + .css(); + return ` + box-shadow: + 0 2px 2px -1px ${rgba}, + 0 1px 5px -2px ${rgba}; + `; +}; + +export const useBottomShadowMedium = ({ + color, + opacity, +}: { + color?: string; + opacity?: number; +} = {}) => { + const { + euiTheme: { colors }, + } = useEuiTheme(); + const rgba = chroma(color || colors.shadow) + .alpha(opacity || 0.2) + .css(); + return ` + box-shadow: + 0 6px 12px -1px ${rgba}, + 0 4px 4px -1px ${rgba}, + 0 2px 2px 0 ${rgba}; + `; +}; + +// Similar to shadow medium but without the bottom depth. Useful for popovers +// that drop UP rather than DOWN. +export const useBottomShadowFlat = ({ + color, + opacity, +}: { + color?: string; + opacity?: number; +} = {}) => { + const { + euiTheme: { colors }, + } = useEuiTheme(); + const rgba = chroma(color || colors.shadow) + .alpha(opacity || 0.2) + .css(); + return ` + box-shadow: + 0 0 12px -1px ${rgba}, + 0 0 4px -1px ${rgba}, + 0 0 2px 0 ${rgba}; + `; +}; + +// adjustBorder allows the border color to match the drop shadow better so that there's better +// distinction between element bounds and the shadow (crisper borders) +export const useBottomShadow = ({ + color: _color, + opacity, + adjustBorders, +}: { + color?: string; + opacity?: number; + adjustBorders?: boolean; +} = {}) => { + const { + euiTheme: { border, colors }, + } = useEuiTheme(); + const color = _color || colors.shadow; + const rgba = chroma(color) + .alpha(opacity || 0.2) + .css(); + + const adjustedBorders = + adjustBorders && !(lightness(border.color) < 50) + ? ` + border-color: ${tint(color, 0.75)}; + border-top-color: ${tint(color, 0.8)}; + border-bottom-color: ${tint(color, 0.55)}; + ` + : ''; + + return ` + box-shadow: + 0 12px 24px 0 ${rgba}, + 0 6px 12px 0 ${rgba}, + 0 4px 4px 0 ${rgba}, + 0 2px 2px 0 ${rgba}; + ${adjustedBorders} + `; +}; + +export const useBottomShadowLarge = ({ + color: _color, + opacity, + adjustBorders, + reverse, +}: { + color?: string; + opacity?: number; + adjustBorders?: boolean; + reverse?: boolean; +} = {}) => { + const { + euiTheme: { border, colors }, + } = useEuiTheme(); + const color = _color || colors.shadow; + const rgba = chroma(color) + .alpha(opacity || 0.1) + .css(); + + // Never adjust borders if the border color is already on the dark side (dark theme) + const adjustedBorders = + adjustBorders && !(lightness(border.color) < 50) + ? ` + border-color: ${tint(color, 0.75)}; + border-top-color: ${tint(color, 0.8)}; + border-bottom-color: ${tint(color, 0.55)}; + ` + : ''; + + if (reverse) { + return ` + box-shadow: + 0 -40px 64px 0 ${rgba}, + 0 -24px 32px 0 ${rgba}, + 0 -16px 16px 0 ${rgba}, + 0 -8px 8px 0 ${rgba}; + ${adjustedBorders} + `; + } else { + return ` + box-shadow: + 0 40px 64px 0 ${rgba}, + 0 24px 32px 0 ${rgba}, + 0 16px 16px 0 ${rgba}, + 0 8px 8px 0 ${rgba}, + 0 4px 4px 0 ${rgba}, + 0 2px 2px 0 ${rgba}; + ${adjustedBorders} + `; + } +}; + +export const useSlightShadowHover = ({ + color, + opacity: _opacity, +}: { + color?: string; + opacity?: number; +} = {}) => { + const { + euiTheme: { colors }, + } = useEuiTheme(); + const opacity = _opacity || 0.3; + const rgba1 = chroma(color || colors.shadow) + .alpha(opacity) + .css(); + const rgba2 = chroma(color || colors.shadow) + .alpha(opacity / 2) + .css(); + return ` + box-shadow: + 0 4px 8px 0 ${rgba2}, + 0 2px 2px -1px ${rgba1}; + `; +}; + +export const useSlightShadowActive = useSlightShadowHover; + +export const useOverflowShadow = ({ + direction: _direction, + side: _side, +}: { + direction?: 'y' | 'x'; + side?: 'both' | 'start' | 'end'; +} = {}) => { + const direction = _direction || 'y'; + const side = _side || 'both'; + const { + euiTheme: { size }, + } = useEuiTheme(); + const hideHeight = `calc(${size.base} * 0.75 * 1.25)`; + const gradientStart = ` + ${transparentize('red', 0.9)} 0%, + ${transparentize('red', 0)} ${hideHeight}; + `; + const gradientEnd = ` + ${transparentize('red', 0)} calc(100% - ${hideHeight}), + ${transparentize('red', 0.9)} 100%; + `; + let gradient = ''; + if (side) { + if (side === 'both') { + gradient = `${gradientStart}, ${gradientEnd}`; + } else if (side === 'start') { + gradient = `${gradientStart}`; + } else { + gradient = `${gradientEnd}`; + } + } + + if (direction === 'y') { + return `mask-image: linear-gradient(to bottom, ${gradient})`; + } else { + return `mask-image: linear-gradient(to right, ${gradient})`; + } +}; diff --git a/src/global_styling/variables/_animations.ts b/src/global_styling/variables/_animations.ts new file mode 100644 index 00000000000..5c083375f16 --- /dev/null +++ b/src/global_styling/variables/_animations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CSSProperties } from 'react'; + +export interface _EuiThemeAnimationSpeed { + extraFast: CSSProperties['animationDuration']; + fast: CSSProperties['animationDuration']; + normal: CSSProperties['animationDuration']; + slow: CSSProperties['animationDuration']; + extraSlow: CSSProperties['animationDuration']; +} +export interface _EuiThemeAnimationEasing { + bounce: CSSProperties['animationTimingFunction']; + resistance: CSSProperties['animationTimingFunction']; +} + +export type EuiThemeAnimation = _EuiThemeAnimationEasing & + _EuiThemeAnimationSpeed; + +export const animation_speed: _EuiThemeAnimationSpeed = { + extraFast: '90ms', + fast: '150ms', + normal: '250ms', + slow: '350ms', + extraSlow: '500ms', +}; + +export const animation_ease: _EuiThemeAnimationEasing = { + bounce: 'cubic-bezier(.34, 1.61, .7, 1)', + resistance: 'cubic-bezier(.694, .0482, .335, 1)', +}; + +export const animation: EuiThemeAnimation = { + ...animation_speed, + ...animation_ease, +}; diff --git a/src/global_styling/variables/_borders.ts b/src/global_styling/variables/_borders.ts new file mode 100644 index 00000000000..8353e7050fd --- /dev/null +++ b/src/global_styling/variables/_borders.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CSSProperties } from 'react'; +import { ColorModeSwitch } from '../../services/theme/types'; +import { computed } from '../../services/theme/utils'; +import { sizeToPixel } from '../../services/theme/size'; + +export interface _EuiThemeBorderWidthValues { + /** + * Thinnest width for border + */ + thin: CSSProperties['borderWidth']; + /** + * Thickest width for border + */ + thick: CSSProperties['borderWidth']; +} + +export interface _EuiThemeBorderRadiusValues { + /** + * Primary corner radius size + */ + medium: CSSProperties['borderRadius']; + /** + * Small corner radius size + */ + small: CSSProperties['borderRadius']; +} + +export interface _EuiThemeBorderColorValues { + /** + * Color for all borders; Default is `colors.lightShade` + */ + color: ColorModeSwitch; +} + +export interface _EuiThemeBorderValues extends _EuiThemeBorderColorValues { + /** + * Varied thicknesses for borders + */ + width: _EuiThemeBorderWidthValues; + /** + * Varied border radii + */ + radius: _EuiThemeBorderRadiusValues; +} + +export interface _EuiThemeBorderTypes { + /** + * Full `border` property string computed using `border.width.thin` and `border.color` + */ + thin: CSSProperties['border']; + /** + * Full `border` property string computed using `border.width.thick` and `border.color` + */ + thick: CSSProperties['border']; + /** + * Full editable style `border` property string computed using `border.width.thick` and `border.color` + */ + editable: CSSProperties['border']; +} + +export type EuiThemeBorder = _EuiThemeBorderValues & _EuiThemeBorderTypes; + +export const border: EuiThemeBorder = { + color: computed(([lightShade]) => lightShade, ['colors.lightShade']), + width: { + thin: '1px', + thick: '2px', + }, + radius: { + medium: computed(sizeToPixel(0.25)), + small: computed(sizeToPixel(0.125)), + }, + thin: computed(([width, color]) => `${width.thin} solid ${color}`, [ + 'border.width', + 'border.color', + ]), + thick: computed(([width, color]) => `${width.thick} solid ${color}`, [ + 'border.width', + 'border.color', + ]), + editable: computed(([width, color]) => `${width.thick} dotted ${color}`, [ + 'border.width', + 'border.color', + ]), +}; diff --git a/src/global_styling/variables/_breakpoint.ts b/src/global_styling/variables/_breakpoint.ts new file mode 100644 index 00000000000..c3643f06a78 --- /dev/null +++ b/src/global_styling/variables/_breakpoint.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type _EuiBreakpointSize = 'xs' | 's' | 'm' | 'l' | 'xl'; + +export type EuiThemeBreakpoint = { + /** + * Set the minimum window width at which to start to the breakpoint + */ + [key in _EuiBreakpointSize]: number; +}; + +export const breakpoint: EuiThemeBreakpoint = { + xl: 1200, + l: 992, + m: 768, + s: 575, + xs: 0, +}; diff --git a/src/global_styling/variables/_colors.ts b/src/global_styling/variables/_colors.ts new file mode 100644 index 00000000000..48a1fefae65 --- /dev/null +++ b/src/global_styling/variables/_colors.ts @@ -0,0 +1,289 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { saturate, shade, tint } from '../../services/color'; +import { computed } from '../../services/theme/utils'; +import { + ColorModeSwitch, + StrictColorModeSwitch, +} from '../../services/theme/types'; +import { + makeDisabledContrastColor, + makeHighContrastColor, +} from '../../services/color/contrast'; + +/* + * TYPES + */ + +/** + * Top 5 colors + */ +export type _EuiThemeBrandColors = { + /** + * Main brand color and used for most call to actions like buttons and links. + */ + primary: ColorModeSwitch; + /** + * Pulls attention to key indicators like notifications or number of selections. + */ + accent: ColorModeSwitch; + /** + * Used for positive messages/graphics and additive actions. + */ + success: ColorModeSwitch; + /** + * Used for warnings and actions that have a potential to be destructive. + */ + warning: ColorModeSwitch; + /** + * Used for negative messages/graphics like errors and destructive actions. + */ + danger: ColorModeSwitch; +}; + +/** + * Every brand color must have a contrast computed text equivelant + */ +export type _EuiThemeBrandTextColors = { + /** + * Typically computed against colors.primary + */ + primaryText: ColorModeSwitch; + /** + * Typically computed against colors.accent + */ + accentText: ColorModeSwitch; + /** + * Typically computed against colors.success + */ + successText: ColorModeSwitch; + /** + * Typically computed against colors.warning + */ + warningText: ColorModeSwitch; + /** + * Typically computed against colors.danger + */ + dangerText: ColorModeSwitch; +}; + +export type _EuiThemeShadeColors = { + /** + * Used as the background color of primary page content and panels including modals and flyouts. + */ + emptyShade: ColorModeSwitch; + /** + * Used to lightly shade areas that contain secondary content or contain panel-like components. + */ + lightestShade: ColorModeSwitch; + /** + * Used for most borders and dividers (horizontal rules). + */ + lightShade: ColorModeSwitch; + /** + * The middle gray for all themes; this is the base for colors.subdued. + */ + mediumShade: ColorModeSwitch; + /** + * Slightly subtle graphic color + */ + darkShade: ColorModeSwitch; + /** + * Used as the text color and the background color for inverted components like tooltips and the control bar. + */ + darkestShade: ColorModeSwitch; + /** + * The opposide of `emptyShade` + */ + fullShade: ColorModeSwitch; +}; + +export type _EuiThemeTextColors = { + /** + * Computed against colors.darkestShade + */ + text: ColorModeSwitch; + /** + * Computed against colors.text. + */ + title: ColorModeSwitch; + /** + * Computed against colors.mediumShade + */ + subdued: ColorModeSwitch; + /** + * Computed against colors.primaryText + */ + link: ColorModeSwitch; +}; + +export type _EuiThemeSpecialColors = { + /** + * The background color for the whole window (body) and is a computed value of colors.lightestShade. + * Provides denominator (background) value for contrast calculations. + */ + body: ColorModeSwitch; + /** + * Used to highlight text when matching against search strings + */ + highlight: ColorModeSwitch; + /** + * Computed against colors.darkestShade + */ + disabled: ColorModeSwitch; + /** + * Computed against colors.disabled + */ + disabledText: ColorModeSwitch; + /** + * Base color for shadows that gets transparentized + */ + shadow: ColorModeSwitch; +}; + +export type _EuiThemeConstantColors = { + /** + * Purest white + */ + ghost: string; + /** + * Purest black + */ + ink: string; +}; + +export type _EuiThemeColors = _EuiThemeBrandColors & + _EuiThemeBrandTextColors & + _EuiThemeShadeColors & + _EuiThemeSpecialColors & + _EuiThemeTextColors; + +/* + * LIGHT THEME + * Only split up in the light theme to access the keys by section in the docs + */ + +export const brand_colors: _EuiThemeBrandColors = { + primary: '#006BB4', + accent: '#DD0A73', + success: '#017D73', + warning: '#F5A700', + danger: '#BD271E', +}; + +export const brand_text_colors: _EuiThemeBrandTextColors = { + primaryText: computed(makeHighContrastColor('colors.primary')), + accentText: computed(makeHighContrastColor('colors.accent')), + successText: computed(makeHighContrastColor('colors.success')), + warningText: computed(makeHighContrastColor('colors.warning')), + dangerText: computed(makeHighContrastColor('colors.danger')), +}; + +export const shade_colors: _EuiThemeShadeColors = { + emptyShade: '#FFF', + lightestShade: '#F5F7FA', + lightShade: '#D3DAE6', + mediumShade: '#98A2B3', + darkShade: '#69707D', + darkestShade: '#343741', + fullShade: '#000', +}; + +export const special_colors: _EuiThemeSpecialColors = { + body: computed(([lightestShade]) => tint(lightestShade, 0.5), [ + 'colors.lightestShade', + ]), + highlight: '#FFFCDD', + disabled: computed(([darkestShade]) => tint(darkestShade, 0.7), [ + 'colors.darkestShade', + ]), + disabledText: computed(makeDisabledContrastColor('colors.disabled')), + shadow: computed(({ colors }) => + shade(saturate(colors.mediumShade, 0.25), 0.5) + ), +}; + +export const text_colors: _EuiThemeTextColors = { + text: computed(makeHighContrastColor('colors.darkestShade')), + title: computed( + ([{ text, body }]) => makeHighContrastColor(shade(text, 0.5))(body), + ['colors'] + ), + subdued: computed(makeHighContrastColor('colors.mediumShade')), + link: computed(([primaryText]) => primaryText, ['colors.primaryText']), +}; + +export const light_colors: _EuiThemeColors = { + ...brand_colors, + ...shade_colors, + ...special_colors, + // Need to come after special colors so they can react to `body` + ...brand_text_colors, + ...text_colors, +}; + +/* + * DARK THEME + */ + +export const dark_shades: _EuiThemeShadeColors = { + emptyShade: '#1D1E24', + lightestShade: '#25262E', + lightShade: '#343741', + mediumShade: '#535966', + darkShade: '#98A2B3', + darkestShade: '#D4DAE5', + fullShade: '#FFF', +}; + +export const dark_colors: _EuiThemeColors = { + // Brand + primary: '#1BA9F5', + accent: '#F990C0', + success: '#7DE2D1', + warning: '#FFCE7A', + danger: '#F66', + ...dark_shades, + + // Special + body: computed(([lightestShade]) => shade(lightestShade, 0.45), [ + 'colors.lightestShade', + ]), + highlight: '#2E2D25', + disabled: computed(([darkestShade]) => tint(darkestShade, 0.7), [ + 'colors.darkestShade', + ]), + disabledText: computed(makeDisabledContrastColor('colors.disabled')), + shadow: computed(({ colors }) => + shade(saturate(colors.mediumShade, 0.25), 0.5) + ), + + // Need to come after special colors so they can react to `body` + ...brand_text_colors, + + // Text + text: '#DFE5EF', + title: computed(([text]) => text, ['colors.text']), + subdued: computed(makeHighContrastColor('colors.mediumShade')), + link: computed(([primaryText]) => primaryText, ['colors.primaryText']), +}; + +/* + * FULL + */ + +export type EuiThemeColors = StrictColorModeSwitch<_EuiThemeColors> & + _EuiThemeConstantColors; + +export const colors: EuiThemeColors = { + ghost: '#FFF', + ink: '#000', + LIGHT: light_colors, + DARK: dark_colors, +}; diff --git a/src/global_styling/variables/_colors_vis.ts b/src/global_styling/variables/_colors_vis.ts new file mode 100644 index 00000000000..d430491d0f5 --- /dev/null +++ b/src/global_styling/variables/_colors_vis.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +// 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', + }, +}; + +export 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, +}; diff --git a/src/global_styling/variables/_size.ts b/src/global_styling/variables/_size.ts new file mode 100644 index 00000000000..8c5b29122c3 --- /dev/null +++ b/src/global_styling/variables/_size.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { computed } from '../../services/theme/utils'; +import { sizeToPixel } from '../../services/theme/size'; + +export type EuiThemeBase = number; + +export const base: EuiThemeBase = 16; + +export type EuiThemeSize = { + xxs: string; + xs: string; + s: string; + m: string; + base: string; + l: string; + xl: string; + xxl: string; + xxxl: string; + xxxxl: string; +}; + +export const size: EuiThemeSize = { + xxs: computed(sizeToPixel(0.125)), + xs: computed(sizeToPixel(0.25)), + s: computed(sizeToPixel(0.5)), + m: computed(sizeToPixel(0.75)), + base: computed(sizeToPixel()), + l: computed(sizeToPixel(1.5)), + xl: computed(sizeToPixel(2)), + xxl: computed(sizeToPixel(2.5)), + xxxl: computed(sizeToPixel(3)), + xxxxl: computed(sizeToPixel(4)), +}; diff --git a/src/global_styling/variables/_states.ts b/src/global_styling/variables/_states.ts new file mode 100644 index 00000000000..75bb752f247 --- /dev/null +++ b/src/global_styling/variables/_states.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ColorModeSwitch } from '../../services/theme/types'; +import { computed } from '../../services/theme/utils'; +import { sizeToPixel } from '../../services/theme/size'; +import { shade, tint, transparentize } from '../../services/color'; +import { CSSProperties } from 'react'; + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +export interface _EuiThemeFocusOutline { + /** + * A single CSS property: value + */ + [key: string]: ColorModeSwitch; +} + +export interface _EuiThemeFocus { + /** + * Color is used deterministically by the legacy theme, and as fallback for Amsterdam + */ + color: ColorModeSwitch; + /** + * Used to transprentize any color at certain values + */ + transparency: ColorModeSwitch; + /** + * Default color plus transparency + */ + backgroundColor: ColorModeSwitch; + /** + * Width is the thickness of the outline or faux ring + */ + width: CSSProperties['borderWidth']; + /** + * Larger thickness of the outline for larger components + */ + widthLarge: CSSProperties['borderWidth']; + /** + * Using `outline` is new for Amsterdam but is set to `none` in legacy theme + */ + outline: _EuiThemeFocusOutline; +} + +export const focus: _EuiThemeFocus = { + color: computed(({ colors }) => transparentize(colors.primary, 0.3)), + transparency: { LIGHT: 0.1, DARK: 0.3 }, + backgroundColor: { + LIGHT: computed( + ([primary, transparency]) => tint(primary, 1 - transparency), + ['colors.primary', 'focus.transparency'] + ), + DARK: computed( + ([primary, transparency]) => shade(primary, 1 - transparency), + ['colors.primary', 'focus.transparency'] + ), + }, + + // Sizing + widthLarge: computed(sizeToPixel(0.25)), + width: computed(sizeToPixel(0.125)), + + // Outline + outline: { + 'box-shadow': computed(([color, width]) => `0 0 0 ${width} ${color}`, [ + 'focus.color', + 'focus.width', + ]), + }, +}; diff --git a/src/global_styling/variables/_typography.ts b/src/global_styling/variables/_typography.ts new file mode 100644 index 00000000000..e344fe8202b --- /dev/null +++ b/src/global_styling/variables/_typography.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CSSProperties } from 'react'; +import { keysOf } from '../../components/common'; +import { computed } from '../../services/theme/utils'; + +/* + * Font scale + */ + +// Typographic scale -- loosely based on Major Third (1.250) +export const fontScale = { + xxxs: 0.5625, + xxs: 0.6875, + xs: 0.75, + s: 0.875, + m: 1, + l: 1.25, + xl: 1.75, + xxl: 2.125, +}; + +export const SCALES = keysOf(fontScale); +export type _EuiThemeFontScale = keyof typeof fontScale; + +/* + * Font base settings + */ + +export type _EuiThemeFontBase = { + /** + * The whole font family stack for all parts of the UI. + * We encourage only customizing the first font in the stack. + */ + family: string; + /** + * The font family used for monospace UI elements like EuiCode + */ + familyCode?: string; + /** + * Controls advanced features OpenType fonts. + * https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings + */ + featureSettings?: string; + /** + * A computed number that is 1/4 of `base` + */ + baseline: number; + /** + * Establishes the ideal line-height percentage, but it is the `baseline` integer that establishes the final pixel/rem value + */ + lineHeightMultiplier: number; +}; + +// Families & base font settings +export const fontBase: _EuiThemeFontBase = { + family: "'Inter UI', BlinkMacSystemFont, Helvetica, Arial, sans-serif", + familyCode: "'Roboto Mono', Menlo, Courier, monospace", + + // Careful using ligatures. Code editors like ACE will often error because of width calculations + featureSettings: "'calt' 1, 'kern' 1, 'liga' 1", + + baseline: computed(([base]) => base / 4, ['base']), + lineHeightMultiplier: 1.5, +}; + +/* + * Font weights + */ +export interface _EuiThemeFontWeight { + light: CSSProperties['fontWeight']; + regular: CSSProperties['fontWeight']; + medium: CSSProperties['fontWeight']; + semiBold: CSSProperties['fontWeight']; + bold: CSSProperties['fontWeight']; +} + +export const fontWeight: _EuiThemeFontWeight = { + light: 300, + regular: 400, + medium: 500, + semiBold: 600, + bold: 700, +}; + +/* + * Font + */ + +export type EuiThemeFont = _EuiThemeFontBase & { + scale: { [key in _EuiThemeFontScale]: number }; + weight: _EuiThemeFontWeight; +}; + +export const font: EuiThemeFont = { + ...fontBase, + scale: fontScale, + weight: fontWeight, +}; diff --git a/src/global_styling/variables/_z_index.ts b/src/global_styling/variables/_z_index.ts new file mode 100644 index 00000000000..b0d370f362c --- /dev/null +++ b/src/global_styling/variables/_z_index.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +// Z-Index + +// Remember that z-index is relative to parent and based on the stacking context. +// z-indexes only compete against other z-indexes when they exist as children of +// that shared parent. + +// That means a popover with a settings of 2, will still show above a modal +// with a setting of 100, if it is within that modal and not besides it. + +// Generally that means it's a good idea to consider things added to this file +// as competitive only as siblings. + +// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context + +export interface EuiThemeZIndex { + level0: number; + level1: number; + level2: number; + level3: number; + level4: number; + level5: number; + level6: number; + level7: number; + level8: number; + level9: number; +} + +export const zIndex: EuiThemeZIndex = { + level0: 0, + level1: 1000, + level2: 2000, + level3: 3000, + level4: 4000, + level5: 5000, + level6: 6000, + level7: 7000, + level8: 8000, + level9: 9000, + + // --> These should be declared at the component level + // content: computed(({ zIndex }) => zIndex.level0), + // header: computed(({ zIndex }) => zIndex.level1), + // contentMenu: computed(({ zIndex }) => zIndex.level2), + // flyout: computed(({ zIndex }) => zIndex.level3), + // navigation: computed(({ zIndex }) => zIndex.level4), + // mask: computed(({ zIndex }) => zIndex.level6), + // modal: computed(({ zIndex }) => zIndex.level8), + // toastList: computed(({ zIndex }) => zIndex.level9), +}; diff --git a/src/global_styling/variables/text.ts b/src/global_styling/variables/text.ts new file mode 100644 index 00000000000..4ead53c1cb1 --- /dev/null +++ b/src/global_styling/variables/text.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CSSProperties } from 'react'; +import { computed, lineHeightFromBaseline } from '../../services/theme'; +import { _EuiThemeFontScale, SCALES } from './_typography'; + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +export type EuiThemeFontSize = { + [mapType in _EuiThemeFontScale]: { + fontSize: CSSProperties['fontSize']; + lineHeight: CSSProperties['lineHeight']; + }; +}; + +export const fontSize: EuiThemeFontSize = SCALES.reduce((acc, elem) => { + acc[elem] = { + fontSize: computed(([scale]) => `${scale}rem`, [`font.scale.${elem}`]), + lineHeight: computed( + ([base, font]) => lineHeightFromBaseline(base, font, font.scale[elem]), + ['base', 'font'] + ), + }; + return acc; +}, {} as EuiThemeFontSize); diff --git a/src/global_styling/variables/title.ts b/src/global_styling/variables/title.ts new file mode 100644 index 00000000000..f836974240b --- /dev/null +++ b/src/global_styling/variables/title.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CSSProperties } from 'react'; +import { computed } from '../../services/theme/utils'; +import { _EuiThemeFontScale, SCALES } from './_typography'; + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +export type EuiThemeTitle = { + [size in _EuiThemeFontScale]: { + color: string; + fontSize: string; + fontWeight: CSSProperties['fontWeight']; + letterSpacing?: string; + lineHeight: string; + }; +}; + +const titlesPartial: { + [size in _EuiThemeFontScale]: { + fontWeight: string; + letterSpacing?: string; + }; +} = { + xxxs: { + fontWeight: 'bold', + letterSpacing: undefined, + }, + xxs: { + fontWeight: 'bold', + letterSpacing: undefined, + }, + xs: { + fontWeight: 'bold', + letterSpacing: undefined, + }, + s: { + fontWeight: 'bold', + letterSpacing: undefined, + }, + m: { + fontWeight: 'semiBold', + letterSpacing: '-.02em', + }, + l: { + fontWeight: 'medium', + letterSpacing: '-.025em', + }, + xl: { + fontWeight: 'light', + letterSpacing: '-.04em', + }, + xxl: { + fontWeight: 'light', + letterSpacing: '-.03em', + }, +}; + +export const title: EuiThemeTitle = SCALES.reduce((acc, size) => { + acc[size] = { + fontSize: computed(([{ fontSize }]) => fontSize, [`font.size.${size}`]), + lineHeight: computed(([{ lineHeight }]) => lineHeight, [ + `font.size.${size}`, + ]), + color: computed(([color]) => color, ['colors.title']), + fontWeight: computed(([fontWeight]) => fontWeight, [ + `font.weight.${titlesPartial[size].fontWeight}`, + ]), + letterSpacing: titlesPartial[size].letterSpacing, + }; + return acc; +}, {} as EuiThemeTitle); diff --git a/src/services/color/contrast.ts b/src/services/color/contrast.ts new file mode 100644 index 00000000000..58cbac5d3ba --- /dev/null +++ b/src/services/color/contrast.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chroma from 'chroma-js'; +import { shade, tint, lightness as getLightness } from './manipulation'; +import { getOn } from '../theme/utils'; + +/** + * Creates a new color that meets or exceeds WCAG level AA + * @param foreground - Color to manipulate + * @param ratio - Amount to change in absolute terms. 0-10. + * * + * @param themeOrBackground - Color to use as the contrast basis or just pass EuiTheme + */ +export const makeHighContrastColor = (_foreground: string, ratio = 4.55) => ( + themeOrBackground: + | string + | { + colors: { body: string }; + [key: string]: any; + } +) => { + const foreground = (typeof themeOrBackground === 'object' + ? getOn(themeOrBackground, _foreground) ?? _foreground + : _foreground) as string; + const background = + typeof themeOrBackground === 'object' + ? themeOrBackground.colors.body + : themeOrBackground; + + if (chroma(foreground).alpha() < 1 || chroma(background).alpha() < 1) { + console.warn( + `Contrast cannot be accurately calculated when colors have alpha channel opacity. Make sure the provided foreground and background colors have no transparency: + +Foreground: ${foreground} +Background: ${background}` + ); + } + + let contrast = chroma.contrast(foreground, background); + + // Determine the lightness factor of the background color first to + // determine whether to shade or tint the foreground. + const brightness = getLightness(background); + + let highContrastTextColor = foreground; + + while (contrast < ratio) { + if (brightness > 50) { + highContrastTextColor = shade(highContrastTextColor, 0.05); + } else { + highContrastTextColor = tint(highContrastTextColor, 0.05); + } + + contrast = chroma.contrast(highContrastTextColor, background); + + const lightness = getLightness(highContrastTextColor); + + if (lightness < 5) { + console.warn( + 'High enough contrast could not be determined. Most likely your background color does not adjust for light mode.' + ); + return highContrastTextColor; + } + + if (lightness > 95) { + console.warn( + 'High enough contrast could not be determined. Most likely your background color does not adjust for dark mode.' + ); + return highContrastTextColor; + } + } + + return highContrastTextColor; +}; + +/** + * Creates a new color with increased contrast + * Disabled content only needs a contrast of at least 2 because there is no interaction available + * @param foreground - Color to manipulate + * @param ratio - Amount to change in absolute terms. 0-10. + * * + * @param themeOrBackground - Color to use as the contrast basis + */ +export const makeDisabledContrastColor = (color: string, ratio = 2) => ( + themeOrBackground: + | string + | { + colors: { body: string }; + [key: string]: any; + } +) => makeHighContrastColor(color, ratio)(themeOrBackground); diff --git a/src/services/color/index.ts b/src/services/color/index.ts index ae5beae8b03..c34d5637d4a 100644 --- a/src/services/color/index.ts +++ b/src/services/color/index.ts @@ -39,3 +39,5 @@ export { } from './eui_palettes'; export { rgbDef, HSV, RGB } from './color_types'; export { getSteppedGradient } from './stepped_gradient'; +export * from './manipulation'; +export * from './contrast'; diff --git a/src/services/color/manipulation.ts b/src/services/color/manipulation.ts new file mode 100644 index 00000000000..84cc9e41ad8 --- /dev/null +++ b/src/services/color/manipulation.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chroma from 'chroma-js'; + +/** + * Makes a color more transparent. + * @param color - Color to manipulate + * @param alpha - alpha channel value. From 0-1. + */ +export const transparentize = (color: string, alpha: number) => + chroma(color).alpha(alpha).css(); + +/** + * Mixes a provided color with white. + * @param color - Color to mix with white + * @param ratio - Mix weight. From 0-1. Larger value indicates more white. + */ +export const tint = (color: string, ratio: number) => + chroma.mix(color, '#fff', ratio, 'rgb').css(); + +/** + * Mixes a provided color with black. + * @param color - Color to mix with black + * @param ratio - Mix weight. From 0-1. Larger value indicates more black. + */ +export const shade = (color: string, ratio: number) => + chroma.mix(color, '#000', ratio, 'rgb').css(); + +/** + * Increases the saturation of a color by manipulating the hsl saturation. + * @param color - Color to manipulate + * @param amount - Amount to change in absolute terms. 0-1. + */ +export const saturate = (color: string, amount: number) => + chroma(color).set('hsl.s', `+${amount}`).css(); + +/** + * Decreases the saturation of a color by manipulating the hsl saturation. + * @param color - Color to manipulate + * @param amount - Amount to change in absolute terms. 0-1. + */ +export const desaturate = (color: string, amount: number) => + chroma(color).set('hsl.s', `-${amount}`).css(); + +/** + * Returns the lightness value of a color. 0-100 + * @param color + */ +export const lightness = (color: string) => chroma(color).get('hsl.l') * 100; diff --git a/src/services/color_picker/color_picker.ts b/src/services/color_picker/color_picker.ts index cc922e66967..9610956222b 100644 --- a/src/services/color_picker/color_picker.ts +++ b/src/services/color_picker/color_picker.ts @@ -46,10 +46,16 @@ export const useColorStopsState = ( return [colorStops, updateColorStops, addColor]; }; -export const useColorPickerState = (initialColor = '') => { +export type EuiSetColorMethod = ( + text: string, + { hex, isValid }: { hex: string; isValid: boolean } +) => void; +export const useColorPickerState = ( + initialColor = '' +): [color: string, setColor: EuiSetColorMethod, errors: string[] | null] => { const [color, setColorValue] = useState(initialColor); const [isValid, setIsValid] = useState(true); - const setColor = (text: string, { isValid }: { isValid: boolean }) => { + const setColor: EuiSetColorMethod = (text, { isValid }) => { setColorValue(text); setIsValid(isValid); }; diff --git a/src/services/color_picker/index.ts b/src/services/color_picker/index.ts index 568b05f056f..cc292ce895b 100644 --- a/src/services/color_picker/index.ts +++ b/src/services/color_picker/index.ts @@ -6,4 +6,8 @@ * Side Public License, v 1. */ -export { useColorPickerState, useColorStopsState } from './color_picker'; +export { + useColorPickerState, + useColorStopsState, + EuiSetColorMethod, +} from './color_picker'; diff --git a/src/services/index.ts b/src/services/index.ts index d358f4fd9a6..99c43e28c28 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -62,9 +62,21 @@ export { euiPaletteGray, HSV, getSteppedGradient, + transparentize, + tint, + shade, + saturate, + desaturate, + lightness, + makeHighContrastColor, + makeDisabledContrastColor, } from './color'; -export { useColorPickerState, useColorStopsState } from './color_picker'; +export { + useColorPickerState, + useColorStopsState, + EuiSetColorMethod, +} from './color_picker'; export * from './console'; @@ -119,3 +131,29 @@ export { } from './hooks'; export { throttle } from './throttle'; + +export { + EuiSystemContext, + EuiThemeContext, + EuiModificationsContext, + EuiColorModeContext, + useEuiTheme, + withEuiTheme, + WithEuiThemeProps, + EuiThemeProvider, + buildTheme, + computed, + isInverseColorMode, + getColorMode, + getComputed, + getOn, + mergeDeep, + setOn, + Computed, + ComputedThemeShape, + EuiThemeColorMode, + EuiThemeComputed, + EuiThemeModifications, + EuiThemeShape, + EuiThemeSystem, +} from './theme'; diff --git a/src/services/theme/README.md b/src/services/theme/README.md new file mode 100644 index 00000000000..42a54e78769 --- /dev/null +++ b/src/services/theme/README.md @@ -0,0 +1,153 @@ +# 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 accounting + * Theme consumption is scoped to the current color mode (set in the context provider) +* Style adaptaion based on a smal set of base values + * Text colors are calculated with WCAG Level AA (4.5:1) in mind + * Scalable typographic and spacing rhythms + + +## Layers of the theme system + +### Unbuilt theme + +_See [`euiThemeDefault`](../../themes/eui/theme.ts)_ +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( + ([size]) => size * 2 // predicate. What to do with the dependency values, + ['sizes.euiSize'], // dependency array, referencing other properties in the theme object +) +``` + +The dependency array is optional. Omitting the array gives access to the computed theme. + +```js +computed( + (theme) => theme.sizes.euiSize * 2 +) +``` + +### Theme system (built theme) + +_See [`EuiThemeDefault`](../../themes/eui/theme.ts)_ +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`](../../themes/eui/context.ts)_ +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) + {}, // Modifications object + 'light' // Color mode +) +``` + +#### Modifications + +Because the theme system (built theme) is immutable, modifications can only be made at compute time by providing overrides and extensions for theme property values. These modifications are passed to the `EuiThemeProvider` via the `modify` prop and should match the high-level object shape of the theme. + +#### Color mode + +Think light and dark mode. A theme has built-in color mode support, using the reserved `LIGHT` and `DARK` keys as a marker: + +```js +colors: { + LIGHT: {...} + DARK : {...} +} +``` +The reserved color mode keys can be used at any level and location in a theme. +`getComputed` will only compute and return values in the specified current color mode. + + +## React-specific context + +### EuiThemeProvider + +_See [`EuiThemeProvider`](../../themes/eui/provider.ts)_ +Umbrella provider component that holds the various top-level theme configuration option providers: theme system, color mode, modifications; 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, modifications) 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 + +_See [`useEuiTheme`](../../themes/eui/hooks.tsx)_ +A custom React hook that returns the computed theme. This hook is little more than a wrapper around the `useContext` hook, accessing three of the top-level providers: computed theme, color mode, and modifications. + +```js +const { euiTheme, colorMode, modifications } = useEuiTheme(); +``` + +The `euiTheme` 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 +-
+``` diff --git a/src/services/theme/context.ts b/src/services/theme/context.ts new file mode 100644 index 00000000000..50fade58981 --- /dev/null +++ b/src/services/theme/context.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createContext } from 'react'; +import { + EuiThemeColorMode, + EuiThemeSystem, + EuiThemeModifications, + EuiThemeComputed, +} from './types'; +import { EuiThemeDefault } from '../../themes/eui/theme'; +import { DEFAULT_COLOR_MODE, getComputed } from './utils'; + +export const EuiSystemContext = createContext(EuiThemeDefault); +export const EuiModificationsContext = 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..de4d1d30632 --- /dev/null +++ b/src/services/theme/hooks.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { forwardRef, useContext } from 'react'; + +import { + EuiThemeContext, + EuiModificationsContext, + EuiColorModeContext, +} from './context'; +import { + EuiThemeColorMode, + EuiThemeModifications, + EuiThemeComputed, +} from './types'; + +export const useEuiTheme = (): { + euiTheme: EuiThemeComputed; + colorMode: EuiThemeColorMode; + modifications: EuiThemeModifications; +} => { + const theme = useContext(EuiThemeContext); + const colorMode = useContext(EuiColorModeContext); + const modifications = useContext(EuiModificationsContext); + + return { + euiTheme: theme as EuiThemeComputed, + colorMode, + modifications: modifications as EuiThemeModifications, + }; +}; + +export interface WithEuiThemeProps

{ + theme: { + euiTheme: EuiThemeComputed

; + colorMode: EuiThemeColorMode; + }; +} +export const withEuiTheme = ( + Component: React.ComponentType> +) => { + const componentName = Component.displayName || Component.name || 'Component'; + const Render = ( + props: Omit>, + ref: React.Ref>> + ) => { + const { euiTheme, 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..c62d3ad6565 --- /dev/null +++ b/src/services/theme/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + EuiSystemContext, + EuiThemeContext, + EuiModificationsContext, + EuiColorModeContext, +} from './context'; +export { useEuiTheme, withEuiTheme, WithEuiThemeProps } from './hooks'; +export { EuiThemeProvider } from './provider'; +export { + buildTheme, + computed, + isInverseColorMode, + getColorMode, + getComputed, + getOn, + mergeDeep, + setOn, + Computed, +} from './utils'; +export { + ComputedThemeShape, + EuiThemeColorMode, + EuiThemeComputed, + EuiThemeModifications, + EuiThemeShape, + EuiThemeSystem, +} from './types'; +export { lineHeightFromBaseline } from './typography'; +export { sizeToPixel } from './size'; diff --git a/src/services/theme/provider.tsx b/src/services/theme/provider.tsx new file mode 100644 index 00000000000..826b72da0a9 --- /dev/null +++ b/src/services/theme/provider.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { + useContext, + useEffect, + useRef, + useState, + PropsWithChildren, +} from 'react'; +import isEqual from 'lodash/isEqual'; + +import { + EuiSystemContext, + EuiThemeContext, + EuiModificationsContext, + EuiColorModeContext, +} from './context'; +import { buildTheme, getColorMode, getComputed, mergeDeep } from './utils'; +import { + EuiThemeColorMode, + EuiThemeSystem, + EuiThemeModifications, +} from './types'; + +export interface EuiThemeProviderProps { + theme?: EuiThemeSystem; + colorMode?: EuiThemeColorMode; + modify?: EuiThemeModifications; + children: any; +} + +export function EuiThemeProvider({ + theme: _system, + colorMode: _colorMode, + modify: _modifications, + children, +}: PropsWithChildren>) { + const parentSystem = useContext(EuiSystemContext); + const parentModifications = useContext(EuiModificationsContext); + const parentColorMode = useContext(EuiColorModeContext); + const parentTheme = useContext(EuiThemeContext); + + const [system, setSystem] = useState(_system || parentSystem); + const prevSystemKey = useRef(system.key); + + const [modifications, setModifications] = useState( + mergeDeep(parentModifications, _modifications) + ); + const prevModifications = useRef(modifications); + + const [colorMode, setColorMode] = useState( + getColorMode(_colorMode, parentColorMode) + ); + const prevColorMode = useRef(colorMode); + + const isParentTheme = useRef( + prevSystemKey.current === parentSystem.key && + colorMode === parentColorMode && + isEqual(parentModifications, modifications) + ); + + const [theme, setTheme] = useState( + isParentTheme.current && Object.keys(parentTheme).length + ? parentTheme + : getComputed( + system, + buildTheme(modifications, `_${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 newModifications = mergeDeep(parentModifications, _modifications); + if (!isEqual(prevModifications.current, newModifications)) { + setModifications(newModifications); + prevModifications.current = newModifications; + isParentTheme.current = false; + } + }, [_modifications, parentModifications]); + + 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(modifications, `_${system.key}`) as typeof system, + colorMode + ) + ); + } + }, [colorMode, system, modifications]); + + return ( + + + + + {children} + + + + + ); +} diff --git a/src/services/theme/size.ts b/src/services/theme/size.ts new file mode 100644 index 00000000000..5b3075d976b --- /dev/null +++ b/src/services/theme/size.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Calculates the `px` value based on a scale multiplier + * @param scale - The font scale multiplier + * * + * @param themeOrBase - Theme base value + * * + * @returns string - Rem unit aligned to baseline + */ + +export const sizeToPixel = (scale: number = 1) => ( + themeOrBase: number | { base: number; [key: string]: any } +) => { + const base = typeof themeOrBase === 'object' ? themeOrBase.base : themeOrBase; + return `${base * scale}px`; +}; diff --git a/src/services/theme/types.ts b/src/services/theme/types.ts new file mode 100644 index 00000000000..223f9f2b130 --- /dev/null +++ b/src/services/theme/types.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RecursivePartial, ValueOf } from '../../components/common'; +import { EuiThemeAnimation } from '../../global_styling/variables/_animations'; +import { EuiThemeBreakpoint } from '../../global_styling/variables/_breakpoint'; +import { EuiThemeBorder } from '../../global_styling/variables/_borders'; +import { EuiThemeColors } from '../../global_styling/variables/_colors'; +import { + EuiThemeBase, + EuiThemeSize, +} from '../../global_styling/variables/_size'; +import { EuiThemeFont } from '../../global_styling/variables/_typography'; +import { _EuiThemeFocus } from '../../global_styling/variables/_states'; + +export const COLOR_MODES_STANDARD = { + light: 'LIGHT', + dark: 'DARK', +} as const; +export const COLOR_MODES_INVERSE = 'INVERSE' as const; + +type EuiThemeColorModeInverse = typeof COLOR_MODES_INVERSE; +type EuiThemeColorModeStandard = ValueOf; +export type EuiThemeColorMode = + | string + | EuiThemeColorModeStandard + | EuiThemeColorModeInverse; + +export type ColorModeSwitch = + | { + [key in EuiThemeColorModeStandard]: T; + } + | T; + +export type StrictColorModeSwitch = { + [key in EuiThemeColorModeStandard]: T; +}; + +export type EuiThemeShape = { + colors: EuiThemeColors; + base: EuiThemeBase; + size: EuiThemeSize; + font: EuiThemeFont; + border: EuiThemeBorder; + focus?: _EuiThemeFocus; + animation: EuiThemeAnimation; + breakpoint: EuiThemeBreakpoint; +}; + +export type EuiThemeSystem = { + root: EuiThemeShape & T; + model: EuiThemeShape & T; + key: string; +}; + +export type EuiThemeModifications = RecursivePartial; + +export type ComputedThemeShape< + T, + P = string | number | bigint | boolean | null | undefined +> = T extends P | ColorModeSwitch + ? T extends ColorModeSwitch + ? X extends P + ? X + : { + [K in keyof (X & + Exclude< + T, + keyof X | keyof StrictColorModeSwitch + >)]: ComputedThemeShape< + (X & Exclude)[K], + P + >; + } + : T + : { + [K in keyof T]: ComputedThemeShape; + }; + +export type EuiThemeComputed = ComputedThemeShape & { + themeName: string; +}; diff --git a/src/services/theme/typography.ts b/src/services/theme/typography.ts new file mode 100644 index 00000000000..4fab86d443b --- /dev/null +++ b/src/services/theme/typography.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { _EuiThemeFontBase } from '../../global_styling/variables/_typography'; + +/** + * Calculates the line-height to the closest multiple of the baseline + * EX: A proper line-height for text is 1.5 times the font-size. + * If our base font size (euiFontSize) is 16, and our baseline is 4. To ensure the + * text stays on the baseline, we pass a multiplier to calculate a line-height. + * @param base - Theme base unit + * @param font - Requires numbers for the `lineHeightMultiplier` and `baseline` values + * @param scale - The font scale multiplier + * * + * @returns string - Rem unit aligned to baseline + */ +export function lineHeightFromBaseline( + base: number, + font: { + baseline: _EuiThemeFontBase['baseline']; + lineHeightMultiplier: _EuiThemeFontBase['lineHeightMultiplier']; + }, + scale: number +) { + const { baseline, lineHeightMultiplier } = font; + + const pixelValue = + Math.floor(Math.round(base * scale * lineHeightMultiplier) / baseline) * + baseline; + return `${pixelValue / base}rem`; +} diff --git a/src/services/theme/utils.test.ts b/src/services/theme/utils.test.ts new file mode 100644 index 00000000000..52bc023cebd --- /dev/null +++ b/src/services/theme/utils.test.ts @@ -0,0 +1,278 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + isInverseColorMode, + getColorMode, + getOn, + setOn, + computed, + Computed, + getComputed, + buildTheme, + mergeDeep, +} 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'); + }); +}); + +describe('getOn', () => { + const obj = { + parent: { + child: 'childVal', + }, + other: { + thing: { + string: 'stringVal', + nested: ['array'], + number: 0, + func: () => {}, + }, + }, + colors: { + LIGHT: { primary: '#000' }, + DARK: { primary: '#FFF' }, + }, + }; + 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('can shortcut color modes', () => { + expect(getOn(obj, 'colors.primary', 'LIGHT')).toEqual('#000'); + expect(getOn(obj, 'colors.primary', 'DARK')).toEqual('#FFF'); + }); + 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 with dependencies array', () => { + const output = computed(([path]) => path, ['path.to']); + expect(output).toBeInstanceOf(Computed); + expect(output.computer).toBeInstanceOf(Function); + expect(output.dependencies).toEqual(['path.to']); + }); + it('should transform to Computed with single dependency', () => { + const output = computed((path) => path, 'path.to'); + expect(output).toBeInstanceOf(Computed); + expect(output.computer).toBeInstanceOf(Function); + expect(output.dependencies).toEqual('path.to'); + }); + it('should transform to Computed without dependencies array', () => { + const output = computed((path) => path); + expect(output).toBeInstanceOf(Computed); + }); +}); + +const theme = buildTheme( + { + colors: { + LIGHT: { + primary: '#000', + secondary: computed(([primary]) => `${primary}000`, ['colors.primary']), + }, + DARK: { + primary: '#FFF', + secondary: computed((theme) => `${theme.colors.primary}FFF`), + }, + }, + sizes: { + small: 8, + }, + }, + 'minimal' +); +describe('getComputed', () => { + it('computes all values and returns only the current color mode', () => { + // @ts-expect-error intentionally not using a full EUI theme definition + expect(getComputed(theme, {}, 'LIGHT')).toEqual({ + colors: { primary: '#000', secondary: '#000000' }, + sizes: { small: 8 }, + themeName: 'minimal', + }); + // @ts-expect-error 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-expect-error 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-expect-error 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', + }); + }); + it('respects property extensions', () => { + expect( + getComputed( + // @ts-expect-error intentionally not using a full EUI theme definition + theme, + buildTheme({ colors: { LIGHT: { tertiary: '#333' } } }, ''), + 'LIGHT' + ) + ).toEqual({ + colors: { primary: '#000', secondary: '#000000', tertiary: '#333' }, + sizes: { small: 8 }, + themeName: 'minimal', + }); + }); + it('respects section extensions', () => { + expect( + getComputed( + // @ts-expect-error intentionally not using a full EUI theme definition + theme, + buildTheme({ custom: { myProp: '#333' } }, ''), + 'LIGHT' + ) + ).toEqual({ + colors: { primary: '#000', secondary: '#000000' }, + sizes: { small: 8 }, + custom: { myProp: '#333' }, + themeName: 'minimal', + }); + }); + it('respects extensions in computation', () => { + expect( + getComputed( + // @ts-expect-error intentionally not using a full EUI theme definition + theme, + buildTheme( + { + colors: { + LIGHT: { + tertiary: computed(([primary]) => `${primary}333`, [ + 'colors.primary', + ]), + }, + }, + }, + '' + ), + 'LIGHT' + ) + ).toEqual({ + colors: { primary: '#000', secondary: '#000000', tertiary: '#000333' }, + 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 } } }); + }); +}); diff --git a/src/services/theme/utils.ts b/src/services/theme/utils.ts new file mode 100644 index 00000000000..9d905cc69f2 --- /dev/null +++ b/src/services/theme/utils.ts @@ -0,0 +1,372 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiThemeColorMode, + EuiThemeModifications, + EuiThemeSystem, + EuiThemeShape, + EuiThemeComputed, + COLOR_MODES_STANDARD, + COLOR_MODES_INVERSE, +} from './types'; + +export const DEFAULT_COLOR_MODE = COLOR_MODES_STANDARD.light; + +/** + * Returns whether the parameter is an object + * @param {any} obj - Anything + */ +const isObject = (obj: any) => obj && typeof obj === 'object'; + +/** + * Returns whether the provided color mode is `inverse` + * @param {string} colorMode - `light`, `dark`, or `inverse` + */ +export const isInverseColorMode = (colorMode?: EuiThemeColorMode) => { + return colorMode === COLOR_MODES_INVERSE; +}; + +/** + * Returns the color mode configured in the current EuiThemeProvider. + * Returns the parent color mode if none is explicity set. + * @param {string} coloMode - `light`, `dark`, or `inverse` + * @param {string} parentColorMode - `light`, `dark`, or `inverse`; used as the fallback + */ +export const getColorMode = ( + colorMode?: EuiThemeColorMode, + parentColorMode?: EuiThemeColorMode +) => { + const mode = colorMode?.toUpperCase(); + if (mode == null) { + return parentColorMode || DEFAULT_COLOR_MODE; + } else if (isInverseColorMode(mode)) { + return parentColorMode === COLOR_MODES_STANDARD.dark || + parentColorMode === undefined + ? COLOR_MODES_STANDARD.light + : COLOR_MODES_STANDARD.dark; + } else { + return mode; + } +}; + +/** + * Returns a value at a given path on an object. + * If `colorMode` is provided, will scope the value to the appropriate color mode key (LIGHT\DARK) + * @param {object} model - Object + * @param {string} _path - Dot-notated string to a path on the object + * @param {string} colorMode - `light` or `dark` + */ +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) { + if ( + colorMode && + node.hasOwnProperty(colorMode) === true && + node[colorMode].hasOwnProperty(segment) === true + ) { + if (node[colorMode][segment] instanceof Computed) { + node = node[colorMode][segment].getValue(null, null, node, colorMode); + } else { + node = node[colorMode][segment]; + } + } else { + return undefined; + } + } else { + if (node[segment] instanceof Computed) { + node = node[segment].getValue(null, null, node, colorMode); + } else { + node = node[segment]; + } + } + } + + return node; +}; + +/** + * Sets a value at a given path on an object. + * @param {object} model - Object + * @param {string} _path - Dot-notated string to a path on the object + * @param {any} string - The value to set + */ +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; +}; + +/** + * Creates a class to store the `computer` method and its eventual parameters. + * Allows for on-demand computation with up-to-date parameters via `getValue` method. + * @constructor + * @param {function} computer - Function to be computed + * @param {string | array} dependencies - Dependencies passed to the `computer` as parameters + */ +export class Computed { + constructor( + public computer: (...values: any[]) => T, + public dependencies: string | string[] = [] + ) {} + + /** + * Executes the `computer` method with the current state of the theme + * by taking into account previously computed values and modifications. + * @param {Proxy | object} base - Computed or uncomputed theme + * @param {Proxy | object} modifications - Theme value overrides + * @param {object} working - Partially computed theme + * @param {string} colorMode - `light` or `dark` + */ + getValue( + base: EuiThemeSystem | EuiThemeShape, + modifications: EuiThemeModifications = {}, + working: EuiThemeComputed, + colorMode: EuiThemeColorMode + ) { + if (!this.dependencies.length) { + return this.computer(working); + } + if (!Array.isArray(this.dependencies)) { + return this.computer( + getOn(working, this.dependencies) ?? + getOn(modifications, this.dependencies, colorMode) ?? + getOn(base, this.dependencies, colorMode) + ); + } + return this.computer( + this.dependencies.map((dependency) => { + return ( + getOn(working, dependency) ?? + getOn(modifications, dependency, colorMode) ?? + getOn(base, dependency, colorMode) + ); + }) + ); + } +} + +/** + * Returns a Class (`Computed`) that stores the arbitrary computer method + * and references to its optional dependecies. + * @param {function} computer - Arbitrary method to be called at compute time. + * @param {string | array} dependencies - Values that will be provided to `computer` at compute time. + */ +export function computed(computer: (value: EuiThemeComputed) => T): T; +export function computed( + computer: (value: any[]) => T, + dependencies: string[] +): T; +export function computed( + computer: (value: any) => T, + dependencies: string +): T; +export function computed( + comp: ((value: T) => T) | ((value: any) => T) | ((value: any[]) => T), + dep?: string | string[] +) { + return new Computed(comp, dep); +} + +/** + * Takes an uncomputed theme, and computes and returns all values taking + * into consideration value overrides and configured color mode. + * Overrides take precedence, and only values in the current color mode + * are computed and returned. + * @param {Proxy} base - Object to transform into Proxy + * @param {Proxy | object} over - Unique identifier or name + * @param {string} colorMode - `light` or `dark` + */ +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 }, + checkExisting: boolean = false, + path?: string + ) { + Object.keys(base).forEach((key) => { + let newPath = path ? `${path}.${key}` : `${key}`; + if ([...Object.values(COLOR_MODES_STANDARD), colorMode].includes(key)) { + if (key !== colorMode) { + return; + } else { + const colorModeSegment = new RegExp( + `(\\.${colorMode}\\b)|(\\b${colorMode}\\.)` + ); + newPath = newPath.replace(colorModeSegment, ''); + } + } + const existing = checkExisting && getOn(output, newPath); + if (!existing || isObject(existing)) { + 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) && !Array.isArray(baseValue)) { + loop(baseValue, overValue ?? {}, checkExisting, newPath); + } else { + setOn(output, newPath, overValue ?? baseValue); + } + } + }); + } + // Compute standard theme values and apply overrides + loop(base, over); + // Compute and apply extension values only + loop(over, {}, true); + return output as EuiThemeComputed; +}; + +/** + * Builds a Proxy with a custom `handler` designed to self-reference values + * and prevent arbitrary value overrides. + * @param {object} model - Object to transform into Proxy + * @param {string} key - Unique identifier or name + */ +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 (isObject(value) && !Array.isArray(value)) { + 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; +}; + +/** + * Deeply merges two objects, using `source` values whenever possible. + * @param {object} _target - Object with fallback values + * @param {object} source - Object with desired values + */ +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; +}; diff --git a/src/themes/eui-amsterdam/global_styling/variables/_borders.ts b/src/themes/eui-amsterdam/global_styling/variables/_borders.ts new file mode 100644 index 00000000000..8ab04cf7190 --- /dev/null +++ b/src/themes/eui-amsterdam/global_styling/variables/_borders.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { computed, sizeToPixel } from '../../../../services/theme'; +import { + border, + EuiThemeBorder, +} from '../../../../global_styling/variables/_borders'; + +export const border_ams: EuiThemeBorder = { + ...border, + radius: { + medium: computed(sizeToPixel(0.375)), + small: computed(sizeToPixel(0.25)), + }, +}; diff --git a/src/themes/eui-amsterdam/global_styling/variables/_colors.ts b/src/themes/eui-amsterdam/global_styling/variables/_colors.ts new file mode 100644 index 00000000000..a4f5322f0e3 --- /dev/null +++ b/src/themes/eui-amsterdam/global_styling/variables/_colors.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { shade, tint } from '../../../../services/color'; +import { computed } from '../../../../services/theme/utils'; +import { + makeHighContrastColor, + makeDisabledContrastColor, +} from '../../../../services/color/contrast'; +import { + _EuiThemeColors, + brand_text_colors, + shade_colors, + EuiThemeColors, + dark_shades, +} from '../../../../global_styling/variables/_colors'; + +/* + * LIGHT THEME + */ + +export const light_colors_ams: _EuiThemeColors = { + // Brand + primary: '#07C', + accent: '#F04E98', + success: '#00BFB3', + warning: '#FEC514', + danger: '#BD271E', + + // Shades + ...shade_colors, + lightestShade: '#f0f4fb', + + // Special + body: computed(([lightestShade]) => tint(lightestShade, 0.5), [ + 'colors.lightestShade', + ]), + highlight: computed(([warning]) => tint(warning, 0.9), ['colors.warning']), + disabled: '#ABB4C4', + disabledText: computed(makeDisabledContrastColor('colors.disabled')), + shadow: computed(({ colors }) => colors.ink), + + // Need to come after special colors so they can react to `body` + ...brand_text_colors, + + // Text + text: computed(([darkestShade]) => darkestShade, ['colors.darkestShade']), + title: computed(([text]) => shade(text, 0.5), ['colors.text']), + subdued: computed(makeHighContrastColor('colors.darkShade')), + link: computed(([primaryText]) => primaryText, ['colors.primaryText']), +}; + +/* + * DARK THEME + */ + +export const dark_colors_ams: _EuiThemeColors = { + // Brand + primary: '#36A2EF', + accent: '#F68FBE', + success: '#7DDED8', + warning: '#F3D371', + danger: '#F86B63', + + // Shades + ...dark_shades, + + // Special + body: computed(([lightestShade]) => shade(lightestShade, 0.45), [ + 'colors.lightestShade', + ]), + highlight: '#2E2D25', + disabled: '#515761', + disabledText: computed(makeDisabledContrastColor('colors.disabled')), + shadow: computed(({ colors }) => colors.ink), + + // Need to come after special colors so they can react to `body` + ...brand_text_colors, + + // Text + text: '#DFE5EF', + title: computed(([text]) => text, ['colors.text']), + subdued: computed(makeHighContrastColor('colors.mediumShade')), + link: computed(([primaryText]) => primaryText, ['colors.primaryText']), +}; + +/* + * FULL + */ + +export const colors_ams: EuiThemeColors = { + ghost: '#FFF', + ink: '#000', + LIGHT: light_colors_ams, + DARK: dark_colors_ams, +}; diff --git a/src/themes/eui-amsterdam/global_styling/variables/_states.ts b/src/themes/eui-amsterdam/global_styling/variables/_states.ts new file mode 100644 index 00000000000..29274e87de4 --- /dev/null +++ b/src/themes/eui-amsterdam/global_styling/variables/_states.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { computed } from '../../../../services/theme/utils'; +import { transparentize } from '../../../../services/color'; +import { + focus, + _EuiThemeFocus, +} from '../../../../global_styling/variables/_states'; + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +export const focus_ams: _EuiThemeFocus = { + ...focus, + color: 'currentColor', + transparency: { LIGHT: 0.9, DARK: 0.7 }, + backgroundColor: computed(({ colors, focus }) => + transparentize(colors.primary, focus!.transparency) + ), + + // Outline + outline: { + outline: computed(({ focus }) => `${focus!.width} solid ${focus!.color}`), + }, +}; diff --git a/src/themes/eui-amsterdam/global_styling/variables/_typography.ts b/src/themes/eui-amsterdam/global_styling/variables/_typography.ts new file mode 100644 index 00000000000..ee0c0845ee0 --- /dev/null +++ b/src/themes/eui-amsterdam/global_styling/variables/_typography.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { font } from '../../../../global_styling/variables/_typography'; + +/** + * Amsterdam theme just changes the main font from the beta Inter UI to Inter + */ +export const font_ams = { + ...font, + family: "'Inter', BlinkMacSystemFont, Helvetica, Arial, sans-serif", +}; diff --git a/src/themes/eui-amsterdam/global_styling/variables/title.ts b/src/themes/eui-amsterdam/global_styling/variables/title.ts new file mode 100644 index 00000000000..fae6e5e9792 --- /dev/null +++ b/src/themes/eui-amsterdam/global_styling/variables/title.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * NOTE: These were quick conversions of their Sass counterparts. + * They have yet to be used/tested. + */ + +import { + title, + EuiThemeTitle, +} from '../../../../global_styling/variables/title'; +import { SCALES } from '../../../../global_styling/variables/_typography'; +import { computed } from '../../../../services/theme/utils'; + +// For Amsterdam, change all font-weights to bold and remove letter-spacing + +export const title_ams: EuiThemeTitle = SCALES.reduce((acc, elem) => { + acc[elem] = { + ...title[elem], + fontWeight: computed(([fontWeight]) => fontWeight, ['font.weight.bold']), + letterSpacing: undefined, + }; + return acc; +}, {} as EuiThemeTitle); diff --git a/src/themes/eui-amsterdam/theme.ts b/src/themes/eui-amsterdam/theme.ts new file mode 100644 index 00000000000..9be031362ae --- /dev/null +++ b/src/themes/eui-amsterdam/theme.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { buildTheme, EuiThemeShape } from '../../services/theme'; +import { animation } from '../../global_styling/variables/_animations'; +import { breakpoint } from '../../global_styling/variables/_breakpoint'; +import { base, size } from '../../global_styling/variables/_size'; + +import { colors_ams } from './global_styling/variables/_colors'; +import { font_ams } from './global_styling/variables/_typography'; +import { border_ams } from './global_styling/variables/_borders'; + +export const euiThemeAmsterdam: EuiThemeShape = { + colors: colors_ams, + base, + size, + font: font_ams, + border: border_ams, + animation, + breakpoint, +}; + +export const EuiThemeAmsterdam = buildTheme( + euiThemeAmsterdam, + 'EUI_THEME_AMSTERDAM' +); diff --git a/src/themes/eui/theme.ts b/src/themes/eui/theme.ts new file mode 100644 index 00000000000..9c51e3f34eb --- /dev/null +++ b/src/themes/eui/theme.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { buildTheme } from '../../services/theme/utils'; +import { EuiThemeShape } from '../../services/theme/types'; +import { animation } from '../../global_styling/variables/_animations'; +import { breakpoint } from '../../global_styling/variables/_breakpoint'; +import { colors } from '../../global_styling/variables/_colors'; +import { base, size } from '../../global_styling/variables/_size'; +import { font } from '../../global_styling/variables/_typography'; +import { border } from '../../global_styling/variables/_borders'; + +export const euiThemeDefault: EuiThemeShape = { + colors, + base, + size, + font, + border, + animation, + breakpoint, +}; + +export const EuiThemeDefault = buildTheme(euiThemeDefault, 'EUI_THEME_DEFAULT'); diff --git a/yarn.lock b/yarn.lock index 3589ced359b..d1db42435a8 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,111 @@ 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/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/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" @@ -2914,6 +3061,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" @@ -4290,7 +4446,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== @@ -4718,6 +4874,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" @@ -7804,7 +7965,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== @@ -14726,7 +14887,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= @@ -15261,6 +15422,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"