diff --git a/bower.json b/bower.json index bc1ddf69..effef342 100644 --- a/bower.json +++ b/bower.json @@ -18,6 +18,7 @@ "purescript-coroutines": "^5.0.0", "purescript-fixed-precision": "^4.0.0", "purescript-foldable-traversable": "^4.0.0", + "purescript-foreign-generic": "^10.0.0", "purescript-foreign-object": ">= 1.0.0 < 3.0.0", "purescript-generics-rep": "^6.0.0", "purescript-foreign": "^5.0.0", diff --git a/docs/Examples/Button.example.purs b/docs/Examples/Button.example.purs index db1f5570..09a4fd8e 100644 --- a/docs/Examples/Button.example.purs +++ b/docs/Examples/Button.example.purs @@ -114,6 +114,10 @@ docs = , example $ button primary { buttonState = Loading, size = ExtraLarge } ] + , [ h4_ "Secondary + Loading" + , example + $ button secondary { buttonState = Loading } + ] ] sections :: Array (Array JSX) -> Array JSX diff --git a/docs/Examples/Loader.example.purs b/docs/Examples/Loader.example.purs index d586d99b..d4592a51 100644 --- a/docs/Examples/Loader.example.purs +++ b/docs/Examples/Loader.example.purs @@ -2,8 +2,10 @@ module Lumi.Components.Examples.Loader where import Prelude +import Color (cssStringHSLA, desaturate, lighten) import Data.Nullable (null) -import Lumi.Components.Column (column_) +import Lumi.Components.Color (Color(..), colorNames, colors) +import Lumi.Components.Column (column, column_) import Lumi.Components.Loader (loader) import Lumi.Components.Example (example) import React.Basic (JSX) @@ -13,5 +15,123 @@ docs :: JSX docs = column_ [ example $ - loader { style: R.css {}, testId: null } + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.white + , bgColor: colorNames.primary + } + ] + , style: R.css { backgroundColor: cssStringHSLA $ colors.primary } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black1 + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black2 + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black3 + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black4 + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black5 + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black6 + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black7 + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } + , example $ + column { + children: + [ loader + { style: R.css {} + , testId: null + , color: colorNames.black8 + , bgColor: colorNames.white + } + ] + , style: R.css { backgroundColor: cssStringHSLA colors.white } + } ] diff --git a/src/Lumi/Components/Button.purs b/src/Lumi/Components/Button.purs index 00df4158..e6656b0c 100644 --- a/src/Lumi/Components/Button.purs +++ b/src/Lumi/Components/Button.purs @@ -7,7 +7,8 @@ import Data.Array as Array import Data.Char (fromCharCode) import Data.Foldable (fold) import Data.Maybe (Maybe(..)) -import Data.Nullable (Nullable, toNullable) +import Data.Newtype (unwrap) +import Data.Nullable (Nullable, toMaybe, toNullable) import Data.String (null) import Data.String.CodeUnits (fromCharArray) import Effect.Uncurried (mkEffectFn1) @@ -15,7 +16,7 @@ import Foreign (isNull, isUndefined, unsafeToForeign) import JSS (JSS, jss) import Lumi.Components.Color (ColorName, colorNames, colors) import Lumi.Components.Icon (IconType, icon) -import Lumi.Components.Loader (spinnerMixin) +import Lumi.Components.Loader (loader) import Lumi.Components.Size (Size(..)) import React.Basic (Component, JSX, createComponent, element, makeStateless) import React.Basic.DOM (CSS, css, unsafeCreateDOMComponent) @@ -83,9 +84,32 @@ button = makeStateless component render } where children = - if not null props.title && not (isNull || isUndefined) (unsafeToForeign props.title) - then props.title - else invisibleSpace -- preserves button size + case props.buttonState of + Loading -> + loader + { style: + case props.size of + Small -> R.css { width: "12px", height: "12px" } + Medium -> R.css { width: "18px", height: "18px" } + Large -> R.css { width: "24px", height: "24px" } + ExtraLarge -> R.css { width: "34px", height: "34px" } + , testId: toNullable Nothing + , color: + case map unwrap $ toMaybe props.color of + Just "primary" -> colorNames.white + Just "secondary" -> colorNames.secondary + Just _ -> colorNames.secondary + Nothing -> colorNames.secondary + , bgColor: + case toMaybe props.color of + Nothing -> colorNames.white + Just c -> c + } + _ -> + if not null props.title && not (isNull || isUndefined) (unsafeToForeign props.title) + then R.text props.title + else R.text invisibleSpace -- preserves button size + defaults :: ButtonProps defaults = @@ -191,8 +215,7 @@ styles = jss , "&:hover": { backgroundColor: cssStringHSLA $ darken 0.1 colors.primary } , "&:active": { backgroundColor: cssStringHSLA $ darken 0.15 colors.primary } , "&:disabled, &[data-loading=\"true\"]": - { backgroundColor: cssStringHSLA colors.primary2 - , cursor: "default" + { cursor: "default" } , "&:focus": { outline: 0 @@ -231,19 +254,8 @@ styles = jss , "&:disabled, &[data-loading=\"true\"]": { color: cssStringHSLA colors.black2 , borderColor: cssStringHSLA colors.black3 - } - } - , "&[data-loading=\"true\"]": - { "&:after": spinnerMixin { radius: "16px", borderWidth: "2px" } - , "@media (min-width: $break-point-mobile)": - { "&[data-size=\"small\"]": - { "&:after": spinnerMixin { radius: "12px", borderWidth: "2px" } - } - , "&[data-size=\"large\"]": - { "&:after": spinnerMixin { radius: "24px", borderWidth: "3px" } - } - , "&[data-size=\"extra-large\"]": - { "&:after": spinnerMixin { radius: "34px", borderWidth: "4px" } + , "& lumi-loader": + { "&::after": { background: cssStringHSLA colors.white } } } } @@ -309,5 +321,8 @@ styles = jss , "&:active": { backgroundColor: cssStringHSLA $ darken 0.15 value } , "&:disabled, &[data-loading=\"true\"]": { backgroundColor: cssStringHSLA $ lighten 0.4137 $ desaturate 0.1972 $ value + , "& lumi-loader": + { "&::after": { background: cssStringHSLA $ lighten 0.4137 $ desaturate 0.1972 $ value } + } } } diff --git a/src/Lumi/Components/Color.purs b/src/Lumi/Components/Color.purs index fd120666..41b8cc23 100644 --- a/src/Lumi/Components/Color.purs +++ b/src/Lumi/Components/Color.purs @@ -9,12 +9,15 @@ module Lumi.Components.Color import Color (rgb, rgba) import Color as C import Data.Newtype (class Newtype) +import Foreign.Generic (class Decode, class Encode) type Color = C.Color newtype ColorName = ColorName String derive instance newtypeColorName :: Newtype ColorName _ +derive newtype instance decodeColorName :: Decode ColorName +derive newtype instance encodeColorName :: Encode ColorName type ColorMap a = { black :: a @@ -89,4 +92,3 @@ colorNames = , white: ColorName "white" , transparent: ColorName "transparent" } - diff --git a/src/Lumi/Components/Form.purs b/src/Lumi/Components/Form.purs index 9811c6e5..d7c1a54e 100644 --- a/src/Lumi/Components/Form.purs +++ b/src/Lumi/Components/Form.purs @@ -62,7 +62,7 @@ import Effect (Effect) import Effect.Aff (Aff) import Effect.Class (liftEffect) import JSS (JSS, jss) -import Lumi.Components.Color (colors) +import Lumi.Components.Color (colorNames, colors) import Lumi.Components.Column (column) import Lumi.Components.FetchCache as FetchCache import Lumi.Components.Form.Defaults (formDefaults) as Defaults @@ -448,10 +448,13 @@ asyncSelectByKey getData loadOptions fromId toId toSelectOption optionRenderer = Nothing -> empty Just _ -> alignToInput case data_ of - Nothing -> loader - { style: R.css { width: "20px", height: "20px", borderWidth: "2px" } - , testId: toNullable Nothing - } + Nothing -> + loader + { style: R.css { width: "20px", height: "20px", borderWidth: "2px" } + , testId: toNullable Nothing + , color: colorNames.secondary + , bgColor: colorNames.white + } Just data_' -> text body { children = [ optionRenderer data_' ] } diff --git a/src/Lumi/Components/Loader.purs b/src/Lumi/Components/Loader.purs index 8121696d..0da1108b 100644 --- a/src/Lumi/Components/Loader.purs +++ b/src/Lumi/Components/Loader.purs @@ -2,15 +2,17 @@ module Lumi.Components.Loader where import Prelude -import Color (cssStringHSLA) +import Color (cssStringHSLA, desaturate, lighten) import Data.Nullable (Nullable) import JSS (JSS, jss) -import Lumi.Components.Color (colors) +import Lumi.Components.Color (ColorName, colors) import React.Basic (Component, JSX, createComponent, element, makeStateless) import React.Basic.DOM (CSS, unsafeCreateDOMComponent) type LoaderProps = { style :: CSS + , color :: ColorName + , bgColor :: ColorName , testId :: Nullable String } @@ -24,29 +26,103 @@ loader = makeStateless component $ loaderElement <<< mapProps mapProps props = { style: props.style , "data-testid": props.testId + , "data-color": props.color + , "data-bg-color": props.bgColor } styles :: JSS styles = jss { "@global": - { "lumi-loader": spinnerMixin { radius: "3.8rem", borderWidth: "0.5rem" } + { "lumi-loader": + -- fading loader color + { "&[data-color=\"primary\"]": loaderColorMixin colors.primary + , "&[data-color=\"primary-1\"]": loaderColorMixin colors.primary1 + , "&[data-color=\"primary-2\"]": loaderColorMixin colors.primary2 + , "&[data-color=\"primary-3\"]": loaderColorMixin colors.primary3 + , "&[data-color=\"primary-4\"]": loaderColorMixin colors.primary4 + , "&[data-color=\"secondary\"]": loaderColorMixin colors.secondary + , "&[data-color=\"white\"]": loaderColorMixin colors.white + , "&[data-color=\"black\"]": loaderColorMixin colors.black + , "&[data-color=\"black-1\"]": loaderColorMixin colors.black1 + , "&[data-color=\"black-2\"]": loaderColorMixin colors.black2 + , "&[data-color=\"black-3\"]": loaderColorMixin colors.black3 + , "&[data-color=\"black-4\"]": loaderColorMixin colors.black4 + , "&[data-color=\"black-5\"]": loaderColorMixin colors.black5 + , "&[data-color=\"black-6\"]": loaderColorMixin colors.black6 + , "&[data-color=\"black-7\"]": loaderColorMixin colors.black7 + , "&[data-color=\"black-8\"]": loaderColorMixin colors.black8 + , "&[data-color=\"accent-1\"]": loaderColorMixin colors.accent1 + , "&[data-color=\"accent-2\"]": loaderColorMixin colors.accent2 + , "&[data-color=\"accent-3\"]": loaderColorMixin colors.accent3 + , "&[data-color=\"accent-33\"]": loaderColorMixin colors.accent33 + -- bg color (needed for fading loader center) + , "&[data-bg-color=\"primary\"]::after": loaderBgColorMixin colors.primary + , "&[data-bg-color=\"primary-1\"]::after": loaderBgColorMixin colors.primary1 + , "&[data-bg-color=\"primary-2\"]::after": loaderBgColorMixin colors.primary2 + , "&[data-bg-color=\"primary-3\"]::after": loaderBgColorMixin colors.primary3 + , "&[data-bg-color=\"primary-4\"]::after": loaderBgColorMixin colors.primary4 + , "&[data-bg-color=\"secondary\"]::after": loaderBgColorMixin colors.secondary + , "&[data-bg-color=\"white\"]::after": loaderBgColorMixin colors.white + , "&[data-bg-color=\"black\"]::after": loaderBgColorMixin colors.black + , "&[data-bg-color=\"black-1\"]::after": loaderBgColorMixin colors.black1 + , "&[data-bg-color=\"black-2\"]::after": loaderBgColorMixin colors.black2 + , "&[data-bg-color=\"black-3\"]::after": loaderBgColorMixin colors.black3 + , "&[data-bg-color=\"black-4\"]::after": loaderBgColorMixin colors.black4 + , "&[data-bg-color=\"black-5\"]::after": loaderBgColorMixin colors.black5 + , "&[data-bg-color=\"black-6\"]::after": loaderBgColorMixin colors.black6 + , "&[data-bg-color=\"black-7\"]::after": loaderBgColorMixin colors.black7 + , "&[data-bg-color=\"black-8\"]::after": loaderBgColorMixin colors.black8 + , "&[data-bg-color=\"accent-1\"]::after": loaderBgColorMixin colors.accent1 + , "&[data-bg-color=\"accent-2\"]::after": loaderBgColorMixin colors.accent2 + , "&[data-bg-color=\"accent-3\"]::after": loaderBgColorMixin colors.accent3 + , "&[data-bg-color=\"accent-33\"]::after": loaderBgColorMixin colors.accent33 + } , "@keyframes spin": { from: { transform: "rotate(0deg)" } , to: { transform: "rotate(360deg)" } } } } + where + loaderColorMixin value = spinnerMixin + { radius: "3.8rem" + , borderWidth: "0.5rem" + , color: cssStringHSLA value + } -spinnerMixin :: { radius :: String, borderWidth :: String } -> JSS -spinnerMixin { radius, borderWidth } = jss - { boxSizing: "border-box" - , content: "\"\"" - , display: "inline-block" + loaderBgColorMixin value = + { background: cssStringHSLA value + } + +spinnerMixin :: { radius :: String, borderWidth :: String, color :: String } -> JSS +spinnerMixin { radius, borderWidth, color } = jss + { width: radius , height: radius - , width: radius - , border: [ borderWidth, "solid", cssStringHSLA colors.black1 ] - , borderTopColor: cssStringHSLA colors.black4 , borderRadius: "50%" + , background: "linear-gradient(to right, " <> color <> " 10%, rgba(255, 255, 255, 0) 42%)" + , position: "relative" , animation: "spin 1s infinite linear" , animationName: "spin" + , "&::before": + { width: "50%" + , height: "50%" + , background: color + , borderRadius: "100% 0 0 0" + , position: "absolute" + , top: "0" + , left: "0" + , content: "\"\"" + } + , "&::after": + { width: "75%" + , height: "75%" + , borderRadius: "50%" + , content: "\"\"" + , margin: "auto" + , position: "absolute" + , top: "0" + , left: "0" + , bottom: "0" + , right: "0" + } } diff --git a/src/Lumi/Components/Select.purs b/src/Lumi/Components/Select.purs index e1ac75f0..c3eb9c33 100644 --- a/src/Lumi/Components/Select.purs +++ b/src/Lumi/Components/Select.purs @@ -26,7 +26,7 @@ import Data.String as String import Effect (Effect) import Effect.Aff (Aff) import JSS (JSS, jss) -import Lumi.Components.Color (colors) +import Lumi.Components.Color (colorNames, colors) import Lumi.Components.Icon as Icon import Lumi.Components.Input (lumiInputDisabledStyles, lumiInputFocusStyles, lumiInputHoverStyles, lumiInputStyles) import Lumi.Components.Loader (loader) @@ -253,7 +253,13 @@ select = makeStateless component render where renderSelectedValues = if props.loading - then loader { style: css { width: "20px", height: "20px", borderWidth: "2px" }, testId: toNullable Nothing } + then + loader + { style: css { width: "20px", height: "20px", borderWidth: "2px" } + , testId: toNullable Nothing + , color: colorNames.secondary + , bgColor: colorNames.white + } else lumiSelectInputSelectedValuesElement { children: let renderedSelectedValues = @@ -317,7 +323,12 @@ select = makeStateless component render [ lumiSelectMenuLoaderElement { key: "loading-text" , children: - loader { style: css { width: "20px", height: "20px", borderWidth: "2px" }, testId: toNullable Nothing } + loader + { style: css { width: "20px", height: "20px", borderWidth: "2px" } + , testId: toNullable Nothing + , color: colorNames.secondary + , bgColor: colorNames.white + } } ] Failed error ->