diff --git a/ui/app/components/ui/box/box.js b/ui/app/components/ui/box/box.js new file mode 100644 index 000000000..d8a630bad --- /dev/null +++ b/ui/app/components/ui/box/box.js @@ -0,0 +1,149 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { + ALIGN_ITEMS, + BLOCK_SIZES, + BORDER_STYLE, + COLORS, + DISPLAY, + JUSTIFY_CONTENT, + SIZES, +} from '../../../helpers/constants/design-system' + +const ValidSize = PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) +const ArrayOfValidSizes = PropTypes.arrayOf(ValidSize) +const MultipleSizes = PropTypes.oneOf([ValidSize, ArrayOfValidSizes]) + +function generateSizeClasses(baseClass, type, main, top, right, bottom, left) { + const arr = Array.isArray(main) ? main : [] + const singleDigit = Array.isArray(main) ? undefined : main + if (Array.isArray(main) && ![2, 3, 4].includes(main.length)) { + throw new Error( + `Expected prop ${type} to have length between 2 and 4, received ${main.length}`, + ) + } + + const isHorizontalAndVertical = arr.length === 2 + const isTopHorizontalAndBottom = arr.length === 3 + const isAllFour = arr.length === 4 + const hasAtLeastTwo = arr.length >= 2 + const hasAtLeastThree = arr.length >= 3 + return { + [`${baseClass}--${type}-${singleDigit}`]: singleDigit !== undefined, + [`${baseClass}--${type}-top-${top}`]: typeof top === 'number', + [`${baseClass}--${type}-right-${right}`]: typeof right === 'number', + [`${baseClass}--${type}-bottom-${bottom}`]: typeof bottom === 'number', + [`${baseClass}--${type}-left-${left}`]: typeof left === 'number', + // As long as an array of length >= 2 has been provided, the first number + // will always be for the top value. + [`${baseClass}--${type}-top-${arr?.[0]}`]: hasAtLeastTwo, + // As long as an array of length >= 2 has been provided, the second number + // will always be for the right value. + [`${baseClass}--${type}-right-${arr?.[1]}`]: hasAtLeastTwo, + // If an array has 2 values, the first number is the bottom value. If + // instead if has 3 or more values, the third number will be the bottom. + [`${baseClass}--${type}-bottom-${arr?.[2]}`]: hasAtLeastThree, + [`${baseClass}--${type}-bottom-${arr?.[0]}`]: isHorizontalAndVertical, + // If an array has 2 or 3 values, the second number will be the left value + [`${baseClass}--${type}-left-${arr?.[1]}`]: + isHorizontalAndVertical || isTopHorizontalAndBottom, + // If an array has 4 values, the fourth number is the left value + [`${baseClass}--${type}-left-${arr?.[3]}`]: isAllFour, + } +} + +export default function Box({ + padding, + paddingTop, + paddingRight, + paddingBottom, + paddingLeft, + margin, + marginTop, + marginRight, + marginBottom, + marginLeft, + borderColor, + borderWidth, + borderRadius, + borderStyle, + alignItems, + justifyContent, + display, + width, + height, + children, +}) { + const boxClassName = classnames('box', { + // ---Borders--- + // if borderWidth or borderColor is supplied w/o style, default to solid + 'box--border-style-solid': + !borderStyle && (Boolean(borderWidth) || Boolean(borderColor)), + // if borderColor supplied w/o width, default to 1 + 'box--border-size-1': !borderWidth && Boolean(borderColor), + [`box--border-color-${borderColor}`]: Boolean(borderColor), + [`box--rounded-${borderRadius}`]: Boolean(borderRadius), + [`box--border-style-${borderStyle}`]: Boolean(borderStyle), + [`box--border-size-${borderWidth}`]: Boolean(borderWidth), + // Margin + ...generateSizeClasses( + 'box', + 'margin', + margin, + marginTop, + marginRight, + marginBottom, + marginLeft, + ), + // Padding + ...generateSizeClasses( + 'box', + 'padding', + padding, + paddingTop, + paddingRight, + paddingBottom, + paddingLeft, + ), + // ---Flex/Grid alignment--- + // if justifyContent or alignItems supplied w/o display, default to flex + 'box--display-flex': + !display && (Boolean(justifyContent) || Boolean(alignItems)), + [`box--justify-content-${justifyContent}`]: Boolean(justifyContent), + [`box--align-items-${alignItems}`]: Boolean(alignItems), + // display + [`box--display-${display}`]: Boolean(display), + // width & height + [`box--width-${width}`]: Boolean(width), + [`box--height-${height}`]: Boolean(height), + }) + // Apply Box styles to any other component using function pattern + if (typeof children === 'function') { + return children(boxClassName) + } + return
{children}
+} + +Box.propTypes = { + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + margin: MultipleSizes, + marginTop: ValidSize, + marginBottom: ValidSize, + marginRight: ValidSize, + marginLeft: ValidSize, + padding: MultipleSizes, + paddingTop: ValidSize, + paddingBottom: ValidSize, + paddingRight: ValidSize, + paddingLeft: ValidSize, + borderColor: PropTypes.oneOf(Object.values(COLORS)), + borderWidth: PropTypes.number, + borderRadius: PropTypes.oneOf(Object.values(SIZES)), + borderStyle: PropTypes.oneOf(Object.values(BORDER_STYLE)), + alignItems: PropTypes.oneOf(Object.values(ALIGN_ITEMS)), + justifyContent: PropTypes.oneOf(Object.values(JUSTIFY_CONTENT)), + display: PropTypes.oneOf(Object.values(DISPLAY)), + width: PropTypes.oneOf(Object.values(BLOCK_SIZES)), + height: PropTypes.oneOf(Object.values(BLOCK_SIZES)), +} diff --git a/ui/app/components/ui/box/box.scss b/ui/app/components/ui/box/box.scss new file mode 100644 index 000000000..6ef762766 --- /dev/null +++ b/ui/app/components/ui/box/box.scss @@ -0,0 +1,134 @@ +@use "sass:map"; +@use "design-system"; +@use "utilities"; + +$attributes: padding, margin; + +.box { + // Padding and Margin + @each $attribute in $attributes { + @each $size in design-system.$sizes-numeric { + &--#{$attribute}-#{$size} { + #{$attribute}: utilities.get-spacing($size); + } + } + + @each $size in design-system.$sizes-numeric { + @each $direction in design-system.$directions { + &--#{$attribute}-#{$direction}-#{$size} { + #{$attribute}-#{$direction}: utilities.get-spacing($size); + } + } + } + } + + // Borders + @each $size in design-system.$sizes-numeric { + &--border-size-#{$size} { + border-width: #{$size}px; + } + } + + @each $variant, $color in design-system.$color-map { + &--border-color-#{$variant} { + border-color: $color; + } + } + + @each $border-style in design-system.$border-style { + &--border-style-#{$border-style} { + border-style: $border-style; + } + } + + &--rounded-none { + border-radius: 0; + } + + &--rounded-xs { + border-radius: 0.125rem; + } + + &--rounded-sm { + border-radius: 0.25rem; + } + + &--rounded-md { + border-radius: 0.375rem; + } + + &--rounded-lg { + border-radius: 0.5rem; + } + + &--rounded-xl { + border-radius: 0.75rem; + } + + // Display and Flex/Grid alignment + @each $display in design-system.$display { + &--display-#{$display} { + display: $display; + } + } + + @each $alignment in design-system.$align-items { + &--align-items-#{$alignment} { + align-items: $alignment; + } + } + + @each $justification in design-system.$justify-content { + &--justify-content-#{$justification} { + justify-content: $justification; + } + } + + // Width and Height + &--width-full { + width: 100%; + } + + &--height-full { + height: 100%; + } + + @each $fraction, $value in design-system.$fractions { + &--width-#{$fraction} { + width: $value; + } + + &--height-#{$fraction} { + height: $value; + } + } + + &--height-screen { + height: 100vh; + } + + &--width-screen { + width: 100vw; + } + + &--height-max { + height: max-content; + } + + &--width-max { + width: max-content; + } + + &--height-min { + height: min-content; + } + + &--width-min { + width: min-content; + } + + // text + @each $alignment in design-system.$text-align { + text-align: $alignment; + } +} diff --git a/ui/app/components/ui/box/box.stories.js b/ui/app/components/ui/box/box.stories.js new file mode 100644 index 000000000..324e642b2 --- /dev/null +++ b/ui/app/components/ui/box/box.stories.js @@ -0,0 +1,79 @@ +import { number, select } from '@storybook/addon-knobs' +import React from 'react' +import { + ALIGN_ITEMS, + BLOCK_SIZES, + BORDER_STYLE, + COLORS, + DISPLAY, + JUSTIFY_CONTENT, +} from '../../../helpers/constants/design-system' +import Box from './box' + +export default { + title: 'Box', +} + +const sizeKnobOptions = [undefined, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + +export const box = () => { + const items = [] + const size = number( + 'size', + 100, + { range: true, min: 50, max: 500, step: 10 }, + 'children', + ) + for (let $i = 0; $i < number('items', 1, {}, 'children'); $i++) { + items.push() + } + return ( + + {items} + + ) +} diff --git a/ui/app/components/ui/box/index.js b/ui/app/components/ui/box/index.js new file mode 100644 index 000000000..e5b58f373 --- /dev/null +++ b/ui/app/components/ui/box/index.js @@ -0,0 +1 @@ +export { default } from './box' diff --git a/ui/app/components/ui/chip/chip.js b/ui/app/components/ui/chip/chip.js index 78dc8173d..1fa3faf3d 100644 --- a/ui/app/components/ui/chip/chip.js +++ b/ui/app/components/ui/chip/chip.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import classnames from 'classnames' import { omit } from 'lodash' import Typography from '../typography' -import { COLORS } from '../../../helpers/constants/design-system' +import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system' export default function Chip({ className, @@ -37,9 +37,9 @@ export default function Chip({ {children ?? ( {label} @@ -54,7 +54,9 @@ Chip.propTypes = { borderColor: PropTypes.oneOf(Object.values(COLORS)), label: PropTypes.string, children: PropTypes.node, - labelProps: PropTypes.shape(omit(Typography.propTypes, ['className'])), + labelProps: PropTypes.shape({ + ...omit(Typography.propTypes, ['className']), + }), leftIcon: PropTypes.node, rightIcon: PropTypes.node, className: PropTypes.string, diff --git a/ui/app/components/ui/typography/typography.js b/ui/app/components/ui/typography/typography.js index 93990b00b..f3ea8fac7 100644 --- a/ui/app/components/ui/typography/typography.js +++ b/ui/app/components/ui/typography/typography.js @@ -1,7 +1,13 @@ import React from 'react' import classnames from 'classnames' import PropTypes from 'prop-types' -import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system' +import { + COLORS, + FONT_WEIGHT, + TEXT_ALIGN, + TYPOGRAPHY, +} from '../../../helpers/constants/design-system' +import Box from '../box' const { H6, H7, H8, H9 } = TYPOGRAPHY @@ -11,16 +17,15 @@ export default function Typography({ color = COLORS.BLACK, tag, children, - spacing = 1, fontWeight = 'normal', align, + boxProps = {}, }) { const computedClassName = classnames( 'typography', className, `typography--${variant}`, `typography--align-${align}`, - `typography--spacing-${spacing}`, `typography--color-${color}`, `typography--weight-${fontWeight}`, ) @@ -33,7 +38,15 @@ export default function Typography({ Tag = H6 } - return {children} + return ( + + {(boxClassName) => ( + + {children} + + )} + + ) } Typography.propTypes = { @@ -41,9 +54,11 @@ Typography.propTypes = { children: PropTypes.node.isRequired, color: PropTypes.oneOf(Object.values(COLORS)), className: PropTypes.string, - align: PropTypes.oneOf(['center', 'right']), - spacing: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8]), - fontWeight: PropTypes.oneOf(['bold', 'normal']), + align: PropTypes.oneOf(Object.values(TEXT_ALIGN)), + boxProps: PropTypes.shape({ + ...Box.propTypes, + }), + fontWeight: PropTypes.oneOf(Object.values(FONT_WEIGHT)), tag: PropTypes.oneOf([ 'p', 'h1', diff --git a/ui/app/components/ui/typography/typography.scss b/ui/app/components/ui/typography/typography.scss index e377f31d2..50a2ec622 100644 --- a/ui/app/components/ui/typography/typography.scss +++ b/ui/app/components/ui/typography/typography.scss @@ -4,6 +4,10 @@ .typography { @include design-system.Paragraph; + & b { + font-weight: 700; + } + @each $variant in map.keys(design-system.$typography-variants) { &--#{$variant} { @include design-system.typography($variant); @@ -16,18 +20,16 @@ } } - @each $variant, $weight in design-system.$typography-font-weights { - &--weight-#{$variant} { + @each $weight in design-system.$font-weight { + &--weight-#{$weight} { font-weight: $weight; } } - &--align-center { - text-align: center; - } - - &--align-right { - text-align: right; + @each $alignment in design-system.$text-align { + &--align-#{$alignment} { + text-align: $alignment; + } } @for $i from 1 through 8 { diff --git a/ui/app/components/ui/typography/typography.stories.js b/ui/app/components/ui/typography/typography.stories.js index 68d567e35..0d0d486b8 100644 --- a/ui/app/components/ui/typography/typography.stories.js +++ b/ui/app/components/ui/typography/typography.stories.js @@ -1,23 +1,17 @@ import React from 'react' import { number, select, text } from '@storybook/addon-knobs' -import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system' +import { + COLORS, + FONT_WEIGHT, + TEXT_ALIGN, + TYPOGRAPHY, +} from '../../../helpers/constants/design-system' import Typography from '.' export default { title: 'Typography', } -const fontWeightOptions = { - bold: 'bold', - normal: 'normal', -} - -const alignOptions = { - left: undefined, - center: 'center', - right: 'right', -} - export const list = () => (
{Object.values(TYPOGRAPHY).map((variant) => ( @@ -26,8 +20,12 @@ export const list = () => ( variant={variant} color={select('color', COLORS, COLORS.BLACK)} spacing={number('spacing', 1, { range: true, min: 1, max: 8 })} - align={select('align', alignOptions, undefined)} - fontWeight={select('font weight', fontWeightOptions, 'normal')} + align={select('align', TEXT_ALIGN, undefined)} + fontWeight={select( + 'font weight', + Object.values(FONT_WEIGHT), + FONT_WEIGHT.NORMAL, + )} > {variant} @@ -43,8 +41,8 @@ export const TheQuickOrangeFox = () => ( color={select('color', COLORS, COLORS.BLACK)} variant={select('variant', TYPOGRAPHY, TYPOGRAPHY.Paragraph)} spacing={number('spacing', 1, { range: true, min: 1, max: 8 })} - align={select('align', alignOptions, undefined)} - fontWeight={select('font weight', fontWeightOptions, 'normal')} + align={select('align', TEXT_ALIGN, undefined)} + fontWeight={select('font weight', FONT_WEIGHT, FONT_WEIGHT.NORMAL)} > {text('content', 'The quick orange fox jumped over the lazy dog.')} diff --git a/ui/app/components/ui/ui-components.scss b/ui/app/components/ui/ui-components.scss index 2b76068d2..09382d2a3 100644 --- a/ui/app/components/ui/ui-components.scss +++ b/ui/app/components/ui/ui-components.scss @@ -2,6 +2,7 @@ @import 'account-mismatch-warning/index'; @import 'alert-circle-icon/index'; @import 'alert/index'; +@import 'box/box'; @import 'breadcrumbs/index'; @import 'button-group/index'; @import 'button/buttons'; diff --git a/ui/app/css/design-system/attributes.scss b/ui/app/css/design-system/attributes.scss new file mode 100644 index 000000000..2176cddb9 --- /dev/null +++ b/ui/app/css/design-system/attributes.scss @@ -0,0 +1,72 @@ +$align-items: + baseline, + center, + flex-end, + flex-start, + stretch; + +$justify-content: + center, + flex-end, + flex-start, + space-around, + space-between, + space-evenly; + +$fractions: ( + 1\/2: 50%, + 1\/3: 33.333333%, + 2\/3: 66.666667%, + 1\/4: 25%, + 2\/4: 50%, + 3\/4: 75%, + 1\/5: 20%, + 2\/5: 40%, + 3\/5: 60%, + 4\/5: 80%, + 1\/6: 16.666667%, + 2\/6: 33.333333%, + 3\/6: 50%, + 4\/6: 66.666667%, + 5\/6: 83.333333%, + 1\/12: 8.333333%, + 2\/12: 16.666667%, + 3\/12: 25%, + 4\/12: 33.333333%, + 5\/12: 41.666667%, + 6\/12: 50%, + 7\/12: 58.333333%, + 8\/12: 66.666667%, + 9\/12: 75%, + 10\/12: 83.333333%, + 11\/12: 91.666667%, +); + +$sizes-numeric: + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12; + +$sizes-strings: + xs, + sm, + md, + lg, + xl, + none; + +$border-style: solid, double, none, dashed, dotted; +$directions: top, right, bottom, left; +$display: block, grid, flex, inline-block, inline-grid, inline-flex, list-item; +$text-align: left, right, center, justify; +$font-weight: bold, normal, 100, 200, 300, 400, 500, 600, 700, 800, 900; diff --git a/ui/app/css/design-system/index.scss b/ui/app/css/design-system/index.scss index d4f8980b9..3d5e5e410 100644 --- a/ui/app/css/design-system/index.scss +++ b/ui/app/css/design-system/index.scss @@ -1,3 +1,4 @@ +@forward 'attributes'; @forward 'breakpoints'; @forward 'colors'; @forward 'deprecated-colors'; diff --git a/ui/app/css/design-system/typography.scss b/ui/app/css/design-system/typography.scss index 73ccab414..707170c24 100644 --- a/ui/app/css/design-system/typography.scss +++ b/ui/app/css/design-system/typography.scss @@ -82,11 +82,6 @@ $typography-variants: ( 'h9': 0.5rem, ); -$typography-font-weights: ( - 'bold': 700, - 'normal': 400, -); - $font-size-h1: map-get($typography-variants, 'h1'); $font-size-h2: map-get($typography-variants, 'h2'); $font-size-h3: map-get($typography-variants, 'h3'); diff --git a/ui/app/css/utilities/_spacing.scss b/ui/app/css/utilities/_spacing.scss new file mode 100644 index 000000000..ab87c3380 --- /dev/null +++ b/ui/app/css/utilities/_spacing.scss @@ -0,0 +1,7 @@ +$theme-spacing-value: 4px; + +@function get-spacing($spacing) { + $spacingInPx: $spacing * 4px; + + @return $spacingInPx; +} diff --git a/ui/app/css/utilities/index.scss b/ui/app/css/utilities/index.scss index 701193836..abbfed74a 100644 --- a/ui/app/css/utilities/index.scss +++ b/ui/app/css/utilities/index.scss @@ -1 +1,2 @@ @forward 'colors'; +@forward 'spacing'; diff --git a/ui/app/helpers/constants/design-system.js b/ui/app/helpers/constants/design-system.js index 288a1f0be..3663cd879 100644 --- a/ui/app/helpers/constants/design-system.js +++ b/ui/app/helpers/constants/design-system.js @@ -1,3 +1,9 @@ +/** + * A note about the existence of both singular and plural variable names here: + * When dealing with a literal property name, e.g. ALIGN_ITEMS, the constant + * should match the property. When detailing a collection of things, it should + * match the plural form of the thing. e.g. COLORS, TYPOGRAPHY + */ export const COLORS = { UI1: 'ui-1', UI2: 'ui-2', @@ -40,3 +46,111 @@ export const TYPOGRAPHY = { H9: 'h9', Paragraph: 'paragraph', } + +const NONE = 'none' + +export const SIZES = { + XS: 'xs', + SM: 'sm', + MD: 'md', + LG: 'lg', + XL: 'xl', + NONE, +} + +export const BORDER_STYLE = { + DASHED: 'dashed', + SOLID: 'solid', + DOTTED: 'dotted', + DOUBLE: 'double', + NONE, +} + +const FLEX_END = 'flex-end' +const FLEX_START = 'flex-start' +const CENTER = 'center' + +export const ALIGN_ITEMS = { + FLEX_START, + FLEX_END, + CENTER, + BASELINE: 'baseline', + STRETCH: 'stretch', +} + +export const JUSTIFY_CONTENT = { + FLEX_START, + FLEX_END, + CENTER, + SPACE_AROUND: 'space-around', + SPACE_BETWEEN: 'space-between', + SPACE_EVENLY: 'space-evenly', +} + +export const DISPLAY = { + BLOCK: 'block', + FLEX: 'flex', + GRID: 'grid', + INLINE_BLOCK: 'inline-block', + INLINE_FLEX: 'inline-flex', + INLINE_GRID: 'inline-grid', + LIST_ITEM: 'list-item', +} + +const FRACTIONS = { + HALF: '1/2', + ONE_THIRD: '1/3', + TWO_THIRDS: '2/3', + ONE_FOURTH: '1/4', + TWO_FOURTHS: '2/4', + THREE_FOURTHS: '3/4', + ONE_FIFTH: '1/5', + TWO_FIFTHS: '2/5', + THREE_FIFTHS: '3/5', + FOUR_FIFTHS: '4/5', + ONE_SIXTH: '1/6', + TWO_SIXTHS: '2/6', + THREE_SIXTHS: '3/6', + FOUR_SIXTHS: '4/6', + FIVE_SIXTHS: '5/6', + ONE_TWELFTH: '1/12', + TWO_TWELFTHS: '2/12', + THREE_TWELFTHS: '3/12', + FOUR_TWELFTHS: '4/12', + FIVE_TWELFTHS: '5/12', + SIX_TWELFTHS: '6/12', + SEVEN_TWELFTHS: '7/12', + EIGHT_TWELFTHS: '8/12', + NINE_TWELFTHS: '9/12', + TEN_TWELFTHS: '10/12', + ELEVEN_TWELFTHS: '11/12', +} + +export const BLOCK_SIZES = { + ...FRACTIONS, + SCREEN: 'screen', + MAX: 'max', + MIN: 'min', + FULL: 'full', +} + +export const TEXT_ALIGN = { + LEFT: 'left', + CENTER: 'center', + RIGHT: 'right', + JUSTIFY: 'justify', +} + +export const FONT_WEIGHT = { + BOLD: 'bold', + NORMAL: 'normal', + 100: 100, + 200: 200, + 300: 300, + 400: 400, + 500: 500, + 600: 600, + 700: 700, + 800: 800, + 900: 900, +}