From a036b0ebcd4a8b9634f82e7f01ed88eaa94d3699 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Fri, 22 Jan 2021 15:45:37 -0600 Subject: [PATCH] Add color indicator component (#10209) Co-authored-by: Mark Stacey --- ui/app/components/app/account-menu/index.scss | 2 +- .../ui/color-indicator/color-indicator.js | 50 +++++++++++++ .../ui/color-indicator/color-indicator.scss | 61 +++++++++++++++ .../color-indicator.stories.js | 27 +++++++ ui/app/components/ui/color-indicator/index.js | 1 + ui/app/components/ui/ui-components.scss | 1 + ui/app/css/design-system/typography.scss | 20 ++--- ui/app/css/utilities/_colors.scss | 75 +++++++++++++++++++ ui/app/css/utilities/index.scss | 1 + 9 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 ui/app/components/ui/color-indicator/color-indicator.js create mode 100644 ui/app/components/ui/color-indicator/color-indicator.scss create mode 100644 ui/app/components/ui/color-indicator/color-indicator.stories.js create mode 100644 ui/app/components/ui/color-indicator/index.js create mode 100644 ui/app/css/utilities/_colors.scss create mode 100644 ui/app/css/utilities/index.scss diff --git a/ui/app/components/app/account-menu/index.scss b/ui/app/components/app/account-menu/index.scss index ee3e82bc0..39c5b97f9 100644 --- a/ui/app/components/app/account-menu/index.scss +++ b/ui/app/components/app/account-menu/index.scss @@ -203,7 +203,7 @@ } &__check-mark-icon { - background-image: url("images/check-white.svg"); + background-image: url("/images/check-white.svg"); height: 18px; width: 18px; background-repeat: no-repeat; diff --git a/ui/app/components/ui/color-indicator/color-indicator.js b/ui/app/components/ui/color-indicator/color-indicator.js new file mode 100644 index 000000000..83718a0f7 --- /dev/null +++ b/ui/app/components/ui/color-indicator/color-indicator.js @@ -0,0 +1,50 @@ +import React from 'react' +import classnames from 'classnames' +import PropTypes from 'prop-types' +import { COLORS } from '../../../helpers/constants/design-system' + +export default function ColorIndicator({ + size = 'small', + type = 'outlined', + color = COLORS.UI4, + borderColor, + iconClassName, +}) { + const colorIndicatorClassName = classnames('color-indicator', { + 'color-indicator--filled': type === 'filled' || Boolean(iconClassName), + 'color-indicator--partial-filled': type === 'partial-filled', + [`color-indicator--border-color-${borderColor}`]: Boolean(borderColor), + [`color-indicator--color-${color}`]: true, + [`color-indicator--size-${size}`]: true, + }) + + return ( +
+ {iconClassName ? ( + + ) : ( + + )} +
+ ) +} + +ColorIndicator.SIZES = { + LARGE: 'large', + MEDIUM: 'medium', + SMALL: 'small,', +} + +ColorIndicator.TYPES = { + FILLED: 'filled', + PARTIAL: 'partial-filled', + OUTLINE: 'outline', +} + +ColorIndicator.propTypes = { + color: PropTypes.oneOf(Object.values(COLORS)), + borderColor: PropTypes.oneOf(Object.values(COLORS)), + size: PropTypes.oneOf(Object.values(ColorIndicator.SIZES)), + iconClassName: PropTypes.string, + type: PropTypes.oneOf(Object.values(ColorIndicator.TYPES)), +} diff --git a/ui/app/components/ui/color-indicator/color-indicator.scss b/ui/app/components/ui/color-indicator/color-indicator.scss new file mode 100644 index 000000000..b6eb93104 --- /dev/null +++ b/ui/app/components/ui/color-indicator/color-indicator.scss @@ -0,0 +1,61 @@ +@use "utilities"; +@use "design-system"; + +$sizes: ( + 'large': 6, + 'medium': 5, + 'small': 4, +); + +.color-indicator { + $self: &; + + border: 1px solid transparent; + display: flex; + align-items: center; + justify-content: center; + + &__inner-circle { + background-color: transparent; + } + + @each $variant, $size in $sizes { + &--size-#{$variant} { + height: #{2 * $size}px; + width: #{2 * $size}px; + border-radius: #{$size}px; + + #{$self}__inner-circle { + border-radius: #{$size}px; + height: #{$size}px; + width: #{$size}px; + } + + #{$self}__icon { + font-size: #{1.25 * $size}px; + } + } + } + + @each $variant, $color in design-system.$color-map { + &--color-#{$variant} { + border-color: $color; + &#{$self}--partial-filled #{$self}__inner-circle { + background-color: $color; + } + &#{$self}--filled { + background-color: $color; + } + & #{$self}__icon { + color: #{utilities.choose-contrast-color($color)}; + } + } + } + + // separate iterator to ensure borderColor takes precedence + @each $variant, $color in design-system.$color-map { + &--border-color-#{$variant} { + border-color: $color; + } + } +} diff --git a/ui/app/components/ui/color-indicator/color-indicator.stories.js b/ui/app/components/ui/color-indicator/color-indicator.stories.js new file mode 100644 index 000000000..81d7a2159 --- /dev/null +++ b/ui/app/components/ui/color-indicator/color-indicator.stories.js @@ -0,0 +1,27 @@ +import React from 'react' +import { select } from '@storybook/addon-knobs' +import { COLORS } from '../../../helpers/constants/design-system' +import ColorIndicator from './color-indicator' + +export default { + title: 'ColorIndicator', +} + +export const colorIndicator = () => ( + +) + +export const withIcon = () => ( + +) diff --git a/ui/app/components/ui/color-indicator/index.js b/ui/app/components/ui/color-indicator/index.js new file mode 100644 index 000000000..257ecdc0b --- /dev/null +++ b/ui/app/components/ui/color-indicator/index.js @@ -0,0 +1 @@ +export { default } from './color-indicator' diff --git a/ui/app/components/ui/ui-components.scss b/ui/app/components/ui/ui-components.scss index f7f18b0a2..2317072e1 100644 --- a/ui/app/components/ui/ui-components.scss +++ b/ui/app/components/ui/ui-components.scss @@ -9,6 +9,7 @@ @import 'check-box/index'; @import 'chip/chip'; @import 'circle-icon/index'; +@import 'color-indicator/color-indicator'; @import 'currency-display/index'; @import 'currency-input/index'; @import 'dialog/dialog'; diff --git a/ui/app/css/design-system/typography.scss b/ui/app/css/design-system/typography.scss index ff0725ea8..73ccab414 100644 --- a/ui/app/css/design-system/typography.scss +++ b/ui/app/css/design-system/typography.scss @@ -1,4 +1,4 @@ -$fa-font-path: 'fonts/fontawesome'; +$fa-font-path: '/fonts/fontawesome'; @import '../../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome'; @import '../../../../node_modules/@fortawesome/fontawesome-free/scss/solid'; @@ -8,63 +8,63 @@ $fa-font-path: 'fonts/fontawesome'; font-family: 'Roboto'; font-style: normal; font-weight: 100; - src: local('Roboto Thin'), local('Roboto-Thin'), url('fonts/Roboto/Roboto-Thin.ttf') format('truetype'); + src: local('Roboto Thin'), local('Roboto-Thin'), url('/fonts/Roboto/Roboto-Thin.ttf') format('truetype'); } @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 300; - src: local('Roboto Light'), local('Roboto-Light'), url('fonts/Roboto/Roboto-Light.ttf') format('truetype'); + src: local('Roboto Light'), local('Roboto-Light'), url('/fonts/Roboto/Roboto-Light.ttf') format('truetype'); } @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url('fonts/Roboto/Roboto-Regular.ttf') format('truetype'); + src: local('Roboto'), local('Roboto-Regular'), url('/fonts/Roboto/Roboto-Regular.ttf') format('truetype'); } @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url('fonts/Roboto/Roboto-Medium.ttf') format('truetype'); + src: local('Roboto Medium'), local('Roboto-Medium'), url('/fonts/Roboto/Roboto-Medium.ttf') format('truetype'); } @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; - src: local('Roboto Bold'), local('Roboto-Bold'), url('fonts/Roboto/Roboto-Bold.ttf') format('truetype'); + src: local('Roboto Bold'), local('Roboto-Bold'), url('/fonts/Roboto/Roboto-Bold.ttf') format('truetype'); } @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 900; - src: local('Roboto Black'), local('Roboto-Black'), url('fonts/Roboto/Roboto-Black.ttf') format('truetype'); + src: local('Roboto Black'), local('Roboto-Black'), url('/fonts/Roboto/Roboto-Black.ttf') format('truetype'); } @font-face { font-family: 'Euclid'; font-style: normal; font-weight: 400; - src: url('fonts/Euclid/EuclidCircularB-Regular-WebXL.ttf') format('truetype'); + src: url('/fonts/Euclid/EuclidCircularB-Regular-WebXL.ttf') format('truetype'); } @font-face { font-family: 'Euclid'; font-style: italic; font-weight: 400; - src: url('fonts/Euclid/EuclidCircularB-RegularItalic-WebXL.ttf') format('truetype'); + src: url('/fonts/Euclid/EuclidCircularB-RegularItalic-WebXL.ttf') format('truetype'); } @font-face { font-family: 'Euclid'; font-style: normal; font-weight: 700; - src: url('fonts/Euclid/EuclidCircularB-Bold-WebXL.ttf') format('truetype'); + src: url('/fonts/Euclid/EuclidCircularB-Bold-WebXL.ttf') format('truetype'); } $font-family: Euclid, Roboto, Helvetica, Arial, sans-serif; diff --git a/ui/app/css/utilities/_colors.scss b/ui/app/css/utilities/_colors.scss new file mode 100644 index 000000000..da0cb9a66 --- /dev/null +++ b/ui/app/css/utilities/_colors.scss @@ -0,0 +1,75 @@ +@use "sass:math"; +@use "sass:map"; + +/** + * Calculate the luminance for a color. + * See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests + * + * Note: + * @Gudahtt and @brad-decker have identified a potential performance concern + * that **might** impact build times if this function becomes widely used. It + * has not been validated, and is predicated on user-land math.pow functions + * prior to dart-sass 1.25.0. However, if the build times of sass raise + * exponentially a lookup table of all possible values for the math.pow call + * can be used to bypass the issue. This will require some logic updates as + * well. For reference, the blog post where the performance concern was + * originally made is here https://bit.ly/3c3qXzq (css-tricks) + */ +@function luminance($color) { + $channels: ( + 'red': red($color), + 'green': green($color), + 'blue': blue($color), + ); + + $values: ( + 'red': map.get($channels, 'red') / 255, + 'blue': map.get($channels, 'red') / 255, + 'green': map.get($channels, 'red') / 255, + ); + + @each $name, $value in $values { + @if $value <= 0.03928 { + $value: $value / 12.92; + } + + @else { + $value: ($value + 0.055) / 1.055; + $value: math.pow($value, 2.4); + } + + $values: map.merge($values, ($name: $value)); + } + + @return (0.2126 * map.get($values, 'red')) + + (0.7152 * map.get($values, 'green')) + + (0.0722 * map.get($values, 'blue')); +} + +/** + * Calculate the contrast ratio between two colors. + * See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests + */ +@function contrast($back, $front) { + $backLum: luminance($back) + 0.05; + $foreLum: luminance($front) + 0.05; + + @return max($backLum, $foreLum) / min($backLum, $foreLum); +} + +/** + * Determine whether to use dark or light text on top of given color. + * Returns black for dark text and white for light text. + */ +@function choose-contrast-color($color) { + $lightContrast: contrast($color, white); + $darkContrast: contrast($color, black); + + @if ($lightContrast > $darkContrast) { + @return white; + } + + @else { + @return black; + } +} diff --git a/ui/app/css/utilities/index.scss b/ui/app/css/utilities/index.scss new file mode 100644 index 000000000..701193836 --- /dev/null +++ b/ui/app/css/utilities/index.scss @@ -0,0 +1 @@ +@forward 'colors';