From 31a24ed0c69e8f9dd081c51d6e40b29117e13324 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Tue, 10 Aug 2021 15:06:22 -0400 Subject: [PATCH] [CSS-in-JS] Global theme (#4643) Here we go! --- .../babel/proptypes-from-ts-props/index.js | 2 +- .../components/with_theme/theme_context.tsx | 8 +- src-docs/src/routes.js | 29 +- src-docs/src/services/playground/knobs.js | 120 ++--- src-docs/src/views/code/code_example.js | 4 +- src-docs/src/views/emotion/canopy.tsx | 310 ----------- src-docs/src/views/guidelines/_index.scss | 2 + src-docs/src/views/guidelines/sass.js | 20 +- src-docs/src/views/theme/_animation.js | 198 +++++++ src-docs/src/views/theme/_border.js | 198 +++++++ src-docs/src/views/theme/_breakpoints.js | 64 +++ src-docs/src/views/theme/_colors.js | 476 +++++++++++++++++ src-docs/src/views/theme/_focus.js | 95 ++++ src-docs/src/views/theme/_props.tsx | 82 +++ src-docs/src/views/theme/_size.js | 201 ++++++++ src-docs/src/views/theme/_theme_section.tsx | 81 +++ src-docs/src/views/theme/_typography.js | 393 ++++++++++++++ src-docs/src/views/theme/_values.tsx | 209 ++++++++ src-docs/src/views/theme/computed.tsx | 23 +- src-docs/src/views/theme/consuming.tsx | 6 +- src-docs/src/views/theme/consuming_hoc.tsx | 6 +- src-docs/src/views/theme/create_computed.tsx | 28 +- src-docs/src/views/theme/hooks.tsx | 72 +++ src-docs/src/views/theme/inverse.tsx | 6 +- src-docs/src/views/theme/override_simple.tsx | 10 +- src-docs/src/views/theme/theme_example.js | 61 ++- src-docs/src/views/theme/values.js | 148 ++++++ .../color_picker/_color_picker_swatch.scss | 6 +- src/components/common.ts | 7 + src/global_styling/functions/_colors.ts | 86 ++++ src/global_styling/functions/_typography.ts | 34 ++ src/global_styling/mixins/_helpers.ts | 136 +++++ src/global_styling/mixins/_shadow.ts | 246 +++++++++ src/global_styling/variables/_animations.ts | 42 ++ src/global_styling/variables/_borders.ts | 72 +++ src/global_styling/variables/_breakpoint.ts | 24 + src/global_styling/variables/_colors.ts | 289 +++++++++++ src/global_styling/variables/_colors_vis.ts | 79 +++ src/global_styling/variables/_size.ts | 48 ++ src/global_styling/variables/_states.ts | 74 +++ src/global_styling/variables/_typography.ts | 105 ++++ src/global_styling/variables/_z_index.ts | 59 +++ src/global_styling/variables/text.ts | 36 ++ src/global_styling/variables/title.ts | 76 +++ src/services/color/index.ts | 1 + src/services/color/manipulation.ts | 74 +++ src/services/color_picker/color_picker.ts | 10 +- src/services/color_picker/index.ts | 6 +- src/services/index.ts | 18 +- src/services/theme/README.md | 10 +- src/services/theme/context.ts | 2 +- src/services/theme/index.ts | 8 +- src/services/theme/theme.ts | 487 ------------------ src/services/theme/types.ts | 79 ++- src/services/theme/utils.test.ts | 128 ++--- src/services/theme/utils.ts | 149 +++--- .../global_styling/variables/_borders.ts | 20 + .../global_styling/variables/_colors.ts | 101 ++++ .../global_styling/variables/_states.ts | 28 + .../global_styling/variables/_typography.ts | 17 + .../global_styling/variables/title.ts | 25 + src/themes/eui-amsterdam/theme.ts | 37 ++ src/themes/eui/theme.ts | 34 ++ 63 files changed, 4382 insertions(+), 1123 deletions(-) delete mode 100644 src-docs/src/views/emotion/canopy.tsx create mode 100644 src-docs/src/views/theme/_animation.js create mode 100644 src-docs/src/views/theme/_border.js create mode 100644 src-docs/src/views/theme/_breakpoints.js create mode 100644 src-docs/src/views/theme/_colors.js create mode 100644 src-docs/src/views/theme/_focus.js create mode 100644 src-docs/src/views/theme/_props.tsx create mode 100644 src-docs/src/views/theme/_size.js create mode 100644 src-docs/src/views/theme/_theme_section.tsx create mode 100644 src-docs/src/views/theme/_typography.js create mode 100644 src-docs/src/views/theme/_values.tsx create mode 100644 src-docs/src/views/theme/hooks.tsx create mode 100644 src-docs/src/views/theme/values.js create mode 100644 src/global_styling/functions/_colors.ts create mode 100644 src/global_styling/functions/_typography.ts create mode 100644 src/global_styling/mixins/_helpers.ts create mode 100644 src/global_styling/mixins/_shadow.ts create mode 100644 src/global_styling/variables/_animations.ts create mode 100644 src/global_styling/variables/_borders.ts create mode 100644 src/global_styling/variables/_breakpoint.ts create mode 100644 src/global_styling/variables/_colors.ts create mode 100644 src/global_styling/variables/_colors_vis.ts create mode 100644 src/global_styling/variables/_size.ts create mode 100644 src/global_styling/variables/_states.ts create mode 100644 src/global_styling/variables/_typography.ts create mode 100644 src/global_styling/variables/_z_index.ts create mode 100644 src/global_styling/variables/text.ts create mode 100644 src/global_styling/variables/title.ts create mode 100644 src/services/color/manipulation.ts delete mode 100644 src/services/theme/theme.ts create mode 100644 src/themes/eui-amsterdam/global_styling/variables/_borders.ts create mode 100644 src/themes/eui-amsterdam/global_styling/variables/_colors.ts create mode 100644 src/themes/eui-amsterdam/global_styling/variables/_states.ts create mode 100644 src/themes/eui-amsterdam/global_styling/variables/_typography.ts create mode 100644 src/themes/eui-amsterdam/global_styling/variables/title.ts create mode 100644 src/themes/eui-amsterdam/theme.ts create mode 100644 src/themes/eui/theme.ts 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/src/components/with_theme/theme_context.tsx b/src-docs/src/components/with_theme/theme_context.tsx index 37f5c2bb901..33ba46ab35c 100644 --- a/src-docs/src/components/with_theme/theme_context.tsx +++ b/src-docs/src/components/with_theme/theme_context.tsx @@ -2,11 +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, - EuiThemeDefault, - EuiThemeAmsterdam, -} from '../../../../src/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); diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 9cd3b97cdc3..6fe77377183 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -223,6 +223,7 @@ 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 */ @@ -238,10 +239,6 @@ import { ElasticChartsPieExample } from './views/elastic_charts/pie_example'; import { ElasticChartsAccessibilityExample } from './views/elastic_charts/accessibility_example'; -/** ! Temporary ! */ - -import Canopy from './views/emotion/canopy'; - const createExample = (example, customTitle) => { if (!example) { throw new Error( @@ -324,18 +321,6 @@ const createMarkdownExample = (example, title) => { }; const navigation = [ - { - name: 'Temporary', - items: [ - createExample( - { - intro: , - sections: [], - }, - 'Canopy' - ), - ], - }, { name: 'Guidelines', items: [ @@ -496,10 +481,20 @@ const navigation = [ ResizeObserverExample, ResponsiveExample, TextDiffExample, - ThemeExample, 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 73f46159645..c802ac5bd37 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) => ( @@ -359,70 +390,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) @@ -430,16 +410,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/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/emotion/canopy.tsx b/src-docs/src/views/emotion/canopy.tsx deleted file mode 100644 index efa9e84416b..00000000000 --- a/src-docs/src/views/emotion/canopy.tsx +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as React from 'react'; -import { css } from '@emotion/react'; -import chroma from 'chroma-js'; -import { EuiSpacer } from '../../../../src/components/spacer'; -import { EuiIcon } from '../../../../src/components/icon'; -import { - mergeDeep, - useEuiTheme, - withEuiTheme, - WithEuiThemeProps, - EuiThemeProvider, - computed, - euiThemeDefault, - buildTheme, - EuiThemeModifications, -} from '../../../../src/services'; - -const View = () => { - const { euiTheme, colorMode } = useEuiTheme(); - return ( -

-
- {colorMode} -
-          {JSON.stringify(euiTheme, null, 2)}
-        
-
-
-

-

-

-

-

-

-
-
- ); -}; - -const View3 = () => { - const overrides = { - colors: { - light: { euiColorPrimary: '#8A07BD' }, - dark: { euiColorPrimary: '#BD07A5' }, - }, - }; - return ( - <> - - - - - Overriding primary - - - - ); -}; - -const View2 = () => { - const overrides = { - colors: { - light: { - euiColorSecondary: computed( - ['colors.euiColorPrimary'], - () => '#85E89d' - ), - }, - dark: { euiColorSecondary: '#F0FFF4' }, - }, - }; - return ( - <> - - - Overriding secondary - - - - ); -}; - -interface BlockProps extends WithEuiThemeProps { - size?: 'xxl' | 'xl'; -} -// eslint-disable-next-line react/prefer-stateless-function -class Block extends React.Component { - render() { - const { - theme: { euiTheme }, - size = 'xxl', - ...props - } = this.props; - const blockStyle = css` - color: ${euiTheme.colors.euiColorPrimary}; - border-radius: ${euiTheme.borders.euiBorderRadiusSmall}; - border: ${euiTheme.borders.euiBorderEditable}; - `; - return ( -
-
- ); - } -} -const BlockWithTheme = withEuiTheme(Block); - -export default () => { - // const [colorMode, setColorMode] = React.useState('light'); - const toggleTheme = () => { - // setColorMode((mode) => (mode === 'light' ? 'dark' : 'light')); - }; - const [overrides, setOverrides] = React.useState({}); - const lightColors = () => { - setOverrides( - mergeDeep(overrides, { - colors: { - light: { - euiColorPrimary: chroma.random().hex(), - }, - }, - }) - ); - }; - const darkColors = () => { - setOverrides( - mergeDeep(overrides, { - colors: { - dark: { - euiColorPrimary: chroma.random().hex(), - }, - }, - }) - ); - }; - - const newTheme = buildTheme( - { - ...euiThemeDefault, - custom: '#000', - }, - 'CUSTOM' - ); - - // Difference is due to automatic colorMode reduction during value computation. - // Makes typing slightly inconvenient, but makes consuming values very convenient. - type ExtensionsUncomputed = { - colors: { light: { myColor: string }; dark: { myColor: string } }; - custom: { - colors: { - light: { customColor: string }; - dark: { customColor: string }; - }; - mySize: number; - }; - }; - type ExtensionsComputed = { - colors: { myColor: string }; - custom: { colors: { customColor: string }; mySize: number }; - }; - - // Type (EuiThemeModifications) only necessary if you want IDE autocomplete support here - const extend: EuiThemeModifications = { - colors: { - light: { - euiColorPrimary: '#F56407', - myColor: computed(['colors.euiColorPrimary'], ([primary]) => primary), - }, - dark: { - euiColorPrimary: '#FA924F', - myColor: computed(['colors.euiColorPrimary'], ([primary]) => primary), - }, - }, - custom: { - colors: { - light: { customColor: '#080AEF' }, - dark: { customColor: '#087EEF' }, - }, - mySize: 5, - }, - }; - - const Extend = () => { - // Generic type (ExtensionsComputed) necessary if accessing extensions/custom properties - const { - euiTheme: { colors, custom }, - colorMode, - } = useEuiTheme(); - return ( -
-
- {colorMode} -
-            {JSON.stringify({ colors, custom }, null, 2)}
-          
-
-
-

-

-

-

-

-

-
-
- ); - }; - - return ( - <> - - - - - {' '} - - - Default view - - - - - theme={newTheme} - colorMode="inverse"> - Inverse colorMode - - withEuiTheme - - - - - {/* Generic type is not necessary here. Note that it should be the uncomputed type */} - modify={extend}> - Extensions - - - - ); -}; 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 657411bc14b..ad4afecfa13 100644 --- a/src-docs/src/views/guidelines/sass.js +++ b/src-docs/src/views/guidelines/sass.js @@ -354,7 +354,25 @@ export const SassGuidelines = ({ selectedTheme }) => { const palette = getSassVars(selectedTheme); return ( - + +

+ EUI is highly tokenized and highly recommends 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/theme/_animation.js b/src-docs/src/views/theme/_animation.js new file mode 100644 index 00000000000..a3b35547a37 --- /dev/null +++ b/src-docs/src/views/theme/_animation.js @@ -0,0 +1,198 @@ +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 and 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 constants to maintain a similar + 'bounce' 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..e531629b771 --- /dev/null +++ b/src-docs/src/views/theme/_border.js @@ -0,0 +1,198 @@ +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, + EuiThemeBorderValues, +} 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 valueProps = getPropsFromThemeKey(EuiThemeBorderValues); + const typeProps = getPropsFromThemeKey(EuiThemeBorderTypes); + + const style = css` + width: ${euiTheme.size.xl}; + height: ${euiTheme.size.xl}; + border-radius: ${euiTheme.border.radiusSmall}; + `; + + const wrappingExampleStyle = { + padding: euiTheme.size.s, + }; + + return ( +
+ +

+ Border EuiThemeBorder +

+

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

+
+ + + + + + These basic properties make up the thickness, color and + corner radii which can be used individually. +

+ } + themeValues={Object.keys(valueProps).map((prop) => ( + + updateBorder(prop, value)} + example={ + prop === 'color' ? ( + + ) : undefined + } + /> + + ))} + /> + + + + 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.widthThick} dashed ${euiTheme.border.color}`} +
+ } + snippet={ + 'border: ${euiTheme.border.widthThick} dashed ${euiTheme.border.color};' + } + /> + + + {`border-radius: ${euiTheme.border.radius}`} +
+ } + snippet={'border-radius: ${euiTheme.border.radius};'} + /> + + + ), + }, + ]} + /> +
+ ); +}; 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..176faad3e89 --- /dev/null +++ b/src-docs/src/views/theme/_colors.js @@ -0,0 +1,476 @@ +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, + EuiTitle, + 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/global_styling/functions/_colors'; + +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} +

+
+
+
+
+
+
+ + + + + + + + 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..7d6b2dcbe65 --- /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.radiusSmall}; + `; + + 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..40474862192 --- /dev/null +++ b/src-docs/src/views/theme/_props.tsx @@ -0,0 +1,82 @@ +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 { + _EuiThemeBorderValues, + _EuiThemeBorderTypes, +} from '../../../../src/global_styling/variables/_borders'; + +export const EuiThemeBorderValues: FunctionComponent<_EuiThemeBorderValues> = () => ( +
+); +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..dcc43b7976d --- /dev/null +++ b/src-docs/src/views/theme/_size.js @@ -0,0 +1,201 @@ +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. +

