diff --git a/convert-more.fish b/convert-more.fish index 3db31fe..2c860ef 100644 --- a/convert-more.fish +++ b/convert-more.fish @@ -1,5 +1,5 @@ for f in (ls src/Yoga/Block/Icon/SVG/*.svg) - npx svgo -i $f + svgo -i $f end node convert-svgs.js for f in (ls src/Yoga/Block/Icon/SVG/*.purs) diff --git a/packages.dhall b/packages.dhall index 4bc844e..1dd4ae2 100644 --- a/packages.dhall +++ b/packages.dhall @@ -119,12 +119,12 @@ let additions = let upstream = - https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20201217/packages.dhall sha256:f46d45e29977f3b57717b56d20a5ceac12532224516eea3012a4688f22ac1539 + https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20201222/packages.dhall sha256:620d0e4090cf1216b3bcbe7dd070b981a9f5578c38e810bbd71ece1794bfe13b let overrides = - { spec-discovery = upstream.spec-discovery // { version = "master" } - , react-basic = upstream.react-basic // { version = "main" } + { react-basic = upstream.react-basic // { version = "main" } , react-basic-hooks = upstream.react-basic-hooks // { version = "main " } + , react-testing-library = upstream.react-testing-library // { version = "main" } } let additions = @@ -166,27 +166,6 @@ let additions = , repo = "https://github.com/jvliwanag/purescript-untagged-union.git" , version = "master" } - , react-testing-library = - { dependencies = - [ "aff-promise" - , "console" - , "debug" - , "effect" - , "foreign" - , "foreign-object" - , "psci-support" - , "react-basic-dom" - , "react-basic-hooks" - , "remotedata" - , "run" - , "simple-json" - , "spec" - , "spec-discovery" - ] - , repo = - "https://github.com/i-am-the-slime/purescript-react-testing-library.git" - , version = "7726ad443ad8c67f94932ab8934640df0ea39fb2" - } , react-basic-dom = { dependencies = [ "effect" diff --git a/spago.dhall b/spago.dhall index 2934604..583822d 100644 --- a/spago.dhall +++ b/spago.dhall @@ -5,6 +5,7 @@ You can edit this file as you like. { name = "ry-blocks" , dependencies = [ "console" + , "debug" , "effect" , "heterogeneous" , "interpolate" diff --git a/src/Framer/Motion.js b/src/Framer/Motion.js index 94c6fb0..b64bf63 100644 --- a/src/Framer/Motion.js +++ b/src/Framer/Motion.js @@ -1,6 +1,7 @@ const framerMotion = require("framer-motion") exports.divImpl = framerMotion.motion.div +exports.spanImpl = framerMotion.motion.span exports.buttonImpl = framerMotion.motion.button exports.h1Impl = framerMotion.motion.h1 exports.svgImpl = framerMotion.motion.svg diff --git a/src/Framer/Motion.purs b/src/Framer/Motion.purs index 8dfb7d3..a1c102a 100644 --- a/src/Framer/Motion.purs +++ b/src/Framer/Motion.purs @@ -7,7 +7,10 @@ module Framer.Motion , class EffectFnMaker , toEffectFn , OnHoverStart + , span , onHoverStart + , LayoutId + , layoutId , customProp , OnHoverEnd , onHoverEnd @@ -94,7 +97,7 @@ import Heterogeneous.Mapping (class HMapWithIndex, class MappingWithIndex, hmapW import Literals.Undefined (Undefined) import Prim.Row (class Nub, class Union) import React.Basic (JSX, ReactComponent) -import React.Basic.DOM (CSS, Props_div, Props_h1, Props_button, css) +import React.Basic.DOM (CSS, Props_button, Props_div, Props_h1, Props_span, css) import React.Basic.DOM.Internal (SharedSVGProps) import React.Basic.DOM.SVG (Props_svg, Props_rect, Props_path) import React.Basic.Events (EventHandler) @@ -111,6 +114,8 @@ import Yoga.Block.Internal (Id) foreign import divImpl ∷ ∀ a. ReactComponent { | a } +foreign import spanImpl ∷ ∀ a. ReactComponent { | a } + foreign import buttonImpl ∷ ∀ a. ReactComponent { | a } foreign import h1Impl ∷ ∀ a. ReactComponent { | a } @@ -285,6 +290,12 @@ onDrag fn2 = cast (mkEffectFn2 fn2) customProp ∷ ∀ a. a -> Foreign customProp = unsafeToForeign +type LayoutId = + String |+| Undefined + +layoutId ∷ ∀ a. Castable a LayoutId => a -> LayoutId +layoutId = cast + type MotionPropsF f r = ( initial ∷ f Initial , animate ∷ f Animate @@ -300,7 +311,7 @@ type MotionPropsF f r = , variants ∷ f Variants , transition ∷ f Transition , layout ∷ f Layout - , layoutId ∷ f String |+| Undefined + , layoutId ∷ f LayoutId , whileTap ∷ f WhileTap , onTap ∷ f OnTap , onTapStart ∷ f OnTapStart @@ -360,6 +371,9 @@ makeVariantLabels = hmapWithIndex MakeVariantLabel div ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + Props_div) => ReactComponent { | attrs } div = divImpl +span ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + Props_span) => ReactComponent { | attrs } +span = spanImpl + button ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + Props_button) => ReactComponent { | attrs } button = buttonImpl diff --git a/src/React/Basic/Popper/Types.purs b/src/React/Basic/Popper/Types.purs index 8029260..5cb90a9 100644 --- a/src/React/Basic/Popper/Types.purs +++ b/src/React/Basic/Popper/Types.purs @@ -37,4 +37,5 @@ modifierOffset { x, y } = unsafeCoerce { name: "offset", options: { offset: [ x, type Options = ( modifiers ∷ Array Modifier , strategy ∷ String + , placement ∷ String ) diff --git a/src/Yoga/Block/Atom/Icon/Style.purs b/src/Yoga/Block/Atom/Icon/Style.purs index f4fc117..728255d 100644 --- a/src/Yoga/Block/Atom/Icon/Style.purs +++ b/src/Yoga/Block/Atom/Icon/Style.purs @@ -19,6 +19,9 @@ span props = css { "--stroke-colour": (props.stroke <|> props.colour) ?|| (str colour.text) , "--fill-colour": (props.fill <|> props.colour) ?|| (str "transparent") + , display: inlineFlex + , justifyContent: center + , alignItems: center , "& > svg": nest { width: (props.width <|> props.size) ?|| (str "auto") diff --git a/src/Yoga/Block/Atom/Input/Placement/Types.purs b/src/Yoga/Block/Atom/Input/Placement/Types.purs new file mode 100644 index 0000000..29c7264 --- /dev/null +++ b/src/Yoga/Block/Atom/Input/Placement/Types.purs @@ -0,0 +1,36 @@ +module Yoga.Block.Atom.Input.Placement.Types (render, Placement(..), Primary(..), Secondary(..)) where + +import Prelude +import Data.Foldable (foldMap) +import Data.Maybe (Maybe) + +data Placement + = Placement Primary (Maybe Secondary) + +render ∷ Placement -> String +render (Placement primary maybeSecondary) = do + renderPrimary primary <> foldMap ("-" <> _) (renderSecondary <$> maybeSecondary) + +data Primary + = Auto + | Left + | Right + | Top + | Bottom + +data Secondary + = Start + | End + +renderPrimary ∷ Primary -> String +renderPrimary = case _ of + Auto -> "auto" + Left -> "left" + Right -> "right" + Top -> "top" + Bottom -> "bottom" + +renderSecondary ∷ Secondary -> String +renderSecondary = case _ of + Start -> "start" + End -> "end" diff --git a/src/Yoga/Block/Atom/Input/Story.purs b/src/Yoga/Block/Atom/Input/Story.purs index d52a268..19743aa 100644 --- a/src/Yoga/Block/Atom/Input/Story.purs +++ b/src/Yoga/Block/Atom/Input/Story.purs @@ -1,13 +1,19 @@ module Yoga.Block.Atom.Input.Story where import Prelude +import Data.String.NonEmpty.Internal (NonEmptyString(..), nes) +import Data.Symbol (SProxy(..)) import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) +import Foreign.Object as Object import React.Basic (JSX, element, fragment) import React.Basic.DOM as R import React.Basic.Emotion as E import React.Basic.Events (handler_) +import Yoga (el) +import Yoga.Block as Block import Yoga.Block.Atom.Input as Input +import Yoga.Block.Atom.Input.Types as HTMLInput import Yoga.Block.Container.Style as Styles default ∷ @@ -30,27 +36,56 @@ input = do pure $ fragment [ R.div_ - [ R.h2_ [ R.text "Generic Input" ] + [ R.h1_ [ R.text "Input Examples" ] + , R.h2_ [ R.text "Generic Input" ] , element Input.component { value: "A Generic Input", onChange: handler_ mempty } + , R.h2_ [ R.text "Validation on text Input" ] + , element Input.component { type: HTMLInput.Text, label: nes (SProxy ∷ _ "Is undefined a function?"), value: "Yes, why not?", onChange: handler_ mempty, _aria: Object.singleton "invalid" "true" } + , element Input.component { type: HTMLInput.Text, label: nes (SProxy ∷ _ "What's the type of null?"), value: "object", onChange: handler_ mempty, _aria: Object.singleton "invalid" "false" } + , R.h2_ [ R.text "Required fields" ] + , element Input.component + { type: HTMLInput.Text + , label: nes (SProxy ∷ _ "I am so important") + , _aria: Object.singleton "required" "true" + } + , R.h3_ [ R.text "With a label" ] + , el Block.cluster {} + [ el R.form' {} + [ element Input.component { label: nes (SProxy ∷ _ "This has a label"), value: "And text", onChange: handler_ mempty } + , element Input.component { label: nes (SProxy ∷ _ "This has a label"), value: "", onChange: handler_ mempty } + , element Input.component { label: nes (SProxy ∷ _ "This has a label"), placeholder: "A very long placeholder, too..." } + , element Input.component { label: nes (SProxy ∷ _ "Pig nose"), leading: R.text "🐽🤣" } + , element Input.component { label: nes (SProxy ∷ _ "Pig nose"), trailing: R.text "🤫" } + , element Input.component { label: nes (SProxy ∷ _ "Pig nose"), leading: R.text "🌭" } + , element Input.component { label: nes (SProxy ∷ _ "Pig nose"), leading: R.text "⭐", trailing: R.text "🔮" } + ] + ] + , R.h2_ [ R.text "[BUG] Overflowing label" ] + , element Input.component + { type: HTMLInput.Text + , label: nes (SProxy ∷ _ "Is undefined really a function?") + } , R.h2_ [ R.text "Password" ] - , element Input.component { type: "password" } + , element Input.component { type: HTMLInput.Password } , R.h2_ [ R.text "Text Input" ] - , element Input.component { type: "text", value: "Some text", onChange: handler_ mempty } + , element Input.component { type: HTMLInput.Text, value: "Some text", onChange: handler_ mempty } + , element Input.component { type: HTMLInput.Text, placeholder: "Placeholder", onChange: handler_ mempty } + , element Input.component { type: HTMLInput.Text, value: "", label: NonEmptyString "Heinzi", onChange: handler_ mempty } , R.h2_ [ R.text "Search Input" ] - , element Input.component { type: "search", value: "Search...", onChange: handler_ mempty } + , element Input.component { type: HTMLInput.Search, placeholder: "Search...", onChange: handler_ mempty } , R.h2_ [ R.text "Button" ] - , element Input.component { type: "button", value: "A button", onChange: handler_ mempty } + , element Input.component { type: HTMLInput.Button, value: "A button", onChange: handler_ mempty } , R.h2_ [ R.text "Submit" ] - , element Input.component { type: "submit" } - , R.h2_ [ R.text "Radio" ] - , element Input.component { type: "radio" } - , R.h2_ [ R.text "Checkbox" ] - , element Input.component { type: "checkbox" } - , R.h2_ [ R.text "File" ] - , element Input.component { type: "file" } - , R.h2_ [ R.text "Image" ] - , element Input.component { type: "image" } - , R.h2_ [ R.text "Number" ] - , element Input.component { type: "number" } + -- , element Input.component { type: "submit" } + -- , R.h2_ [ R.text "Radio" ] + -- , element Input.component { type: "radio" } + -- , R.h2_ [ R.text "Checkbox" ] + -- , element Input.component { type: "checkbox" } + -- , R.h2_ [ R.text "File" ] + -- , element Input.component { type: "file" } + -- , R.h2_ [ R.text "Image" ] + -- , element Input.component { type: "image" } + -- , R.h2_ [ R.text "Number" ] + -- , element Input.component { type: "number" } ] ] diff --git a/src/Yoga/Block/Atom/Input/Style.purs b/src/Yoga/Block/Atom/Input/Style.purs index 3bafc2a..ca726a2 100644 --- a/src/Yoga/Block/Atom/Input/Style.purs +++ b/src/Yoga/Block/Atom/Input/Style.purs @@ -1,8 +1,8 @@ module Yoga.Block.Atom.Input.Style where import Yoga.Prelude.Style +import Data.Interpolate (i) import Yoga.Block.Container.Style (colour) -import Yoga.Prelude.Style as Color type Props f r = ( css ∷ f Style @@ -15,41 +15,164 @@ leftIconSize = var "--left-icon-size" rightIconSize ∷ StyleProperty rightIconSize = var "--right-icon-size" -leftIconStyle ∷ Style -leftIconStyle = +gapSize ∷ String +gapSize = "var(--s-3)" + +iconContainer ∷ Style +iconContainer = css - { "--stroke-colour": str colour.interfaceTextDisabled - , marginTop: str "4px" - , marginLeft: str "-4px" + { display: inlineFlex + , alignItems: center + -- , padding: str "0px 10px" + -- , background: str $ colour.interfaceBackground + -- , borderLeft: str $ "1px solid " <> colour.interfaceBackgroundShadow } -rightIconStyle ∷ Style -rightIconStyle = +labelAndInputWrapper ∷ Style +labelAndInputWrapper = css - { "--stroke-colour": str colour.interfaceTextDisabled - , paddingTop: str "6px" - , marginRight: str "6px" + { position: relative + , display: inlineBlock + , "--left-icon-size": var "--s0" + , "--right-icon-size": str "calc(var(--s0) * 1.2)" + , "--input-border-radius": var "--s-1" + , "--input-side-padding": var "--s-1" } -textWrapper ∷ ∀ r. { | Props OptionalProp r } -> Style -textWrapper props = +labelContainer ∷ Style +labelContainer = css - { fontFamily: var "--main-font" + { position: absolute + , top: _0 + , left: _0 + , display: inlineBlock + , zIndex: str "2" + , pointerEvents: none + } + +labelSmall ∷ Style +labelSmall = + css + { fontSize: var "--s-1" + , marginTop: str "calc(var(--s-3) * -1)" + , marginLeft: var "--s-2" + , """&[data-required="true"] > span:after""": + nest + { content: str "'*'" + } + , "& > span": + nest + { fontWeight: str "500" + , background: str colour.inputBackground + , color: str colour.text + , borderRadius: var "--s-4" + , paddingLeft: var "--s-4" + , paddingRight: var "--s-4" + , paddingTop: var "--s-5" + , paddingBottom: var "--s-5" + } + , """&[data-invalid="true"] > span""": + nest + { background: str colour.invalid + , color: str colour.invalidText + } + , """&[data-has-focus="true"] > span""": + nest + { background: str colour.highlight + , color: str colour.highlightText + } + } + +labelLarge ∷ { leftIconWidth ∷ Maybe Number } -> Style +labelLarge { leftIconWidth } = + css + { fontSize: str "calc(var(--s0) * 0.85)" + , padding: _0 + , marginTop: str "calc(var(--s-1) + var(--s-5))" + , marginBottom: str "calc(var(--s-1) + var(--s-5))" + , marginLeft: + case leftIconWidth of + Nothing -> var "--input-side-padding" + Just size -> str $ i "calc(var(--input-side-padding) + " gapSize " + " size "px)" + , marginRight: str "var(--input-side-padding)" + , color: str colour.placeholderText + , fontWeight: str "400" + , whiteSpace: nowrap -- force on one line + , """&[data-required="true"]:after""": + nest + { content: str "'*'" + , color: str colour.required + , fontFamily: str "Helvetica, Arial, Inter, sans-serif" + , fontSize: str "calc(var(--s0))" + } + } + +rightIconContainer ∷ Style +rightIconContainer = + iconContainer + <> css + { display: inlineFlex + , alignItems: center + , justifyContent: center + , "& > *": + nest + { display: inlineFlex + , alignItems: center + , justifyContent: center + } + , ".ry-icon": + nest + { "--stroke-colour": str colour.highlight + } + } + +leftIconContainer ∷ Style +leftIconContainer = + iconContainer + <> css + { borderRadius: str "var(--input-border-radius) 0 0 var(--input-border-radius)" + , ".ry-icon": + nest + { "--stroke-colour": str colour.text + } + } + +inputWrapper ∷ ∀ r. { | Props OptionalProp r } -> Style +inputWrapper props = + css + { position: relative , boxSizing: borderBox - , backgroundColor: str $ colour.background07 + , backgroundColor: str colour.inputBackground , display: inlineFlex , "--left-icon-size": var "--s0" , "--right-icon-size": str "calc(var(--s0) * 1.2)" + , "--input-border-radius": var "--s-1" + , "--input-side-padding": var "--s-1" + , "--input-top-padding": var "--s-5" + , "--input-bottom-padding": var "--s-5" , alignItems: center , justifyContent: flexStart - , paddingLeft: var "--s-1" - , gap: var "--s-3" - , "--border-width": var "--s-5" + , paddingLeft: str "calc(var(--input-side-padding) - var(--border-width))" + , paddingRight: str "calc(var(--input-side-padding) - var(--border-width))" + , paddingTop: str "calc(var(--input-top-padding) - var(--border-width))" + , paddingBottom: str "calc(var(--input-bottom-padding) - var(--border-width))" + , gap: str "calc(var(--input-side-padding) / 2)" + , "--border-width": str "1px" , border: str $ "var(--border-width) solid " <> colour.inputBorder - , borderRadius: var "--s-1" + , borderRadius: var "--input-border-radius" + , """&[data-invalid="false"]""": + nest + { borderColor: str colour.success + } + , """&[data-invalid="true"]""": + nest + { borderColor: str colour.invalid + } , "&:focus-within": nest - {} + { "--border-width": str "var(--s-5)" + , borderColor: str colour.highlight + } } input ∷ ∀ r. { | Props OptionalProp r } -> Style @@ -58,15 +181,23 @@ input props = { "&[type=text],&[type=search],&[type=password],&[type=number],&:not([type])": nest { color: str colour.text + , width: _100percent + , flex: str "1" + , "--padding-top": var "--s-1" + , "--padding-bottom": var "--s-1" + , "&[aria-labelledby]": + nest + { paddingTop: str "calc(var(--padding-top) + (var(--s-5)/2))" + , paddingBottom: str "calc(var(--padding-bottom) - (var(--s-5)/2))" + } , background: str "transparent" - , alignSelf: stretch - , "--padding-top": str "var(--s-1)" - , "--padding-bottom": str "calc(var(--padding-top) * 0.85)" + , margin: _0 , paddingTop: var "--padding-top" , paddingBottom: var "--padding-bottom" , paddingLeft: _0 , paddingRight: _0 - -- , background: str colour.inputBackground + , fontSize: str "calc(var(--s0) * 0.85)" + , gap: str gapSize , border: none } , "&[type=search]": diff --git a/src/Yoga/Block/Atom/Input/Types.purs b/src/Yoga/Block/Atom/Input/Types.purs new file mode 100644 index 0000000..ec46257 --- /dev/null +++ b/src/Yoga/Block/Atom/Input/Types.purs @@ -0,0 +1,54 @@ +module Yoga.Block.Atom.Input.Types where + +import Prelude + +data HTMLInput + = Button + | Checkbox + | Color + | Date + | DatetimeLocal + | Email + | File + | Hidden + | Image + | Month + | Number + | Password + | Radio + | Range + | Reset + | Search + | Submit + | Tel + | Text + | Time + | Url + | Week + +derive instance eqHTMLInput ∷ Eq HTMLInput + +toString ∷ HTMLInput -> String +toString = case _ of + Button -> "button" + Checkbox -> "checkbox" + Color -> "color" + Date -> "date" + DatetimeLocal -> "datetime-local" + Email -> "email" + File -> "file" + Hidden -> "hidden" + Image -> "image" + Month -> "month" + Number -> "number" + Password -> "password" + Radio -> "radio" + Range -> "range" + Reset -> "reset" + Search -> "search" + Submit -> "submit" + Tel -> "tel" + Text -> "text" + Time -> "time" + Url -> "url" + Week -> "week" diff --git a/src/Yoga/Block/Atom/Input/View.purs b/src/Yoga/Block/Atom/Input/View.purs index 86cc9bc..d9ecda0 100644 --- a/src/Yoga/Block/Atom/Input/View.purs +++ b/src/Yoga/Block/Atom/Input/View.purs @@ -1,21 +1,27 @@ module Yoga.Block.Atom.Input.View where import Yoga.Prelude.View +import Data.String.NonEmpty (NonEmptyString) +import Data.String.NonEmpty as NonEmptyString +import Data.Symbol (SProxy(..)) +import Foreign.Object (Object) +import Foreign.Object as Object import Framer.Motion as M import React.Basic.DOM (css) import React.Basic.DOM as R -import React.Basic.DOM.SVG as SVG import React.Basic.Hooks as React -import Record.Extra (pick) -import Unsafe.Coerce (unsafeCoerce) +import Web.HTML.HTMLInputElement as InputElement import Yoga.Block.Atom.Icon as Icon import Yoga.Block.Atom.Input.Style as Style -import Yoga.Block.Atom.Tooltip as Tooltip +import Yoga.Block.Atom.Input.Types (HTMLInput) +import Yoga.Block.Atom.Input.Types as HTMLInput import Yoga.Block.Icon.SVG as SVGIcon type PropsF f = - ( iconLeft ∷ f JSX - , iconRight ∷ f JSX + ( leading ∷ f JSX + , trailing ∷ f JSX + , label ∷ f NonEmptyString + , type ∷ f HTMLInput | Style.Props f InputProps ) @@ -30,51 +36,154 @@ component = rawComponent mkLeftIcon ∷ JSX -> JSX mkLeftIcon icon = - styledLeaf - Icon.component - { size: Style.leftIconSize - , className: "ry-input-right-icon" - , css: Style.leftIconStyle - , icon - } - -mkRightIcon ∷ JSX -> JSX -mkRightIcon icon = - styledLeaf Icon.component - { size: Style.rightIconSize - , className: "ry-input-left-icon" - , css: Style.rightIconStyle - , icon + styled R.div' + { className: "ry-input-left-icon-container" + , css: Style.leftIconContainer } + [ el_ + Icon.component + { size: Style.leftIconSize + , icon + } + ] rawComponent ∷ ∀ p. ReactComponent { | p } rawComponent = mkForwardRefComponent "Input" do - \(props ∷ { | PropsOptional }) ref -> React.do + \(props ∷ { | PropsOptional }) propsRef -> React.do + inputBbox /\ setInputBbox <- useState' (zero ∷ DOMRect) + hasFocus /\ setHasFocus <- useState' false + backupRef ∷ NodeRef <- useRef null -- [TODO] test this + let ref = forwardedRefAsMaybe propsRef # fromMaybe backupRef + useEffectOnce do + maybeBBox <- getBoundingBoxFromRef ref + for_ maybeBBox setInputBbox + mempty let - iconLeft = - props.iconLeft - ?|| if (((cast props.type) ?|| "") == "search") then mkLeftIcon SVGIcon.magnifyingGlass else mempty - iconRight = - props.iconRight - ?|| if (((cast props.type) ?|| "") == "password") then mkRightIcon SVGIcon.eyeOpen else mempty - pure - $ case props.type # cast # opToMaybe of - Just "password" -> el_ password props - _ -> - styled R.div' - { className: "ry-input-wrapper" - , css: Style.textWrapper props - } - [ iconLeft - , emotionInput - ref - props - { className: "ry-input" - , css: Style.input props - } - , iconRight + focusInput = do + unless hasFocus do + maybeHTMLElement <- getHTMLElementFromRef ref + for_ maybeHTMLElement focus + -- Left icon width to correctly place the label + leftIconRef ∷ NodeRef <- useRef null + leftIconBbox /\ setLeftIconBbox <- useState' Nothing + useEffectAlways do + when (isJust (props.leading # opToMaybe) && leftIconBbox == Nothing) do + maybeBBox <- getBoundingBoxFromRef leftIconRef + for_ maybeBBox (setLeftIconBbox <<< Just) + mempty + let (maybeValue ∷ Maybe String) = cast props.value # opToMaybe + hasValue /\ setHasValue <- useState' ((maybeValue # isJust) && (maybeValue /= Just "")) + let + aria ∷ Object String + aria = props._aria # cast # opToMaybe # fold + labelId ∷ String + labelId = props.id # cast # opToMaybe # fold # (_ <> "-label") + renderLargeLabel ∷ Boolean + renderLargeLabel = not hasFocus && not hasValue + maybeLabelText ∷ Maybe NonEmptyString + maybeLabelText = props.label # opToMaybe + mkLabel ∷ NonEmptyString -> JSX + mkLabel labelText = + styled R.div' + { className: "ry-input-label-container" + , css: Style.labelContainer + } + [ el M.animateSharedLayout + { type: M.switch } + [ guard (inputBbox /= zero) + $ styled M.div + { className: if renderLargeLabel then "ry-input-label-large" else "ry-input-label-small" + , layoutId: M.layoutId "ry-input-label" + , css: + if renderLargeLabel then + Style.labelLarge { leftIconWidth: leftIconBbox <#> _.width } + else + Style.labelSmall + , layout: M.layout true + , transition: M.transition { duration: 0.18, ease: "easeOut" } + , _data: + Object.fromHomogeneous + { "has-focus": show hasFocus + , "invalid": aria # Object.lookup "invalid" # fromMaybe "" + , "required": aria # Object.lookup "required" # fromMaybe "" + } + , initial: M.initial false + } + [ el M.span + { onClick: handler preventDefault (const focusInput) + , layout: M.layout true + , layoutId: M.layoutId "ry-input-label-text" + , htmlFor: props.id + , id: labelId + } + [ R.text $ NonEmptyString.toString labelText ] + ] ] + ] + leading ∷ Maybe JSX + leading = + opToMaybe props.leading + <|> if (props.type <#> (_ == HTMLInput.Search)) ?|| false then + Just $ mkLeftIcon SVGIcon.magnifyingGlass + else + Nothing + trailing ∷ Maybe JSX + trailing = opToMaybe props.trailing + maybePlaceholder ∷ Maybe String + maybePlaceholder = do + given <- props.placeholder # cast # opToMaybe + if isJust maybeLabelText && hasFocus then Just given else Nothing + inputWrapper = + styled R.div' + { className: "ry-input-wrapper" + , css: Style.inputWrapper props + , _data: + Object.fromHomogeneous + { "invalid": aria # Object.lookup "invalid" # fromMaybe "" + } + } + [ leading # foldMap \l -> el R.div' { ref: leftIconRef } [ l ] + , emotionInput + ref + ( props { type = HTMLInput.toString <$> props.type # unsafeUnOptional } + # setOrDelete (SProxy ∷ _ "placeholder") (maybePlaceholder # maybeToOp) + # setOrDelete (SProxy ∷ _ "value") (maybeValue # maybeToOp) + ) + { className: "ry-input" + , css: Style.input props + , onFocus: handler preventDefault (const $ unless hasFocus $ setHasFocus true) + , onBlur: + handler preventDefault + ( const do + when hasFocus $ setHasFocus false + el <- getHTMLElementFromRef ref + let inputEl = InputElement.fromHTMLElement =<< el + for_ inputEl \ie -> do + v <- InputElement.value ie + setHasValue (v /= "") + ) + , _aria: + if props.label # opToMaybe # isJust then + aria # Object.insert "labelledby" labelId + else + aria + } + , trailing # foldMap \t -> el R.div' {} [ t ] + ] + pure + $ case props.type # opToMaybe of + Just HTMLInput.Password -> el_ password props + _ -> case maybeLabelText of + Nothing -> inputWrapper + Just labelText -> + styled R.div' + { className: "ry-label-and-input-wrapper" + , css: Style.labelAndInputWrapper + } + [ inputWrapper + , mkLabel $ labelText + ] password ∷ ∀ p. ReactComponent { | p } password = @@ -83,60 +192,56 @@ password = hidePassword /\ modifyHidePassword <- useState true let eyeCon = - el_ Tooltip.component - { target: - el R.div' - { onClick: handler preventDefault \_ -> modifyHidePassword not - } - [ el M.animatePresence - { exitBeforeEnter: true - } - [ if hidePassword then - el M.div - { key: "eyeOpen" - , initial: M.initial $ css { scaleY: 0 } - , animate: M.animate $ css { scaleY: 1 } - , exit: M.exit $ css { scaleY: 0.9 } - , transition: M.transition { duration: 0.15 } - } - [ styledLeaf Icon.component - { size: Style.rightIconSize - , className: "ry-input-left-icon" - , css: Style.rightIconStyle - , icon: SVGIcon.eyeOpen - } - ] - else - el M.div - { key: "eyeClosed" - , initial: M.initial $ css { scaleY: 0.0 } - , animate: M.animate $ css { scaleY: 1 } - , exit: M.exit $ css { scaleY: 0.9 } - , transition: M.transition { duration: 0.15 } - } - [ styledLeaf Icon.component - { size: Style.rightIconSize - , className: "ry-input-left-icon" - , css: Style.rightIconStyle - , icon: SVGIcon.eyeClosed - } - ] - ] - ] - , theTip: R.text if hidePassword then "Show password" else "Hide password" + styled R.div' + { onClick: handler preventDefault \_ -> modifyHidePassword not + , className: "ry-input-right-icon-container" + , css: Style.rightIconContainer } - let iconRight = props.iconRight ?|| eyeCon + [ el M.animatePresence + { exitBeforeEnter: true + } + [ if hidePassword then + el M.div + { key: "eyeOpen" + , initial: M.initial $ css { scaleY: 0.2 } + , animate: M.animate $ css { scaleY: 1.0 } + , exit: M.exit $ css {} + , transition: M.transition { scaleY: { type: "spring", duration: 0.12, bounce: 0.00 } } + } + [ el_ Icon.component + { size: Style.rightIconSize + , icon: SVGIcon.eyeOpen + } + ] + else + el M.div + { key: "eyeClosed" + , initial: M.initial $ css { scaleY: 1.0 } + , animate: M.animate $ css { scaleY: 0.4 } + , exit: M.exit $ css { scaleY: 0.2 } + , transition: M.transition { scaleY: { type: "spring", duration: 0.12, bounce: 0.00 } } + } + [ el_ Icon.component + { size: Style.rightIconSize + , icon: SVGIcon.eyeClosed + } + ] + ] + ] + let trailing = props.trailing ?|| eyeCon + let leading = props.leading ?|| mempty pure $ styled R.div' { className: "ry-input-wrapper" - , css: Style.textWrapper props + , css: Style.inputWrapper props } - [ emotionInput + [ leading + , emotionInput ref - props + props { type = HTMLInput.toString <$> props.type # unsafeUnOptional } { className: "ry-input" , css: Style.input props , type: if hidePassword then "password" else "text" } - , iconRight + , trailing ] diff --git a/src/Yoga/Block/Atom/Popover.purs b/src/Yoga/Block/Atom/Popover.purs new file mode 100644 index 0000000..019ed9f --- /dev/null +++ b/src/Yoga/Block/Atom/Popover.purs @@ -0,0 +1,5 @@ +module Yoga.Block.Atom.Popover + ( module Yoga.Block.Atom.Popover.View + ) where + +import Yoga.Block.Atom.Popover.View (component, Props) diff --git a/src/Yoga/Block/Atom/Popover/Spec.purs b/src/Yoga/Block/Atom/Popover/Spec.purs new file mode 100644 index 0000000..d9207f3 --- /dev/null +++ b/src/Yoga/Block/Atom/Popover/Spec.purs @@ -0,0 +1,16 @@ +module Yoga.Block.Atom.Popover.Spec where + +import Yoga.Prelude.Spec +import React.Basic.DOM as R +import Yoga.Block.Atom.Popover as Popover + +spec ∷ Spec Unit +spec = + after_ cleanup do + describe "The popover" do + it "renders without errors" do + void + $ renderComponent Popover.component + { theTip: R.text "Tip" + , target: R.text "Target" + } diff --git a/src/Yoga/Block/Atom/Popover/Story.purs b/src/Yoga/Block/Atom/Popover/Story.purs new file mode 100644 index 0000000..6f93fe3 --- /dev/null +++ b/src/Yoga/Block/Atom/Popover/Story.purs @@ -0,0 +1,254 @@ +module Yoga.Block.Atom.Popover.Story where + +import Prelude +import Data.Array as Array +import Data.Foldable (traverse_) +import Data.Maybe (Maybe(..)) +import Data.String as String +import Data.String.NonEmpty.Internal (nes) +import Data.Symbol (SProxy(..)) +import Data.Tuple.Nested ((/\)) +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) +import Framer.Motion (animatePresence, animateSharedLayout) +import Framer.Motion as Motion +import React.Basic (JSX, ReactComponent, element, fragment) +import React.Basic.DOM (CSS, css) +import React.Basic.DOM as R +import React.Basic.DOM.Events (targetValue) +import React.Basic.Emotion as E +import React.Basic.Events (handler, handler_) +import React.Basic.Hooks (reactComponent) +import React.Basic.Hooks as React +import Yoga (el, el_) +import Yoga.Block.Atom.Input as Input +import Yoga.Block.Atom.Input.Placement.Types (Placement(..)) +import Yoga.Block.Atom.Input.Placement.Types as Placement +import Yoga.Block.Atom.Input.Types as HTMLInput +import Yoga.Block.Atom.Popover as Popover +import Yoga.Block.Container.Style as Styles +import Yoga.Block.Layout.Box as Box +import Yoga.Block.Layout.Stack as Stack + +default ∷ + { decorators ∷ Array (Effect JSX -> JSX) + , title ∷ String + } +default = + { title: "Atom/Popover" + , decorators: + [ \storyFn -> + R.div_ + [ element E.global { styles: Styles.global } + , unsafePerformEffect storyFn + ] + ] + } + +itemVariants ∷ + { hidden ∷ CSS + , visible ∷ CSS + } +itemVariants = + { visible: + css + { left: 0 + } + , hidden: + css + { left: -20 + } + } + +containerVariants ∷ + { hidden ∷ CSS + , visible ∷ CSS + } +containerVariants = + { visible: + css + { transition: { when: "beforeChildren" } + , staggerChildren: 0.3 + , opacity: 0 + } + , hidden: + css + { transition: { when: "afterChildren" } + } + } + +containerVariant ∷ + { hidden ∷ Motion.VariantLabel + , visible ∷ Motion.VariantLabel + } +containerVariant = Motion.makeVariantLabels containerVariants + +popover ∷ Effect JSX +popover = do + autosuggest <- mkAutosuggest + pure + $ fragment + [ R.div_ + [ R.h2_ [ R.text "Autosuggest" ] + , el_ autosuggest {} + ] + ] + where + mkAutosuggest ∷ Effect (ReactComponent {}) + mkAutosuggest = do + motionStack <- Motion.custom Stack.component + reactComponent "PopoverExample" \props -> React.do + text /\ setText <- React.useState' "" + visible /\ setVisible <- React.useState' false + let + matchingAuthors = + authors + # Array.filter (\a -> String.contains (String.Pattern (String.toLower text)) (String.toLower a)) + let + target = + el_ Input.component + { type: HTMLInput.Text + , id: "author" + , label: nes (SProxy ∷ _ "Author") + , placeholder: "e.g. William Shakespeare" + , value: text + , onChange: handler targetValue $ traverse_ setText + , onBlur: handler_ (setVisible false) + , onFocus: handler_ (setVisible true) + } + pop = + el Popover.component + { target + , placement: Placement Placement.Bottom (Just Placement.Start) + } + box = + el Box.component + { style: + css + { background: "rgba(128, 128, 128, 0.5)" + , borderRadius: "5px" + } + } + stack = + el motionStack + $ Motion.motion + { variants: Motion.variants containerVariants + , animate: Motion.animate (if visible then containerVariant.visible else containerVariant.hidden) + , layout: Motion.layout true + } + {} + entry a = + el Motion.div + { key: a + , layout: Motion.layout true + , variants: Motion.variants itemVariants + } + [ R.text a ] + pure + $ fragment + [ pop + [ box + [ el animateSharedLayout {} [ stack (entry <$> matchingAuthors) ] + ] + ] + ] + + authors = + [ "William Shakespeare" + , "Agatha Christie" + , "Barbara Cartland" + , "Danielle Steel" + , "Harold Robbins" + , "Georges Simenon" + , "Enid Blyton" + , "Sidney Sheldon" + , "J. K. Rowling" + , "Gilbert Patten" + , "Dr. Seuss" + , "Eiichiro Oda" + , "Leo Tolstoy" + , "Corín Tellado" + , "Jackie Collins" + , "Horatio Alger" + , "R. L. Stine" + , "Dean Koontz" + , "Nora Roberts" + , "Alexander Pushkin" + , "Stephen King" + , "Paulo Coelho" + , "Jeffrey Archer" + , "Louis L'Amour" + , "Jirō Akagawa" + , "René Goscinny" + , "Erle Stanley Gardner" + , "Edgar Wallace" + , "Jin Yong" + , "Janet Dailey" + , "Robert Ludlum" + , "Akira Toriyama" + , "Osamu Tezuka" + , "James Patterson" + , "Frédéric Dard" + , "Stan and Jan Berenstain" + , "Roald Dahl" + , "John Grisham" + , "Zane Grey" + , "Irving Wallace" + , "J. R. R. Tolkien" + , "Masashi Kishimoto" + , "Karl May" + , "Carter Brown" + , "Mickey Spillane" + , "C. S. Lewis" + , "Kyotaro Nishimura" + , "Mitsuru Adachi" + , "Rumiko Takahashi" + , "Gosho Aoyama" + , "Dan Brown" + , "Ann M. Martin" + , "Ryōtarō Shiba" + , "Arthur Hailey" + , "Gérard de Villiers" + , "Beatrix Potter" + , "Michael Crichton" + , "Richard Scarry" + , "Clive Cussler" + , "Alistair MacLean" + , "Ken Follett" + , "Astrid Lindgren" + , "Debbie Macomber" + , "EL James" + , "Tite Kubo" + , "Eiji Yoshikawa" + , "Catherine Cookson" + , "Stephenie Meyer" + , "Norman Bridwell" + , "David Baldacci" + , "Nicholas Sparks" + , "Hirohiko Araki" + , "Evan Hunter" + , "Andrew Neiderman" + , "Roger Hargreaves" + , "Anne Rice" + , "Robin Cook" + , "Wilbur Smith" + , "Erskine Caldwell" + , "Judith Krantz" + , "Eleanor Hibbert" + , "Lewis Carroll" + , "Denise Robins" + , "Cao Xueqin" + , "Ian Fleming" + , "Hermann Hesse" + , "Rex Stout" + , "Anne Golon" + , "Frank G. Slaughter" + , "Edgar Rice Burroughs" + , "John Creasey" + , "James A. Michener" + , "Yasuo Uchida" + , "Seiichi Morimura" + , "Mary Higgins Clark" + , "Penny Jordan" + , "Patricia Cornwell" + ] diff --git a/src/Yoga/Block/Atom/Popover/Style.purs b/src/Yoga/Block/Atom/Popover/Style.purs new file mode 100644 index 0000000..09a7121 --- /dev/null +++ b/src/Yoga/Block/Atom/Popover/Style.purs @@ -0,0 +1,19 @@ +module Yoga.Block.Atom.Popover.Style where + +import Yoga.Prelude.Style + +type Props f r = + ( css ∷ f Style + | r + ) + +content ∷ Style +content = + css + { zIndex: str "-1" + } + +popper ∷ Style +popper = + css + {} diff --git a/src/Yoga/Block/Atom/Popover/View.purs b/src/Yoga/Block/Atom/Popover/View.purs new file mode 100644 index 0000000..e43a209 --- /dev/null +++ b/src/Yoga/Block/Atom/Popover/View.purs @@ -0,0 +1,82 @@ +module Yoga.Block.Atom.Popover.View (component, MandatoryProps, Props, PropsF) where + +import Yoga.Prelude.View +import Effect.Uncurried (mkEffectFn1) +import Framer.Motion as Motion +import React.Basic.DOM (css) +import React.Basic.DOM as R +import React.Basic.Hooks as React +import React.Basic.Popper.Hook (usePopper) +import React.Basic.Popper.Types (modifierOffset, nullRef) +import Unsafe.Coerce (unsafeCoerce) +import Yoga.Block.Atom.Input.Placement.Types (Placement) +import Yoga.Block.Atom.Input.Placement.Types as Placement +import Yoga.Block.Atom.Popover.Style as Style + +type PropsF f = + ( className ∷ f String + , placement ∷ f Placement + | Style.Props f (MandatoryProps ()) + ) + +type MandatoryProps r = + ( children ∷ Array JSX + , target ∷ JSX + | r + ) + +type Props = + PropsF Id + +type PropsOptional = + PropsF OptionalProp + +component ∷ ∀ p p_. Union p p_ Props => ReactComponent { | MandatoryProps p } +component = rawComponent + +rawComponent ∷ ∀ p. ReactComponent { | p } +rawComponent = + mkForwardRefComponent "Popover" do + \(props ∷ { | PropsOptional }) ref -> React.do + -- Hooks + referenceElement /\ setReferenceElement <- React.useState' nullRef + popperElement /\ setPopperElement <- React.useState' nullRef + { styles, attributes } <- + usePopper referenceElement popperElement + { modifiers: + [ modifierOffset { x: 0.0, y: 0.0 } + ] + , placement: props.placement <#> Placement.render # unsafeUnOptional + } + -- Handlers + -- Elements + let + result = + fragment + $ [ refElem + , popperEl + [ content + props.children + ] + ] + content = + styled R.div' + { className: "popper-element-content" + , css: Style.content + , key: "container" + } + popperEl = + styled R.div' + { className: "popper-element" + , css: Style.popper + , ref: unsafeCoerce (mkEffectFn1 setPopperElement) + , style: styles.popper + , _data: attributes.popper + } + refElem = + element Motion.div + { ref: unsafeCoerce (mkEffectFn1 setReferenceElement) + , style: css { display: "inline-block" } + , children: [ props.target ] + } + pure result diff --git a/src/Yoga/Block/Container/Style.purs b/src/Yoga/Block/Container/Style.purs index 20445a5..d5a8962 100644 --- a/src/Yoga/Block/Container/Style.purs +++ b/src/Yoga/Block/Container/Style.purs @@ -41,7 +41,7 @@ mkGlobal maybeMode = } , ":root": nested $ variables - <> fontVariables { main: "Inter, system-ui, sans-serif", mono: "Victor Mono, Menlo, Consolas, Monaco, Liberation Mono, Lucida Console" } + <> fontVariables { main: "Inter", mono: "Victor Mono, Menlo, Consolas, Monaco, Liberation Mono, Lucida Console" } , html: nested $ css @@ -63,24 +63,39 @@ mkGlobal maybeMode = nest { fontFamily: str "var(--mono-font)" } - , "h1,h2,h3,h4,h5": - nest - { fontWeight: str "800" - } , h1: - nested $ css { fontSize: em 3.5 } + nested + $ css + { fontSize: var "--s4" + , lineHeight: str "calc(var(--s4) * 1.4)" + , fontWeight: str "900" + , letterSpacing: str "calc(var(--s-5) * -1)" + } , h2: - nested $ css { fontSize: em 2.7 } + nested + $ css + { fontSize: var "--s3" + , lineHeight: str "calc(var(--s3) * 1.4)" + , fontWeight: str "800" + , letterSpacing: str "calc(var(--s-5) * -0.9)" + } + , h3: + nested + $ css + { fontSize: var "--s2" + , lineHeight: str "calc(var(--s2) * 1.4)" + , fontWeight: str "700" + , letterSpacing: str "calc(var(--s-5) * -0.8)" + } + , "::selection": + nest + { background: str colour.highlight + } , "*, *:before, *:after": nested $ css { boxSizing: str "inherit" } - , form: - nested - $ css - { backgroundColor: str colour.background07 - } } defaultColours ∷ Colours @@ -103,16 +118,23 @@ defaultColours = , background80: darken 0.8 lightBg , background90: darken 0.9 lightBg , background100: darken 1.0 lightBg + , backgroundInverted: darken 0.85 lightBg + , textInverted: lightBg , success , successText + , invalid + , invalidText + , required , interfaceBackground: lightBg , interfaceTextDisabled: darken 0.33 lightBg , interfaceBackgroundHighlight: darken 0.07 lightBg , interfaceBackgroundShadow: darken 0.1 lightBg - , inputBackground: darken 0.03 lightBg + , inputBackground: lightBg , inputBorder: darken 0.1 lightBg - , highlight: highlight - , text: darkBg + , highlight + , highlightText + , text: textLightTheme + , placeholderText: lighten 0.4 darkBg } , dark: { background0: darkBg @@ -132,26 +154,44 @@ defaultColours = , background80: lighten 0.8 darkBg , background90: lighten 0.9 darkBg , background100: lighten 1.0 darkBg + , textInverted: darkBg + , backgroundInverted: lightBg , interfaceBackground: lighten 0.4 darkBg , interfaceTextDisabled: lighten 0.8 darkBg , interfaceBackgroundHighlight: lighten 0.5 darkBg , interfaceBackgroundShadow: lighten 0.4 darkBg - , inputBackground: lighten 0.10 darkBg + , inputBackground: darkBg , inputBorder: lighten 0.17 darkBg , success , successText - , highlight + , required + , invalid + , invalidText + , highlight: highlightDark + , highlightText , text: lightBg + , placeholderText: darken 0.4 lightBg } } where highlight = Color.rgb 0x00 0x99 0xFF + highlightDark = Color.rgb 0x88 0x33 0xFF + + highlightText = Color.rgb 0xFF 0xFF 0xFF + success = Color.rgb 20 200 60 successText = Color.rgb 250 250 250 - -- highlight = Color.rgb 0x10 0x45 0x4A + invalid = Color.rgb 220 40 70 + + invalidText = successText + + required = Color.rgb 200 50 80 + + textLightTheme = Color.rgb 16 16 32 + darkBg = Color.rgb 0 0 0 lightBg = Color.rgb 250 250 250 @@ -174,6 +214,7 @@ type FlatTheme a = , background80 ∷ a , background90 ∷ a , background100 ∷ a + , backgroundInverted ∷ a , interfaceBackground ∷ a , interfaceTextDisabled ∷ a , interfaceBackgroundHighlight ∷ a @@ -181,9 +222,15 @@ type FlatTheme a = , inputBackground ∷ a , inputBorder ∷ a , highlight ∷ a + , highlightText ∷ a , success ∷ a , successText ∷ a + , invalid ∷ a + , invalidText ∷ a + , required ∷ a , text ∷ a + , textInverted ∷ a + , placeholderText ∷ a } type Colours = diff --git a/src/Yoga/Block/Icon/SVG/EyeClosed.purs b/src/Yoga/Block/Icon/SVG/EyeClosed.purs index 5775e53..385933f 100644 --- a/src/Yoga/Block/Icon/SVG/EyeClosed.purs +++ b/src/Yoga/Block/Icon/SVG/EyeClosed.purs @@ -1,7 +1,6 @@ module Yoga.Block.Icon.SVG.EyeClosed where import React.Basic (JSX) -import React.Basic.DOM as R import React.Basic.DOM.SVG as SVG eyeClosed ∷ JSX @@ -19,12 +18,8 @@ eyeClosed = , d: "M0 0h100v100H0z" } , SVG.path - { d: "M5.492 63.902c-1.539-2.426-2.587-11.465-1.593-13.076.352.369 4.353-.86 4.732-.54 24.519 20.777 51.057 22.859 83.889.676.892-.603 2.312-1.948 3.567-.637.892 2.137 2.479 6.187 2.439 10.011-.858 3.046-4.013 8.107-6.554 10.666.091-3.033-1.654-9.054-2.787-11.919-1.169 1.053-4.237 3.361-5.557 4.507 2.021 6.439 3.704 10.343 4.342 13.77-1.418 2.716-6.433 6.382-8.172 6.837-.695-3.519-3.589-12.626-4.791-16.545-1.721 1.014-7.746 2.85-7.739 2.967.294 5.01 1.269 11.753 3.159 17.431-2.331 1.521-7.403 2.045-10.644 2.233-1.477-3.05-1.902-13.039-2.221-17.74-1.768.532-6.838.5-8.865.679-.682 5.829-.99 11.62-.47 17.094-2.634-.024-8.846-1.614-10.179-2.127-.34-.886.675-13.622.808-16.23-1.442-.074-7.906-2.268-8.801-2.513-.501 3.058-1.153 12.152-.578 14.598-1.516-.601-8.651-6.109-9.148-6.3-.617-3.543-.097-9.17.23-12.361-.919-.313-5.78-3.571-6.551-4.211-.87 8.919-.1 11.254.106 11.869-1.502-1.338-7.45-7.873-8.622-9.139z" + { d: "M50.173 21.996c23.346 0 42.3 27.978 42.3 27.978s-18.954 27.979-42.3 27.979c-23.346 0-42.301-27.979-42.301-27.979s18.955-27.978 42.301-27.978z" , fill: "var(--stroke-colour)" - } - , SVG.path - { d: "M50.026 26.996c23.346 0 42.3 24.595 42.3 24.595s-18.954 17.592-42.3 17.592c-23.347 0-42.301-17.592-42.301-17.592s18.954-24.595 42.301-24.595z" - , fill: "none" , stroke: "var(--stroke-colour)" , strokeWidth: "8" } diff --git a/src/Yoga/Block/Icon/SVG/EyeOpen.purs b/src/Yoga/Block/Icon/SVG/EyeOpen.purs index ab7bedc..6a3fa04 100644 --- a/src/Yoga/Block/Icon/SVG/EyeOpen.purs +++ b/src/Yoga/Block/Icon/SVG/EyeOpen.purs @@ -1,7 +1,6 @@ module Yoga.Block.Icon.SVG.EyeOpen where import React.Basic (JSX) -import React.Basic.DOM as R import React.Basic.DOM.SVG as SVG eyeOpen ∷ JSX @@ -27,18 +26,14 @@ eyeOpen = , SVG.circle { cx: "49.963" , cy: "50.087" - , r: "24.205" + , r: "19.869" , fill: "none" , stroke: "var(--stroke-colour)" , strokeWidth: "6" } - , SVG.circle - { cx: "49.963" - , cy: "50.087" - , r: "9.748" + , SVG.path + { d: "M49.963 40.748c5.155 0 9.339 4.184 9.339 9.339 0 5.154-4.184 9.339-9.339 9.339-5.154 0-9.339-4.185-9.339-9.339 0-5.155 4.185-9.339 9.339-9.339zm2.405 2.279a3.517 3.517 0 013.516 3.515 3.517 3.517 0 01-3.516 3.516 3.517 3.517 0 010-7.031z" , fill: "var(--stroke-colour)" - , stroke: "var(--stroke-colour)" - , strokeWidth: "8" } ] } diff --git a/src/Yoga/Block/Icon/SVG/Icon.purs b/src/Yoga/Block/Icon/SVG/Icon.purs index 48110ee..cddc0d4 100644 --- a/src/Yoga/Block/Icon/SVG/Icon.purs +++ b/src/Yoga/Block/Icon/SVG/Icon.purs @@ -4,6 +4,8 @@ module Yoga.Block.Icon.SVG , module Yoga.Block.Icon.SVG.MagnifyingGlass , module Yoga.Block.Icon.SVG.EyeClosed , module Yoga.Block.Icon.SVG.EyeOpen + , module Yoga.Block.Icon.SVG.Key + , module Yoga.Block.Icon.SVG.SimpleKey ) where import Yoga.Block.Icon.SVG.On (on) @@ -11,3 +13,5 @@ import Yoga.Block.Icon.SVG.Off (off) import Yoga.Block.Icon.SVG.MagnifyingGlass (magnifyingGlass) import Yoga.Block.Icon.SVG.EyeClosed (eyeClosed) import Yoga.Block.Icon.SVG.EyeOpen (eyeOpen) +import Yoga.Block.Icon.SVG.Key (key) +import Yoga.Block.Icon.SVG.SimpleKey (simpleKey) diff --git a/src/Yoga/Block/Icon/SVG/Key.purs b/src/Yoga/Block/Icon/SVG/Key.purs new file mode 100644 index 0000000..c8e72e9 --- /dev/null +++ b/src/Yoga/Block/Icon/SVG/Key.purs @@ -0,0 +1,28 @@ +module Yoga.Block.Icon.SVG.Key where + +import React.Basic (JSX) +import React.Basic.DOM.SVG as SVG + +key ∷ JSX +key = + SVG.svg + { viewBox: "0 0 100 100" + , xmlns: "http://www.w3.org/2000/svg" + , fillRule: "evenodd" + , clipRule: "evenodd" + , strokeLinejoin: "round" + , strokeMiterlimit: "2" + , children: + [ SVG.g + { fill: "var(--stroke-colour)" + , children: + [ SVG.path + { d: "M30.65 43.906c14.689 0 26.614 11.937 26.614 26.64 0 14.702-11.925 26.639-26.614 26.639-14.688 0-26.614-11.937-26.614-26.639 0-14.703 11.926-26.64 26.614-26.64zm-9.215 28.741a6.454 6.454 0 010 12.907 6.454 6.454 0 010-12.907z" + } + , SVG.path + { d: "M79.123 7.855h10.701l.833.776-47.719 44.401 2.491 2.286 45.494-43.326v12.21l-8.681 3.927-2.311 10.777-12.195 6.78-6.678 14.839-17.216 8.651-20.256-9.646L79.123 7.855z" + } + ] + } + ] + } diff --git a/src/Yoga/Block/Icon/SVG/SimpleKey.purs b/src/Yoga/Block/Icon/SVG/SimpleKey.purs new file mode 100644 index 0000000..7461461 --- /dev/null +++ b/src/Yoga/Block/Icon/SVG/SimpleKey.purs @@ -0,0 +1,31 @@ +module Yoga.Block.Icon.SVG.SimpleKey where + +import React.Basic (JSX) +import React.Basic.DOM.SVG as SVG + +simpleKey ∷ JSX +simpleKey = + SVG.svg + { viewBox: "0 0 100 100" + , xmlns: "http://www.w3.org/2000/svg" + , fillRule: "evenodd" + , clipRule: "evenodd" + , strokeLinejoin: "round" + , strokeMiterlimit: "2" + , children: + [ SVG.g + { fill: "var(--stroke-colour)" + , children: + [ SVG.ellipse + { cx: "30.65" + , cy: "70.546" + , rx: "26.614" + , ry: "26.639" + } + , SVG.path + { d: "M79.123 7.855h10.701l.833.776.266 3.361v12.21l-8.681 3.927-2.311 10.777-12.195 6.78-6.678 14.839-17.216 8.651-20.256-9.646L79.123 7.855z" + } + ] + } + ] + } diff --git a/src/Yoga/Block/Icon/SVG/eyeClosed.svg b/src/Yoga/Block/Icon/SVG/eyeClosed.svg index 6b23a08..129c71c 100644 --- a/src/Yoga/Block/Icon/SVG/eyeClosed.svg +++ b/src/Yoga/Block/Icon/SVG/eyeClosed.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/Yoga/Block/Icon/SVG/eyeOpen.svg b/src/Yoga/Block/Icon/SVG/eyeOpen.svg index d955416..86f4567 100644 --- a/src/Yoga/Block/Icon/SVG/eyeOpen.svg +++ b/src/Yoga/Block/Icon/SVG/eyeOpen.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/Yoga/Block/Icon/SVG/key.svg b/src/Yoga/Block/Icon/SVG/key.svg new file mode 100644 index 0000000..30bff7d --- /dev/null +++ b/src/Yoga/Block/Icon/SVG/key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Yoga/Block/Icon/SVG/simpleKey.svg b/src/Yoga/Block/Icon/SVG/simpleKey.svg new file mode 100644 index 0000000..dd43a45 --- /dev/null +++ b/src/Yoga/Block/Icon/SVG/simpleKey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Yoga/Block/Internal.purs b/src/Yoga/Block/Internal.purs index e30aa0c..531da41 100644 --- a/src/Yoga/Block/Internal.purs +++ b/src/Yoga/Block/Internal.purs @@ -1,6 +1,7 @@ module Yoga.Block.Internal ( mkForwardRefComponent , mkForwardRefComponentEffect + , forwardedRefAsMaybe , unsafeEmotion , unsafeDiv , dangerous @@ -8,6 +9,7 @@ module Yoga.Block.Internal , DivPropsF , InputProps , InputPropsF + , NodeRef , emotionDiv , emotionInput , module Yoga.Block.Internal.OptionalProp @@ -15,12 +17,18 @@ module Yoga.Block.Internal , unsafeUnionDroppingUndefined , unsafeMergeSecond , createRef + , getBoundingBoxFromRef + , getHTMLElementFromRef ) where import Prelude +import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT) import Data.Array as Array import Data.Function.Uncurried (Fn3, runFn3) +import Data.Maybe (Maybe) import Data.Nullable (Nullable) +import Data.Nullable as Nullable +import Data.Traversable (for) import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) import Foreign.Object (Object) @@ -31,12 +39,16 @@ import React.Basic.DOM (CSS, unsafeCreateDOMComponent) import React.Basic.Emotion (Style) import React.Basic.Emotion as E import React.Basic.Events (EventHandler) -import React.Basic.Hooks (JSX, ReactComponent, Ref, Render) +import React.Basic.Hooks (JSX, ReactComponent, Ref, Render, readRefMaybe) import Record.Extra (class Keys, keys) import Type.Data.Row (RProxy(..)) +import Unsafe.Coerce (unsafeCoerce) +import Untagged.Union (UndefinedOr, uorToMaybe) import Web.DOM (Node) +import Web.HTML.HTMLElement (DOMRect, HTMLElement, getBoundingClientRect) +import Web.HTML.HTMLElement as HTMLElement import Yoga.Block.Internal.CSS (_0) -import Yoga.Block.Internal.OptionalProp (Id, OptionalProp(..), appendIfDefined, getOr, getOrFlipped, ifTrue, isTruthy, maybeToOp, opToMaybe, unsafeUnOptional, (<>?), (?||)) +import Yoga.Block.Internal.OptionalProp (Id, OptionalProp(..), appendIfDefined, getOr, getOrFlipped, ifTrue, isTruthy, maybeToOp, opToMaybe, setOrDelete, unsafeUnMaybe, unsafeUnOptional, (<>?), (?||)) foreign import mkForwardRefComponent ∷ ∀ inputProps props a hooks. @@ -52,6 +64,26 @@ foreign import mkForwardRefComponentEffect ∷ foreign import createRef ∷ ∀ a. Effect (Ref a) +type NodeRef = + Ref (Nullable Node) + +getBoundingBoxFromRef ∷ Ref (Nullable Node) -> Effect (Maybe DOMRect) +getBoundingBoxFromRef itemRef = do + htmlElem <- getHTMLElementFromRef itemRef + for htmlElem getBoundingClientRect + +getHTMLElementFromRef ∷ Ref (Nullable Node) -> Effect (Maybe HTMLElement) +getHTMLElementFromRef itemRef = + runMaybeT do + node <- MaybeT $ readRefMaybe itemRef + MaybeT $ pure $ HTMLElement.fromNode node + +forwardedRefAsMaybe ∷ ∀ a. Ref a -> Maybe (Ref a) +forwardedRefAsMaybe r = safelyWrapped # uorToMaybe >>= Nullable.toMaybe + where + safelyWrapped ∷ UndefinedOr (Nullable (Ref a)) + safelyWrapped = unsafeCoerce r + foreign import unsafeUnionDroppingUndefined ∷ ∀ r1 r2 r3. Record r1 -> Record r2 -> Record r3 foreign import unsafeMergeSecond ∷ ∀ r1 r2 r3. Record r1 -> Record r2 -> Record r3 diff --git a/src/Yoga/Block/Internal/OptionalProp.purs b/src/Yoga/Block/Internal/OptionalProp.purs index 3a4607a..49eb955 100644 --- a/src/Yoga/Block/Internal/OptionalProp.purs +++ b/src/Yoga/Block/Internal/OptionalProp.purs @@ -3,8 +3,12 @@ module Yoga.Block.Internal.OptionalProp where import Prelude import Control.Alt (class Alt, (<|>)) import Data.Foldable (class Foldable, foldMap, foldl, foldr) -import Data.Maybe (Maybe, maybe) +import Data.Maybe (Maybe(..), maybe) import Data.Newtype (class Newtype) +import Data.Symbol (class IsSymbol, SProxy, reflectSymbol) +import Prim.Row (class Cons) +import Record (set) +import Record.Unsafe (unsafeDelete) import Unsafe.Coerce (unsafeCoerce) import Untagged.Castable (class Castable) import Untagged.Union (UndefinedOr, defined, fromUndefinedOr, maybeToUor, uorToMaybe) @@ -14,9 +18,24 @@ type Id a = newtype OptionalProp a = OptionalProp (UndefinedOr a) +setOrDelete ∷ + ∀ r a rNoA key. + IsSymbol key => + Cons key a rNoA r => + SProxy key -> + OptionalProp a -> + { | r } -> + { | r } +setOrDelete key v = case opToMaybe v of + Nothing -> unsafeDelete (reflectSymbol key) + Just v' -> set key v' + unsafeUnOptional ∷ ∀ a. OptionalProp a -> a unsafeUnOptional = unsafeCoerce +unsafeUnMaybe ∷ ∀ a. Maybe a -> a +unsafeUnMaybe = maybeToOp >>> unsafeUnOptional + opToMaybe ∷ ∀ a. OptionalProp a -> Maybe a opToMaybe (OptionalProp x) = uorToMaybe x @@ -44,7 +63,7 @@ instance functorOptionalProp ∷ Functor OptionalProp where instance altOptionalProp ∷ Alt OptionalProp where alt op1 op2 = maybeToOp $ (opToMaybe op1) <|> (opToMaybe op2) -instance coercibleOptionalProp ∷ Castable a (OptionalProp a) +instance castableOptionalProp ∷ Castable a (OptionalProp a) getOr ∷ ∀ a. a -> OptionalProp a -> a getOr default (OptionalProp o) = fromUndefinedOr default o diff --git a/src/Yoga/Prelude/Default.purs b/src/Yoga/Prelude/Default.purs index cb01c73..7ff4eb6 100644 --- a/src/Yoga/Prelude/Default.purs +++ b/src/Yoga/Prelude/Default.purs @@ -19,7 +19,7 @@ import Control.Alt ((<|>)) import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT) import Control.Monad.Trans.Class (lift) import Data.Either (Either(..), note, hush) -import Data.Foldable (foldMap, for_, intercalate, traverse_) +import Data.Foldable (fold, foldMap, for_, intercalate, traverse_) import Data.FoldableWithIndex (foldMapWithIndex) import Data.FunctorWithIndex (mapWithIndex) import Data.Maybe (Maybe(..), fromMaybe, fromMaybe', isJust, maybe) diff --git a/src/Yoga/Prelude/View.purs b/src/Yoga/Prelude/View.purs index 6bdad6e..d8ae431 100644 --- a/src/Yoga/Prelude/View.purs +++ b/src/Yoga/Prelude/View.purs @@ -12,8 +12,6 @@ module Yoga.Prelude.View , module Web.HTML.HTMLElement , module Data.Nullable , module Untagged.Castable - , NodeRef - , getBoundingBoxFromRef ) where import Yoga.Prelude.Default @@ -27,16 +25,5 @@ import Type.Row (type (+)) import Untagged.Castable (cast) import Web.DOM (Node) import Web.HTML.HTMLElement (HTMLElement, DOMRect, blur, focus, getBoundingClientRect) -import Web.HTML.HTMLElement as HTMLElement import Yoga (el, el_, styled, styledLeaf, yogaElement) -import Yoga.Block.Internal (DivProps, DivPropsF, Id, InputProps, InputPropsF, OptionalProp(..), _0, appendIfDefined, createRef, dangerous, emotionDiv, emotionInput, getOr, getOrFlipped, ifTrue, isTruthy, maybeToOp, mkForwardRefComponent, mkForwardRefComponentEffect, opToMaybe, unsafeDiv, unsafeEmotion, unsafeUnOptional, (<>?), (?||)) - -type NodeRef = - Ref (Nullable Node) - -getBoundingBoxFromRef ∷ Ref (Nullable Node) -> Effect (Maybe DOMRect) -getBoundingBoxFromRef itemRef = - runMaybeT do - node <- MaybeT $ readRefMaybe itemRef - htmlElement <- MaybeT $ pure $ HTMLElement.fromNode node - lift $ getBoundingClientRect htmlElement +import Yoga.Block.Internal (DivProps, DivPropsF, Id, InputProps, InputPropsF, NodeRef, OptionalProp(..), _0, appendIfDefined, createRef, dangerous, emotionDiv, emotionInput, forwardedRefAsMaybe, getBoundingBoxFromRef, getHTMLElementFromRef, getOr, getOrFlipped, ifTrue, isTruthy, maybeToOp, mkForwardRefComponent, mkForwardRefComponentEffect, opToMaybe, unsafeDiv, unsafeEmotion, unsafeMergeSecond, unsafeUnMaybe, unsafeUnOptional, unsafeUnionDroppingUndefined, (<>?), (?||), setOrDelete)