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)