Popover work

This commit is contained in:
Mark Eibes 2020-12-24 10:50:57 +01:00
parent c32928933f
commit 189267c594
31 changed files with 1086 additions and 210 deletions

View File

@ -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)

View File

@ -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"

View File

@ -5,6 +5,7 @@ You can edit this file as you like.
{ name = "ry-blocks"
, dependencies =
[ "console"
, "debug"
, "effect"
, "heterogeneous"
, "interpolate"

View File

@ -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

View File

@ -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

View File

@ -37,4 +37,5 @@ modifierOffset { x, y } = unsafeCoerce { name: "offset", options: { offset: [ x,
type Options =
( modifiers ∷ Array Modifier
, strategy ∷ String
, placement ∷ String
)

View File

@ -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")

View File

@ -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"

View File

@ -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" }
]
]

View File

@ -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]":

View File

@ -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"

View File

@ -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
]

View File

@ -0,0 +1,5 @@
module Yoga.Block.Atom.Popover
( module Yoga.Block.Atom.Popover.View
) where
import Yoga.Block.Atom.Popover.View (component, Props)

View File

@ -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"
}

View File

@ -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"
]

View File

@ -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
{}

View File

@ -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

View File

@ -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 =

View File

@ -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"
}

View File

@ -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"
}
]
}

View File

@ -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)

View File

@ -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"
}
]
}
]
}

View File

@ -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"
}
]
}
]
}

View File

@ -1 +1 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5"><path fill="none" d="M0 0h100v100H0z"/><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" fill="#333"/><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="#333" stroke-width="8"/></svg>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5"><path fill="none" d="M0 0h100v100H0z"/><path 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="#333" stroke="#333" stroke-width="8"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 391 B

View File

@ -1 +1 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5"><path fill="none" d="M0 0h100v100H0z"/><path 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="none" stroke="#333" stroke-width="8"/><circle cx="49.963" cy="50.087" r="24.205" fill="none" stroke="#333" stroke-width="6"/><circle cx="49.963" cy="50.087" r="9.748" fill="#333" stroke="#333" stroke-width="8"/></svg>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5"><path fill="none" d="M0 0h100v100H0z"/><path 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="none" stroke="#333" stroke-width="8"/><circle cx="49.963" cy="50.087" r="19.869" fill="none" stroke="#333" stroke-width="6"/><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="#333"/></svg>

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 739 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path fill="none" d="M0 0h100v100H0z"/><g fill="#333"><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"/><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"/></g></svg>

After

Width:  |  Height:  |  Size: 626 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path fill="none" d="M0 0h100v100H0z"/><g fill="#333"><ellipse cx="30.65" cy="70.546" rx="26.614" ry="26.639"/><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"/></g></svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)