+
+ + + + + + All sizing values are calculated from a single{' '} + base integer and converted to pixel + string values. +

+ } + themeValues={ + + updateBase('base', value)} + /> + + } + /> + + + It is recommended not to adjust the computed sizes and + only adjust the top level base value. +

+ } + 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 values as they are is straight foward.

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

+ When doing calculations on top of the size values, you + have to use the{' '} + + CSS calc() method + {' '} + because the value that is returned is only a string + value with the 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..232bddfded3 --- /dev/null +++ b/src-docs/src/views/theme/_theme_section.tsx @@ -0,0 +1,81 @@ +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..a8ab1bd8ce4 --- /dev/null +++ b/src-docs/src/views/theme/_typography.js @@ -0,0 +1,393 @@ +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 = font.family.split(','); + const codeFontFamilies = font.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 established + 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={ + <> + + updateFont('family', value)} + /> + + {/* The loop below renders each font family applied to a span. */} + + {fontFamilies.map((family, i) => ( + + {family} + {i < fontFamilies.length - 1 ? ', ' : ''} + + ))} + + + + updateFont('familyCode', value)} + /> + + {/* 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 + + . +

+

+ You do not want to consume this scale directly. Instead, + you will want to use the calculated font sizing keys + such as font.s or{' '} + font.s.fontSize. See the{' '} + Usage tab for more details. +

+ + } + 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 this base font family. However, you + can use it to override a custom component that is not + inheriting this family. +

+ } + example={ + + {'I am a code element forced to default font family'} + + } + snippet={'font-family: ${euiTheme.font.family};'} + /> + + + To maintain consistency, EUI established 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 alignment + with the imported font family. +

+ } + example={ +
+ {'I am proper bold'} +
+ } + snippet={'font-weight: ${euiTheme.font.weight.bold};'} + /> + + +

+ To ensure proper baseline alignment and readable + line-height, the theme's font sizing keys come with + both font-size and line-height. Because + of this, we recommend the array approach to applying + with Emotion. +

+

+ While this works well, we still recommend using the{' '} + + EuiText + {' '} + component directly and sticking to the sizing props + provided. +

+ + } + example={ +
+

+ Orbiting this at a distance of roughly ninety-two + million miles is an utterly insignificant little blue + green planet whose ape- descended life forms are so + amazingly primitive that they still think digital + watches are a pretty neat idea. +

+
+ } + customSnippet={'css={[euiTheme.font.s]}'} + /> + + +

+ You can still grab just the font-size value if need be. +

+ + } + example={ +
+

+ Orbiting this at a distance of roughly ninety-two + million miles is an utterly insignificant little blue + green planet whose ape- descended life forms are so + amazingly primitive that they still think digital + watches are a pretty neat idea. +

+
+ } + snippet={'font-size: ${euiTheme.font.s.fontSize};'} + /> + + + ), + }, + ]} + /> +
+ ); +}; diff --git a/src-docs/src/views/theme/_values.tsx b/src-docs/src/views/theme/_values.tsx new file mode 100644 index 00000000000..fc662213d7b --- /dev/null +++ b/src-docs/src/views/theme/_values.tsx @@ -0,0 +1,209 @@ +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 index d2c404a0bc1..4d09c998e43 100644 --- a/src-docs/src/views/theme/computed.tsx +++ b/src-docs/src/views/theme/computed.tsx @@ -10,13 +10,12 @@ const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { return (

- {' '} - {children} + {children}

); @@ -25,11 +24,11 @@ const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { export default () => { const primaryOverrides = { colors: { - light: { - euiColorPrimary: '#db1dde', + LIGHT: { + primary: '#db1dde', }, - dark: { - euiColorPrimary: '#e378e4', + DARK: { + primary: '#e378e4', }, }, }; @@ -38,10 +37,10 @@ export default () => {
- The euiColorPrimary color has been changed to{' '} + The colors.primary value has been changed to{' '} #db1dde (#e378e4 for dark mode) - and so the calculated values of euiColorPrimaryText{' '} - and euiFocusBackgroundColor have also been updated. + 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 index 4fe1eb59f97..c013804fbb0 100644 --- a/src-docs/src/views/theme/consuming.tsx +++ b/src-docs/src/views/theme/consuming.tsx @@ -12,15 +12,15 @@ export default () => { {' '} This primary color will adjust based on the light or dark theme value

The padding of this box is created using calc(){' '} diff --git a/src-docs/src/views/theme/consuming_hoc.tsx b/src-docs/src/views/theme/consuming_hoc.tsx index 71126c7d213..7fb7b8acc11 100644 --- a/src-docs/src/views/theme/consuming_hoc.tsx +++ b/src-docs/src/views/theme/consuming_hoc.tsx @@ -9,9 +9,9 @@ class Block extends React.Component { const { theme } = this.props; const divStyle = css` - background: ${theme.euiTheme.colors.euiColorLightShade}; - padding: ${theme.euiTheme.sizes.euiSizeXL}; - border-radius: ${theme.euiTheme.borders.euiBorderRadius}; + background: ${theme.euiTheme.colors.lightShade}; + padding: ${theme.euiTheme.size.xl}; + border-radius: ${theme.euiTheme.border.radius}; `; return ( diff --git a/src-docs/src/views/theme/create_computed.tsx b/src-docs/src/views/theme/create_computed.tsx index 9bc013ffaab..2ce8fd6f4a3 100644 --- a/src-docs/src/views/theme/create_computed.tsx +++ b/src-docs/src/views/theme/create_computed.tsx @@ -1,5 +1,4 @@ import React, { FunctionComponent, ReactNode } from 'react'; -import { tint, shade } from '../../../../src/services/theme/theme'; import { EuiIcon } from '../../../../src/components/icon'; import { EuiCode } from '../../../../src/components/code'; import { EuiText } from '../../../../src/components/text'; @@ -8,6 +7,7 @@ import { EuiThemeProvider, useEuiTheme, } from '../../../../src/services'; +import { shade, tint } from '../../../../src/services/color'; interface ThemeExtensions { colors: { @@ -24,7 +24,7 @@ const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => {

@@ -38,28 +38,26 @@ const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { export default () => { const primaryOverrides = { colors: { - light: { + LIGHT: { customColorPrimary: 'rgb(29, 222, 204)', customColorPrimaryHighlight: computed( - ['colors.customColorPrimary'], - ([customColorPrimary]) => tint(customColorPrimary, 0.8) + (customColorPrimary) => tint(customColorPrimary, 0.8), + 'colors.customColorPrimary' ), - // Need a global contrast function customColorPrimaryText: computed( - ['colors.customColorPrimary'], - ([customColorPrimary]) => shade(customColorPrimary, 0.8) + (customColorPrimary) => shade(customColorPrimary, 0.8), + 'colors.customColorPrimary' ), }, - dark: { + DARK: { customColorPrimary: 'rgb(29, 222, 204)', customColorPrimaryHighlight: computed( - ['colors.customColorPrimary'], - ([customColorPrimary]) => shade(customColorPrimary, 0.8) + ([customColorPrimary]) => shade(customColorPrimary, 0.8), + ['colors.customColorPrimary'] ), - // Need a global contrast function customColorPrimaryText: computed( - ['colors.customColorPrimary'], - ([customColorPrimary]) => tint(customColorPrimary, 0.8) + ([customColorPrimary]) => tint(customColorPrimary, 0.8), + ['colors.customColorPrimary'] ), }, }, @@ -69,7 +67,7 @@ export default () => {

- A new key of customColorPrimary has been added as + A new key of customColorPrimary has been added as{' '} rgb(29, 222, 204).

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 index 3eec2b1b138..2726fc50b5b 100644 --- a/src-docs/src/views/theme/inverse.tsx +++ b/src-docs/src/views/theme/inverse.tsx @@ -9,9 +9,9 @@ const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { return (

{children}

diff --git a/src-docs/src/views/theme/override_simple.tsx b/src-docs/src/views/theme/override_simple.tsx index 23b7b103118..cd4e8c6068e 100644 --- a/src-docs/src/views/theme/override_simple.tsx +++ b/src-docs/src/views/theme/override_simple.tsx @@ -8,8 +8,8 @@ const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { return (

{children}

@@ -19,8 +19,8 @@ const Box: FunctionComponent<{ children: ReactNode }> = ({ children }) => { export default () => { const overrides = { colors: { - light: { euiColorLightShade: '#d3e6df' }, - dark: { euiColorLightShade: '#394c4b' }, + LIGHT: { lightShade: '#d3e6df' }, + DARK: { lightShade: '#394c4b' }, }, }; @@ -29,7 +29,7 @@ export default () => { The background of this box is using the locally overridden value of{' '} - theme.colors.euiColorLightShade + euiTheme.colors.lightShade
diff --git a/src-docs/src/views/theme/theme_example.js b/src-docs/src/views/theme/theme_example.js index 00adf63bca5..6adfcaa1888 100644 --- a/src-docs/src/views/theme/theme_example.js +++ b/src-docs/src/views/theme/theme_example.js @@ -9,6 +9,7 @@ import { EuiSpacer, EuiCallOut, EuiCode, + EuiLink, } from '../../../../src/components'; import { EuiThemeProvider } from '../../../../src/services'; @@ -38,6 +39,8 @@ const createComputedHtml = renderToHtml(CreateComputed); export const ThemeExample = { title: 'Theme provider', + isNew: true, + beta: true, intro: ( <> @@ -52,13 +55,51 @@ export const ThemeExample = { size="s" title="The following examples assume that you have wrapped your entire application with this provider." /> - ), sections: [ { title: 'EuiThemeProvider', - text:

TODO

, + text: ( + <> +

+ The context layer that enables theming (including the default theme + styles) comes from EuiThemeProvider. Simply put, + this 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{' '} + EuiTheme page. +

+ + ), props: { EuiThemeProvider }, }, { @@ -144,10 +185,11 @@ export const ThemeExample = { text: ( <>

- While it is usually best to keep all components rendering in the - same light or dark color mode, some components benefit from an - exaggerated change in contrast from the current theme. For this you - can specify EuiThemeProvider's{' '} + 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 @@ -206,8 +248,8 @@ export const ThemeExample = {

For instance, we compute text variants of our base colors. So - locally overriding the euiColorPrimary color will - automatically cascade to the euiColorPrimaryText. + 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.

@@ -235,9 +277,6 @@ export const ThemeExample = { specific theme variables. Instead, you should append custom keys to the theme.

-

- TODO: Indicate type support for custom keys. -

), 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..d52f49c2cc5 --- /dev/null +++ b/src-docs/src/views/theme/values.js @@ -0,0 +1,148 @@ +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 094b2aec3dd..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 diff --git a/src/global_styling/functions/_colors.ts b/src/global_styling/functions/_colors.ts new file mode 100644 index 00000000000..22006541e1f --- /dev/null +++ b/src/global_styling/functions/_colors.ts @@ -0,0 +1,86 @@ +/* + * 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 '../../services/color'; +import { getOn } from '../../services/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-1. + * * + * @param themeOrBackground - Color to use as the contrast basis + */ +export const makeHighContrastColor = (_foreground: string, ratio = 4.5) => ( + themeOrBackground: + | string + | { + colors: { body: string }; + [key: string]: any; + } +) => { + const foreground = (typeof themeOrBackground === 'object' + ? getOn(themeOrBackground, _foreground) + : _foreground) as string; + const background = + typeof themeOrBackground === 'object' + ? themeOrBackground.colors.body + : themeOrBackground; + 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 themeOrBackground - Color to use as the contrast basis + */ +export const makeDisabledContrastColor = ($color: string) => ( + themeOrBackground: + | string + | { + colors: { body: string }; + [key: string]: any; + } +) => makeHighContrastColor($color, 2)(themeOrBackground); diff --git a/src/global_styling/functions/_typography.ts b/src/global_styling/functions/_typography.ts new file mode 100644 index 00000000000..3c44e485cd7 --- /dev/null +++ b/src/global_styling/functions/_typography.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 { _EuiThemeFontBase } from '../variables/_typography'; + +// Typography functions + +// 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. + +export function fontSizeFromScale(base: number, scale: number) { + const pixelValue = Math.round(base * scale); + return `${pixelValue / base}rem`; +} + +export function lineHeightFromBaseline( + base: number, + font: _EuiThemeFontBase, + scale: number +) { + const { lineHeightMultiplier, baseline } = font; + + const pixelValue = + Math.floor(Math.round(base * scale * lineHeightMultiplier) / baseline) * + baseline; + return `${pixelValue / base}rem`; +} diff --git a/src/global_styling/mixins/_helpers.ts b/src/global_styling/mixins/_helpers.ts new file mode 100644 index 00000000000..1961440107f --- /dev/null +++ b/src/global_styling/mixins/_helpers.ts @@ -0,0 +1,136 @@ +/* + * 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'; + +// Helper mixins + +// 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..b4c9b89d20a --- /dev/null +++ b/src/global_styling/mixins/_shadow.ts @@ -0,0 +1,246 @@ +/* + * 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'; + +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..6cb4adbc9de --- /dev/null +++ b/src/global_styling/variables/_borders.ts @@ -0,0 +1,72 @@ +/* + * 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 './_size'; + +export interface _EuiThemeBorderValues { + /** + * Color for all borders; Default is `colors.lightShade` + */ + color: ColorModeSwitch; + /** + * Thinnest width for border + */ + widthThin: CSSProperties['borderWidth']; + /** + * Thickest width for border + */ + widthThick: CSSProperties['borderWidth']; + /** + * Main corner radius size + */ + radius: CSSProperties['borderRadius']; + /** + * Small corner radius size + */ + radiusSmall: CSSProperties['borderRadius']; +} + +export interface _EuiThemeBorderTypes { + /** + * Full `border` property string computed using `border.widthThin` and `border.color` + */ + thin: CSSProperties['border']; + /** + * Full `border` property string computed using `border.widthThick` and `border.color` + */ + thick: CSSProperties['border']; + /** + * Full editable style `border` property string computed using `border.widthThick` and `border.color` + */ + editable: CSSProperties['border']; +} + +export type EuiThemeBorder = _EuiThemeBorderValues & _EuiThemeBorderTypes; + +export const border: EuiThemeBorder = { + color: computed(([lightShade]) => lightShade, ['colors.lightShade']), + widthThin: '1px', + widthThick: '2px', + radius: computed(sizeToPixel(0.25)), + radiusSmall: computed(sizeToPixel(0.125)), + thin: computed(([widthThin, color]) => `${widthThin} solid ${color}`, [ + 'border.widthThin', + 'border.color', + ]), + thick: computed(([widthThick, color]) => `${widthThick} solid ${color}`, [ + 'border.widthThick', + 'border.color', + ]), + editable: computed(([widthThick, color]) => `${widthThick} dotted ${color}`, [ + 'border.widthThick', + '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..30dd95fbdc0 --- /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 '../functions/_colors'; + +/* + * 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..84c6b24bd51 --- /dev/null +++ b/src/global_styling/variables/_colors_vis.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. + */ + +// Visualization colors + +// Maps allow for easier JSON usage +// Use map_merge(euiColorVisColors, $yourMap) to change individual colors after importing ths file +// The `behindText` variant is a direct copy of the hex output by the JS euiPaletteColorBlindBehindText() function +const euiPaletteColorBlind = { + euiColorVis0: { + graphic: '#54B399', + behindText: '#6DCCB1', + }, + euiColorVis1: { + graphic: '#6092C0', + behindText: '#79AAD9', + }, + euiColorVis2: { + graphic: '#D36086', + behindText: '#EE789D', + }, + euiColorVis3: { + graphic: '#9170B8', + behindText: '#A987D1', + }, + euiColorVis4: { + graphic: '#CA8EAE', + behindText: '#E4A6C7', + }, + euiColorVis5: { + graphic: '#D6BF57', + behindText: '#F1D86F', + }, + euiColorVis6: { + graphic: '#B9A888', + behindText: '#D2C0A0', + }, + euiColorVis7: { + graphic: '#DA8B45', + behindText: '#F5A35C', + }, + euiColorVis8: { + graphic: '#AA6556', + behindText: '#C47C6C', + }, + euiColorVis9: { + graphic: '#E7664C', + behindText: '#FF7E62', + }, +}; + +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..100ca3aa438 --- /dev/null +++ b/src/global_styling/variables/_size.ts @@ -0,0 +1,48 @@ +/* + * 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'; + +// const usingFullTheme = `sizeToPixel(0.25)({ base: 16, [...] })` +// const usingBaseValue = `sizeToPixel(0.25)(16)` +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`; +}; + +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..0b67436fcfd --- /dev/null +++ b/src/global_styling/variables/_states.ts @@ -0,0 +1,74 @@ +/* + * 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 { ColorModeSwitch } from '../../services/theme/types'; +import { shade, tint, transparentize } from '../../services/color'; +import { CSSProperties } from 'react'; +import { sizeToPixel } from './_size'; + +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..2661f1c47e7 --- /dev/null +++ b/src/global_styling/variables/_z_index.ts @@ -0,0 +1,59 @@ +/* + * 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'; + +// 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..9486350dabb --- /dev/null +++ b/src/global_styling/variables/text.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 { CSSProperties } from 'react'; +import { computed } from '../../services/theme/utils'; +import { + fontSizeFromScale, + lineHeightFromBaseline, +} from '../functions/_typography'; +import { _EuiThemeFontScale, SCALES } from './_typography'; + +export type EuiThemeFontSize = { + [mapType in _EuiThemeFontScale]: { + fontSize: CSSProperties['fontSize']; + lineHeight: CSSProperties['lineHeight']; + }; +}; + +export const fontSize: EuiThemeFontSize = SCALES.reduce((acc, elem) => { + acc[elem] = { + fontSize: computed(([base, scale]) => fontSizeFromScale(base, scale), [ + 'base', + `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..35605012a19 --- /dev/null +++ b/src/global_styling/variables/title.ts @@ -0,0 +1,76 @@ +/* + * 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'; + +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/index.ts b/src/services/color/index.ts index ae5beae8b03..03a6bdf33b2 100644 --- a/src/services/color/index.ts +++ b/src/services/color/index.ts @@ -39,3 +39,4 @@ export { } from './eui_palettes'; export { rgbDef, HSV, RGB } from './color_types'; export { getSteppedGradient } from './stepped_gradient'; +export * from './manipulation'; diff --git a/src/services/color/manipulation.ts b/src/services/color/manipulation.ts new file mode 100644 index 00000000000..07533aeb4c2 --- /dev/null +++ b/src/services/color/manipulation.ts @@ -0,0 +1,74 @@ +/* + * 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. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chroma from 'chroma-js'; + +/** + * 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').hex(); + +/** + * 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').hex(); + +/** + * 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 8c005b840bf..3c9f32efcbf 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -62,9 +62,19 @@ export { euiPaletteGray, HSV, getSteppedGradient, + transparentize, + tint, + shade, + saturate, + desaturate, + lightness, } from './color'; -export { useColorPickerState, useColorStopsState } from './color_picker'; +export { + useColorPickerState, + useColorStopsState, + EuiSetColorMethod, +} from './color_picker'; export * from './console'; @@ -138,11 +148,7 @@ export { mergeDeep, setOn, Computed, - euiThemeDefault, - EuiThemeDefault, - EuiThemeAmsterdam, - euiThemeAmsterdam, - EuiThemeColor, + ComputedThemeShape, EuiThemeColorMode, EuiThemeComputed, EuiThemeModifications, diff --git a/src/services/theme/README.md b/src/services/theme/README.md index 59aea98c0d0..6dcc3a975d5 100644 --- a/src/services/theme/README.md +++ b/src/services/theme/README.md @@ -30,8 +30,16 @@ These properties specify that the value depends upon some other value in the the ```js computed( + ([size]) => size * 2 // predicate. What to do with the dependency values, ['sizes.euiSize'], // dependency array, referencing other properties in the theme object - ([size]) => size * 2 // predicate. What to do with the dependency values +) +``` + +The dependency array is optional. Omitting the array gives access to the computed theme. + +```js +computed( + (theme) => theme.sizes.euiSize * 2 ) ``` diff --git a/src/services/theme/context.ts b/src/services/theme/context.ts index 568e28f8d55..50fade58981 100644 --- a/src/services/theme/context.ts +++ b/src/services/theme/context.ts @@ -13,7 +13,7 @@ import { EuiThemeModifications, EuiThemeComputed, } from './types'; -import { EuiThemeDefault } from './theme'; +import { EuiThemeDefault } from '../../themes/eui/theme'; import { DEFAULT_COLOR_MODE, getComputed } from './utils'; export const EuiSystemContext = createContext(EuiThemeDefault); diff --git a/src/services/theme/index.ts b/src/services/theme/index.ts index 660f4a4d968..219ad81a4db 100644 --- a/src/services/theme/index.ts +++ b/src/services/theme/index.ts @@ -26,16 +26,10 @@ export { Computed, } from './utils'; export { - EuiThemeColor, + ComputedThemeShape, EuiThemeColorMode, EuiThemeComputed, EuiThemeModifications, EuiThemeShape, EuiThemeSystem, } from './types'; -export { - EuiThemeDefault, - euiThemeDefault, - EuiThemeAmsterdam, - euiThemeAmsterdam, -} from './theme'; diff --git a/src/services/theme/theme.ts b/src/services/theme/theme.ts deleted file mode 100644 index 0b8c8d50465..00000000000 --- a/src/services/theme/theme.ts +++ /dev/null @@ -1,487 +0,0 @@ -/* - * 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 { buildTheme, computed, COLOR_MODE_KEY } from './utils'; - -export const tint = (color: string, ratio: number) => - chroma.mix(color, '#fff', ratio).hex(); -export const shade = (color: string, ratio: number) => - chroma.mix(color, '#000', ratio).hex(); -// TODO -const makeHighContrastColor = (color: string) => color; -// TODO -const makeDisabledContrastColor = (color: string) => color; -// TODO -const transparentize = (color: string, ratio: number) => - ratio ? color : color; - -const poles = { - euiColorGhost: '#FFF', - euiColorInk: '#000', -}; - -const graysLight = { - euiColorEmptyShade: '#FFF', - euiColorLightestShade: '#F5F7FA', - euiColorLightShade: '#D3DAE6', - euiColorMediumShade: '#98A2B3', - euiColorDarkShade: '#69707D', - euiColorDarkestShade: '#343741', - euiColorFullShade: '#000', -}; - -const textVariants = { - euiColorPrimaryText: computed( - ['colors.euiColorPrimary'], - ([euiColorPrimary]) => makeHighContrastColor(euiColorPrimary) - ), - euiColorSecondaryText: computed( - ['colors.euiColorSecondary'], - ([euiColorSecondary]) => makeHighContrastColor(euiColorSecondary) - ), - euiColorAccentText: computed(['colors.euiColorAccent'], ([euiColorAccent]) => - makeHighContrastColor(euiColorAccent) - ), - euiColorWarningText: computed( - ['colors.euiColorWarning'], - ([euiColorWarning]) => makeHighContrastColor(euiColorWarning) - ), - euiColorDangerText: computed(['colors.euiColorDanger'], ([euiColorDanger]) => - makeHighContrastColor(euiColorDanger) - ), - euiColorDisabledText: computed( - ['colors.euiColorDisabled'], - ([euiColorDisabled]) => makeDisabledContrastColor(euiColorDisabled) - ), - euiColorSuccessText: computed( - ['colors.euiColorSecondaryText'], - ([euiColorSecondaryText]) => euiColorSecondaryText - ), - euiLinkColor: computed( - ['colors.euiColorPrimaryText'], - ([euiColorPrimaryText]) => euiColorPrimaryText - ), -}; - -/* DEFAULT THEME */ - -export const light = { - euiColorPrimary: '#006BB4', - euiColorSecondary: '#017D73', - euiColorAccent: '#DD0A73', - - // These colors stay the same no matter the theme - ...poles, - - // Status - euiColorSuccess: computed( - ['colors.euiColorSecondary'], - ([euiColorSecondary]) => euiColorSecondary - ), - euiColorDanger: '#BD271E', - euiColorWarning: '#F5A700', - - // Grays - ...graysLight, - - // Backgrounds - euiPageBackgroundColor: computed( - ['colors.euiColorLightestShade'], - ([euiColorLightestShade]) => tint(euiColorLightestShade, 0.5) - ), - euiColorHighlight: '#FFFCDD', - - // Every color below must be based mathematically on the set above and in a particular order. - euiTextColor: computed( - ['colors.euiColorDarkestShade'], - ([euiColorDarkestShade]) => euiColorDarkestShade - ), - euiTitleColor: computed(['colors.euiTextColor'], ([euiTextColor]) => - shade(euiTextColor, 0.5) - ), - euiTextSubduedColor: computed( - ['colors.euiColorMediumShade'], - ([euiColorMediumShade]) => makeHighContrastColor(euiColorMediumShade) - ), - euiColorDisabled: computed(['colors.euiTextColor'], ([euiTextColor]) => - tint(euiTextColor, 0.7) - ), - - // Contrasty text variants - ...textVariants, - - // State - euiFocusTransparency: 0.1, - euiFocusBackgroundColor: computed( - ['colors.euiColorPrimary', 'colors.euiFocusTransparency'], - ([euiColorPrimary, euiFocusTransparency]) => - tint(euiColorPrimary, 1 - euiFocusTransparency) - ), -}; - -const graysDark = { - euiColorEmptyShade: '#1D1E24', - euiColorLightestShade: '#25262E', - euiColorLightShade: '#343741', - euiColorMediumShade: '#535966', - euiColorDarkShade: '#98A2B3', - euiColorDarkestShade: '#D4DAE5', - euiColorFullShade: '#FFF', -}; - -export const dark = { - // These colors stay the same no matter the theme - ...poles, - - // Core - euiColorPrimary: '#1BA9F5', - euiColorSecondary: '#7DE2D1', - euiColorAccent: '#F990C0', - - // Status - euiColorSuccess: computed( - ['colors.euiColorSecondary'], - ([euiColorSecondary]) => euiColorSecondary - ), - euiColorWarning: '#FFCE7A', - euiColorDanger: '#F66', - - // Grays - ...graysDark, - - // Backgrounds - euiPageBackgroundColor: computed( - ['colors.euiColorLightestShade'], - ([euiColorLightestShade]) => shade(euiColorLightestShade, 0.3) - ), - euiColorHighlight: '#2E2D25', - - // Variations from core - euiTextColor: '#DFE5EF', - euiTitleColor: computed( - ['colors.euiTextColor'], - ([euiTextColor]) => euiTextColor - ), - euiTextSubduedColor: computed( - ['colors.euiColorMediumShade'], - ([euiColorMediumShade]) => makeHighContrastColor(euiColorMediumShade) - ), - euiColorDisabled: computed(['colors.euiTextColor'], ([euiTextColor]) => - shade(euiTextColor, 0.7) - ), - - // Contrasty text variants - ...textVariants, - - // State - euiFocusTransparency: 0.3, - euiFocusBackgroundColor: computed( - ['colors.euiColorPrimary', 'colors.euiFocusTransparency'], - ([euiColorPrimary, euiFocusTransparency]) => - shade(euiColorPrimary, 1 - euiFocusTransparency) - ), -}; - -// Visualization colors - -// Maps allow for easier JSON usage -// Use map_merge(euiColorVisColors, $yourMap) to change individual colors after importing ths file -// The `behindText` variant is a direct copy of the hex output by the JS euiPaletteColorBlindBehindText() function -const euiPaletteColorBlind = { - euiColorVis0: { - graphic: '#54B399', - behindText: '#6DCCB1', - }, - euiColorVis1: { - graphic: '#6092C0', - behindText: '#79AAD9', - }, - euiColorVis2: { - graphic: '#D36086', - behindText: '#EE789D', - }, - euiColorVis3: { - graphic: '#9170B8', - behindText: '#A987D1', - }, - euiColorVis4: { - graphic: '#CA8EAE', - behindText: '#E4A6C7', - }, - euiColorVis5: { - graphic: '#D6BF57', - behindText: '#F1D86F', - }, - euiColorVis6: { - graphic: '#B9A888', - behindText: '#D2C0A0', - }, - euiColorVis7: { - graphic: '#DA8B45', - behindText: '#F5A35C', - }, - euiColorVis8: { - graphic: '#AA6556', - behindText: '#C47C6C', - }, - euiColorVis9: { - graphic: '#E7664C', - behindText: '#FF7E62', - }, -}; - -const colorVis = { - euiColorVis0: euiPaletteColorBlind.euiColorVis0.graphic, - euiColorVis1: euiPaletteColorBlind.euiColorVis1.graphic, - euiColorVis2: euiPaletteColorBlind.euiColorVis2.graphic, - euiColorVis3: euiPaletteColorBlind.euiColorVis3.graphic, - euiColorVis4: euiPaletteColorBlind.euiColorVis4.graphic, - euiColorVis5: euiPaletteColorBlind.euiColorVis5.graphic, - euiColorVis6: euiPaletteColorBlind.euiColorVis6.graphic, - euiColorVis7: euiPaletteColorBlind.euiColorVis7.graphic, - euiColorVis8: euiPaletteColorBlind.euiColorVis8.graphic, - euiColorVis9: euiPaletteColorBlind.euiColorVis9.graphic, - - euiColorVis0_behindText: euiPaletteColorBlind.euiColorVis0.behindText, - euiColorVis1_behindText: euiPaletteColorBlind.euiColorVis1.behindText, - euiColorVis2_behindText: euiPaletteColorBlind.euiColorVis2.behindText, - euiColorVis3_behindText: euiPaletteColorBlind.euiColorVis3.behindText, - euiColorVis4_behindText: euiPaletteColorBlind.euiColorVis4.behindText, - euiColorVis5_behindText: euiPaletteColorBlind.euiColorVis5.behindText, - euiColorVis6_behindText: euiPaletteColorBlind.euiColorVis6.behindText, - euiColorVis7_behindText: euiPaletteColorBlind.euiColorVis7.behindText, - euiColorVis8_behindText: euiPaletteColorBlind.euiColorVis8.behindText, - euiColorVis9_behindText: euiPaletteColorBlind.euiColorVis9.behindText, -}; - -const base = 16; - -const sizes = { - euiSize: computed(['base'], ([base]) => `${base}px`), - euiSizeXS: computed(['base'], ([base]) => `${base * 0.25}px`), - euiSizeS: computed(['base'], ([base]) => `${base * 0.5}px`), - euiSizeM: computed(['base'], ([base]) => `${base * 0.75}px`), - euiSizeL: computed(['base'], ([base]) => `${base * 1.5}px`), - euiSizeXL: computed(['base'], ([base]) => `${base * 2}px`), - euiSizeXXL: computed(['base'], ([base]) => `${base * 2.5}px`), - - euiButtonMinWidth: computed(['base'], ([base]) => `${base * 7}px`), - - euiScrollBar: computed(['sizes.euiSize'], ([euiSize]) => euiSize), - euiScrollBarCorner: computed( - ['sizes.euiSizeS'], - ([euiSizeS]) => `calc(${euiSizeS} * 0.75)` - ), -}; - -const borderRadius = { - euiBorderRadius: '4px', - euiBorderRadiusSmall: computed( - ['borders.euiBorderRadius'], - ([euiBorderRadius]) => `calc(${euiBorderRadius} * 0.5)` - ), -}; - -const borders = { - euiBorderWidthThin: '1px', - euiBorderWidthThick: '2px', - - euiBorderColor: computed( - ['colors.euiColorLightShade'], - ([euiColorLightShade]) => euiColorLightShade - ), - - euiBorderThick: computed( - ['borders.euiBorderWidthThick', 'borders.euiBorderColor'], - ([euiBorderWidthThick, euiBorderColor]) => - `${euiBorderWidthThick} solid ${euiBorderColor}` - ), - euiBorderThin: computed( - ['borders.euiBorderWidthThin', 'borders.euiBorderColor'], - ([euiBorderWidthThin, euiBorderColor]) => - `${euiBorderWidthThin} solid ${euiBorderColor}` - ), - euiBorderEditable: computed( - ['borders.euiBorderWidthThick', 'borders.euiBorderColor'], - ([euiBorderWidthThick, euiBorderColor]) => - `${euiBorderWidthThick} dotted ${euiBorderColor}` - ), -}; - -export const euiThemeDefault = { - [COLOR_MODE_KEY]: { - light, - dark, - }, - colorVis, - base, - sizes, - borders: { - ...borderRadius, - ...borders, - }, - buttons: { - [COLOR_MODE_KEY]: { - light: { - custom: computed( - ['colors.euiColorPrimary'], - ([primary]) => primary /*'#000'*/ - ), - }, - dark: { custom: '#fff' }, - }, - }, -}; - -export const EuiThemeDefault = buildTheme(euiThemeDefault, 'EUI_THEME_DEFAULT'); - -/* AMSTERDAM THEME */ - -export const amsterdam_light = { - euiColorPrimary: '#07C', - euiColorSecondary: '#00BFB3', - euiColorAccent: '#F04E98', - - // These colors stay the same no matter the theme - ...poles, - - // Status - euiColorSuccess: computed( - ['colors.euiColorSecondary'], - ([euiColorSecondary]) => euiColorSecondary - ), - euiColorDanger: '#BD271E', - euiColorWarning: '#FEC514', - euiColorDisabled: '#ABB4C4', - - // Grays - ...graysLight, - - // Backgrounds - euiPageBackgroundColor: computed( - ['colors.euiColorLightestShade'], - ([euiColorLightestShade]) => tint(euiColorLightestShade, 0.5) - ), - euiColorHighlight: computed(['colors.euiColorWarning'], ([euiColorWarning]) => - tint(euiColorWarning, 0.9) - ), - - // Every color below must be based mathematically on the set above and in a particular order. - euiTextColor: computed( - ['colors.euiColorDarkestShade'], - ([euiColorDarkestShade]) => euiColorDarkestShade - ), - euiTitleColor: computed(['colors.euiTextColor'], ([euiTextColor]) => - shade(euiTextColor, 0.5) - ), - euiTextSubduedColor: computed( - ['colors.euiColorDarkShade'], - ([euiColorDarkShade]) => euiColorDarkShade - ), - - // Contrasty text variants - ...textVariants, - - // State - euiFocusTransparency: 0.9, - euiFocusBackgroundColor: computed( - ['colors.euiColorPrimary', 'colors.euiFocusTransparency'], - ([euiColorPrimary, euiFocusTransparency]) => - transparentize(euiColorPrimary, euiFocusTransparency) - ), -}; - -export const amsterdam_dark = { - // These colors stay the same no matter the theme - ...poles, - - // Core - euiColorPrimary: '#36A2EF', - euiColorSecondary: '#7DDED8', - euiColorAccent: '#F68FBE', - - // Status - euiColorSuccess: computed( - ['colors.euiColorSecondary'], - ([euiColorSecondary]) => euiColorSecondary - ), - euiColorWarning: '#F3D371', - euiColorDanger: '#F86B63', - euiColorDisabled: '#515761', - - // Grays - ...graysDark, - - // Backgrounds - euiPageBackgroundColor: computed( - ['colors.euiColorLightestShade'], - ([euiColorLightestShade]) => shade(euiColorLightestShade, 0.3) - ), - euiColorHighlight: '#2E2D25', - - // Variations from core - euiTextColor: '#DFE5EF', - euiTitleColor: computed( - ['colors.euiTextColor'], - ([euiTextColor]) => euiTextColor - ), - euiTextSubduedColor: computed( - ['colors.euiColorMediumShade'], - ([euiColorMediumShade]) => makeHighContrastColor(euiColorMediumShade) - ), - - // Contrasty text variants - ...textVariants, - - // State - euiFocusTransparency: 0.7, - euiFocusBackgroundColor: computed( - ['colors.euiColorPrimary', 'colors.euiFocusTransparency'], - ([euiColorPrimary, euiFocusTransparency]) => - transparentize(euiColorPrimary, euiFocusTransparency) - ), -}; - -const amsterdam_borderRadius = { - euiBorderRadius: computed( - ['sizes.euiSizeS'], - ([euiSizeS]) => `calc(${euiSizeS} * 0.75)` - ), - euiBorderRadiusSmall: computed( - ['sizes.euiSizeS'], - ([euiSizeS]) => `calc(${euiSizeS} * 0.5)` - ), -}; - -export const euiThemeAmsterdam = { - [COLOR_MODE_KEY]: { - light: amsterdam_light, - dark: amsterdam_dark, - }, - colorVis, - base, - sizes, - borders: { - ...amsterdam_borderRadius, - ...borders, - }, - buttons: { - [COLOR_MODE_KEY]: { - light: { - custom: '#000', - }, - dark: { custom: '#fff' }, - }, - }, -}; - -export const EuiThemeAmsterdam = buildTheme( - euiThemeAmsterdam, - 'EUI_THEME_AMSTERDAM' -); diff --git a/src/services/theme/types.ts b/src/services/theme/types.ts index bd5b079a6c0..76dd5a3dc03 100644 --- a/src/services/theme/types.ts +++ b/src/services/theme/types.ts @@ -6,18 +6,51 @@ * Side Public License, v 1. */ -import { RecursiveOmit, RecursivePartial } from '../../components/common'; -import { euiThemeDefault } from './theme'; +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'; -type EuiThemeColorModeInverse = 'inverse'; -type EuiThemeColorModeStandard = 'light' | 'dark'; +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 EuiThemeShape = typeof euiThemeDefault; -export type EuiThemeColor = EuiThemeShape['colors']['light']; +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; @@ -27,14 +60,28 @@ export type EuiThemeSystem = { export type EuiThemeModifications = RecursivePartial; -type Colorless = RecursiveOmit; -// I don't like this. -// Requires manually maintaining sections (e.g., `buttons`) containing colorMode options. -// Also cannot account for extended theme sections (`T`) that use colorMode options. -export type EuiThemeComputed = Colorless & { +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; - colors: EuiThemeColor; - buttons: Colorless & { - colors: EuiThemeShape['buttons']['colors']['light']; - }; -} & T; +}; diff --git a/src/services/theme/utils.test.ts b/src/services/theme/utils.test.ts index f408d359fc7..52bc023cebd 100644 --- a/src/services/theme/utils.test.ts +++ b/src/services/theme/utils.test.ts @@ -16,36 +16,29 @@ import { getComputed, buildTheme, mergeDeep, - currentColorModeOnly, } from './utils'; describe('isInverseColorMode', () => { it("true only if 'inverse'", () => { - expect(isInverseColorMode('light')).toBe(false); - expect(isInverseColorMode('dark')).toBe(false); + expect(isInverseColorMode('LIGHT')).toBe(false); + expect(isInverseColorMode('DARK')).toBe(false); expect(isInverseColorMode('custom')).toBe(false); expect(isInverseColorMode()).toBe(false); - expect(isInverseColorMode('inverse')).toBe(true); + expect(isInverseColorMode('INVERSE')).toBe(true); }); }); describe('getColorMode', () => { - it("defaults to 'light'", () => { - expect(getColorMode()).toEqual('light'); + it("defaults to 'LIGHT'", () => { + expect(getColorMode()).toEqual('LIGHT'); }); it('uses `parentMode` as fallback', () => { - expect(getColorMode(undefined, 'dark')).toEqual('dark'); + expect(getColorMode(undefined, 'DARK')).toEqual('DARK'); }); - it("understands 'inverse'", () => { - expect(getColorMode('inverse', 'dark')).toEqual('light'); - expect(getColorMode('inverse', 'light')).toEqual('dark'); - expect(getColorMode('inverse')).toEqual('light'); - }); - it('respects custom modes', () => { - expect(getColorMode('custom')).toEqual('custom'); - expect(getColorMode('custom', 'light')).toEqual('custom'); - expect(getColorMode(undefined, 'custom')).toEqual('custom'); - expect(getColorMode('light', 'custom')).toEqual('light'); + it("understands 'INVERSE'", () => { + expect(getColorMode('INVERSE', 'DARK')).toEqual('LIGHT'); + expect(getColorMode('INVERSE', 'LIGHT')).toEqual('DARK'); + expect(getColorMode('INVERSE')).toEqual('LIGHT'); }); }); @@ -63,9 +56,8 @@ describe('getOn', () => { }, }, colors: { - light: { primary: '#000' }, - dark: { primary: '#FFF' }, - custom: { primary: '#333' }, + LIGHT: { primary: '#000' }, + DARK: { primary: '#FFF' }, }, }; it('gets values at the given path', () => { @@ -80,10 +72,9 @@ describe('getOn', () => { expect(getOn(obj, 'other.thing.number', '')).toEqual(0); expect(getOn(obj, 'other.thing.func', '')).toBeInstanceOf(Function); }); - it('does can shortcut color modes', () => { - expect(getOn(obj, 'colors.primary', 'light')).toEqual('#000'); - expect(getOn(obj, 'colors.primary', 'dark')).toEqual('#FFF'); - expect(getOn(obj, 'colors.primary', 'custom')).toEqual('#333'); + it('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); @@ -131,24 +122,34 @@ describe('setOn', () => { }); describe('computed', () => { - it('should transform to Computed', () => { - const output = computed(['path.to'], ([path]) => path); + 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: { + LIGHT: { primary: '#000', - secondary: computed(['colors.primary'], ([primary]) => `${primary}000`), + secondary: computed(([primary]) => `${primary}000`, ['colors.primary']), }, - dark: { + DARK: { primary: '#FFF', - secondary: computed(['colors.primary'], ([primary]) => `${primary}FFF`), + secondary: computed((theme) => `${theme.colors.primary}FFF`), }, }, sizes: { @@ -159,14 +160,14 @@ const theme = buildTheme( ); describe('getComputed', () => { it('computes all values and returns only the current color mode', () => { - // @ts-ignore intentionally not using a full EUI theme definition - expect(getComputed(theme, {}, 'light')).toEqual({ + // @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-ignore intentionally not using a full EUI theme definition - expect(getComputed(theme, {}, 'dark')).toEqual({ + // @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', @@ -174,8 +175,8 @@ describe('getComputed', () => { }); it('respects simple overrides', () => { expect( - // @ts-ignore intentionally not using a full EUI theme definition - getComputed(theme, buildTheme({ sizes: { small: 4 } }, ''), 'light') + // @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 }, @@ -185,10 +186,10 @@ describe('getComputed', () => { it('respects overrides in computation', () => { expect( getComputed( - // @ts-ignore intentionally not using a full EUI theme definition + // @ts-expect-error intentionally not using a full EUI theme definition theme, - buildTheme({ colors: { light: { primary: '#CCC' } } }, ''), - 'light' + buildTheme({ colors: { LIGHT: { primary: '#CCC' } } }, ''), + 'LIGHT' ) ).toEqual({ colors: { primary: '#CCC', secondary: '#CCC000' }, @@ -199,10 +200,10 @@ describe('getComputed', () => { it('respects property extensions', () => { expect( getComputed( - // @ts-ignore intentionally not using a full EUI theme definition + // @ts-expect-error intentionally not using a full EUI theme definition theme, - buildTheme({ colors: { light: { tertiary: '#333' } } }, ''), - 'light' + buildTheme({ colors: { LIGHT: { tertiary: '#333' } } }, ''), + 'LIGHT' ) ).toEqual({ colors: { primary: '#000', secondary: '#000000', tertiary: '#333' }, @@ -213,10 +214,10 @@ describe('getComputed', () => { it('respects section extensions', () => { expect( getComputed( - // @ts-ignore intentionally not using a full EUI theme definition + // @ts-expect-error intentionally not using a full EUI theme definition theme, buildTheme({ custom: { myProp: '#333' } }, ''), - 'light' + 'LIGHT' ) ).toEqual({ colors: { primary: '#000', secondary: '#000000' }, @@ -228,22 +229,21 @@ describe('getComputed', () => { it('respects extensions in computation', () => { expect( getComputed( - // @ts-ignore intentionally not using a full EUI theme definition + // @ts-expect-error intentionally not using a full EUI theme definition theme, buildTheme( { colors: { - light: { - tertiary: computed( - ['colors.primary'], - ([primary]) => `${primary}333` - ), + LIGHT: { + tertiary: computed(([primary]) => `${primary}333`, [ + 'colors.primary', + ]), }, }, }, '' ), - 'light' + 'LIGHT' ) ).toEqual({ colors: { primary: '#000', secondary: '#000000', tertiary: '#000333' }, @@ -276,29 +276,3 @@ describe('mergeDeep', () => { ).toEqual({ a: 1, b: { c: { d: 3, e: 5 } } }); }); }); - -describe('currentColorModeOnly', () => { - const theme = { - colors: { - light: { - primary: '#000', - }, - dark: { - primary: '#FFF', - }, - }, - sizes: { - small: 8, - }, - }; - it('object with only the current color mode colors', () => { - expect(currentColorModeOnly('light', theme)).toEqual({ - colors: { primary: '#000' }, - sizes: { small: 8 }, - }); - expect(currentColorModeOnly('dark', theme)).toEqual({ - colors: { primary: '#FFF' }, - sizes: { small: 8 }, - }); - }); -}); diff --git a/src/services/theme/utils.ts b/src/services/theme/utils.ts index 2cfc6687324..9257033e8ce 100644 --- a/src/services/theme/utils.ts +++ b/src/services/theme/utils.ts @@ -12,29 +12,32 @@ import { EuiThemeSystem, EuiThemeShape, EuiThemeComputed, + COLOR_MODES_STANDARD, + COLOR_MODES_INVERSE, } from './types'; -export const COLOR_MODE_KEY = 'colors'; -export const DEFAULT_COLOR_MODE = 'light'; +export const DEFAULT_COLOR_MODE = COLOR_MODES_STANDARD.light; const isObject = (obj: any) => obj && typeof obj === 'object'; export const isInverseColorMode = (colorMode?: EuiThemeColorMode) => { - return colorMode === 'inverse'; + return colorMode === COLOR_MODES_INVERSE; }; export const getColorMode = ( colorMode?: EuiThemeColorMode, parentColorMode?: EuiThemeColorMode ) => { - if (colorMode == null) { + const mode = colorMode?.toUpperCase(); + if (mode == null) { return parentColorMode || DEFAULT_COLOR_MODE; - } else if (isInverseColorMode(colorMode)) { - return parentColorMode === 'dark' || parentColorMode === undefined - ? 'light' - : 'dark'; + } else if (isInverseColorMode(mode)) { + return parentColorMode === COLOR_MODES_STANDARD.dark || + parentColorMode === undefined + ? COLOR_MODES_STANDARD.light + : COLOR_MODES_STANDARD.dark; } else { - return colorMode; + return mode; } }; @@ -47,14 +50,20 @@ export const getOn = ( let node = model; while (path.length) { const segment = path.shift()!; + if (node.hasOwnProperty(segment) === false) { - return undefined; - } - if (colorMode && segment === COLOR_MODE_KEY) { - if (node[segment].hasOwnProperty(colorMode) === false) { - return undefined; + 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 { - node = node[segment][colorMode]; + return undefined; } } else { if (node[segment] instanceof Computed) { @@ -91,8 +100,8 @@ export const setOn = ( export class Computed { constructor( - public dependencies: string[], - public computer: (...values: any[]) => T + public computer: (...values: any[]) => T, + public dependencies: string | string[] = [] ) {} getValue( @@ -101,10 +110,20 @@ export class Computed { 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, colorMode) ?? + getOn(working, dependency) ?? getOn(modifications, dependency, colorMode) ?? getOn(base, dependency, colorMode) ); @@ -113,12 +132,21 @@ export class Computed { } } -export const computed = ( - dependencies: string[], - computer: (values: any[]) => T -) => { - return (new Computed(dependencies, computer) as unknown) as T; -}; +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); +} export const getComputed = ( base: EuiThemeSystem, @@ -134,27 +162,31 @@ export const getComputed = ( path?: string ) { Object.keys(base).forEach((key) => { - const arr = path?.split('.') || []; - const last = arr[arr.length - 1]; - if (last === COLOR_MODE_KEY && key !== colorMode) { - // Intentional no-op - } else { - const newPath = path ? `${path}.${key}` : `${key}`; - const 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)) { - loop(baseValue, overValue ?? {}, checkExisting, newPath); - } else { - setOn(output, newPath, overValue ?? baseValue); - } + 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); } } }); @@ -163,7 +195,7 @@ export const getComputed = ( loop(base, over); // Compute and apply extension values only loop(over, {}, true); - return currentColorModeOnly(colorMode, (output as unknown) as T); + return output as EuiThemeComputed; }; export const buildTheme = (model: T, key: string) => { @@ -208,7 +240,7 @@ export const buildTheme = (model: T, key: string) => { const target = property === 'root' ? _target : _target.model || _target; // @ts-ignore `string` index signature const value = target[property]; - if (typeof value === 'object' && value !== null) { + if (isObject(value) && !Array.isArray(value)) { return new Proxy( { model: value, @@ -270,26 +302,3 @@ export const mergeDeep = ( return target; }; - -export const currentColorModeOnly = ( - colorMode: EuiThemeColorMode, - _theme: { [key: string]: any } -): EuiThemeComputed => { - const theme: { [key: string]: any } = {}; - - Object.keys(_theme).forEach((key) => { - if (key === COLOR_MODE_KEY) { - theme[key] = _theme[key][colorMode]; - } else { - const themeValue = _theme[key]; - - if (isObject(themeValue)) { - theme[key] = currentColorModeOnly(colorMode, themeValue); - } else { - theme[key] = themeValue; - } - } - }); - - return theme as EuiThemeComputed; -}; diff --git a/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..cdba42f89df --- /dev/null +++ b/src/themes/eui-amsterdam/global_styling/variables/_borders.ts @@ -0,0 +1,20 @@ +/* + * 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 { + border, + EuiThemeBorder, +} from '../../../../global_styling/variables/_borders'; +import { sizeToPixel } from '../../../../global_styling/variables/_size'; + +export const border_ams: EuiThemeBorder = { + ...border, + radius: computed(sizeToPixel(0.375)), + radiusSmall: 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..4b39e31df95 --- /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 '../../../../global_styling/functions/_colors'; +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..caf7d7d0b92 --- /dev/null +++ b/src/themes/eui-amsterdam/global_styling/variables/_states.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 { computed } from '../../../../services/theme/utils'; +import { transparentize } from '../../../../services/color'; +import { + focus, + _EuiThemeFocus, +} from '../../../../global_styling/variables/_states'; + +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..9e8f2c22535 --- /dev/null +++ b/src/themes/eui-amsterdam/global_styling/variables/title.ts @@ -0,0 +1,25 @@ +/* + * 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 { + 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..669d9fd7bc4 --- /dev/null +++ b/src/themes/eui-amsterdam/theme.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. + */ + +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'; +import { focus_ams } from './global_styling/variables/_states'; +import { fontSize } from '../../global_styling/variables/text'; + +export const euiThemeAmsterdam: EuiThemeShape = { + colors: colors_ams, + base, + size, + font: { + ...font_ams, + ...fontSize, + }, + border: border_ams, + focus: focus_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..3bec96cbf84 --- /dev/null +++ b/src/themes/eui/theme.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 { 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 { focus } from '../../global_styling/variables/_states'; +import { font } from '../../global_styling/variables/_typography'; +import { border } from '../../global_styling/variables/_borders'; +import { fontSize } from '../../global_styling/variables/text'; + +export const euiThemeDefault: EuiThemeShape = { + colors, + base, + size, + font: { + ...font, + ...fontSize, + }, + border, + focus, + animation, + breakpoint, +}; + +export const EuiThemeDefault = buildTheme(euiThemeDefault, 'EUI_THEME_DEFAULT');